use anyhow::{format_err, Result}; use io::Error as IoError; use thiserror::Error; use rustfmt_nightly as rustfmt; use std::collections::HashMap; use std::env; use std::fs::File; use std::io::{self, stdout, Read, Write}; use std::path::{Path, PathBuf}; use std::str::FromStr; use getopts::{Matches, Options}; use crate::rustfmt::{ load_config, CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName, FormatReportFormatterBuilder, Input, Session, Verbosity, }; fn main() { env_logger::Builder::from_env("RUSTFMT_LOG").init(); let opts = make_opts(); let exit_code = match execute(&opts) { Ok(code) => code, Err(e) => { eprintln!("{:#}", e); 1 } }; // Make sure standard output is flushed before we exit. std::io::stdout().flush().unwrap(); // Exit with given exit code. // // NOTE: this immediately terminates the process without doing any cleanup, // so make sure to finish all necessary cleanup before this is called. std::process::exit(exit_code); } /// Rustfmt operations. enum Operation { /// Format files and their child modules. Format { files: Vec, minimal_config_path: Option, }, /// Print the help message. Help(HelpOp), /// Print version information Version, /// Output default config to a file, or stdout if None ConfigOutputDefault { path: Option }, /// Output current config (as if formatting to a file) to stdout ConfigOutputCurrent { path: Option }, /// No file specified, read from stdin Stdin { input: String }, } /// Rustfmt operations errors. #[derive(Error, Debug)] pub enum OperationError { /// An unknown help topic was requested. #[error("Unknown help topic: `{0}`.")] UnknownHelpTopic(String), /// An unknown print-config option was requested. #[error("Unknown print-config option: `{0}`.")] UnknownPrintConfigTopic(String), /// Attempt to generate a minimal config from standard input. #[error("The `--print-config=minimal` option doesn't work with standard input.")] MinimalPathWithStdin, /// An io error during reading or writing. #[error("{0}")] IoError(IoError), /// Attempt to use --emit with a mode which is not currently /// supported with stdandard input. #[error("Emit mode {0} not supported with standard output.")] StdinBadEmit(EmitMode), } impl From for OperationError { fn from(e: IoError) -> OperationError { OperationError::IoError(e) } } /// Arguments to `--help` enum HelpOp { None, Config, FileLines, } fn make_opts() -> Options { let mut opts = Options::new(); opts.optflag( "", "check", "Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits \ with 1 and prints a diff if formatting is required.", ); let is_nightly = is_nightly(); let emit_opts = if is_nightly { "[files|stdout|coverage|checkstyle|json]" } else { "[files|stdout]" }; opts.optopt("", "emit", "What data to emit and how", emit_opts); opts.optflag("", "backup", "Backup any modified files."); opts.optopt( "", "config-path", "Recursively searches the given path for the rustfmt.toml config file. If not \ found reverts to the input file path", "[Path for the configuration file]", ); opts.optopt("", "edition", "Rust edition to use", "[2015|2018|2021]"); opts.optopt( "", "color", "Use colored output (if supported)", "[always|never|auto]", ); opts.optopt( "", "print-config", "Dumps a default or minimal config to PATH. A minimal config is the \ subset of the current config file used for formatting the current program. \ `current` writes to stdout current config as if formatting the file at PATH.", "[default|minimal|current] PATH", ); opts.optflag( "l", "files-with-diff", "Prints the names of mismatched files that were formatted. Prints the names of \ files that would be formated when used with `--check` mode. ", ); opts.optmulti( "", "config", "Set options from command line. These settings take priority over .rustfmt.toml", "[key1=val1,key2=val2...]", ); if is_nightly { opts.optflag( "", "unstable-features", "Enables unstable features. Only available on nightly channel.", ); opts.optopt( "", "file-lines", "Format specified line ranges. Run with `--help=file-lines` for \ more detail (unstable).", "JSON", ); opts.optflag( "", "error-on-unformatted", "Error if unable to get comments or string literals within max_width, \ or they are left with trailing whitespaces (unstable).", ); opts.optflag( "", "skip-children", "Don't reformat child modules (unstable).", ); } opts.optflag("v", "verbose", "Print verbose output"); opts.optflag("q", "quiet", "Print less output"); opts.optflag("V", "version", "Show version information"); let help_topics = if is_nightly { "`config` or `file-lines`" } else { "`config`" }; let mut help_topic_msg = "Show this message or help about a specific topic: ".to_owned(); help_topic_msg.push_str(help_topics); opts.optflagopt("h", "help", &help_topic_msg, "=TOPIC"); opts } fn is_nightly() -> bool { option_env!("CFG_RELEASE_CHANNEL").map_or(true, |c| c == "nightly" || c == "dev") } // Returned i32 is an exit code fn execute(opts: &Options) -> Result { let matches = opts.parse(env::args().skip(1))?; let options = GetOptsOptions::from_matches(&matches)?; match determine_operation(&matches)? { Operation::Help(HelpOp::None) => { print_usage_to_stdout(opts, ""); Ok(0) } Operation::Help(HelpOp::Config) => { Config::print_docs(&mut stdout(), options.unstable_features); Ok(0) } Operation::Help(HelpOp::FileLines) => { print_help_file_lines(); Ok(0) } Operation::Version => { print_version(); Ok(0) } Operation::ConfigOutputDefault { path } => { let toml = Config::default().all_options().to_toml()?; if let Some(path) = path { let mut file = File::create(path)?; file.write_all(toml.as_bytes())?; } else { io::stdout().write_all(toml.as_bytes())?; } Ok(0) } Operation::ConfigOutputCurrent { path } => { let path = match path { Some(path) => path, None => return Err(format_err!("PATH required for `--print-config current`")), }; let file = PathBuf::from(path); let file = file.canonicalize().unwrap_or(file); let (config, _) = load_config(Some(file.parent().unwrap()), Some(options))?; let toml = config.all_options().to_toml()?; io::stdout().write_all(toml.as_bytes())?; Ok(0) } Operation::Stdin { input } => format_string(input, options), Operation::Format { files, minimal_config_path, } => format(files, minimal_config_path, &options), } } fn format_string(input: String, options: GetOptsOptions) -> Result { // try to read config from local directory let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?; if options.check { config.set().emit_mode(EmitMode::Diff); } else { match options.emit_mode { // Emit modes which work with standard input // None means default, which is Stdout. None | Some(EmitMode::Stdout) | Some(EmitMode::Checkstyle) | Some(EmitMode::Json) => {} Some(emit_mode) => { return Err(OperationError::StdinBadEmit(emit_mode).into()); } } config .set() .emit_mode(options.emit_mode.unwrap_or(EmitMode::Stdout)); } config.set().verbose(Verbosity::Quiet); // parse file_lines config.set().file_lines(options.file_lines); for f in config.file_lines().files() { match *f { FileName::Stdin => {} _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f), } } let out = &mut stdout(); let mut session = Session::new(config, Some(out)); format_and_emit_report(&mut session, Input::Text(input)); let exit_code = if session.has_operational_errors() || session.has_parsing_errors() { 1 } else { 0 }; Ok(exit_code) } fn format( files: Vec, minimal_config_path: Option, options: &GetOptsOptions, ) -> Result { options.verify_file_lines(&files); let (config, config_path) = load_config(None, Some(options.clone()))?; if config.verbose() == Verbosity::Verbose { if let Some(path) = config_path.as_ref() { println!("Using rustfmt config file {}", path.display()); } } let out = &mut stdout(); let mut session = Session::new(config, Some(out)); for file in files { if !file.exists() { eprintln!("Error: file `{}` does not exist", file.to_str().unwrap()); session.add_operational_error(); } else if file.is_dir() { eprintln!("Error: `{}` is a directory", file.to_str().unwrap()); session.add_operational_error(); } else { // Check the file directory if the config-path could not be read or not provided if config_path.is_none() { let (local_config, config_path) = load_config(Some(file.parent().unwrap()), Some(options.clone()))?; if local_config.verbose() == Verbosity::Verbose { if let Some(path) = config_path { println!( "Using rustfmt config file {} for {}", path.display(), file.display() ); } } session.override_config(local_config, |sess| { format_and_emit_report(sess, Input::File(file)) }); } else { format_and_emit_report(&mut session, Input::File(file)); } } } // If we were given a path via dump-minimal-config, output any options // that were used during formatting as TOML. if let Some(path) = minimal_config_path { let mut file = File::create(path)?; let toml = session.config.used_options().to_toml()?; file.write_all(toml.as_bytes())?; } let exit_code = if session.has_operational_errors() || session.has_parsing_errors() || ((session.has_diff() || session.has_check_errors()) && options.check) { 1 } else { 0 }; Ok(exit_code) } fn format_and_emit_report(session: &mut Session<'_, T>, input: Input) { match session.format(input) { Ok(report) => { if report.has_warnings() { eprintln!( "{}", FormatReportFormatterBuilder::new(&report) .enable_colors(should_print_with_colors(session)) .build() ); } } Err(msg) => { eprintln!("Error writing files: {}", msg); session.add_operational_error(); } } } fn should_print_with_colors(session: &mut Session<'_, T>) -> bool { match term::stderr() { Some(ref t) if session.config.color().use_colored_tty() && t.supports_color() && t.supports_attr(term::Attr::Bold) => { true } _ => false, } } fn print_usage_to_stdout(opts: &Options, reason: &str) { let sep = if reason.is_empty() { String::new() } else { format!("{}\n\n", reason) }; let msg = format!( "{}Format Rust code\n\nusage: rustfmt [options] ...", sep ); println!("{}", opts.usage(&msg)); } fn print_help_file_lines() { println!( "If you want to restrict reformatting to specific sets of lines, you can use the `--file-lines` option. Its argument is a JSON array of objects with `file` and `range` properties, where `file` is a file name, and `range` is an array representing a range of lines like `[7,13]`. Ranges are 1-based and inclusive of both end points. Specifying an empty array will result in no files being formatted. For example, ``` rustfmt --file-lines '[ {{\"file\":\"src/lib.rs\",\"range\":[7,13]}}, {{\"file\":\"src/lib.rs\",\"range\":[21,29]}}, {{\"file\":\"src/foo.rs\",\"range\":[10,11]}}, {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]' ``` would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`, and `15` of `src/foo.rs`. No other files would be formatted, even if they are included as out of line modules from `src/lib.rs`." ); } fn print_version() { let version_info = format!( "{}-{}", option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"), include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")) ); println!("rustfmt {}", version_info); } fn determine_operation(matches: &Matches) -> Result { if matches.opt_present("h") { let topic = matches.opt_str("h"); if topic == None { return Ok(Operation::Help(HelpOp::None)); } else if topic == Some("config".to_owned()) { return Ok(Operation::Help(HelpOp::Config)); } else if topic == Some("file-lines".to_owned()) && is_nightly() { return Ok(Operation::Help(HelpOp::FileLines)); } else { return Err(OperationError::UnknownHelpTopic(topic.unwrap())); } } let mut free_matches = matches.free.iter(); let mut minimal_config_path = None; if let Some(kind) = matches.opt_str("print-config") { let path = free_matches.next().cloned(); match kind.as_str() { "default" => return Ok(Operation::ConfigOutputDefault { path }), "current" => return Ok(Operation::ConfigOutputCurrent { path }), "minimal" => { minimal_config_path = path; if minimal_config_path.is_none() { eprintln!("WARNING: PATH required for `--print-config minimal`."); } } _ => { return Err(OperationError::UnknownPrintConfigTopic(kind)); } } } if matches.opt_present("version") { return Ok(Operation::Version); } let files: Vec<_> = free_matches .map(|s| { let p = PathBuf::from(s); // we will do comparison later, so here tries to canonicalize first // to get the expected behavior. p.canonicalize().unwrap_or(p) }) .collect(); // if no file argument is supplied, read from stdin if files.is_empty() { if minimal_config_path.is_some() { return Err(OperationError::MinimalPathWithStdin); } let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer)?; return Ok(Operation::Stdin { input: buffer }); } Ok(Operation::Format { files, minimal_config_path, }) } const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff]; /// Parsed command line options. #[derive(Clone, Debug, Default)] struct GetOptsOptions { skip_children: Option, quiet: bool, verbose: bool, config_path: Option, inline_config: HashMap, emit_mode: Option, backup: bool, check: bool, edition: Option, color: Option, file_lines: FileLines, // Default is all lines in all files. unstable_features: bool, error_on_unformatted: Option, print_misformatted_file_names: bool, } impl GetOptsOptions { pub fn from_matches(matches: &Matches) -> Result { let mut options = GetOptsOptions::default(); options.verbose = matches.opt_present("verbose"); options.quiet = matches.opt_present("quiet"); if options.verbose && options.quiet { return Err(format_err!("Can't use both `--verbose` and `--quiet`")); } let rust_nightly = is_nightly(); if rust_nightly { options.unstable_features = matches.opt_present("unstable-features"); if options.unstable_features { if matches.opt_present("skip-children") { options.skip_children = Some(true); } if matches.opt_present("error-on-unformatted") { options.error_on_unformatted = Some(true); } if let Some(ref file_lines) = matches.opt_str("file-lines") { options.file_lines = file_lines.parse()?; } } else { let mut unstable_options = vec![]; if matches.opt_present("skip-children") { unstable_options.push("`--skip-children`"); } if matches.opt_present("error-on-unformatted") { unstable_options.push("`--error-on-unformatted`"); } if matches.opt_present("file-lines") { unstable_options.push("`--file-lines`"); } if !unstable_options.is_empty() { let s = if unstable_options.len() == 1 { "" } else { "s" }; return Err(format_err!( "Unstable option{} ({}) used without `--unstable-features`", s, unstable_options.join(", "), )); } } } options.config_path = matches.opt_str("config-path").map(PathBuf::from); options.inline_config = matches .opt_strs("config") .iter() .flat_map(|config| config.split(',')) .map( |key_val| match key_val.char_indices().find(|(_, ch)| *ch == '=') { Some((middle, _)) => { let (key, val) = (&key_val[..middle], &key_val[middle + 1..]); if !Config::is_valid_key_val(key, val) { Err(format_err!("invalid key=val pair: `{}`", key_val)) } else { Ok((key.to_string(), val.to_string())) } } None => Err(format_err!( "--config expects comma-separated list of key=val pairs, found `{}`", key_val )), }, ) .collect::, _>>()?; options.check = matches.opt_present("check"); if let Some(ref emit_str) = matches.opt_str("emit") { if options.check { return Err(format_err!("Invalid to use `--emit` and `--check`")); } options.emit_mode = Some(emit_mode_from_emit_str(emit_str)?); } if let Some(ref edition_str) = matches.opt_str("edition") { options.edition = Some(edition_from_edition_str(edition_str)?); } if matches.opt_present("backup") { options.backup = true; } if matches.opt_present("files-with-diff") { options.print_misformatted_file_names = true; } if !rust_nightly { if let Some(ref emit_mode) = options.emit_mode { if !STABLE_EMIT_MODES.contains(emit_mode) { return Err(format_err!( "Invalid value for `--emit` - using an unstable \ value without `--unstable-features`", )); } } } if let Some(ref color) = matches.opt_str("color") { match Color::from_str(color) { Ok(color) => options.color = Some(color), _ => return Err(format_err!("Invalid color: {}", color)), } } Ok(options) } fn verify_file_lines(&self, files: &[PathBuf]) { for f in self.file_lines.files() { match *f { FileName::Real(ref f) if files.contains(f) => {} FileName::Real(_) => { eprintln!("Warning: Extra file listed in file_lines option '{}'", f) } FileName::Stdin => eprintln!("Warning: Not a file '{}'", f), } } } } impl CliOptions for GetOptsOptions { fn apply_to(self, config: &mut Config) { if self.verbose { config.set().verbose(Verbosity::Verbose); } else if self.quiet { config.set().verbose(Verbosity::Quiet); } else { config.set().verbose(Verbosity::Normal); } config.set().file_lines(self.file_lines); config.set().unstable_features(self.unstable_features); if let Some(skip_children) = self.skip_children { config.set().skip_children(skip_children); } if let Some(error_on_unformatted) = self.error_on_unformatted { config.set().error_on_unformatted(error_on_unformatted); } if let Some(edition) = self.edition { config.set().edition(edition); } if self.check { config.set().emit_mode(EmitMode::Diff); } else if let Some(emit_mode) = self.emit_mode { config.set().emit_mode(emit_mode); } if self.backup { config.set().make_backup(true); } if let Some(color) = self.color { config.set().color(color); } if self.print_misformatted_file_names { config.set().print_misformatted_file_names(true); } for (key, val) in self.inline_config { config.override_value(&key, &val); } } fn config_path(&self) -> Option<&Path> { self.config_path.as_deref() } } fn edition_from_edition_str(edition_str: &str) -> Result { match edition_str { "2015" => Ok(Edition::Edition2015), "2018" => Ok(Edition::Edition2018), "2021" => Ok(Edition::Edition2021), "2024" => Ok(Edition::Edition2024), _ => Err(format_err!("Invalid value for `--edition`")), } } fn emit_mode_from_emit_str(emit_str: &str) -> Result { match emit_str { "files" => Ok(EmitMode::Files), "stdout" => Ok(EmitMode::Stdout), "coverage" => Ok(EmitMode::Coverage), "checkstyle" => Ok(EmitMode::Checkstyle), "json" => Ok(EmitMode::Json), _ => Err(format_err!("Invalid value for `--emit`")), } }