1 // Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
11 extern crate env_logger;
15 extern crate rustfmt_nightly as rustfmt;
19 use std::io::{self, stdout, Read, Write};
20 use std::path::{Path, PathBuf};
21 use std::str::FromStr;
25 use getopts::{Matches, Options};
28 load_config, CliOptions, Color, Config, Edition, EmitMode, ErrorKind, FileLines, FileName,
29 Input, Session, Verbosity,
34 let opts = make_opts();
36 let exit_code = match execute(&opts) {
39 eprintln!("{}", e.to_string());
43 // Make sure standard output is flushed before we exit.
44 std::io::stdout().flush().unwrap();
46 // Exit with given exit code.
48 // NOTE: This immediately terminates the process without doing any cleanup,
49 // so make sure to finish all necessary cleanup before this is called.
50 std::process::exit(exit_code);
53 /// Rustfmt operations.
55 /// Format files and their child modules.
58 minimal_config_path: Option<String>,
60 /// Print the help message.
62 // Print version information
64 /// Output default config to a file, or stdout if None
68 /// No file specified, read from stdin
74 /// Arguments to `--help`
81 fn make_opts() -> Options {
82 let mut opts = Options::new();
87 "Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits \
88 with 1 and prints a diff if formatting is required.",
90 let is_nightly = is_nightly();
91 let emit_opts = if is_nightly {
92 "[files|stdout|coverage|checkstyle]"
96 opts.optopt("", "emit", "What data to emit and how", emit_opts);
97 opts.optflag("", "backup", "Backup any modified files.");
101 "Recursively searches the given path for the rustfmt.toml config file. If not \
102 found reverts to the input file path",
103 "[Path for the configuration file]",
105 opts.optopt("", "edition", "Rust edition to use", "[2015|2018]");
109 "Use colored output (if supported)",
110 "[always|never|auto]",
115 "Dumps a default or minimal config to PATH. A minimal config is the \
116 subset of the current config file used for formatting the current program.",
117 "[minimal|default] PATH",
124 "Enables unstable features. Only available on nightly channel.",
129 "Format specified line ranges. Run with `--help=file-lines` for \
130 more detail (unstable).",
135 "error-on-unformatted",
136 "Error if unable to get comments or string literals within max_width, \
137 or they are left with trailing whitespaces (unstable).",
142 "Don't reformat child modules (unstable).",
146 opts.optflag("v", "verbose", "Print verbose output");
147 opts.optflag("q", "quiet", "Print less output");
148 opts.optflag("V", "version", "Show version information");
152 "Show this message or help about a specific topic: `config` or `file-lines`",
159 fn is_nightly() -> bool {
160 option_env!("CFG_RELEASE_CHANNEL").map_or(false, |c| c == "nightly" || c == "dev")
163 // Returned i32 is an exit code
164 fn execute(opts: &Options) -> Result<i32, failure::Error> {
165 let matches = opts.parse(env::args().skip(1))?;
166 let options = GetOptsOptions::from_matches(&matches)?;
168 match determine_operation(&matches)? {
169 Operation::Help(HelpOp::None) => {
170 print_usage_to_stdout(opts, "");
173 Operation::Help(HelpOp::Config) => {
174 Config::print_docs(&mut stdout(), options.unstable_features);
177 Operation::Help(HelpOp::FileLines) => {
178 print_help_file_lines();
181 Operation::Version => {
185 Operation::ConfigOutputDefault { path } => {
186 let toml = Config::default().all_options().to_toml().map_err(err_msg)?;
187 if let Some(path) = path {
188 let mut file = File::create(path)?;
189 file.write_all(toml.as_bytes())?;
191 io::stdout().write_all(toml.as_bytes())?;
195 Operation::Stdin { input } => format_string(input, options),
199 } => format(files, minimal_config_path, &options),
203 fn format_string(input: String, options: GetOptsOptions) -> Result<i32, failure::Error> {
204 // try to read config from local directory
205 let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
207 // emit mode is always Stdout for Stdin.
208 config.set().emit_mode(EmitMode::Stdout);
209 config.set().verbose(Verbosity::Quiet);
212 config.set().file_lines(options.file_lines);
213 for f in config.file_lines().files() {
215 FileName::Stdin => {}
216 _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
220 let out = &mut stdout();
221 let mut session = Session::new(config, Some(out));
222 format_and_emit_report(&mut session, Input::Text(input));
224 let exit_code = if session.has_operational_errors() || session.has_parsing_errors() {
234 minimal_config_path: Option<String>,
235 options: &GetOptsOptions,
236 ) -> Result<i32, failure::Error> {
237 options.verify_file_lines(&files);
238 let (config, config_path) = load_config(None, Some(options.clone()))?;
240 if config.verbose() == Verbosity::Verbose {
241 if let Some(path) = config_path.as_ref() {
242 println!("Using rustfmt config file {}", path.display());
246 let out = &mut stdout();
247 let mut session = Session::new(config, Some(out));
251 eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
252 session.add_operational_error();
253 } else if file.is_dir() {
254 eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
255 session.add_operational_error();
257 // Check the file directory if the config-path could not be read or not provided
258 if config_path.is_none() {
259 let (local_config, config_path) =
260 load_config(Some(file.parent().unwrap()), Some(options.clone()))?;
261 if local_config.verbose() == Verbosity::Verbose {
262 if let Some(path) = config_path {
264 "Using rustfmt config file {} for {}",
271 session.override_config(local_config, |sess| {
272 format_and_emit_report(sess, Input::File(file))
275 format_and_emit_report(&mut session, Input::File(file));
280 // If we were given a path via dump-minimal-config, output any options
281 // that were used during formatting as TOML.
282 if let Some(path) = minimal_config_path {
283 let mut file = File::create(path)?;
284 let toml = session.config.used_options().to_toml().map_err(err_msg)?;
285 file.write_all(toml.as_bytes())?;
288 let exit_code = if session.has_operational_errors()
289 || session.has_parsing_errors()
290 || ((session.has_diff() || session.has_check_errors()) && options.check)
299 fn format_and_emit_report<T: Write>(session: &mut Session<T>, input: Input) {
300 match session.format(input) {
302 if report.has_warnings() {
303 match term::stderr() {
305 if session.config.color().use_colored_tty()
306 && t.supports_color()
307 && t.supports_attr(term::Attr::Bold) =>
309 match report.fancy_print(term::stderr().unwrap()) {
311 Err(..) => panic!("Unable to write to stderr: {}", report),
314 _ => eprintln!("{}", report),
319 eprintln!("Error writing files: {}", msg);
320 session.add_operational_error();
325 fn print_usage_to_stdout(opts: &Options, reason: &str) {
326 let sep = if reason.is_empty() {
329 format!("{}\n\n", reason)
332 "{}Format Rust code\n\nusage: {} [options] <file>...",
334 env::args_os().next().unwrap().to_string_lossy()
336 println!("{}", opts.usage(&msg));
339 fn print_help_file_lines() {
341 "If you want to restrict reformatting to specific sets of lines, you can
342 use the `--file-lines` option. Its argument is a JSON array of objects
343 with `file` and `range` properties, where `file` is a file name, and
344 `range` is an array representing a range of lines like `[7,13]`. Ranges
345 are 1-based and inclusive of both end points. Specifying an empty array
346 will result in no files being formatted. For example,
349 rustfmt --file-lines '[
350 {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
351 {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
352 {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
353 {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
356 would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
357 and `15` of `src/foo.rs`. No other files would be formatted, even if they
358 are included as out of line modules from `src/lib.rs`."
363 let version_info = format!(
365 option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
366 include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
369 println!("rustfmt {}", version_info);
372 fn determine_operation(matches: &Matches) -> Result<Operation, ErrorKind> {
373 if matches.opt_present("h") {
374 let topic = matches.opt_str("h");
376 return Ok(Operation::Help(HelpOp::None));
377 } else if topic == Some("config".to_owned()) {
378 return Ok(Operation::Help(HelpOp::Config));
379 } else if topic == Some("file-lines".to_owned()) {
380 return Ok(Operation::Help(HelpOp::FileLines));
382 println!("Unknown help topic: `{}`\n", topic.unwrap());
383 return Ok(Operation::Help(HelpOp::None));
387 let mut minimal_config_path = None;
388 if let Some(ref kind) = matches.opt_str("print-config") {
389 let path = matches.free.get(0).cloned();
390 if kind == "default" {
391 return Ok(Operation::ConfigOutputDefault { path });
392 } else if kind == "minimal" {
393 minimal_config_path = path;
394 if minimal_config_path.is_none() {
395 println!("WARNING: PATH required for `--print-config minimal`");
400 if matches.opt_present("version") {
401 return Ok(Operation::Version);
404 // if no file argument is supplied, read from stdin
405 if matches.free.is_empty() {
406 let mut buffer = String::new();
407 io::stdin().read_to_string(&mut buffer)?;
409 return Ok(Operation::Stdin { input: buffer });
412 let files: Vec<_> = matches
416 let p = PathBuf::from(s);
417 // we will do comparison later, so here tries to canonicalize first
418 // to get the expected behavior.
419 p.canonicalize().unwrap_or(p)
423 Ok(Operation::Format {
429 const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
431 /// Parsed command line options.
432 #[derive(Clone, Debug, Default)]
433 struct GetOptsOptions {
434 skip_children: Option<bool>,
437 config_path: Option<PathBuf>,
441 edition: Option<Edition>,
442 color: Option<Color>,
443 file_lines: FileLines, // Default is all lines in all files.
444 unstable_features: bool,
445 error_on_unformatted: Option<bool>,
448 impl GetOptsOptions {
449 pub fn from_matches(matches: &Matches) -> Result<GetOptsOptions, failure::Error> {
450 let mut options = GetOptsOptions::default();
451 options.verbose = matches.opt_present("verbose");
452 options.quiet = matches.opt_present("quiet");
453 if options.verbose && options.quiet {
454 return Err(format_err!("Can't use both `--verbose` and `--quiet`"));
457 let rust_nightly = is_nightly();
460 options.unstable_features = matches.opt_present("unstable-features");
462 if options.unstable_features {
463 if matches.opt_present("skip-children") {
464 options.skip_children = Some(true);
466 if matches.opt_present("error-on-unformatted") {
467 options.error_on_unformatted = Some(true);
469 if let Some(ref file_lines) = matches.opt_str("file-lines") {
470 options.file_lines = file_lines.parse().map_err(err_msg)?;
473 let mut unstable_options = vec![];
474 if matches.opt_present("skip-children") {
475 unstable_options.push("`--skip-children`");
477 if matches.opt_present("error-on-unformatted") {
478 unstable_options.push("`--error-on-unformatted`");
480 if matches.opt_present("file-lines") {
481 unstable_options.push("`--file-lines`");
483 if !unstable_options.is_empty() {
484 let s = if unstable_options.len() == 1 { "" } else { "s" };
485 return Err(format_err!(
486 "Unstable option{} ({}) used without `--unstable-features`",
488 unstable_options.join(", "),
494 options.config_path = matches.opt_str("config-path").map(PathBuf::from);
496 options.check = matches.opt_present("check");
497 if let Some(ref emit_str) = matches.opt_str("emit") {
499 return Err(format_err!("Invalid to use `--emit` and `--check`"));
502 options.emit_mode = emit_mode_from_emit_str(emit_str)?;
505 if let Some(ref edition_str) = matches.opt_str("edition") {
506 options.edition = Some(edition_from_edition_str(edition_str)?);
509 if matches.opt_present("backup") {
510 options.backup = true;
514 if !STABLE_EMIT_MODES.contains(&options.emit_mode) {
515 return Err(format_err!(
516 "Invalid value for `--emit` - using an unstable \
517 value without `--unstable-features`",
522 if let Some(ref color) = matches.opt_str("color") {
523 match Color::from_str(color) {
524 Ok(color) => options.color = Some(color),
525 _ => return Err(format_err!("Invalid color: {}", color)),
532 fn verify_file_lines(&self, files: &[PathBuf]) {
533 for f in self.file_lines.files() {
535 FileName::Real(ref f) if files.contains(f) => {}
536 FileName::Real(_) => {
537 eprintln!("Warning: Extra file listed in file_lines option '{}'", f)
539 FileName::Stdin => eprintln!("Warning: Not a file '{}'", f),
545 impl CliOptions for GetOptsOptions {
546 fn apply_to(self, config: &mut Config) {
548 config.set().verbose(Verbosity::Verbose);
549 } else if self.quiet {
550 config.set().verbose(Verbosity::Quiet);
552 config.set().verbose(Verbosity::Normal);
554 config.set().file_lines(self.file_lines);
555 config.set().unstable_features(self.unstable_features);
556 if let Some(skip_children) = self.skip_children {
557 config.set().skip_children(skip_children);
559 if let Some(error_on_unformatted) = self.error_on_unformatted {
560 config.set().error_on_unformatted(error_on_unformatted);
562 if let Some(edition) = self.edition {
563 config.set().edition(edition);
566 config.set().emit_mode(EmitMode::Diff);
568 config.set().emit_mode(self.emit_mode);
571 config.set().make_backup(true);
573 if let Some(color) = self.color {
574 config.set().color(color);
578 fn config_path(&self) -> Option<&Path> {
579 self.config_path.as_ref().map(|p| &**p)
583 fn edition_from_edition_str(edition_str: &str) -> Result<Edition, failure::Error> {
585 "2015" => Ok(Edition::Edition2015),
586 "2018" => Ok(Edition::Edition2018),
587 _ => Err(format_err!("Invalid value for `--edition`")),
591 fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode, failure::Error> {
593 "files" => Ok(EmitMode::Files),
594 "stdout" => Ok(EmitMode::Stdout),
595 "coverage" => Ok(EmitMode::Coverage),
596 "checkstyle" => Ok(EmitMode::Checkstyle),
597 _ => Err(format_err!("Invalid value for `--emit`")),