diff --git a/src/bin/cargo/commands/run.rs b/src/bin/cargo/commands/run.rs index 170c7ddf1a0..50153ceeefd 100644 --- a/src/bin/cargo/commands/run.rs +++ b/src/bin/cargo/commands/run.rs @@ -7,7 +7,9 @@ use crate::util::restricted_names::is_glob_pattern; use cargo::core::Verbosity; use cargo::core::Workspace; use cargo::ops::{self, CompileFilter, Packages}; +use cargo::util::closest; use cargo_util::ProcessError; +use itertools::Itertools as _; pub fn cli() -> Command { subcommand("run") @@ -97,11 +99,81 @@ pub fn is_manifest_command(arg: &str) -> bool { } pub fn exec_manifest_command(config: &mut Config, cmd: &str, args: &[OsString]) -> CliResult { - if !config.cli_unstable().script { - return Err(anyhow::anyhow!("running `{cmd}` requires `-Zscript`").into()); + let manifest_path = Path::new(cmd); + match (manifest_path.is_file(), config.cli_unstable().script) { + (true, true) => {} + (true, false) => { + return Err(anyhow::anyhow!("running the file `{cmd}` requires `-Zscript`").into()); + } + (false, true) => { + let possible_commands = crate::list_commands(config); + let is_dir = if manifest_path.is_dir() { + format!("\n\t`{cmd}` is a directory") + } else { + "".to_owned() + }; + let suggested_command = if let Some(suggested_command) = possible_commands + .keys() + .filter(|c| cmd.starts_with(c.as_str())) + .max_by_key(|c| c.len()) + { + let actual_args = cmd.strip_prefix(suggested_command).unwrap(); + let args = if args.is_empty() { + "".to_owned() + } else { + format!( + " {}", + args.into_iter().map(|os| os.to_string_lossy()).join(" ") + ) + }; + format!("\n\tDid you mean the command `{suggested_command} {actual_args}{args}`") + } else { + "".to_owned() + }; + let suggested_script = if let Some(suggested_script) = suggested_script(cmd) { + format!("\n\tDid you mean the file `{suggested_script}`") + } else { + "".to_owned() + }; + return Err(anyhow::anyhow!( + "no such file or subcommand `{cmd}`{is_dir}{suggested_command}{suggested_script}" + ) + .into()); + } + (false, false) => { + // HACK: duplicating the above for minor tweaks but this will all go away on + // stabilization + let possible_commands = crate::list_commands(config); + let suggested_command = if let Some(suggested_command) = possible_commands + .keys() + .filter(|c| cmd.starts_with(c.as_str())) + .max_by_key(|c| c.len()) + { + let actual_args = cmd.strip_prefix(suggested_command).unwrap(); + let args = if args.is_empty() { + "".to_owned() + } else { + format!( + " {}", + args.into_iter().map(|os| os.to_string_lossy()).join(" ") + ) + }; + format!("\n\tDid you mean the command `{suggested_command} {actual_args}{args}`") + } else { + "".to_owned() + }; + let suggested_script = if let Some(suggested_script) = suggested_script(cmd) { + format!("\n\tDid you mean the file `{suggested_script}` with `-Zscript`") + } else { + "".to_owned() + }; + return Err(anyhow::anyhow!( + "no such subcommand `{cmd}`{suggested_command}{suggested_script}" + ) + .into()); + } } - let manifest_path = Path::new(cmd); let manifest_path = root_manifest(Some(manifest_path), config)?; // Treat `cargo foo.rs` like `cargo install --path foo` and re-evaluate the config based on the @@ -123,6 +195,39 @@ pub fn exec_manifest_command(config: &mut Config, cmd: &str, args: &[OsString]) cargo::ops::run(&ws, &compile_opts, args).map_err(|err| to_run_error(config, err)) } +fn suggested_script(cmd: &str) -> Option { + let cmd_path = Path::new(cmd); + let mut suggestion = Path::new(".").to_owned(); + for cmd_part in cmd_path.components() { + let exact_match = suggestion.join(cmd_part); + suggestion = if exact_match.exists() { + exact_match + } else { + let possible: Vec<_> = std::fs::read_dir(suggestion) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| p.to_str().is_some()) + .collect(); + if let Some(possible) = closest( + cmd_part.as_os_str().to_str().unwrap(), + possible.iter(), + |p| p.file_name().unwrap().to_str().unwrap(), + ) { + possible.to_owned() + } else { + return None; + } + }; + } + if suggestion.is_dir() { + None + } else { + suggestion.into_os_string().into_string().ok() + } +} + fn to_run_error(config: &cargo::util::Config, err: anyhow::Error) -> CliError { let proc_err = match err.downcast_ref::() { Some(e) => e, diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs index adca8c869cf..2b6fa823620 100644 --- a/tests/testsuite/script.rs +++ b/tests/testsuite/script.rs @@ -183,7 +183,7 @@ fn requires_nightly() { .with_stdout("") .with_stderr( "\ -error: running `echo.rs` requires `-Zscript` +[ERROR] running the file `echo.rs` requires `-Zscript` ", ) .run(); @@ -201,7 +201,7 @@ fn requires_z_flag() { .with_stdout("") .with_stderr( "\ -error: running `echo.rs` requires `-Zscript` +[ERROR] running the file `echo.rs` requires `-Zscript` ", ) .run(); @@ -600,7 +600,8 @@ fn script_like_dir() { .with_status(101) .with_stderr( "\ -[ERROR] manifest path `foo.rs` is a directory but expected a file +[ERROR] no such file or subcommand `foo.rs` +`foo.rs` is a directory ", ) .run(); @@ -615,7 +616,7 @@ fn non_existent_rs() { .with_status(101) .with_stderr( "\ -[ERROR] manifest path `foo.rs` does not exist +[ERROR] no such file or subcommand `foo.rs` ", ) .run(); @@ -631,7 +632,7 @@ fn non_existent_rs_stable() { .with_stdout("") .with_stderr( "\ -[ERROR] running `foo.rs` requires `-Zscript` +[ERROR] no such subcommand `foo.rs` ", ) .run(); @@ -649,7 +650,8 @@ fn did_you_mean_file() { .with_stdout("") .with_stderr( "\ -[ERROR] manifest path `foo.rs` does not exist +[ERROR] no such file or subcommand `foo.rs` +Did you mean the file `./food.rs` ", ) .run(); @@ -667,7 +669,8 @@ fn did_you_mean_file_stable() { .with_stdout("") .with_stderr( "\ -[ERROR] running `foo.rs` requires `-Zscript` +[ERROR] no such subcommand `foo.rs` +Did you mean the file `./food.rs` with `-Zscript` ", ) .run(); @@ -683,7 +686,8 @@ fn did_you_mean_command() { .with_stdout("") .with_stderr( "\ -[ERROR] manifest path `build--manifest-path=./Cargo.toml` does not exist +[ERROR] no such file or subcommand `build--manifest-path=./Cargo.toml` +Did you mean the command `build --manifest-path=./Cargo.toml` ", ) .run(); @@ -699,7 +703,8 @@ fn did_you_mean_command_stable() { .with_stdout("") .with_stderr( "\ -[ERROR] running `build--manifest-path=./Cargo.toml` requires `-Zscript` +[ERROR] no such subcommand `build--manifest-path=./Cargo.toml` +Did you mean the command `build --manifest-path=./Cargo.toml` ", ) .run();