#![cfg(not(test))]
-#[macro_use]
+
extern crate log;
extern crate rustfmt;
extern crate toml;
extern crate env_logger;
extern crate getopts;
-use rustfmt::{run, run_from_stdin};
+use rustfmt::{run, Input, Summary};
+use rustfmt::file_lines::FileLines;
use rustfmt::config::{Config, WriteMode};
-use std::env;
+use std::{env, error};
use std::fs::{self, File};
use std::io::{self, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
+use std::str::FromStr;
use getopts::{Matches, Options};
+// Include git commit hash and worktree status; contents are like
+// const COMMIT_HASH: Option<&'static str> = Some("c31a366");
+// const WORKTREE_CLEAN: Option<bool> = Some(false);
+// with `None` if running git failed, eg if it is not installed.
+include!(concat!(env!("OUT_DIR"), "/git_info.rs"));
+
+type FmtError = Box<error::Error + Send + Sync>;
+type FmtResult<T> = std::result::Result<T, FmtError>;
+
/// Rustfmt operations.
enum Operation {
/// Format files and their child modules.
- Format(Vec<PathBuf>, WriteMode),
+ Format {
+ files: Vec<PathBuf>,
+ config_path: Option<PathBuf>,
+ minimal_config_path: Option<String>,
+ },
/// Print the help message.
Help,
// Print version information
Version,
/// Print detailed configuration help.
ConfigHelp,
- /// Invalid program input, including reason.
- InvalidInput(String),
+ /// Output default config to a file
+ ConfigOutputDefault { path: String },
/// No file specified, read from stdin
- Stdin(String, WriteMode),
+ Stdin {
+ input: String,
+ config_path: Option<PathBuf>,
+ },
+}
+
+/// Parsed command line options.
+#[derive(Clone, Debug, Default)]
+struct CliOptions {
+ skip_children: bool,
+ verbose: bool,
+ write_mode: Option<WriteMode>,
+ file_lines: FileLines, // Default is all lines in all files.
}
+impl CliOptions {
+ fn from_matches(matches: &Matches) -> FmtResult<CliOptions> {
+ 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);
+ }
+ }
+}
+
+const CONFIG_FILE_NAMES: [&'static str; 2] = [".rustfmt.toml", "rustfmt.toml"];
+
/// Try to find a project file in the given directory and its parents. Returns the path of a the
/// nearest project file if one exists, or `None` if no project file was found.
-fn lookup_project_file(dir: &Path) -> io::Result<Option<PathBuf>> {
+fn lookup_project_file(dir: &Path) -> FmtResult<Option<PathBuf>> {
let mut current = if dir.is_relative() {
- try!(env::current_dir()).join(dir)
+ env::current_dir()?.join(dir)
} else {
dir.to_path_buf()
};
- current = try!(fs::canonicalize(current));
+ current = fs::canonicalize(current)?;
loop {
- let config_file = current.join("rustfmt.toml");
- match fs::metadata(&config_file) {
- Ok(md) => {
- // Properly handle unlikely situation of a directory named `rustfmt.toml`.
- if md.is_file() {
- return Ok(Some(config_file));
- }
- }
- // If it's not found, we continue searching; otherwise something went wrong and we
- // return the error.
- Err(e) => {
- if e.kind() != ErrorKind::NotFound {
- return Err(e);
+ for config_file_name in &CONFIG_FILE_NAMES {
+ let config_file = current.join(config_file_name);
+ match fs::metadata(&config_file) {
+ // Only return if it's a file to handle the unlikely situation of a directory named
+ // `rustfmt.toml`.
+ Ok(ref md) if md.is_file() => return Ok(Some(config_file)),
+ // Return the error if it's something other than `NotFound`; otherwise we didn't
+ // find the project file yet, and continue searching.
+ Err(e) => {
+ if e.kind() != ErrorKind::NotFound {
+ return Err(FmtError::from(e));
+ }
}
+ _ => {}
}
}
}
}
+fn open_config_file(file_path: &Path) -> FmtResult<(Config, Option<PathBuf>)> {
+ let mut file = File::open(&file_path)?;
+ let mut toml = String::new();
+ file.read_to_string(&mut toml)?;
+ match Config::from_toml(&toml) {
+ Ok(cfg) => Ok((cfg, Some(file_path.to_path_buf()))),
+ Err(err) => Err(FmtError::from(err)),
+ }
+}
+
/// Resolve the config for input in `dir`.
///
/// Returns the `Config` to use, and the path of the project file if there was
/// one.
-fn resolve_config(dir: &Path) -> io::Result<(Config, Option<PathBuf>)> {
- let path = try!(lookup_project_file(dir));
+fn resolve_config(dir: &Path) -> FmtResult<(Config, Option<PathBuf>)> {
+ let path = lookup_project_file(dir)?;
if path.is_none() {
return Ok((Config::default(), None));
}
- let path = path.unwrap();
- let mut file = try!(File::open(&path));
- let mut toml = String::new();
- try!(file.read_to_string(&mut toml));
- Ok((Config::from_toml(&toml), Some(path)))
+ open_config_file(&path.unwrap())
}
-fn update_config(config: &mut Config, matches: &Matches) {
- config.verbose = matches.opt_present("verbose");
- config.skip_children = matches.opt_present("skip-children");
+/// read the given config file path recursively if present else read the project file path
+fn match_cli_path_or_file(config_path: Option<PathBuf>,
+ input_file: &Path)
+ -> FmtResult<(Config, Option<PathBuf>)> {
+
+ if let Some(config_file) = config_path {
+ let (toml, path) = open_config_file(config_file.as_ref())?;
+ if path.is_some() {
+ return Ok((toml, path));
+ }
+ }
+ resolve_config(input_file)
}
-fn execute() -> i32 {
+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", "show progress");
+ opts.optflag("v", "verbose", "print verbose output");
opts.optopt("",
"write-mode",
"mode to write in (not usable when piping from stdin)",
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");
- let matches = match opts.parse(env::args().skip(1)) {
- Ok(m) => m,
- Err(e) => {
- print_usage(&opts, &e.to_string());
- return 1;
- }
- };
+ opts
+}
- let operation = determine_operation(&matches);
+fn execute(opts: &Options) -> FmtResult<Summary> {
+ let matches = opts.parse(env::args().skip(1))?;
- match operation {
- Operation::InvalidInput(reason) => {
- print_usage(&opts, &reason);
- 1
- }
+ match determine_operation(&matches)? {
Operation::Help => {
- print_usage(&opts, "");
- 0
+ print_usage(opts, "");
+ Summary::print_exit_codes();
+ Ok(Summary::new())
}
Operation::Version => {
print_version();
- 0
+ Ok(Summary::new())
}
Operation::ConfigHelp => {
Config::print_docs();
- 0
+ 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, write_mode) => {
+ Operation::Stdin { input, config_path } => {
// try to read config from local directory
- let (config, _) = resolve_config(&env::current_dir().unwrap())
- .expect("Error resolving config");
+ 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);
+ }
+ }
+ }
- run_from_stdin(input, write_mode, &config);
- 0
+ Ok(run(Input::Text(input), &config))
}
- Operation::Format(files, write_mode) => {
+ 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();
+ let mut path = None;
+ // Load the config path file if provided
+ if let Some(config_file) = config_path {
+ let (cfg_tmp, path_tmp) = open_config_file(config_file.as_ref())?;
+ config = cfg_tmp;
+ path = path_tmp;
+ };
+
+ if options.verbose {
+ if let Some(path) = path.as_ref() {
+ println!("Using rustfmt config file {}", path.display());
+ }
+ }
+
+ let mut error_summary = Summary::new();
for file in files {
- let (mut config, path) = resolve_config(file.parent().unwrap())
- .expect(&format!("Error resolving config for {}",
- file.display()));
- if let Some(path) = path {
- println!("Using rustfmt config file {} for {}",
- path.display(),
- file.display());
+ 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 path.is_none() {
+ let (config_tmp, path_tmp) = resolve_config(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));
}
+ }
- update_config(&mut config, &matches);
- run(&file, write_mode, &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())?;
}
- 0
+
+ Ok(error_summary)
}
}
}
fn main() {
let _ = env_logger::init();
- let exit_code = execute();
+ 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();
}
fn print_usage(opts: &Options, reason: &str) {
- let reason = format!("{}\nusage: {} [options] <file>...",
+ let reason = format!("{}\n\nusage: {} [options] <file>...",
reason,
env::args_os().next().unwrap().to_string_lossy());
println!("{}", opts.usage(&reason));
}
fn print_version() {
- println!("{}.{}.{}{}",
- option_env!("CARGO_PKG_VERSION_MAJOR").unwrap_or("X"),
- option_env!("CARGO_PKG_VERSION_MINOR").unwrap_or("X"),
- option_env!("CARGO_PKG_VERSION_PATCH").unwrap_or("X"),
- option_env!("CARGO_PKG_VERSION_PRE").unwrap_or(""));
+ println!("{} ({}{})",
+ option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
+ COMMIT_HASH.unwrap_or("git commit unavailable"),
+ match WORKTREE_CLEAN {
+ Some(false) => " worktree dirty",
+ _ => "",
+ });
}
-fn determine_operation(matches: &Matches) -> Operation {
+fn determine_operation(matches: &Matches) -> FmtResult<Operation> {
if matches.opt_present("h") {
- return Operation::Help;
+ return Ok(Operation::Help);
}
if matches.opt_present("config-help") {
- return Operation::ConfigHelp;
+ return Ok(Operation::ConfigHelp);
}
- if matches.opt_present("version") {
- return Operation::Version;
+ if let Some(path) = matches.opt_str("dump-default-config") {
+ return Ok(Operation::ConfigOutputDefault { path });
}
- // if no file argument is supplied, read from stdin
- if matches.free.is_empty() {
-
- let mut buffer = String::new();
- match io::stdin().read_to_string(&mut buffer) {
- Ok(..) => (),
- Err(e) => return Operation::InvalidInput(e.to_string()),
- }
-
- // WriteMode is always plain for Stdin
- return Operation::Stdin(buffer, WriteMode::Plain);
+ if matches.opt_present("version") {
+ return Ok(Operation::Version);
}
- let write_mode = match matches.opt_str("write-mode") {
- Some(mode) => {
- match mode.parse() {
- Ok(mode) => mode,
- Err(..) => return Operation::InvalidInput("Unrecognized write mode".into()),
+ let config_path_not_found = |path: &str| -> FmtResult<Operation> {
+ 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<PathBuf> = 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 mut config_file_path = None;
+ for config_file_name in &CONFIG_FILE_NAMES {
+ let temp_path = path.join(config_file_name);
+ if temp_path.is_file() {
+ config_file_path = Some(temp_path);
+ }
+ }
+ if config_file_path.is_some() {
+ config_file_path
+ } else {
+ return config_path_not_found(path.to_str().unwrap());
}
}
- None => WriteMode::Default,
+ path @ _ => path,
};
- let files: Vec<_> = matches.free.iter().map(PathBuf::from).collect();
+ // 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,
+ });
+ }
- Operation::Format(files, write_mode)
+ 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,
+ })
}