]> git.lizzy.rs Git - rust.git/blob - src/bin/rustfmt.rs
rustfmt: Move getopts::Options creation to its own function
[rust.git] / src / bin / rustfmt.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
14 extern crate log;
15 extern crate rustfmt;
16 extern crate toml;
17 extern crate env_logger;
18 extern crate getopts;
19
20 use rustfmt::{run, Input};
21 use rustfmt::config::{Config, WriteMode};
22
23 use std::env;
24 use std::fs::{self, File};
25 use std::io::{self, ErrorKind, Read, Write};
26 use std::path::{Path, PathBuf};
27 use std::str::FromStr;
28
29 use getopts::{Matches, Options};
30
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     },
39     /// Print the help message.
40     Help,
41     // Print version information
42     Version,
43     /// Print detailed configuration help.
44     ConfigHelp,
45     /// Invalid program input.
46     InvalidInput {
47         reason: String,
48     },
49     /// No file specified, read from stdin
50     Stdin {
51         input: String,
52         config_path: Option<PathBuf>,
53     },
54 }
55
56 /// Try to find a project file in the given directory and its parents. Returns the path of a the
57 /// nearest project file if one exists, or `None` if no project file was found.
58 fn lookup_project_file(dir: &Path) -> io::Result<Option<PathBuf>> {
59     let mut current = if dir.is_relative() {
60         try!(env::current_dir()).join(dir)
61     } else {
62         dir.to_path_buf()
63     };
64
65     current = try!(fs::canonicalize(current));
66
67     loop {
68         let config_file = current.join("rustfmt.toml");
69         match fs::metadata(&config_file) {
70             Ok(md) => {
71                 // Properly handle unlikely situation of a directory named `rustfmt.toml`.
72                 if md.is_file() {
73                     return Ok(Some(config_file));
74                 }
75             }
76             // If it's not found, we continue searching; otherwise something went wrong and we
77             // return the error.
78             Err(e) => {
79                 if e.kind() != ErrorKind::NotFound {
80                     return Err(e);
81                 }
82             }
83         }
84
85         // If the current directory has no parent, we're done searching.
86         if !current.pop() {
87             return Ok(None);
88         }
89     }
90 }
91
92 /// Resolve the config for input in `dir`.
93 ///
94 /// Returns the `Config` to use, and the path of the project file if there was
95 /// one.
96 fn resolve_config(dir: &Path) -> io::Result<(Config, Option<PathBuf>)> {
97     let path = try!(lookup_project_file(dir));
98     if path.is_none() {
99         return Ok((Config::default(), None));
100     }
101     let path = path.unwrap();
102     let mut file = try!(File::open(&path));
103     let mut toml = String::new();
104     try!(file.read_to_string(&mut toml));
105     Ok((Config::from_toml(&toml), Some(path)))
106 }
107
108 /// read the given config file path recursively if present else read the project file path
109 fn match_cli_path_or_file(config_path: Option<PathBuf>,
110                           input_file: &Path)
111                           -> io::Result<(Config, Option<PathBuf>)> {
112
113     if let Some(config_file) = config_path {
114         let (toml, path) = try!(resolve_config(config_file.as_ref()));
115         if path.is_some() {
116             return Ok((toml, path));
117         }
118     }
119     resolve_config(input_file)
120 }
121
122 fn update_config(config: &mut Config, matches: &Matches) -> Result<(), String> {
123     config.verbose = matches.opt_present("verbose");
124     config.skip_children = matches.opt_present("skip-children");
125
126     let write_mode = matches.opt_str("write-mode");
127     match matches.opt_str("write-mode").map(|wm| WriteMode::from_str(&wm)) {
128         None => Ok(()),
129         Some(Ok(write_mode)) => {
130             config.write_mode = write_mode;
131             Ok(())
132         }
133         Some(Err(_)) => Err(format!("Invalid write-mode: {}", write_mode.expect("cannot happen"))),
134     }
135 }
136
137 fn make_opts() -> Options {
138     let mut opts = Options::new();
139     opts.optflag("h", "help", "show this message");
140     opts.optflag("V", "version", "show version information");
141     opts.optflag("v", "verbose", "show progress");
142     opts.optopt("",
143                 "write-mode",
144                 "mode to write in (not usable when piping from stdin)",
145                 "[replace|overwrite|display|diff|coverage|checkstyle]");
146     opts.optflag("", "skip-children", "don't reformat child modules");
147
148     opts.optflag("",
149                  "config-help",
150                  "show details of rustfmt configuration options");
151     opts.optopt("",
152                 "config-path",
153                 "Recursively searches the given path for the rustfmt.toml config file. If not \
154                  found reverts to the input file path",
155                 "[Path for the configuration file]");
156
157     opts
158 }
159
160 fn execute() -> i32 {
161     let opts = make_opts();
162
163     let matches = match opts.parse(env::args().skip(1)) {
164         Ok(m) => m,
165         Err(e) => {
166             print_usage(&opts, &e.to_string());
167             return 1;
168         }
169     };
170
171     let operation = determine_operation(&matches);
172
173     match operation {
174         Operation::InvalidInput { reason } => {
175             print_usage(&opts, &reason);
176             1
177         }
178         Operation::Help => {
179             print_usage(&opts, "");
180             0
181         }
182         Operation::Version => {
183             print_version();
184             0
185         }
186         Operation::ConfigHelp => {
187             Config::print_docs();
188             0
189         }
190         Operation::Stdin { input, config_path } => {
191             // try to read config from local directory
192             let (mut config, _) = match_cli_path_or_file(config_path, &env::current_dir().unwrap())
193                                       .expect("Error resolving config");
194
195             // write_mode is always Plain for Stdin.
196             config.write_mode = WriteMode::Plain;
197
198             run(Input::Text(input), &config);
199             0
200         }
201         Operation::Format { files, config_path } => {
202             let mut config = Config::default();
203             let mut path = None;
204             // Load the config path file if provided
205             if let Some(config_file) = config_path {
206                 let (cfg_tmp, path_tmp) = resolve_config(config_file.as_ref())
207                                               .expect(&format!("Error resolving config for {:?}",
208                                                                config_file));
209                 config = cfg_tmp;
210                 path = path_tmp;
211             };
212             if let Some(path) = path.as_ref() {
213                 println!("Using rustfmt config file {}", path.display());
214             }
215             for file in files {
216                 // Check the file directory if the config-path could not be read or not provided
217                 if path.is_none() {
218                     let (config_tmp, path_tmp) = resolve_config(file.parent().unwrap())
219                                                      .expect(&format!("Error resolving config \
220                                                                        for {}",
221                                                                       file.display()));
222                     if let Some(path) = path_tmp.as_ref() {
223                         println!("Using rustfmt config file {} for {}",
224                                  path.display(),
225                                  file.display());
226                     }
227                     config = config_tmp;
228                 }
229
230                 if let Err(e) = update_config(&mut config, &matches) {
231                     print_usage(&opts, &e);
232                     return 1;
233                 }
234                 run(Input::File(file), &config);
235             }
236             0
237         }
238     }
239 }
240
241 fn main() {
242     let _ = env_logger::init();
243     let exit_code = execute();
244
245     // Make sure standard output is flushed before we exit.
246     std::io::stdout().flush().unwrap();
247
248     // Exit with given exit code.
249     //
250     // NOTE: This immediately terminates the process without doing any cleanup,
251     // so make sure to finish all necessary cleanup before this is called.
252     std::process::exit(exit_code);
253 }
254
255 fn print_usage(opts: &Options, reason: &str) {
256     let reason = format!("{}\nusage: {} [options] <file>...",
257                          reason,
258                          env::args_os().next().unwrap().to_string_lossy());
259     println!("{}", opts.usage(&reason));
260 }
261
262 fn print_version() {
263     println!("{}.{}.{}{}",
264              option_env!("CARGO_PKG_VERSION_MAJOR").unwrap_or("X"),
265              option_env!("CARGO_PKG_VERSION_MINOR").unwrap_or("X"),
266              option_env!("CARGO_PKG_VERSION_PATCH").unwrap_or("X"),
267              option_env!("CARGO_PKG_VERSION_PRE").unwrap_or(""));
268 }
269
270 fn determine_operation(matches: &Matches) -> Operation {
271     if matches.opt_present("h") {
272         return Operation::Help;
273     }
274
275     if matches.opt_present("config-help") {
276         return Operation::ConfigHelp;
277     }
278
279     if matches.opt_present("version") {
280         return Operation::Version;
281     }
282
283     // Read the config_path and convert to parent dir if a file is provided.
284     let config_path: Option<PathBuf> = matches.opt_str("config-path")
285                                               .map(PathBuf::from)
286                                               .and_then(|dir| {
287                                                   if dir.is_file() {
288                                                       return dir.parent().map(|v| v.into());
289                                                   }
290                                                   Some(dir)
291                                               });
292
293     // if no file argument is supplied, read from stdin
294     if matches.free.is_empty() {
295
296         let mut buffer = String::new();
297         match io::stdin().read_to_string(&mut buffer) {
298             Ok(..) => (),
299             Err(e) => return Operation::InvalidInput { reason: e.to_string() },
300         }
301
302         return Operation::Stdin {
303             input: buffer,
304             config_path: config_path,
305         };
306     }
307
308     let files: Vec<_> = matches.free.iter().map(PathBuf::from).collect();
309
310     Operation::Format {
311         files: files,
312         config_path: config_path,
313     }
314 }