]> git.lizzy.rs Git - rust.git/blob - src/bin/main.rs
Merge pull request #3159 from scampi/issue-3132
[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 extern crate env_logger;
12 #[macro_use]
13 extern crate failure;
14 extern crate getopts;
15 extern crate rustfmt_nightly as rustfmt;
16
17 use std::env;
18 use std::fs::File;
19 use std::io::{self, stdout, Read, Write};
20 use std::path::{Path, PathBuf};
21 use std::str::FromStr;
22
23 use failure::err_msg;
24
25 use getopts::{Matches, Options};
26
27 use rustfmt::{
28     load_config, CliOptions, Color, Config, Edition, EmitMode, ErrorKind, FileLines, FileName,
29     Input, Session, Verbosity,
30 };
31
32 fn main() {
33     env_logger::init();
34     let opts = make_opts();
35
36     let exit_code = match execute(&opts) {
37         Ok(code) => code,
38         Err(e) => {
39             eprintln!("{}", e.to_string());
40             1
41         }
42     };
43     // Make sure standard output is flushed before we exit.
44     std::io::stdout().flush().unwrap();
45
46     // Exit with given exit code.
47     //
48     // NOTE: This immediately terminates the process without doing any cleanup,
49     // so make sure to finish all necessary cleanup before this is called.
50     std::process::exit(exit_code);
51 }
52
53 /// Rustfmt operations.
54 enum Operation {
55     /// Format files and their child modules.
56     Format {
57         files: Vec<PathBuf>,
58         minimal_config_path: Option<String>,
59     },
60     /// Print the help message.
61     Help(HelpOp),
62     // Print version information
63     Version,
64     /// Output default config to a file, or stdout if None
65     ConfigOutputDefault {
66         path: Option<String>,
67     },
68     /// No file specified, read from stdin
69     Stdin {
70         input: String,
71     },
72 }
73
74 /// Arguments to `--help`
75 enum HelpOp {
76     None,
77     Config,
78     FileLines,
79 }
80
81 fn make_opts() -> Options {
82     let mut opts = Options::new();
83
84     opts.optflag(
85         "",
86         "check",
87         "Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits \
88          with 1 and prints a diff if formatting is required.",
89     );
90     let is_nightly = is_nightly();
91     let emit_opts = if is_nightly {
92         "[files|stdout|coverage|checkstyle]"
93     } else {
94         "[files|stdout]"
95     };
96     opts.optopt("", "emit", "What data to emit and how", emit_opts);
97     opts.optflag("", "backup", "Backup any modified files.");
98     opts.optopt(
99         "",
100         "config-path",
101         "Recursively searches the given path for the rustfmt.toml config file. If not \
102          found reverts to the input file path",
103         "[Path for the configuration file]",
104     );
105     opts.optopt("", "edition", "Rust edition to use", "[2015|2018]");
106     opts.optopt(
107         "",
108         "color",
109         "Use colored output (if supported)",
110         "[always|never|auto]",
111     );
112     opts.optopt(
113         "",
114         "print-config",
115         "Dumps a default or minimal config to PATH. A minimal config is the \
116          subset of the current config file used for formatting the current program.",
117         "[minimal|default] PATH",
118     );
119
120     if is_nightly {
121         opts.optflag(
122             "",
123             "unstable-features",
124             "Enables unstable features. Only available on nightly channel.",
125         );
126         opts.optopt(
127             "",
128             "file-lines",
129             "Format specified line ranges. Run with `--help=file-lines` for \
130              more detail (unstable).",
131             "JSON",
132         );
133         opts.optflag(
134             "",
135             "error-on-unformatted",
136             "Error if unable to get comments or string literals within max_width, \
137              or they are left with trailing whitespaces (unstable).",
138         );
139         opts.optflag(
140             "",
141             "skip-children",
142             "Don't reformat child modules (unstable).",
143         );
144     }
145
146     opts.optflag("v", "verbose", "Print verbose output");
147     opts.optflag("q", "quiet", "Print less output");
148     opts.optflag("V", "version", "Show version information");
149     opts.optflagopt(
150         "h",
151         "help",
152         "Show this message or help about a specific topic: `config` or `file-lines`",
153         "=TOPIC",
154     );
155
156     opts
157 }
158
159 fn is_nightly() -> bool {
160     option_env!("CFG_RELEASE_CHANNEL").map_or(false, |c| c == "nightly" || c == "dev")
161 }
162
163 // Returned i32 is an exit code
164 fn execute(opts: &Options) -> Result<i32, failure::Error> {
165     let matches = opts.parse(env::args().skip(1))?;
166     let options = GetOptsOptions::from_matches(&matches)?;
167
168     match determine_operation(&matches)? {
169         Operation::Help(HelpOp::None) => {
170             print_usage_to_stdout(opts, "");
171             Ok(0)
172         }
173         Operation::Help(HelpOp::Config) => {
174             Config::print_docs(&mut stdout(), options.unstable_features);
175             Ok(0)
176         }
177         Operation::Help(HelpOp::FileLines) => {
178             print_help_file_lines();
179             Ok(0)
180         }
181         Operation::Version => {
182             print_version();
183             Ok(0)
184         }
185         Operation::ConfigOutputDefault { path } => {
186             let toml = Config::default().all_options().to_toml().map_err(err_msg)?;
187             if let Some(path) = path {
188                 let mut file = File::create(path)?;
189                 file.write_all(toml.as_bytes())?;
190             } else {
191                 io::stdout().write_all(toml.as_bytes())?;
192             }
193             Ok(0)
194         }
195         Operation::Stdin { input } => format_string(input, options),
196         Operation::Format {
197             files,
198             minimal_config_path,
199         } => format(files, minimal_config_path, &options),
200     }
201 }
202
203 fn format_string(input: String, options: GetOptsOptions) -> Result<i32, failure::Error> {
204     // try to read config from local directory
205     let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
206
207     // emit mode is always Stdout for Stdin.
208     config.set().emit_mode(EmitMode::Stdout);
209     config.set().verbose(Verbosity::Quiet);
210
211     // parse file_lines
212     config.set().file_lines(options.file_lines);
213     for f in config.file_lines().files() {
214         match *f {
215             FileName::Stdin => {}
216             _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
217         }
218     }
219
220     let out = &mut stdout();
221     let mut session = Session::new(config, Some(out));
222     format_and_emit_report(&mut session, Input::Text(input));
223
224     let exit_code = if session.has_operational_errors() || session.has_parsing_errors() {
225         1
226     } else {
227         0
228     };
229     Ok(exit_code)
230 }
231
232 fn format(
233     files: Vec<PathBuf>,
234     minimal_config_path: Option<String>,
235     options: &GetOptsOptions,
236 ) -> Result<i32, failure::Error> {
237     options.verify_file_lines(&files);
238     let (config, config_path) = load_config(None, Some(options.clone()))?;
239
240     if config.verbose() == Verbosity::Verbose {
241         if let Some(path) = config_path.as_ref() {
242             println!("Using rustfmt config file {}", path.display());
243         }
244     }
245
246     let out = &mut stdout();
247     let mut session = Session::new(config, Some(out));
248
249     for file in files {
250         if !file.exists() {
251             eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
252             session.add_operational_error();
253         } else if file.is_dir() {
254             eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
255             session.add_operational_error();
256         } else {
257             // Check the file directory if the config-path could not be read or not provided
258             if config_path.is_none() {
259                 let (local_config, config_path) =
260                     load_config(Some(file.parent().unwrap()), Some(options.clone()))?;
261                 if local_config.verbose() == Verbosity::Verbose {
262                     if let Some(path) = config_path {
263                         println!(
264                             "Using rustfmt config file {} for {}",
265                             path.display(),
266                             file.display()
267                         );
268                     }
269                 }
270
271                 session.override_config(local_config, |sess| {
272                     format_and_emit_report(sess, Input::File(file))
273                 });
274             } else {
275                 format_and_emit_report(&mut session, Input::File(file));
276             }
277         }
278     }
279
280     // If we were given a path via dump-minimal-config, output any options
281     // that were used during formatting as TOML.
282     if let Some(path) = minimal_config_path {
283         let mut file = File::create(path)?;
284         let toml = session.config.used_options().to_toml().map_err(err_msg)?;
285         file.write_all(toml.as_bytes())?;
286     }
287
288     let exit_code = if session.has_operational_errors()
289         || session.has_parsing_errors()
290         || ((session.has_diff() || session.has_check_errors()) && options.check)
291     {
292         1
293     } else {
294         0
295     };
296     Ok(exit_code)
297 }
298
299 fn format_and_emit_report<T: Write>(session: &mut Session<T>, input: Input) {
300     match session.format(input) {
301         Ok(report) => {
302             if report.has_warnings() {
303                 match term::stderr() {
304                     Some(ref t)
305                         if session.config.color().use_colored_tty()
306                             && t.supports_color()
307                             && t.supports_attr(term::Attr::Bold) =>
308                     {
309                         match report.fancy_print(term::stderr().unwrap()) {
310                             Ok(..) => (),
311                             Err(..) => panic!("Unable to write to stderr: {}", report),
312                         }
313                     }
314                     _ => eprintln!("{}", report),
315                 }
316             }
317         }
318         Err(msg) => {
319             eprintln!("Error writing files: {}", msg);
320             session.add_operational_error();
321         }
322     }
323 }
324
325 fn print_usage_to_stdout(opts: &Options, reason: &str) {
326     let sep = if reason.is_empty() {
327         String::new()
328     } else {
329         format!("{}\n\n", reason)
330     };
331     let msg = format!(
332         "{}Format Rust code\n\nusage: {} [options] <file>...",
333         sep,
334         env::args_os().next().unwrap().to_string_lossy()
335     );
336     println!("{}", opts.usage(&msg));
337 }
338
339 fn print_help_file_lines() {
340     println!(
341         "If you want to restrict reformatting to specific sets of lines, you can
342 use the `--file-lines` option. Its argument is a JSON array of objects
343 with `file` and `range` properties, where `file` is a file name, and
344 `range` is an array representing a range of lines like `[7,13]`. Ranges
345 are 1-based and inclusive of both end points. Specifying an empty array
346 will result in no files being formatted. For example,
347
348 ```
349 rustfmt --file-lines '[
350     {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
351     {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
352     {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
353     {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
354 ```
355
356 would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
357 and `15` of `src/foo.rs`. No other files would be formatted, even if they
358 are included as out of line modules from `src/lib.rs`."
359     );
360 }
361
362 fn print_version() {
363     let version_info = format!(
364         "{}-{}",
365         option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
366         include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
367     );
368
369     println!("rustfmt {}", version_info);
370 }
371
372 fn determine_operation(matches: &Matches) -> Result<Operation, ErrorKind> {
373     if matches.opt_present("h") {
374         let topic = matches.opt_str("h");
375         if topic == None {
376             return Ok(Operation::Help(HelpOp::None));
377         } else if topic == Some("config".to_owned()) {
378             return Ok(Operation::Help(HelpOp::Config));
379         } else if topic == Some("file-lines".to_owned()) {
380             return Ok(Operation::Help(HelpOp::FileLines));
381         } else {
382             println!("Unknown help topic: `{}`\n", topic.unwrap());
383             return Ok(Operation::Help(HelpOp::None));
384         }
385     }
386
387     let mut minimal_config_path = None;
388     if let Some(ref kind) = matches.opt_str("print-config") {
389         let path = matches.free.get(0).cloned();
390         if kind == "default" {
391             return Ok(Operation::ConfigOutputDefault { path });
392         } else if kind == "minimal" {
393             minimal_config_path = path;
394             if minimal_config_path.is_none() {
395                 println!("WARNING: PATH required for `--print-config minimal`");
396             }
397         }
398     }
399
400     if matches.opt_present("version") {
401         return Ok(Operation::Version);
402     }
403
404     // if no file argument is supplied, read from stdin
405     if matches.free.is_empty() {
406         let mut buffer = String::new();
407         io::stdin().read_to_string(&mut buffer)?;
408
409         return Ok(Operation::Stdin { input: buffer });
410     }
411
412     let files: Vec<_> = matches
413         .free
414         .iter()
415         .map(|s| {
416             let p = PathBuf::from(s);
417             // we will do comparison later, so here tries to canonicalize first
418             // to get the expected behavior.
419             p.canonicalize().unwrap_or(p)
420         })
421         .collect();
422
423     Ok(Operation::Format {
424         files,
425         minimal_config_path,
426     })
427 }
428
429 const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
430
431 /// Parsed command line options.
432 #[derive(Clone, Debug, Default)]
433 struct GetOptsOptions {
434     skip_children: Option<bool>,
435     quiet: bool,
436     verbose: bool,
437     config_path: Option<PathBuf>,
438     emit_mode: EmitMode,
439     backup: bool,
440     check: bool,
441     edition: Option<Edition>,
442     color: Option<Color>,
443     file_lines: FileLines, // Default is all lines in all files.
444     unstable_features: bool,
445     error_on_unformatted: Option<bool>,
446 }
447
448 impl GetOptsOptions {
449     pub fn from_matches(matches: &Matches) -> Result<GetOptsOptions, failure::Error> {
450         let mut options = GetOptsOptions::default();
451         options.verbose = matches.opt_present("verbose");
452         options.quiet = matches.opt_present("quiet");
453         if options.verbose && options.quiet {
454             return Err(format_err!("Can't use both `--verbose` and `--quiet`"));
455         }
456
457         let rust_nightly = is_nightly();
458
459         if rust_nightly {
460             options.unstable_features = matches.opt_present("unstable-features");
461
462             if options.unstable_features {
463                 if matches.opt_present("skip-children") {
464                     options.skip_children = Some(true);
465                 }
466                 if matches.opt_present("error-on-unformatted") {
467                     options.error_on_unformatted = Some(true);
468                 }
469                 if let Some(ref file_lines) = matches.opt_str("file-lines") {
470                     options.file_lines = file_lines.parse().map_err(err_msg)?;
471                 }
472             } else {
473                 let mut unstable_options = vec![];
474                 if matches.opt_present("skip-children") {
475                     unstable_options.push("`--skip-children`");
476                 }
477                 if matches.opt_present("error-on-unformatted") {
478                     unstable_options.push("`--error-on-unformatted`");
479                 }
480                 if matches.opt_present("file-lines") {
481                     unstable_options.push("`--file-lines`");
482                 }
483                 if !unstable_options.is_empty() {
484                     let s = if unstable_options.len() == 1 { "" } else { "s" };
485                     return Err(format_err!(
486                         "Unstable option{} ({}) used without `--unstable-features`",
487                         s,
488                         unstable_options.join(", "),
489                     ));
490                 }
491             }
492         }
493
494         options.config_path = matches.opt_str("config-path").map(PathBuf::from);
495
496         options.check = matches.opt_present("check");
497         if let Some(ref emit_str) = matches.opt_str("emit") {
498             if options.check {
499                 return Err(format_err!("Invalid to use `--emit` and `--check`"));
500             }
501
502             options.emit_mode = emit_mode_from_emit_str(emit_str)?;
503         }
504
505         if let Some(ref edition_str) = matches.opt_str("edition") {
506             options.edition = Some(edition_from_edition_str(edition_str)?);
507         }
508
509         if matches.opt_present("backup") {
510             options.backup = true;
511         }
512
513         if !rust_nightly {
514             if !STABLE_EMIT_MODES.contains(&options.emit_mode) {
515                 return Err(format_err!(
516                     "Invalid value for `--emit` - using an unstable \
517                      value without `--unstable-features`",
518                 ));
519             }
520         }
521
522         if let Some(ref color) = matches.opt_str("color") {
523             match Color::from_str(color) {
524                 Ok(color) => options.color = Some(color),
525                 _ => return Err(format_err!("Invalid color: {}", color)),
526             }
527         }
528
529         Ok(options)
530     }
531
532     fn verify_file_lines(&self, files: &[PathBuf]) {
533         for f in self.file_lines.files() {
534             match *f {
535                 FileName::Real(ref f) if files.contains(f) => {}
536                 FileName::Real(_) => {
537                     eprintln!("Warning: Extra file listed in file_lines option '{}'", f)
538                 }
539                 FileName::Stdin => eprintln!("Warning: Not a file '{}'", f),
540             }
541         }
542     }
543 }
544
545 impl CliOptions for GetOptsOptions {
546     fn apply_to(self, config: &mut Config) {
547         if self.verbose {
548             config.set().verbose(Verbosity::Verbose);
549         } else if self.quiet {
550             config.set().verbose(Verbosity::Quiet);
551         } else {
552             config.set().verbose(Verbosity::Normal);
553         }
554         config.set().file_lines(self.file_lines);
555         config.set().unstable_features(self.unstable_features);
556         if let Some(skip_children) = self.skip_children {
557             config.set().skip_children(skip_children);
558         }
559         if let Some(error_on_unformatted) = self.error_on_unformatted {
560             config.set().error_on_unformatted(error_on_unformatted);
561         }
562         if let Some(edition) = self.edition {
563             config.set().edition(edition);
564         }
565         if self.check {
566             config.set().emit_mode(EmitMode::Diff);
567         } else {
568             config.set().emit_mode(self.emit_mode);
569         }
570         if self.backup {
571             config.set().make_backup(true);
572         }
573         if let Some(color) = self.color {
574             config.set().color(color);
575         }
576     }
577
578     fn config_path(&self) -> Option<&Path> {
579         self.config_path.as_ref().map(|p| &**p)
580     }
581 }
582
583 fn edition_from_edition_str(edition_str: &str) -> Result<Edition, failure::Error> {
584     match edition_str {
585         "2015" => Ok(Edition::Edition2015),
586         "2018" => Ok(Edition::Edition2018),
587         _ => Err(format_err!("Invalid value for `--edition`")),
588     }
589 }
590
591 fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode, failure::Error> {
592     match emit_str {
593         "files" => Ok(EmitMode::Files),
594         "stdout" => Ok(EmitMode::Stdout),
595         "coverage" => Ok(EmitMode::Coverage),
596         "checkstyle" => Ok(EmitMode::Checkstyle),
597         _ => Err(format_err!("Invalid value for `--emit`")),
598     }
599 }