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