1 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/).
9 use std::cmp::Ordering;
10 use std::collections::{BTreeMap, BTreeSet};
13 use std::hash::{Hash, Hasher};
14 use std::io::{self, Write};
15 use std::iter::FromIterator;
16 use std::path::{Path, PathBuf};
17 use std::process::Command;
20 use getopts::{Matches, Options};
23 let exit_status = execute();
24 std::io::stdout().flush().unwrap();
25 std::process::exit(exit_status);
28 const SUCCESS: i32 = 0;
29 const FAILURE: i32 = 1;
32 let mut opts = getopts::Options::new();
33 opts.optflag("h", "help", "show this message");
34 opts.optflag("q", "quiet", "no output printed to stdout");
35 opts.optflag("v", "verbose", "use verbose output");
39 "specify package to format (only usable in workspaces)",
42 opts.optflag("", "version", "print rustfmt version and exit");
43 opts.optflag("", "all", "format all packages (only usable in workspaces)");
45 // If there is any invalid argument passed to `cargo fmt`, return without formatting.
46 let mut is_package_arg = false;
47 for arg in env::args().skip(2).take_while(|a| a != "--") {
48 if arg.starts_with('-') {
49 is_package_arg = arg.starts_with("--package") | arg.starts_with("-p");
50 } else if !is_package_arg {
51 print_usage_to_stderr(&opts, &format!("Invalid argument: `{}`.", arg));
54 is_package_arg = false;
58 let matches = match opts.parse(env::args().skip(1).take_while(|a| a != "--")) {
61 print_usage_to_stderr(&opts, &e.to_string());
66 let verbosity = match (matches.opt_present("v"), matches.opt_present("q")) {
67 (false, false) => Verbosity::Normal,
68 (false, true) => Verbosity::Quiet,
69 (true, false) => Verbosity::Verbose,
71 print_usage_to_stderr(&opts, "quiet mode and verbose mode are not compatible");
76 if matches.opt_present("h") {
77 print_usage_to_stdout(&opts, "");
81 if matches.opt_present("version") {
82 return handle_command_status(get_version(), &opts);
85 let strategy = CargoFmtStrategy::from_matches(&matches);
86 handle_command_status(format_crate(verbosity, &strategy), &opts)
89 macro_rules! print_usage {
90 ($print:ident, $opts:ident, $reason:expr) => {{
91 let msg = format!("{}\nusage: cargo fmt [options]", $reason);
93 "{}\nThis utility formats all bin and lib files of the current crate using rustfmt. \
94 Arguments after `--` are passed to rustfmt.",
100 fn print_usage_to_stdout(opts: &Options, reason: &str) {
101 print_usage!(println, opts, reason);
104 fn print_usage_to_stderr(opts: &Options, reason: &str) {
105 print_usage!(eprintln, opts, reason);
108 #[derive(Debug, Clone, Copy, PartialEq)]
115 fn handle_command_status(status: Result<i32, io::Error>, opts: &getopts::Options) -> i32 {
118 print_usage_to_stderr(opts, &e.to_string());
121 Ok(status) => status,
125 fn get_version() -> Result<i32, io::Error> {
126 let mut command = Command::new("rustfmt")
127 .stdout(std::process::Stdio::inherit())
128 .args(&[String::from("--version")])
130 .map_err(|e| match e.kind() {
131 io::ErrorKind::NotFound => io::Error::new(
132 io::ErrorKind::Other,
133 "Could not run rustfmt, please make sure it is in your PATH.",
137 let result = command.wait()?;
138 if result.success() {
141 Ok(result.code().unwrap_or(SUCCESS))
145 fn format_crate(verbosity: Verbosity, strategy: &CargoFmtStrategy) -> Result<i32, io::Error> {
146 let rustfmt_args = get_fmt_args();
147 let targets = if rustfmt_args
149 .any(|s| ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str()))
153 get_targets(strategy)?
156 // Currently only bin and lib files get formatted.
157 run_rustfmt(&targets, &rustfmt_args, verbosity)
160 fn get_fmt_args() -> Vec<String> {
161 // All arguments after -- are passed to rustfmt.
162 env::args().skip_while(|a| a != "--").skip(1).collect()
165 /// Target uses a `path` field for equality and hashing.
168 /// A path to the main source file of the target.
170 /// A kind of target (e.g., lib, bin, example, ...).
172 /// Rust edition for this target.
177 pub fn from_target(target: &cargo_metadata::Target) -> Self {
178 let path = PathBuf::from(&target.src_path);
179 let canonicalized = fs::canonicalize(&path).unwrap_or(path);
183 kind: target.kind[0].clone(),
184 edition: target.edition.clone(),
189 impl PartialEq for Target {
190 fn eq(&self, other: &Target) -> bool {
191 self.path == other.path
195 impl PartialOrd for Target {
196 fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
197 Some(self.path.cmp(&other.path))
201 impl Ord for Target {
202 fn cmp(&self, other: &Target) -> Ordering {
203 self.path.cmp(&other.path)
207 impl Eq for Target {}
209 impl Hash for Target {
210 fn hash<H: Hasher>(&self, state: &mut H) {
211 self.path.hash(state);
215 #[derive(Debug, PartialEq, Eq)]
216 pub enum CargoFmtStrategy {
217 /// Format every packages and dependencies.
219 /// Format packages that are specified by the command line argument.
221 /// Format the root packages only.
225 impl CargoFmtStrategy {
226 pub fn from_matches(matches: &Matches) -> CargoFmtStrategy {
227 match (matches.opt_present("all"), matches.opt_present("p")) {
228 (false, false) => CargoFmtStrategy::Root,
229 (true, _) => CargoFmtStrategy::All,
230 (false, true) => CargoFmtStrategy::Some(matches.opt_strs("p")),
235 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
236 fn get_targets(strategy: &CargoFmtStrategy) -> Result<BTreeSet<Target>, io::Error> {
237 let mut targets = BTreeSet::new();
240 CargoFmtStrategy::Root => get_targets_root_only(&mut targets)?,
241 CargoFmtStrategy::All => get_targets_recursive(None, &mut targets, &mut BTreeSet::new())?,
242 CargoFmtStrategy::Some(ref hitlist) => get_targets_with_hitlist(hitlist, &mut targets)?,
245 if targets.is_empty() {
247 io::ErrorKind::Other,
248 "Failed to find targets".to_owned(),
255 fn get_targets_root_only(targets: &mut BTreeSet<Target>) -> Result<(), io::Error> {
256 let metadata = get_cargo_metadata(None)?;
257 let current_dir = env::current_dir()?.canonicalize()?;
258 let current_dir_manifest = current_dir.join("Cargo.toml");
259 let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
260 let in_workspace_root = workspace_root_path == current_dir;
262 for package in metadata.packages {
263 if in_workspace_root || PathBuf::from(&package.manifest_path) == current_dir_manifest {
264 for target in package.targets {
265 targets.insert(Target::from_target(&target));
273 fn get_targets_recursive(
274 manifest_path: Option<&Path>,
275 mut targets: &mut BTreeSet<Target>,
276 visited: &mut BTreeSet<String>,
277 ) -> Result<(), io::Error> {
278 let metadata = get_cargo_metadata(manifest_path)?;
280 for package in metadata.packages {
281 add_targets(&package.targets, &mut targets);
283 // Look for local dependencies.
284 for dependency in package.dependencies {
285 if dependency.source.is_some() || visited.contains(&dependency.name) {
289 let mut manifest_path = PathBuf::from(&package.manifest_path);
292 manifest_path.push(&dependency.name);
293 manifest_path.push("Cargo.toml");
295 if manifest_path.exists() {
296 visited.insert(dependency.name);
297 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
305 fn get_targets_with_hitlist(
307 targets: &mut BTreeSet<Target>,
308 ) -> Result<(), io::Error> {
309 let metadata = get_cargo_metadata(None)?;
311 let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
313 for package in metadata.packages {
314 if workspace_hitlist.remove(&package.name) {
315 for target in package.targets {
316 targets.insert(Target::from_target(&target));
321 if workspace_hitlist.is_empty() {
324 let package = workspace_hitlist.iter().next().unwrap();
326 io::ErrorKind::InvalidInput,
327 format!("package `{}` is not a member of the workspace", package),
332 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
333 for target in target_paths {
334 targets.insert(Target::from_target(target));
339 targets: &BTreeSet<Target>,
341 verbosity: Verbosity,
342 ) -> Result<i32, io::Error> {
343 let by_edition = targets
346 if verbosity == Verbosity::Verbose {
347 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
350 .fold(BTreeMap::new(), |mut h, t| {
351 h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
355 let mut status = vec![];
356 for (edition, files) in by_edition {
357 let stdout = if verbosity == Verbosity::Quiet {
358 std::process::Stdio::null()
360 std::process::Stdio::inherit()
363 if verbosity == Verbosity::Verbose {
365 print!(" --edition {}", edition);
366 fmt_args.iter().for_each(|f| print!(" {}", f));
367 files.iter().for_each(|f| print!(" {}", f.display()));
371 let mut command = Command::new("rustfmt")
374 .args(&["--edition", edition])
377 .map_err(|e| match e.kind() {
378 io::ErrorKind::NotFound => io::Error::new(
379 io::ErrorKind::Other,
380 "Could not run rustfmt, please make sure it is in your PATH.",
385 status.push(command.wait()?);
390 .filter_map(|s| if s.success() { None } else { s.code() })
395 fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
396 let mut cmd = cargo_metadata::MetadataCommand::new();
398 if let Some(manifest_path) = manifest_path {
399 cmd.manifest_path(manifest_path);
402 Ok(metadata) => Ok(metadata),
403 Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),