]> git.lizzy.rs Git - rust.git/blob - src/bin/main.rs
b7bb92c858f4dad88d2116975e6e20b271b82f9e
[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, load_config, CliOptions, Config, FmtResult, WriteMode,
29     WRITE_MODE_LIST,
30 };
31 use rustfmt::{format_and_emit_report, FileName, Input, Summary};
32
33 fn main() {
34     env_logger::init();
35     let opts = make_opts();
36
37     let exit_code = match execute(&opts) {
38         Ok((write_mode, summary)) => {
39             if summary.has_operational_errors()
40                 || summary.has_parsing_errors()
41                 || (summary.has_diff && write_mode == WriteMode::Check)
42             {
43                 1
44             } else {
45                 0
46             }
47         }
48         Err(e) => {
49             eprintln!("{}", e.to_string());
50             1
51         }
52     };
53     // Make sure standard output is flushed before we exit.
54     std::io::stdout().flush().unwrap();
55
56     // Exit with given exit code.
57     //
58     // NOTE: This immediately terminates the process without doing any cleanup,
59     // so make sure to finish all necessary cleanup before this is called.
60     std::process::exit(exit_code);
61 }
62
63 /// Rustfmt operations.
64 enum Operation {
65     /// Format files and their child modules.
66     Format {
67         files: Vec<PathBuf>,
68         minimal_config_path: Option<String>,
69     },
70     /// Print the help message.
71     Help,
72     // Print version information
73     Version,
74     /// Print detailed configuration help.
75     ConfigHelp,
76     /// Output default config to a file, or stdout if None
77     ConfigOutputDefault {
78         path: Option<String>,
79     },
80     /// No file specified, read from stdin
81     Stdin {
82         input: String,
83     },
84 }
85
86 fn make_opts() -> Options {
87     let mut opts = Options::new();
88
89     // Sorted in alphabetical order.
90     opts.optopt(
91         "",
92         "color",
93         "Use colored output (if supported)",
94         "[always|never|auto]",
95     );
96     opts.optflag(
97         "",
98         "config-help",
99         "Show details of rustfmt configuration options",
100     );
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.opt(
109         "",
110         "dump-default-config",
111         "Dumps default configuration to PATH. PATH defaults to stdout, if omitted.",
112         "PATH",
113         getopts::HasArg::Maybe,
114         getopts::Occur::Optional,
115     );
116     opts.optopt(
117         "",
118         "dump-minimal-config",
119         "Dumps configuration options that were checked during formatting to a file.",
120         "PATH",
121     );
122     opts.optflag(
123         "",
124         "error-on-unformatted",
125         "Error if unable to get comments or string literals within max_width, \
126          or they are left with trailing whitespaces",
127     );
128     opts.optopt(
129         "",
130         "file-lines",
131         "Format specified line ranges. See README for more detail on the JSON format.",
132         "JSON",
133     );
134     opts.optflag(
135         "",
136         "verbose-diff",
137         "Emit a more verbose diff, indicating the end of lines.",
138     );
139     opts.optflag("h", "help", "Show this message");
140     opts.optflag("", "skip-children", "Don't reformat child modules");
141     opts.optflag(
142         "",
143         "unstable-features",
144         "Enables unstable features. Only available on nightly channel",
145     );
146     opts.optflag("v", "verbose", "Print verbose output");
147     opts.optflag("V", "version", "Show version information");
148     opts.optopt(
149         "",
150         "write-mode",
151         "How to write output (not usable when piping from stdin)",
152         WRITE_MODE_LIST,
153     );
154
155     opts
156 }
157
158 fn execute(opts: &Options) -> FmtResult<(WriteMode, Summary)> {
159     let matches = opts.parse(env::args().skip(1))?;
160
161     match determine_operation(&matches)? {
162         Operation::Help => {
163             print_usage_to_stdout(opts, "");
164             Summary::print_exit_codes();
165             Ok((WriteMode::None, Summary::default()))
166         }
167         Operation::Version => {
168             print_version();
169             Ok((WriteMode::None, Summary::default()))
170         }
171         Operation::ConfigHelp => {
172             Config::print_docs(&mut stdout(), matches.opt_present("unstable-features"));
173             Ok((WriteMode::None, Summary::default()))
174         }
175         Operation::ConfigOutputDefault { path } => {
176             let toml = Config::default().all_options().to_toml().map_err(err_msg)?;
177             if let Some(path) = path {
178                 let mut file = File::create(path)?;
179                 file.write_all(toml.as_bytes())?;
180             } else {
181                 io::stdout().write_all(toml.as_bytes())?;
182             }
183             Ok((WriteMode::None, Summary::default()))
184         }
185         Operation::Stdin { input } => {
186             // try to read config from local directory
187             let options = CliOptions::from_matches(&matches)?;
188             let (mut config, _) = load_config(Some(Path::new(".")), Some(&options))?;
189
190             // write_mode is always Plain for Stdin.
191             config.set().write_mode(WriteMode::Plain);
192
193             // parse file_lines
194             if let Some(ref file_lines) = matches.opt_str("file-lines") {
195                 config
196                     .set()
197                     .file_lines(file_lines.parse().map_err(err_msg)?);
198                 for f in config.file_lines().files() {
199                     match *f {
200                         FileName::Custom(ref f) if f == "stdin" => {}
201                         _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
202                     }
203                 }
204             }
205
206             let mut error_summary = Summary::default();
207             emit_pre_matter(&config)?;
208             match format_and_emit_report(Input::Text(input), &config) {
209                 Ok(summary) => error_summary.add(summary),
210                 Err(_) => error_summary.add_operational_error(),
211             }
212             emit_post_matter(&config)?;
213
214             Ok((WriteMode::Plain, error_summary))
215         }
216         Operation::Format {
217             files,
218             minimal_config_path,
219         } => {
220             let options = CliOptions::from_matches(&matches)?;
221             format(files, minimal_config_path, options)
222         }
223     }
224 }
225
226 fn format(
227     files: Vec<PathBuf>,
228     minimal_config_path: Option<String>,
229     options: CliOptions,
230 ) -> FmtResult<(WriteMode, Summary)> {
231     options.verify_file_lines(&files);
232     let (config, config_path) = load_config(None, Some(&options))?;
233
234     if config.verbose() {
235         if let Some(path) = config_path.as_ref() {
236             println!("Using rustfmt config file {}", path.display());
237         }
238     }
239
240     emit_pre_matter(&config)?;
241     let mut error_summary = Summary::default();
242
243     for file in files {
244         if !file.exists() {
245             eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
246             error_summary.add_operational_error();
247         } else if file.is_dir() {
248             eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
249             error_summary.add_operational_error();
250         } else {
251             // Check the file directory if the config-path could not be read or not provided
252             let local_config = if config_path.is_none() {
253                 let (local_config, config_path) =
254                     load_config(Some(file.parent().unwrap()), Some(&options))?;
255                 if local_config.verbose() {
256                     if let Some(path) = config_path {
257                         println!(
258                             "Using rustfmt config file {} for {}",
259                             path.display(),
260                             file.display()
261                         );
262                     }
263                 }
264                 local_config
265             } else {
266                 config.clone()
267             };
268
269             match format_and_emit_report(Input::File(file), &local_config) {
270                 Ok(summary) => error_summary.add(summary),
271                 Err(_) => {
272                     error_summary.add_operational_error();
273                     break;
274                 }
275             }
276         }
277     }
278     emit_post_matter(&config)?;
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 = config.used_options().to_toml().map_err(err_msg)?;
285         file.write_all(toml.as_bytes())?;
286     }
287
288     Ok((config.write_mode(), error_summary))
289 }
290
291 fn print_usage_to_stdout(opts: &Options, reason: &str) {
292     let sep = if reason.is_empty() {
293         String::new()
294     } else {
295         format!("{}\n\n", reason)
296     };
297     let msg = format!(
298         "{}Format Rust code\n\nusage: {} [options] <file>...",
299         sep,
300         env::args_os().next().unwrap().to_string_lossy()
301     );
302     println!("{}", opts.usage(&msg));
303 }
304
305 fn print_version() {
306     let version_info = format!(
307         "{}-{}",
308         option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
309         include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
310     );
311
312     println!("rustfmt {}", version_info);
313 }
314
315 fn determine_operation(matches: &Matches) -> FmtResult<Operation> {
316     if matches.opt_present("h") {
317         return Ok(Operation::Help);
318     }
319
320     if matches.opt_present("config-help") {
321         return Ok(Operation::ConfigHelp);
322     }
323
324     if matches.opt_present("dump-default-config") {
325         // NOTE for some reason when configured with HasArg::Maybe + Occur::Optional opt_default
326         // doesn't recognize `--foo bar` as a long flag with an argument but as a long flag with no
327         // argument *plus* a free argument. Thus we check for that case in this branch -- this is
328         // required for backward compatibility.
329         if let Some(path) = matches.free.get(0) {
330             return Ok(Operation::ConfigOutputDefault {
331                 path: Some(path.clone()),
332             });
333         } else {
334             return Ok(Operation::ConfigOutputDefault {
335                 path: matches.opt_str("dump-default-config"),
336             });
337         }
338     }
339
340     if matches.opt_present("version") {
341         return Ok(Operation::Version);
342     }
343
344     // If no path is given, we won't output a minimal config.
345     let minimal_config_path = matches.opt_str("dump-minimal-config");
346
347     // if no file argument is supplied, read from stdin
348     if matches.free.is_empty() {
349         let mut buffer = String::new();
350         io::stdin().read_to_string(&mut buffer)?;
351
352         return Ok(Operation::Stdin { input: buffer });
353     }
354
355     let files: Vec<_> = matches
356         .free
357         .iter()
358         .map(|s| {
359             let p = PathBuf::from(s);
360             // we will do comparison later, so here tries to canonicalize first
361             // to get the expected behavior.
362             p.canonicalize().unwrap_or(p)
363         })
364         .collect();
365
366     Ok(Operation::Format {
367         files,
368         minimal_config_path,
369     })
370 }