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.
12 #![feature(extern_prelude)]
14 extern crate env_logger;
18 extern crate rustfmt_nightly as rustfmt;
22 use std::io::{self, stdout, Read, Write};
23 use std::path::{Path, PathBuf};
24 use std::str::FromStr;
28 use getopts::{Matches, Options};
31 load_config, CliOptions, Color, Config, EmitMode, ErrorKind, FileLines, FileName, Input,
37 let opts = make_opts();
39 let exit_code = match execute(&opts) {
42 eprintln!("{}", e.to_string());
46 // Make sure standard output is flushed before we exit.
47 std::io::stdout().flush().unwrap();
49 // Exit with given exit code.
51 // NOTE: This immediately terminates the process without doing any cleanup,
52 // so make sure to finish all necessary cleanup before this is called.
53 std::process::exit(exit_code);
56 /// Rustfmt operations.
58 /// Format files and their child modules.
61 minimal_config_path: Option<String>,
63 /// Print the help message.
65 // Print version information
67 /// Output default config to a file, or stdout if None
71 /// No file specified, read from stdin
77 /// Arguments to `--help`
84 fn make_opts() -> Options {
85 let mut opts = Options::new();
90 "Run in 'check' mode. Exits with 0 if input if formatted correctly. Exits \
91 with 1 and prints a diff if formatting is required.",
93 let is_nightly = is_nightly();
94 let emit_opts = if is_nightly {
95 "[files|stdout|coverage|checkstyle]"
99 opts.optopt("", "emit", "What data to emit and how", emit_opts);
100 opts.optflag("", "backup", "Backup any modified files.");
104 "Recursively searches the given path for the rustfmt.toml config file. If not \
105 found reverts to the input file path",
106 "[Path for the configuration file]",
111 "Use colored output (if supported)",
112 "[always|never|auto]",
117 "Dumps a default or minimal config to PATH. A minimal config is the \
118 subset of the current config file used for formatting the current program.",
119 "[minimal|default] PATH",
126 "Enables unstable features. Only available on nightly channel.",
131 "Format specified line ranges. Run with `--help=file-lines` for \
132 more detail (unstable).",
137 "error-on-unformatted",
138 "Error if unable to get comments or string literals within max_width, \
139 or they are left with trailing whitespaces (unstable).",
144 "Don't reformat child modules (unstable).",
148 opts.optflag("v", "verbose", "Print verbose output");
149 opts.optflag("q", "quiet", "Print less output");
150 opts.optflag("V", "version", "Show version information");
154 "Show this message or help about a specific topic: `config` or `file-lines`",
161 fn is_nightly() -> bool {
162 option_env!("CFG_RELEASE_CHANNEL")
163 .map(|c| c == "nightly" || c == "dev")
167 // Returned i32 is an exit code
168 fn execute(opts: &Options) -> Result<i32, failure::Error> {
169 let matches = opts.parse(env::args().skip(1))?;
170 let options = GetOptsOptions::from_matches(&matches)?;
172 match determine_operation(&matches)? {
173 Operation::Help(HelpOp::None) => {
174 print_usage_to_stdout(opts, "");
177 Operation::Help(HelpOp::Config) => {
178 Config::print_docs(&mut stdout(), options.unstable_features);
181 Operation::Help(HelpOp::FileLines) => {
182 print_help_file_lines();
185 Operation::Version => {
189 Operation::ConfigOutputDefault { path } => {
190 let toml = Config::default().all_options().to_toml().map_err(err_msg)?;
191 if let Some(path) = path {
192 let mut file = File::create(path)?;
193 file.write_all(toml.as_bytes())?;
195 io::stdout().write_all(toml.as_bytes())?;
199 Operation::Stdin { input } => format_string(input, options),
203 } => format(files, minimal_config_path, options),
207 fn format_string(input: String, options: GetOptsOptions) -> Result<i32, failure::Error> {
208 // try to read config from local directory
209 let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
211 // emit mode is always Stdout for Stdin.
212 config.set().emit_mode(EmitMode::Stdout);
213 config.set().verbose(Verbosity::Quiet);
216 config.set().file_lines(options.file_lines);
217 for f in config.file_lines().files() {
219 FileName::Stdin => {}
220 _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
224 let out = &mut stdout();
225 let mut session = Session::new(config, Some(out));
226 format_and_emit_report(&mut session, Input::Text(input));
228 let exit_code = if session.has_operational_errors() || session.has_parsing_errors() {
238 minimal_config_path: Option<String>,
239 options: GetOptsOptions,
240 ) -> Result<i32, failure::Error> {
241 options.verify_file_lines(&files);
242 let (config, config_path) = load_config(None, Some(options.clone()))?;
244 if config.verbose() == Verbosity::Verbose {
245 if let Some(path) = config_path.as_ref() {
246 println!("Using rustfmt config file {}", path.display());
250 let out = &mut stdout();
251 let mut session = Session::new(config, Some(out));
255 eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
256 session.add_operational_error();
257 } else if file.is_dir() {
258 eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
259 session.add_operational_error();
261 // Check the file directory if the config-path could not be read or not provided
262 if config_path.is_none() {
263 let (local_config, config_path) =
264 load_config(Some(file.parent().unwrap()), Some(options.clone()))?;
265 if local_config.verbose() == Verbosity::Verbose {
266 if let Some(path) = config_path {
268 "Using rustfmt config file {} for {}",
275 session.override_config(local_config, |sess| {
276 format_and_emit_report(sess, Input::File(file))
279 format_and_emit_report(&mut session, Input::File(file));
284 // If we were given a path via dump-minimal-config, output any options
285 // that were used during formatting as TOML.
286 if let Some(path) = minimal_config_path {
287 let mut file = File::create(path)?;
288 let toml = session.config.used_options().to_toml().map_err(err_msg)?;
289 file.write_all(toml.as_bytes())?;
292 let exit_code = if session.has_operational_errors()
293 || session.has_parsing_errors()
294 || ((session.has_diff() || session.has_check_errors()) && options.check)
303 fn format_and_emit_report<T: Write>(session: &mut Session<T>, input: Input) {
304 match session.format(input) {
306 if report.has_warnings() {
307 match term::stderr() {
309 if session.config.color().use_colored_tty()
310 && t.supports_color()
311 && t.supports_attr(term::Attr::Bold) =>
313 match report.fancy_print(term::stderr().unwrap()) {
315 Err(..) => panic!("Unable to write to stderr: {}", report),
318 _ => eprintln!("{}", report),
323 eprintln!("Error writing files: {}", msg);
324 session.add_operational_error();
329 fn print_usage_to_stdout(opts: &Options, reason: &str) {
330 let sep = if reason.is_empty() {
333 format!("{}\n\n", reason)
336 "{}Format Rust code\n\nusage: {} [options] <file>...",
338 env::args_os().next().unwrap().to_string_lossy()
340 println!("{}", opts.usage(&msg));
343 fn print_help_file_lines() {
345 "If you want to restrict reformatting to specific sets of lines, you can
346 use the `--file-lines` option. Its argument is a JSON array of objects
347 with `file` and `range` properties, where `file` is a file name, and
348 `range` is an array representing a range of lines like `[7,13]`. Ranges
349 are 1-based and inclusive of both end points. Specifying an empty array
350 will result in no files being formatted. For example,
353 rustfmt --file-lines '[
354 {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
355 {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
356 {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
357 {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
360 would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
361 and `15` of `src/foo.rs`. No other files would be formatted, even if they
362 are included as out of line modules from `src/lib.rs`."
367 let version_info = format!(
369 option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
370 include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
373 println!("rustfmt {}", version_info);
376 fn determine_operation(matches: &Matches) -> Result<Operation, ErrorKind> {
377 if matches.opt_present("h") {
378 let topic = matches.opt_str("h");
380 return Ok(Operation::Help(HelpOp::None));
381 } else if topic == Some("config".to_owned()) {
382 return Ok(Operation::Help(HelpOp::Config));
383 } else if topic == Some("file-lines".to_owned()) {
384 return Ok(Operation::Help(HelpOp::FileLines));
386 println!("Unknown help topic: `{}`\n", topic.unwrap());
387 return Ok(Operation::Help(HelpOp::None));
391 let mut minimal_config_path = None;
392 if let Some(ref kind) = matches.opt_str("print-config") {
393 let path = matches.free.get(0).cloned();
394 if kind == "default" {
395 return Ok(Operation::ConfigOutputDefault { path });
396 } else if kind == "minimal" {
397 minimal_config_path = path;
398 if minimal_config_path.is_none() {
399 println!("WARNING: PATH required for `--print-config minimal`");
404 if matches.opt_present("version") {
405 return Ok(Operation::Version);
408 // if no file argument is supplied, read from stdin
409 if matches.free.is_empty() {
410 let mut buffer = String::new();
411 io::stdin().read_to_string(&mut buffer)?;
413 return Ok(Operation::Stdin { input: buffer });
416 let files: Vec<_> = matches
420 let p = PathBuf::from(s);
421 // we will do comparison later, so here tries to canonicalize first
422 // to get the expected behavior.
423 p.canonicalize().unwrap_or(p)
426 Ok(Operation::Format {
432 const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
434 /// Parsed command line options.
435 #[derive(Clone, Debug, Default)]
436 struct GetOptsOptions {
437 skip_children: Option<bool>,
440 config_path: Option<PathBuf>,
444 color: Option<Color>,
445 file_lines: FileLines, // Default is all lines in all files.
446 unstable_features: bool,
447 error_on_unformatted: Option<bool>,
450 impl GetOptsOptions {
451 pub fn from_matches(matches: &Matches) -> Result<GetOptsOptions, failure::Error> {
452 let mut options = GetOptsOptions::default();
453 options.verbose = matches.opt_present("verbose");
454 options.quiet = matches.opt_present("quiet");
455 if options.verbose && options.quiet {
456 return Err(format_err!("Can't use both `--verbose` and `--quiet`"));
459 let rust_nightly = is_nightly();
462 options.unstable_features = matches.opt_present("unstable-features");
464 if options.unstable_features {
465 if matches.opt_present("skip-children") {
466 options.skip_children = Some(true);
468 if matches.opt_present("error-on-unformatted") {
469 options.error_on_unformatted = Some(true);
471 if let Some(ref file_lines) = matches.opt_str("file-lines") {
472 options.file_lines = file_lines.parse().map_err(err_msg)?;
475 let mut unstable_options = vec![];
476 if matches.opt_present("skip-children") {
477 unstable_options.push("`--skip-children`");
479 if matches.opt_present("error-on-unformatted") {
480 unstable_options.push("`--error-on-unformatted`");
482 if matches.opt_present("file-lines") {
483 unstable_options.push("`--file-lines`");
485 if !unstable_options.is_empty() {
486 let s = if unstable_options.len() == 1 { "" } else { "s" };
487 return Err(format_err!(
488 "Unstable option{} ({}) used without `--unstable-features`",
490 unstable_options.join(", "),
496 options.config_path = matches.opt_str("config-path").map(PathBuf::from);
498 options.check = matches.opt_present("check");
499 if let Some(ref emit_str) = matches.opt_str("emit") {
501 return Err(format_err!("Invalid to use `--emit` and `--check`"));
503 if let Ok(emit_mode) = emit_mode_from_emit_str(emit_str) {
504 options.emit_mode = emit_mode;
506 return Err(format_err!("Invalid value for `--emit`"));
510 if matches.opt_present("backup") {
511 options.backup = true;
515 if !STABLE_EMIT_MODES.contains(&options.emit_mode) {
516 return Err(format_err!(
517 "Invalid value for `--emit` - using an unstable \
518 value without `--unstable-features`",
523 if let Some(ref color) = matches.opt_str("color") {
524 match Color::from_str(color) {
525 Ok(color) => options.color = Some(color),
526 _ => return Err(format_err!("Invalid color: {}", color)),
533 fn verify_file_lines(&self, files: &[PathBuf]) {
534 for f in self.file_lines.files() {
536 FileName::Real(ref f) if files.contains(f) => {}
537 FileName::Real(_) => {
538 eprintln!("Warning: Extra file listed in file_lines option '{}'", f)
540 FileName::Stdin => eprintln!("Warning: Not a file '{}'", f),
546 impl CliOptions for GetOptsOptions {
547 fn apply_to(self, config: &mut Config) {
549 config.set().verbose(Verbosity::Verbose);
550 } else if self.quiet {
551 config.set().verbose(Verbosity::Quiet);
553 config.set().verbose(Verbosity::Normal);
555 config.set().file_lines(self.file_lines);
556 config.set().unstable_features(self.unstable_features);
557 if let Some(skip_children) = self.skip_children {
558 config.set().skip_children(skip_children);
560 if let Some(error_on_unformatted) = self.error_on_unformatted {
561 config.set().error_on_unformatted(error_on_unformatted);
564 config.set().emit_mode(EmitMode::Diff);
566 config.set().emit_mode(self.emit_mode);
569 config.set().make_backup(true);
571 if let Some(color) = self.color {
572 config.set().color(color);
576 fn config_path(&self) -> Option<&Path> {
577 self.config_path.as_ref().map(|p| &**p)
581 fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode, failure::Error> {
583 "files" => Ok(EmitMode::Files),
584 "stdout" => Ok(EmitMode::Stdout),
585 "coverage" => Ok(EmitMode::Coverage),
586 "checkstyle" => Ok(EmitMode::Checkstyle),
587 _ => Err(format_err!("Invalid value for `--emit`")),