]> git.lizzy.rs Git - rust.git/blob - src/bin/main.rs
Suppress unstable config options by default.
[rust.git] / src / bin / main.rs
1 // Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 #![cfg(not(test))]
12
13 extern crate env_logger;
14 extern crate getopts;
15 extern crate rustfmt_nightly as rustfmt;
16
17 use std::fs::File;
18 use std::io::{self, stdout, Read, Write};
19 use std::path::{Path, PathBuf};
20 use std::str::FromStr;
21 use std::{env, error};
22
23 use getopts::{Matches, Options};
24
25 use rustfmt::config::file_lines::FileLines;
26 use rustfmt::config::{get_toml_path, Color, Config, WriteMode};
27 use rustfmt::{run, FileName, Input, Summary};
28
29 type FmtError = Box<error::Error + Send + Sync>;
30 type FmtResult<T> = std::result::Result<T, FmtError>;
31
32 /// Rustfmt operations.
33 enum Operation {
34     /// Format files and their child modules.
35     Format {
36         files: Vec<PathBuf>,
37         config_path: Option<PathBuf>,
38         minimal_config_path: Option<String>,
39     },
40     /// Print the help message.
41     Help,
42     // Print version information
43     Version,
44     /// Print detailed configuration help.
45     ConfigHelp,
46     /// Output default config to a file, or stdout if None
47     ConfigOutputDefault {
48         path: Option<String>,
49     },
50     /// No file specified, read from stdin
51     Stdin {
52         input: String,
53         config_path: Option<PathBuf>,
54     },
55 }
56
57 /// Parsed command line options.
58 #[derive(Clone, Debug, Default)]
59 struct CliOptions {
60     skip_children: Option<bool>,
61     verbose: bool,
62     write_mode: Option<WriteMode>,
63     color: Option<Color>,
64     file_lines: FileLines, // Default is all lines in all files.
65     unstable_features: bool,
66     error_on_unformatted: Option<bool>,
67 }
68
69 impl CliOptions {
70     fn from_matches(matches: &Matches) -> FmtResult<CliOptions> {
71         let mut options = CliOptions::default();
72         options.verbose = matches.opt_present("verbose");
73         let unstable_features = matches.opt_present("unstable-features");
74         let rust_nightly = option_env!("CFG_RELEASE_CHANNEL")
75             .map(|c| c == "nightly")
76             .unwrap_or(false);
77         if unstable_features && !rust_nightly {
78             return Err(FmtError::from(
79                 "Unstable features are only available on Nightly channel",
80             ));
81         } else {
82             options.unstable_features = unstable_features;
83         }
84
85         if let Some(ref write_mode) = matches.opt_str("write-mode") {
86             if let Ok(write_mode) = WriteMode::from_str(write_mode) {
87                 options.write_mode = Some(write_mode);
88             } else {
89                 return Err(FmtError::from(format!(
90                     "Invalid write-mode: {}",
91                     write_mode
92                 )));
93             }
94         }
95
96         if let Some(ref color) = matches.opt_str("color") {
97             match Color::from_str(color) {
98                 Ok(color) => options.color = Some(color),
99                 _ => return Err(FmtError::from(format!("Invalid color: {}", color))),
100             }
101         }
102
103         if let Some(ref file_lines) = matches.opt_str("file-lines") {
104             options.file_lines = file_lines.parse()?;
105         }
106
107         if matches.opt_present("skip-children") {
108             options.skip_children = Some(true);
109         }
110         if matches.opt_present("error-on-unformatted") {
111             options.error_on_unformatted = Some(true);
112         }
113
114         Ok(options)
115     }
116
117     fn apply_to(self, config: &mut Config) {
118         config.set().verbose(self.verbose);
119         config.set().file_lines(self.file_lines);
120         config.set().unstable_features(self.unstable_features);
121         if let Some(skip_children) = self.skip_children {
122             config.set().skip_children(skip_children);
123         }
124         if let Some(error_on_unformatted) = self.error_on_unformatted {
125             config.set().error_on_unformatted(error_on_unformatted);
126         }
127         if let Some(write_mode) = self.write_mode {
128             config.set().write_mode(write_mode);
129         }
130         if let Some(color) = self.color {
131             config.set().color(color);
132         }
133     }
134 }
135
136 /// read the given config file path recursively if present else read the project file path
137 fn match_cli_path_or_file(
138     config_path: Option<PathBuf>,
139     input_file: &Path,
140 ) -> FmtResult<(Config, Option<PathBuf>)> {
141     if let Some(config_file) = config_path {
142         let toml = Config::from_toml_path(config_file.as_ref())?;
143         return Ok((toml, Some(config_file)));
144     }
145     Config::from_resolved_toml_path(input_file).map_err(FmtError::from)
146 }
147
148 fn make_opts() -> Options {
149     let mut opts = Options::new();
150
151     // Sorted in alphabetical order.
152     opts.optopt(
153         "",
154         "color",
155         "Use colored output (if supported)",
156         "[always|never|auto]",
157     );
158     opts.optflag(
159         "",
160         "config-help",
161         "Show details of rustfmt configuration options",
162     );
163     opts.optopt(
164         "",
165         "config-path",
166         "Recursively searches the given path for the rustfmt.toml config file. If not \
167          found reverts to the input file path",
168         "[Path for the configuration file]",
169     );
170     opts.opt(
171         "",
172         "dump-default-config",
173         "Dumps default configuration to PATH. PATH defaults to stdout, if omitted.",
174         "PATH",
175         getopts::HasArg::Maybe,
176         getopts::Occur::Optional,
177     );
178     opts.optopt(
179         "",
180         "dump-minimal-config",
181         "Dumps configuration options that were checked during formatting to a file.",
182         "PATH",
183     );
184     opts.optflag(
185         "",
186         "error-on-unformatted",
187         "Error if unable to get comments or string literals within max_width, \
188          or they are left with trailing whitespaces",
189     );
190     opts.optopt(
191         "",
192         "file-lines",
193         "Format specified line ranges. See README for more detail on the JSON format.",
194         "JSON",
195     );
196     opts.optflag("h", "help", "Show this message");
197     opts.optflag("", "skip-children", "Don't reformat child modules");
198     opts.optflag(
199         "",
200         "unstable-features",
201         "Enables unstable features. Only available on nightly channel",
202     );
203     opts.optflag("v", "verbose", "Print verbose output");
204     opts.optflag("V", "version", "Show version information");
205     opts.optopt(
206         "",
207         "write-mode",
208         "How to write output (not usable when piping from stdin)",
209         "[replace|overwrite|display|plain|diff|coverage|checkstyle]",
210     );
211
212     opts
213 }
214
215 fn execute(opts: &Options) -> FmtResult<Summary> {
216     let matches = opts.parse(env::args().skip(1))?;
217
218     match determine_operation(&matches)? {
219         Operation::Help => {
220             print_usage_to_stdout(opts, "");
221             Summary::print_exit_codes();
222             Ok(Summary::default())
223         }
224         Operation::Version => {
225             print_version();
226             Ok(Summary::default())
227         }
228         Operation::ConfigHelp => {
229             Config::print_docs(&mut stdout(), matches.opt_present("unstable-features"));
230             Ok(Summary::default())
231         }
232         Operation::ConfigOutputDefault { path } => {
233             let toml = Config::default().all_options().to_toml()?;
234             if let Some(path) = path {
235                 let mut file = File::create(path)?;
236                 file.write_all(toml.as_bytes())?;
237             } else {
238                 io::stdout().write_all(toml.as_bytes())?;
239             }
240             Ok(Summary::default())
241         }
242         Operation::Stdin { input, config_path } => {
243             // try to read config from local directory
244             let (mut config, _) =
245                 match_cli_path_or_file(config_path, &env::current_dir().unwrap())?;
246
247             // write_mode is always Plain for Stdin.
248             config.set().write_mode(WriteMode::Plain);
249
250             // parse file_lines
251             if let Some(ref file_lines) = matches.opt_str("file-lines") {
252                 config.set().file_lines(file_lines.parse()?);
253                 for f in config.file_lines().files() {
254                     match *f {
255                         FileName::Custom(ref f) if f == "stdin" => {}
256                         _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
257                     }
258                 }
259             }
260
261             let mut error_summary = Summary::default();
262             if config.version_meets_requirement(&mut error_summary) {
263                 error_summary.add(run(Input::Text(input), &config));
264             }
265
266             Ok(error_summary)
267         }
268         Operation::Format {
269             files,
270             config_path,
271             minimal_config_path,
272         } => {
273             let options = CliOptions::from_matches(&matches)?;
274
275             for f in options.file_lines.files() {
276                 match *f {
277                     FileName::Real(ref f) if files.contains(f) => {}
278                     FileName::Real(_) => {
279                         eprintln!("Warning: Extra file listed in file_lines option '{}'", f)
280                     }
281                     _ => eprintln!("Warning: Not a file '{}'", f),
282                 }
283             }
284
285             let mut config = Config::default();
286             // Load the config path file if provided
287             if let Some(config_file) = config_path.as_ref() {
288                 config = Config::from_toml_path(config_file.as_ref())?;
289             };
290
291             if options.verbose {
292                 if let Some(path) = config_path.as_ref() {
293                     println!("Using rustfmt config file {}", path.display());
294                 }
295             }
296
297             let mut error_summary = Summary::default();
298             for file in files {
299                 if !file.exists() {
300                     eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
301                     error_summary.add_operational_error();
302                 } else if file.is_dir() {
303                     eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
304                     error_summary.add_operational_error();
305                 } else {
306                     // Check the file directory if the config-path could not be read or not provided
307                     if config_path.is_none() {
308                         let (config_tmp, path_tmp) =
309                             Config::from_resolved_toml_path(file.parent().unwrap())?;
310                         if options.verbose {
311                             if let Some(path) = path_tmp.as_ref() {
312                                 println!(
313                                     "Using rustfmt config file {} for {}",
314                                     path.display(),
315                                     file.display()
316                                 );
317                             }
318                         }
319                         config = config_tmp;
320                     }
321
322                     if !config.version_meets_requirement(&mut error_summary) {
323                         break;
324                     }
325
326                     options.clone().apply_to(&mut config);
327                     error_summary.add(run(Input::File(file), &config));
328                 }
329             }
330
331             // If we were given a path via dump-minimal-config, output any options
332             // that were used during formatting as TOML.
333             if let Some(path) = minimal_config_path {
334                 let mut file = File::create(path)?;
335                 let toml = config.used_options().to_toml()?;
336                 file.write_all(toml.as_bytes())?;
337             }
338
339             Ok(error_summary)
340         }
341     }
342 }
343
344 fn main() {
345     env_logger::init();
346
347     let opts = make_opts();
348
349     let exit_code = match execute(&opts) {
350         Ok(summary) => {
351             if summary.has_operational_errors() {
352                 1
353             } else if summary.has_parsing_errors() {
354                 2
355             } else if summary.has_formatting_errors() {
356                 3
357             } else if summary.has_diff {
358                 // should only happen in diff mode
359                 4
360             } else {
361                 assert!(summary.has_no_errors());
362                 0
363             }
364         }
365         Err(e) => {
366             eprintln!("{}", e.to_string());
367             1
368         }
369     };
370     // Make sure standard output is flushed before we exit.
371     std::io::stdout().flush().unwrap();
372
373     // Exit with given exit code.
374     //
375     // NOTE: This immediately terminates the process without doing any cleanup,
376     // so make sure to finish all necessary cleanup before this is called.
377     std::process::exit(exit_code);
378 }
379
380 fn print_usage_to_stdout(opts: &Options, reason: &str) {
381     let sep = if reason.is_empty() {
382         String::new()
383     } else {
384         format!("{}\n\n", reason)
385     };
386     let msg = format!(
387         "{}Format Rust code\n\nusage: {} [options] <file>...",
388         sep,
389         env::args_os().next().unwrap().to_string_lossy()
390     );
391     println!("{}", opts.usage(&msg));
392 }
393
394 fn print_version() {
395     let version_info = format!(
396         "{}-{}",
397         option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
398         include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
399     );
400
401     println!("rustfmt {}", version_info);
402 }
403
404 fn determine_operation(matches: &Matches) -> FmtResult<Operation> {
405     if matches.opt_present("h") {
406         return Ok(Operation::Help);
407     }
408
409     if matches.opt_present("config-help") {
410         return Ok(Operation::ConfigHelp);
411     }
412
413     if matches.opt_present("dump-default-config") {
414         // NOTE for some reason when configured with HasArg::Maybe + Occur::Optional opt_default
415         // doesn't recognize `--foo bar` as a long flag with an argument but as a long flag with no
416         // argument *plus* a free argument. Thus we check for that case in this branch -- this is
417         // required for backward compatibility.
418         if let Some(path) = matches.free.get(0) {
419             return Ok(Operation::ConfigOutputDefault {
420                 path: Some(path.clone()),
421             });
422         } else {
423             return Ok(Operation::ConfigOutputDefault {
424                 path: matches.opt_str("dump-default-config"),
425             });
426         }
427     }
428
429     if matches.opt_present("version") {
430         return Ok(Operation::Version);
431     }
432
433     let config_path_not_found = |path: &str| -> FmtResult<Operation> {
434         Err(FmtError::from(format!(
435             "Error: unable to find a config file for the given path: `{}`",
436             path
437         )))
438     };
439
440     // Read the config_path and convert to parent dir if a file is provided.
441     // If a config file cannot be found from the given path, return error.
442     let config_path: Option<PathBuf> = match matches.opt_str("config-path").map(PathBuf::from) {
443         Some(ref path) if !path.exists() => return config_path_not_found(path.to_str().unwrap()),
444         Some(ref path) if path.is_dir() => {
445             let config_file_path = get_toml_path(path)?;
446             if config_file_path.is_some() {
447                 config_file_path
448             } else {
449                 return config_path_not_found(path.to_str().unwrap());
450             }
451         }
452         path => path,
453     };
454
455     // If no path is given, we won't output a minimal config.
456     let minimal_config_path = matches.opt_str("dump-minimal-config");
457
458     // if no file argument is supplied, read from stdin
459     if matches.free.is_empty() {
460         let mut buffer = String::new();
461         io::stdin().read_to_string(&mut buffer)?;
462
463         return Ok(Operation::Stdin {
464             input: buffer,
465             config_path,
466         });
467     }
468
469     let files: Vec<_> = matches
470         .free
471         .iter()
472         .map(|s| {
473             let p = PathBuf::from(s);
474             // we will do comparison later, so here tries to canonicalize first
475             // to get the expected behavior.
476             p.canonicalize().unwrap_or(p)
477         })
478         .collect();
479
480     Ok(Operation::Format {
481         files,
482         config_path,
483         minimal_config_path,
484     })
485 }