X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=src%2Fbin%2Fmain.rs;h=03105a902208e68a407a8ab0baacd11e83632e35;hb=6d0695303a97edbf94a7ed605247806af4a9e262;hp=b59e47930e0c44ea965e6a43875e015ce1dce7bd;hpb=2fbdedbf2fbf89bb00743e85c3b19cb765d6c1ad;p=rust.git diff --git a/src/bin/main.rs b/src/bin/main.rs index b59e47930e0..03105a90220 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -11,38 +11,65 @@ #![cfg(not(test))] extern crate env_logger; +extern crate failure; extern crate getopts; extern crate rustfmt_nightly as rustfmt; +use std::env; use std::fs::File; -use std::io::{self, Read, Write}; +use std::io::{self, stdout, Read, Write}; use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::{env, error}; + +use failure::err_msg; use getopts::{Matches, Options}; -use rustfmt::config::file_lines::FileLines; -use rustfmt::config::{get_toml_path, Color, Config, WriteMode}; -use rustfmt::{run, FileName, Input, Summary}; +use rustfmt::{ + emit_post_matter, emit_pre_matter, format_and_emit_report, load_config, CliOptions, Config, + FileName, FmtResult, Input, Summary, Verbosity, WriteMode, +}; -type FmtError = Box; -type FmtResult = std::result::Result; +fn main() { + env_logger::init(); + let opts = make_opts(); + + let exit_code = match execute(&opts) { + Ok((write_mode, summary)) => { + if summary.has_operational_errors() + || summary.has_parsing_errors() + || (summary.has_diff && write_mode == WriteMode::Check) + { + 1 + } else { + 0 + } + } + Err(e) => { + eprintln!("{}", e.to_string()); + 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, - config_path: Option, minimal_config_path: Option, }, /// Print the help message. - Help, + Help(HelpOp), // Print version information Version, - /// Print detailed configuration help. - ConfigHelp, /// Output default config to a file, or stdout if None ConfigOutputDefault { path: Option, @@ -50,110 +77,33 @@ enum Operation { /// No file specified, read from stdin Stdin { input: String, - config_path: Option, }, } -/// Parsed command line options. -#[derive(Clone, Debug, Default)] -struct CliOptions { - skip_children: bool, - verbose: bool, - write_mode: Option, - color: Option, - file_lines: FileLines, // Default is all lines in all files. - unstable_features: bool, - error_on_unformatted: bool, -} - -impl CliOptions { - fn from_matches(matches: &Matches) -> FmtResult { - let mut options = CliOptions::default(); - options.skip_children = matches.opt_present("skip-children"); - options.verbose = matches.opt_present("verbose"); - let unstable_features = matches.opt_present("unstable-features"); - let rust_nightly = option_env!("CFG_RELEASE_CHANNEL") - .map(|c| c == "nightly") - .unwrap_or(false); - if unstable_features && !rust_nightly { - return Err(FmtError::from( - "Unstable features are only available on Nightly channel", - )); - } else { - options.unstable_features = unstable_features; - } - - if let Some(ref write_mode) = matches.opt_str("write-mode") { - if let Ok(write_mode) = WriteMode::from_str(write_mode) { - options.write_mode = Some(write_mode); - } else { - return Err(FmtError::from(format!( - "Invalid write-mode: {}", - write_mode - ))); - } - } - - if let Some(ref color) = matches.opt_str("color") { - match Color::from_str(color) { - Ok(color) => options.color = Some(color), - _ => return Err(FmtError::from(format!("Invalid color: {}", color))), - } - } - - if let Some(ref file_lines) = matches.opt_str("file-lines") { - options.file_lines = file_lines.parse()?; - } - - if matches.opt_present("error-on-unformatted") { - options.error_on_unformatted = true; - } - - Ok(options) - } - - fn apply_to(self, config: &mut Config) { - config.set().skip_children(self.skip_children); - config.set().verbose(self.verbose); - config.set().file_lines(self.file_lines); - config.set().unstable_features(self.unstable_features); - config.set().error_on_unformatted(self.error_on_unformatted); - if let Some(write_mode) = self.write_mode { - config.set().write_mode(write_mode); - } - if let Some(color) = self.color { - config.set().color(color); - } - } -} - -/// read the given config file path recursively if present else read the project file path -fn match_cli_path_or_file( - config_path: Option, - input_file: &Path, -) -> FmtResult<(Config, Option)> { - if let Some(config_file) = config_path { - let toml = Config::from_toml_path(config_file.as_ref())?; - return Ok((toml, Some(config_file))); - } - Config::from_resolved_toml_path(input_file).map_err(FmtError::from) +/// Arguments to `--help` +enum HelpOp { + None, + Config, + FileLines, } fn make_opts() -> Options { let mut opts = Options::new(); // Sorted in alphabetical order. + opts.optflag("", "backup", "Backup any modified files."); + opts.optflag( + "", + "check", + "Run in 'check' mode. Exits with 0 if input if formatted correctly. Exits \ + with 1 and prints a diff if formatting is required.", + ); opts.optopt( "", "color", "Use colored output (if supported)", "[always|never|auto]", ); - opts.optflag( - "", - "config-help", - "Show details of rustfmt configuration options", - ); opts.optopt( "", "config-path", @@ -161,212 +111,194 @@ fn make_opts() -> Options { found reverts to the input file path", "[Path for the configuration file]", ); - opts.optopt( - "", - "dump-default-config", - "Dumps default configuration to PATH. PATH defaults to stdout, if omitted.", - "PATH", - ); - opts.optopt( - "", - "dump-minimal-config", - "Dumps configuration options that were checked during formatting to a file.", - "PATH", - ); - opts.optflag( - "", - "error-on-unformatted", - "Error if unable to get comments or string literals within max_width, \ - or they are left with trailing whitespaces", + let is_nightly = is_nightly(); + let emit_opts = if is_nightly { + "[files|stdout|coverage|checkstyle]" + } else { + "[files|stdout]" + }; + opts.optopt("", "emit", "What data to emit and how", emit_opts); + opts.optflagopt( + "h", + "help", + "Show this message or help about a specific topic: `config` or `file-lines`", + "=TOPIC", ); opts.optopt( "", - "file-lines", - "Format specified line ranges. See README for more detail on the JSON format.", - "JSON", - ); - opts.optflag("h", "help", "Show this message"); - opts.optflag("", "skip-children", "Don't reformat child modules"); - opts.optflag( - "", - "unstable-features", - "Enables unstable features. Only available on nightly channel", + "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.", + "[minimal|default] PATH", ); opts.optflag("v", "verbose", "Print verbose output"); + opts.optflag("q", "quiet", "Print less output"); opts.optflag("V", "version", "Show version information"); - opts.optopt( - "", - "write-mode", - "How to write output (not usable when piping from stdin)", - "[replace|overwrite|display|plain|diff|coverage|checkstyle]", - ); + + if is_nightly { + opts.optflag( + "", + "unstable-features", + "Enables unstable features. Only available on nightly channel.", + ); + 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.optopt( + "", + "file-lines", + "Format specified line ranges. Run with `--help file-lines` for \ + more detail (unstable).", + "JSON", + ); + opts.optflag( + "", + "skip-children", + "Don't reformat child modules (unstable).", + ); + } opts } -fn execute(opts: &Options) -> FmtResult { +fn is_nightly() -> bool { + option_env!("CFG_RELEASE_CHANNEL") + .map(|c| c == "nightly") + .unwrap_or(false) +} + +fn execute(opts: &Options) -> FmtResult<(WriteMode, Summary)> { let matches = opts.parse(env::args().skip(1))?; + let options = CliOptions::from_matches(&matches)?; match determine_operation(&matches)? { - Operation::Help => { + Operation::Help(HelpOp::None) => { print_usage_to_stdout(opts, ""); Summary::print_exit_codes(); - Ok(Summary::default()) + Ok((WriteMode::None, Summary::default())) + } + Operation::Help(HelpOp::Config) => { + Config::print_docs(&mut stdout(), options.unstable_features); + Ok((WriteMode::None, Summary::default())) + } + Operation::Help(HelpOp::FileLines) => { + print_help_file_lines(); + Ok((WriteMode::None, Summary::default())) } Operation::Version => { print_version(); - Ok(Summary::default()) - } - Operation::ConfigHelp => { - Config::print_docs(); - Ok(Summary::default()) + Ok((WriteMode::None, Summary::default())) } Operation::ConfigOutputDefault { path } => { - let toml = Config::default().all_options().to_toml()?; + let toml = Config::default().all_options().to_toml().map_err(err_msg)?; 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(Summary::default()) + Ok((WriteMode::None, Summary::default())) } - Operation::Stdin { input, config_path } => { + Operation::Stdin { input } => { // try to read config from local directory - let (mut config, _) = - match_cli_path_or_file(config_path, &env::current_dir().unwrap())?; + let (mut config, _) = load_config(Some(Path::new(".")), Some(&options))?; - // write_mode is always Plain for Stdin. - config.set().write_mode(WriteMode::Plain); + // write_mode is always Display for Stdin. + config.set().write_mode(WriteMode::Display); + config.set().verbose(Verbosity::Quiet); // parse file_lines - if let Some(ref file_lines) = matches.opt_str("file-lines") { - config.set().file_lines(file_lines.parse()?); - for f in config.file_lines().files() { - match *f { - FileName::Custom(ref f) if f == "stdin" => {} - _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f), - } + config.set().file_lines(options.file_lines); + for f in config.file_lines().files() { + match *f { + FileName::Custom(ref f) if f == "stdin" => {} + _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f), } } let mut error_summary = Summary::default(); - if config.version_meets_requirement(&mut error_summary) { - error_summary.add(run(Input::Text(input), &config)); + emit_pre_matter(&config)?; + match format_and_emit_report(Input::Text(input), &config) { + Ok(summary) => error_summary.add(summary), + Err(_) => error_summary.add_operational_error(), } + emit_post_matter(&config)?; - Ok(error_summary) + Ok((WriteMode::Display, error_summary)) } Operation::Format { files, - config_path, minimal_config_path, - } => { - let options = CliOptions::from_matches(&matches)?; + } => format(files, minimal_config_path, options), + } +} - for f in options.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) - } - _ => eprintln!("Warning: Not a file '{}'", f), - } - } +fn format( + files: Vec, + minimal_config_path: Option, + options: CliOptions, +) -> FmtResult<(WriteMode, Summary)> { + options.verify_file_lines(&files); + let (config, config_path) = load_config(None, Some(&options))?; + + if config.verbose() == Verbosity::Verbose { + if let Some(path) = config_path.as_ref() { + println!("Using rustfmt config file {}", path.display()); + } + } - let mut config = Config::default(); - // Load the config path file if provided - if let Some(config_file) = config_path.as_ref() { - config = Config::from_toml_path(config_file.as_ref())?; - }; + emit_pre_matter(&config)?; + let mut error_summary = Summary::default(); - if options.verbose { - if let Some(path) = config_path.as_ref() { - println!("Using rustfmt config file {}", path.display()); + for file in files { + if !file.exists() { + eprintln!("Error: file `{}` does not exist", file.to_str().unwrap()); + error_summary.add_operational_error(); + } else if file.is_dir() { + eprintln!("Error: `{}` is a directory", file.to_str().unwrap()); + error_summary.add_operational_error(); + } else { + // Check the file directory if the config-path could not be read or not provided + let local_config = if config_path.is_none() { + let (local_config, config_path) = + load_config(Some(file.parent().unwrap()), Some(&options))?; + if local_config.verbose() == Verbosity::Verbose { + if let Some(path) = config_path { + println!( + "Using rustfmt config file {} for {}", + path.display(), + file.display() + ); + } } - } + local_config + } else { + config.clone() + }; - let mut error_summary = Summary::default(); - for file in files { - if !file.exists() { - eprintln!("Error: file `{}` does not exist", file.to_str().unwrap()); - error_summary.add_operational_error(); - } else if file.is_dir() { - eprintln!("Error: `{}` is a directory", file.to_str().unwrap()); + match format_and_emit_report(Input::File(file), &local_config) { + Ok(summary) => error_summary.add(summary), + Err(_) => { error_summary.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 (config_tmp, path_tmp) = - Config::from_resolved_toml_path(file.parent().unwrap())?; - if options.verbose { - if let Some(path) = path_tmp.as_ref() { - println!( - "Using rustfmt config file {} for {}", - path.display(), - file.display() - ); - } - } - config = config_tmp; - } - - if !config.version_meets_requirement(&mut error_summary) { - break; - } - - options.clone().apply_to(&mut config); - error_summary.add(run(Input::File(file), &config)); + break; } } - - // 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 = config.used_options().to_toml()?; - file.write_all(toml.as_bytes())?; - } - - Ok(error_summary) } } -} - -fn main() { - env_logger::init(); - - let opts = make_opts(); - - let exit_code = match execute(&opts) { - Ok(summary) => { - if summary.has_operational_errors() { - 1 - } else if summary.has_parsing_errors() { - 2 - } else if summary.has_formatting_errors() { - 3 - } else if summary.has_diff { - // should only happen in diff mode - 4 - } else { - assert!(summary.has_no_errors()); - 0 - } - } - Err(e) => { - eprintln!("{}", e.to_string()); - 1 - } - }; - // Make sure standard output is flushed before we exit. - std::io::stdout().flush().unwrap(); + emit_post_matter(&config)?; + + // 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 = config.used_options().to_toml().map_err(err_msg)?; + file.write_all(toml.as_bytes())?; + } - // 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); + Ok((config.write_mode(), error_summary)) } fn print_usage_to_stdout(opts: &Options, reason: &str) { @@ -383,6 +315,29 @@ fn print_usage_to_stdout(opts: &Options, reason: &str) { 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!( "{}-{}", @@ -395,26 +350,29 @@ fn print_version() { fn determine_operation(matches: &Matches) -> FmtResult { if matches.opt_present("h") { - return Ok(Operation::Help); - } - - if matches.opt_present("config-help") { - return Ok(Operation::ConfigHelp); + 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()) { + return Ok(Operation::Help(HelpOp::FileLines)); + } else { + println!("Unknown help topic: `{}`\n", topic.unwrap()); + return Ok(Operation::Help(HelpOp::None)); + } } - if matches.opt_present("dump-default-config") { - // NOTE for some reason when configured with HasArg::Maybe + Occur::Optional opt_default - // doesn't recognize `--foo bar` as a long flag with an argument but as a long flag with no - // argument *plus* a free argument. Thus we check for that case in this branch -- this is - // required for backward compatibility. - if let Some(path) = matches.free.get(0) { - return Ok(Operation::ConfigOutputDefault { - path: Some(path.clone()), - }); - } else { - return Ok(Operation::ConfigOutputDefault { - path: matches.opt_str("dump-default-config"), - }); + let mut minimal_config_path = None; + if let Some(ref kind) = matches.opt_str("print-config") { + let path = matches.free.get(0).cloned(); + if kind == "default" { + return Ok(Operation::ConfigOutputDefault { path }); + } else if kind == "minimal" { + minimal_config_path = path; + if minimal_config_path.is_none() { + println!("WARNING: PATH required for `--print-config minimal`"); + } } } @@ -422,40 +380,12 @@ fn determine_operation(matches: &Matches) -> FmtResult { return Ok(Operation::Version); } - let config_path_not_found = |path: &str| -> FmtResult { - Err(FmtError::from(format!( - "Error: unable to find a config file for the given path: `{}`", - path - ))) - }; - - // Read the config_path and convert to parent dir if a file is provided. - // If a config file cannot be found from the given path, return error. - let config_path: Option = match matches.opt_str("config-path").map(PathBuf::from) { - Some(ref path) if !path.exists() => return config_path_not_found(path.to_str().unwrap()), - Some(ref path) if path.is_dir() => { - let config_file_path = get_toml_path(path)?; - if config_file_path.is_some() { - config_file_path - } else { - return config_path_not_found(path.to_str().unwrap()); - } - } - path => path, - }; - - // If no path is given, we won't output a minimal config. - let minimal_config_path = matches.opt_str("dump-minimal-config"); - // if no file argument is supplied, read from stdin if matches.free.is_empty() { let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer)?; - return Ok(Operation::Stdin { - input: buffer, - config_path, - }); + return Ok(Operation::Stdin { input: buffer }); } let files: Vec<_> = matches @@ -471,7 +401,6 @@ fn determine_operation(matches: &Matches) -> FmtResult { Ok(Operation::Format { files, - config_path, minimal_config_path, }) }