]> git.lizzy.rs Git - rust.git/blobdiff - src/bin/rustfmt.rs
Add --dump-default-config and --dump-minimal-config.
[rust.git] / src / bin / rustfmt.rs
index e3ead2a28a13f3ee4c95f00fd24510da2e173b11..73c4439ba2965aa2e713b28e0600a20ca04b90ce 100644 (file)
 
 #![cfg(not(test))]
 
-#[macro_use]
+
 extern crate log;
 extern crate rustfmt;
 extern crate toml;
 extern crate env_logger;
 extern crate getopts;
 
-use rustfmt::{run, Input};
+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 getopts::{Matches, Options};
 
-macro_rules! msg {
-    ($($arg:tt)*) => (
-        match writeln!(&mut ::std::io::stderr(), $($arg)* ) {
-            Ok(_) => {},
-            Err(x) => panic!("Unable to write to stderr: {}", x),
-        }
-    )
-}
+// 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 {
@@ -43,6 +44,7 @@ enum Operation {
     Format {
         files: Vec<PathBuf>,
         config_path: Option<PathBuf>,
+        minimal_config_path: Option<String>,
     },
     /// Print the help message.
     Help,
@@ -50,10 +52,8 @@ enum Operation {
     Version,
     /// Print detailed configuration help.
     ConfigHelp,
-    /// Invalid program input.
-    InvalidInput {
-        reason: String,
-    },
+    /// Output default config to a file
+    ConfigOutputDefault { path: String },
     /// No file specified, read from stdin
     Stdin {
         input: String,
@@ -61,32 +61,74 @@ enum Operation {
     },
 }
 
+/// 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));
+                    }
                 }
+                _ => {}
             }
         }
 
@@ -97,29 +139,35 @@ fn lookup_project_file(dir: &Path) -> io::Result<Option<PathBuf>> {
     }
 }
 
+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())
 }
 
 /// 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)
-                          -> io::Result<(Config, Option<PathBuf>)> {
+                          -> FmtResult<(Config, Option<PathBuf>)> {
 
     if let Some(config_file) = config_path {
-        let (toml, path) = try!(resolve_config(config_file.as_ref()));
+        let (toml, path) = open_config_file(config_file.as_ref())?;
         if path.is_some() {
             return Ok((toml, path));
         }
@@ -127,26 +175,11 @@ fn match_cli_path_or_file(config_path: Option<PathBuf>,
     resolve_config(input_file)
 }
 
-fn update_config(config: &mut Config, matches: &Matches) -> Result<(), String> {
-    config.verbose = matches.opt_present("verbose");
-    config.skip_children = matches.opt_present("skip-children");
-
-    let write_mode = matches.opt_str("write-mode");
-    match matches.opt_str("write-mode").map(|wm| WriteMode::from_str(&wm)) {
-        None => Ok(()),
-        Some(Ok(write_mode)) => {
-            config.write_mode = write_mode;
-            Ok(())
-        }
-        Some(Err(_)) => Err(format!("Invalid write-mode: {}", write_mode.expect("cannot happen"))),
-    }
-}
-
-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)",
@@ -156,94 +189,164 @@ fn execute() -> i32 {
     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, config_path } => {
             // try to read config from local directory
-            let (mut config, _) = match_cli_path_or_file(config_path, &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.write_mode = WriteMode::Plain;
+            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(Input::Text(input), &config);
-            0
+            Ok(run(Input::Text(input), &config))
         }
-        Operation::Format { files, config_path } => {
+        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) = resolve_config(config_file.as_ref())
-                                              .expect(&format!("Error resolving config for {:?}",
-                                                               config_file));
+                let (cfg_tmp, path_tmp) = open_config_file(config_file.as_ref())?;
                 config = cfg_tmp;
                 path = path_tmp;
             };
-            if let Some(path) = path.as_ref() {
-                msg!("Using rustfmt config file {}", path.display());
+
+            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 {
-                // 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())
-                                                     .expect(&format!("Error resolving config \
-                                                                       for {}",
-                                                                      file.display()));
-                    if let Some(path) = path_tmp.as_ref() {
-                        msg!("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;
                     }
-                    config = config_tmp;
-                }
 
-                if let Err(e) = update_config(&mut config, &matches) {
-                    print_usage(&opts, &e);
-                    return 1;
+                    options.clone().apply_to(&mut config);
+                    error_summary.add(run(Input::File(file), &config));
                 }
-                run(Input::File(file), &config);
             }
-            0
+
+            // 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 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();
 
@@ -255,62 +358,93 @@ fn main() {
 }
 
 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 let Some(path) = matches.opt_str("dump-default-config") {
+        return Ok(Operation::ConfigOutputDefault { path });
     }
 
     if matches.opt_present("version") {
-        return Operation::Version;
+        return Ok(Operation::Version);
     }
 
+    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.
-    let config_path: Option<PathBuf> = matches.opt_str("config-path")
-                                              .map(PathBuf::from)
-                                              .and_then(|dir| {
-                                                  if dir.is_file() {
-                                                      return dir.parent().map(|v| v.into());
-                                                  }
-                                                  Some(dir)
-                                              });
+    // 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());
+            }
+        }
+        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();
-        match io::stdin().read_to_string(&mut buffer) {
-            Ok(..) => (),
-            Err(e) => return Operation::InvalidInput { reason: e.to_string() },
-        }
+        io::stdin().read_to_string(&mut buffer)?;
 
-        return Operation::Stdin {
-            input: buffer,
-            config_path: config_path,
-        };
+        return Ok(Operation::Stdin {
+                      input: buffer,
+                      config_path: config_path,
+                  });
     }
 
-    let files: Vec<_> = matches.free.iter().map(PathBuf::from).collect();
-
-    Operation::Format {
-        files: files,
-        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,
+       })
 }