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