// Copyright 2015 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. #![cfg(not(test))] extern crate env_logger; extern crate getopts; extern crate rustfmt_nightly as rustfmt; use std::{env, error}; use std::fs::File; use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use std::str::FromStr; use getopts::{Matches, Options}; use rustfmt::{run, Input, Summary}; use rustfmt::file_lines::FileLines; use rustfmt::config::{get_toml_path, Config, WriteMode}; type FmtError = Box; type FmtResult = std::result::Result; /// 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, // Print version information Version, /// Print detailed configuration help. ConfigHelp, /// Output default config to a file ConfigOutputDefault { path: String }, /// 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, file_lines: FileLines, // Default is all lines in all files. } 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"); 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 file_lines) = matches.opt_str("file-lines") { options.file_lines = file_lines.parse()?; } 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); if let Some(write_mode) = self.write_mode { config.set().write_mode(write_mode); } } } /// 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(|e| FmtError::from(e)) } fn make_opts() -> Options { let mut opts = Options::new(); opts.optflag("h", "help", "show this message"); opts.optflag("V", "version", "show version information"); opts.optflag("v", "verbose", "print verbose output"); opts.optopt( "", "write-mode", "how to write output (not usable when piping from stdin)", "[replace|overwrite|display|plain|diff|coverage|checkstyle]", ); opts.optflag("", "skip-children", "don't reformat child modules"); opts.optflag( "", "config-help", "show details of rustfmt configuration options", ); opts.optopt( "", "dump-default-config", "Dumps the default configuration to a file and exits.", "PATH", ); opts.optopt( "", "dump-minimal-config", "Dumps configuration options that were checked during formatting to a file.", "PATH", ); 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( "", "file-lines", "Format specified line ranges. See README for more detail on the JSON format.", "JSON", ); opts } fn execute(opts: &Options) -> FmtResult { let matches = opts.parse(env::args().skip(1))?; match determine_operation(&matches)? { Operation::Help => { print_usage(opts, ""); Summary::print_exit_codes(); Ok(Summary::new()) } Operation::Version => { print_version(); Ok(Summary::new()) } Operation::ConfigHelp => { Config::print_docs(); Ok(Summary::new()) } Operation::ConfigOutputDefault { path } => { let mut file = File::create(path)?; let toml = Config::default().all_options().to_toml()?; file.write_all(toml.as_bytes())?; Ok(Summary::new()) } Operation::Stdin { input, config_path } => { // try to read config from local directory let (mut config, _) = match_cli_path_or_file(config_path, &env::current_dir().unwrap())?; // write_mode is always Plain for Stdin. config.set().write_mode(WriteMode::Plain); // 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() { if f != "stdin" { println!("Warning: Extra file listed in file_lines option '{}'", f); } } } Ok(run(Input::Text(input), &config)) } Operation::Format { files, config_path, minimal_config_path, } => { let options = CliOptions::from_matches(&matches)?; for f in options.file_lines.files() { if !files.contains(&PathBuf::from(f)) { println!("Warning: Extra file listed in file_lines option '{}'", f); } } 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())?; }; if options.verbose { if let Some(path) = config_path.as_ref() { println!("Using rustfmt config file {}", path.display()); } } let mut error_summary = Summary::new(); for file in files { if !file.exists() { println!("Error: file `{}` does not exist", file.to_str().unwrap()); error_summary.add_operational_error(); } else if file.is_dir() { println!("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 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; } options.clone().apply_to(&mut config); error_summary.add(run(Input::File(file), &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()?; file.write_all(toml.as_bytes())?; } Ok(error_summary) } } } fn main() { let _ = 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) => { print_usage(&opts, &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); } fn print_usage(opts: &Options, reason: &str) { let reason = format!( "{}\n\nusage: {} [options] ...", reason, env::args_os().next().unwrap().to_string_lossy() ); println!("{}", opts.usage(&reason)); } fn print_version() { println!( "{}-nightly{}", env!("CARGO_PKG_VERSION"), include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")) ) } 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); } if let Some(path) = matches.opt_str("dump-default-config") { return Ok(Operation::ConfigOutputDefault { path }); } if matches.opt_present("version") { 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: config_path, }); } let files: Vec<_> = matches .free .iter() .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(); Ok(Operation::Format { files: files, config_path: config_path, minimal_config_path: minimal_config_path, }) }