]> git.lizzy.rs Git - rust.git/blob - src/bin/main.rs
Make some write modes unstable
[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 failure;
15 extern crate getopts;
16 extern crate rustfmt_nightly as rustfmt;
17
18 use std::env;
19 use std::fs::File;
20 use std::io::{self, stdout, Read, Write};
21 use std::path::{Path, PathBuf};
22
23 use failure::err_msg;
24
25 use getopts::{Matches, Options};
26
27 use rustfmt::{
28     emit_post_matter, emit_pre_matter, format_and_emit_report, load_config, CliOptions, Config,
29     FileName, FmtResult, Input, Summary, Verbosity, WriteMode,
30 };
31
32 fn main() {
33     env_logger::init();
34     let opts = make_opts();
35
36     let exit_code = match execute(&opts) {
37         Ok((write_mode, summary)) => {
38             if summary.has_operational_errors()
39                 || summary.has_parsing_errors()
40                 || (summary.has_diff && write_mode == WriteMode::Check)
41             {
42                 1
43             } else {
44                 0
45             }
46         }
47         Err(e) => {
48             eprintln!("{}", e.to_string());
49             1
50         }
51     };
52     // Make sure standard output is flushed before we exit.
53     std::io::stdout().flush().unwrap();
54
55     // Exit with given exit code.
56     //
57     // NOTE: This immediately terminates the process without doing any cleanup,
58     // so make sure to finish all necessary cleanup before this is called.
59     std::process::exit(exit_code);
60 }
61
62 /// Rustfmt operations.
63 enum Operation {
64     /// Format files and their child modules.
65     Format {
66         files: Vec<PathBuf>,
67         minimal_config_path: Option<String>,
68     },
69     /// Print the help message.
70     Help(HelpOp),
71     // Print version information
72     Version,
73     /// Output default config to a file, or stdout if None
74     ConfigOutputDefault {
75         path: Option<String>,
76     },
77     /// No file specified, read from stdin
78     Stdin {
79         input: String,
80     },
81 }
82
83 /// Arguments to `--help`
84 enum HelpOp {
85     None,
86     Config,
87     FileLines,
88 }
89
90 fn make_opts() -> Options {
91     let mut opts = Options::new();
92
93     // Sorted in alphabetical order.
94     opts.optflag("", "backup", "Backup any modified files.");
95     opts.optflag(
96         "",
97         "check",
98         "Run in 'check' mode. Exits with 0 if input if formatted correctly. Exits \
99          with 1 and prints a diff if formatting is required.",
100     );
101     opts.optopt(
102         "",
103         "color",
104         "Use colored output (if supported)",
105         "[always|never|auto]",
106     );
107     opts.optopt(
108         "",
109         "config-path",
110         "Recursively searches the given path for the rustfmt.toml config file. If not \
111          found reverts to the input file path",
112         "[Path for the configuration file]",
113     );
114     let is_nightly = is_nightly();
115     let emit_opts = if is_nightly {
116         "[files|stdout|coverage|checkstyle]"
117     } else {
118         "[files|stdout]"
119     };
120     opts.optopt("", "emit", "What data to emit and how", emit_opts);
121     opts.optflagopt(
122         "h",
123         "help",
124         "Show this message or help about a specific topic: `config` or `file-lines`",
125         "=TOPIC",
126     );
127     opts.optopt(
128         "",
129         "print-config",
130         "Dumps a default or minimal config to PATH. A minimal config is the \
131          subset of the current config file used for formatting the current program.",
132         "[minimal|default] PATH",
133     );
134     opts.optflag("v", "verbose", "Print verbose output");
135     opts.optflag("q", "quiet", "Print less output");
136     opts.optflag("V", "version", "Show version information");
137
138     if is_nightly {
139         opts.optflag(
140             "",
141             "unstable-features",
142             "Enables unstable features. Only available on nightly channel.",
143         );
144         opts.optflag(
145             "",
146             "error-on-unformatted",
147             "Error if unable to get comments or string literals within max_width, \
148              or they are left with trailing whitespaces (unstable).",
149         );
150         opts.optopt(
151             "",
152             "file-lines",
153             "Format specified line ranges. Run with `--help file-lines` for \
154              more detail (unstable).",
155             "JSON",
156         );
157         opts.optflag(
158             "",
159             "skip-children",
160             "Don't reformat child modules (unstable).",
161         );
162     }
163
164     opts
165 }
166
167 fn is_nightly() -> bool {
168     option_env!("CFG_RELEASE_CHANNEL")
169         .map(|c| c == "nightly")
170         .unwrap_or(false)
171 }
172
173 fn execute(opts: &Options) -> FmtResult<(WriteMode, Summary)> {
174     let matches = opts.parse(env::args().skip(1))?;
175     let options = CliOptions::from_matches(&matches)?;
176
177     match determine_operation(&matches)? {
178         Operation::Help(HelpOp::None) => {
179             print_usage_to_stdout(opts, "");
180             Summary::print_exit_codes();
181             Ok((WriteMode::None, Summary::default()))
182         }
183         Operation::Help(HelpOp::Config) => {
184             Config::print_docs(&mut stdout(), options.unstable_features);
185             Ok((WriteMode::None, Summary::default()))
186         }
187         Operation::Help(HelpOp::FileLines) => {
188             print_help_file_lines();
189             Ok((WriteMode::None, Summary::default()))
190         }
191         Operation::Version => {
192             print_version();
193             Ok((WriteMode::None, Summary::default()))
194         }
195         Operation::ConfigOutputDefault { path } => {
196             let toml = Config::default().all_options().to_toml().map_err(err_msg)?;
197             if let Some(path) = path {
198                 let mut file = File::create(path)?;
199                 file.write_all(toml.as_bytes())?;
200             } else {
201                 io::stdout().write_all(toml.as_bytes())?;
202             }
203             Ok((WriteMode::None, Summary::default()))
204         }
205         Operation::Stdin { input } => {
206             // try to read config from local directory
207             let (mut config, _) = load_config(Some(Path::new(".")), Some(&options))?;
208
209             // write_mode is always Display for Stdin.
210             config.set().write_mode(WriteMode::Display);
211             config.set().verbose(Verbosity::Quiet);
212
213             // parse file_lines
214             config.set().file_lines(options.file_lines);
215             for f in config.file_lines().files() {
216                 match *f {
217                     FileName::Custom(ref f) if f == "stdin" => {}
218                     _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
219                 }
220             }
221
222             let mut error_summary = Summary::default();
223             emit_pre_matter(&config)?;
224             match format_and_emit_report(Input::Text(input), &config) {
225                 Ok(summary) => error_summary.add(summary),
226                 Err(_) => error_summary.add_operational_error(),
227             }
228             emit_post_matter(&config)?;
229
230             Ok((WriteMode::Display, error_summary))
231         }
232         Operation::Format {
233             files,
234             minimal_config_path,
235         } => format(files, minimal_config_path, options),
236     }
237 }
238
239 fn format(
240     files: Vec<PathBuf>,
241     minimal_config_path: Option<String>,
242     options: CliOptions,
243 ) -> FmtResult<(WriteMode, Summary)> {
244     options.verify_file_lines(&files);
245     let (config, config_path) = load_config(None, Some(&options))?;
246
247     if config.verbose() == Verbosity::Verbose {
248         if let Some(path) = config_path.as_ref() {
249             println!("Using rustfmt config file {}", path.display());
250         }
251     }
252
253     emit_pre_matter(&config)?;
254     let mut error_summary = Summary::default();
255
256     for file in files {
257         if !file.exists() {
258             eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
259             error_summary.add_operational_error();
260         } else if file.is_dir() {
261             eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
262             error_summary.add_operational_error();
263         } else {
264             // Check the file directory if the config-path could not be read or not provided
265             let local_config = if config_path.is_none() {
266                 let (local_config, config_path) =
267                     load_config(Some(file.parent().unwrap()), Some(&options))?;
268                 if local_config.verbose() == Verbosity::Verbose {
269                     if let Some(path) = config_path {
270                         println!(
271                             "Using rustfmt config file {} for {}",
272                             path.display(),
273                             file.display()
274                         );
275                     }
276                 }
277                 local_config
278             } else {
279                 config.clone()
280             };
281
282             match format_and_emit_report(Input::File(file), &local_config) {
283                 Ok(summary) => error_summary.add(summary),
284                 Err(_) => {
285                     error_summary.add_operational_error();
286                     break;
287                 }
288             }
289         }
290     }
291     emit_post_matter(&config)?;
292
293     // If we were given a path via dump-minimal-config, output any options
294     // that were used during formatting as TOML.
295     if let Some(path) = minimal_config_path {
296         let mut file = File::create(path)?;
297         let toml = config.used_options().to_toml().map_err(err_msg)?;
298         file.write_all(toml.as_bytes())?;
299     }
300
301     Ok((config.write_mode(), error_summary))
302 }
303
304 fn print_usage_to_stdout(opts: &Options, reason: &str) {
305     let sep = if reason.is_empty() {
306         String::new()
307     } else {
308         format!("{}\n\n", reason)
309     };
310     let msg = format!(
311         "{}Format Rust code\n\nusage: {} [options] <file>...",
312         sep,
313         env::args_os().next().unwrap().to_string_lossy()
314     );
315     println!("{}", opts.usage(&msg));
316 }
317
318 fn print_help_file_lines() {
319     println!(
320         "If you want to restrict reformatting to specific sets of lines, you can
321 use the `--file-lines` option. Its argument is a JSON array of objects
322 with `file` and `range` properties, where `file` is a file name, and
323 `range` is an array representing a range of lines like `[7,13]`. Ranges
324 are 1-based and inclusive of both end points. Specifying an empty array
325 will result in no files being formatted. For example,
326
327 ```
328 rustfmt --file-lines '[
329     {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
330     {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
331     {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
332     {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
333 ```
334
335 would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
336 and `15` of `src/foo.rs`. No other files would be formatted, even if they
337 are included as out of line modules from `src/lib.rs`."
338     );
339 }
340
341 fn print_version() {
342     let version_info = format!(
343         "{}-{}",
344         option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
345         include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
346     );
347
348     println!("rustfmt {}", version_info);
349 }
350
351 fn determine_operation(matches: &Matches) -> FmtResult<Operation> {
352     if matches.opt_present("h") {
353         let topic = matches.opt_str("h");
354         if topic == None {
355             return Ok(Operation::Help(HelpOp::None));
356         } else if topic == Some("config".to_owned()) {
357             return Ok(Operation::Help(HelpOp::Config));
358         } else if topic == Some("file-lines".to_owned()) {
359             return Ok(Operation::Help(HelpOp::FileLines));
360         } else {
361             println!("Unknown help topic: `{}`\n", topic.unwrap());
362             return Ok(Operation::Help(HelpOp::None));
363         }
364     }
365
366     let mut minimal_config_path = None;
367     if let Some(ref kind) = matches.opt_str("print-config") {
368         let path = matches.free.get(0).cloned();
369         if kind == "default" {
370             return Ok(Operation::ConfigOutputDefault { path });
371         } else if kind == "minimal" {
372             minimal_config_path = path;
373             if minimal_config_path.is_none() {
374                 println!("WARNING: PATH required for `--print-config minimal`");
375             }
376         }
377     }
378
379     if matches.opt_present("version") {
380         return Ok(Operation::Version);
381     }
382
383     // if no file argument is supplied, read from stdin
384     if matches.free.is_empty() {
385         let mut buffer = String::new();
386         io::stdin().read_to_string(&mut buffer)?;
387
388         return Ok(Operation::Stdin { input: buffer });
389     }
390
391     let files: Vec<_> = matches
392         .free
393         .iter()
394         .map(|s| {
395             let p = PathBuf::from(s);
396             // we will do comparison later, so here tries to canonicalize first
397             // to get the expected behavior.
398             p.canonicalize().unwrap_or(p)
399         })
400         .collect();
401
402     Ok(Operation::Format {
403         files,
404         minimal_config_path,
405     })
406 }