Skip to content

Commit f4bafa7

Browse files
committed
Auto merge of #13346 - epage:error, r=weihanglo
fix(cli): Improve errors related to cargo script ### What does this PR try to resolve? Fixes #13332 ### How should we test and review this PR? See tests in last commit to see how this changes error messages. This is a lot of duplication with minor tweaking that will go away on stabilization ### Additional information
2 parents 29386b9 + 51b1200 commit f4bafa7

File tree

3 files changed

+217
-15
lines changed

3 files changed

+217
-15
lines changed

src/bin/cargo/commands/run.rs

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use crate::util::restricted_names::is_glob_pattern;
77
use cargo::core::Verbosity;
88
use cargo::core::Workspace;
99
use cargo::ops::{self, CompileFilter, Packages};
10+
use cargo::util::closest;
1011
use cargo_util::ProcessError;
12+
use itertools::Itertools as _;
1113

1214
pub fn cli() -> Command {
1315
subcommand("run")
@@ -97,11 +99,81 @@ pub fn is_manifest_command(arg: &str) -> bool {
9799
}
98100

99101
pub fn exec_manifest_command(config: &mut Config, cmd: &str, args: &[OsString]) -> CliResult {
100-
if !config.cli_unstable().script {
101-
return Err(anyhow::anyhow!("running `{cmd}` requires `-Zscript`").into());
102+
let manifest_path = Path::new(cmd);
103+
match (manifest_path.is_file(), config.cli_unstable().script) {
104+
(true, true) => {}
105+
(true, false) => {
106+
return Err(anyhow::anyhow!("running the file `{cmd}` requires `-Zscript`").into());
107+
}
108+
(false, true) => {
109+
let possible_commands = crate::list_commands(config);
110+
let is_dir = if manifest_path.is_dir() {
111+
format!("\n\t`{cmd}` is a directory")
112+
} else {
113+
"".to_owned()
114+
};
115+
let suggested_command = if let Some(suggested_command) = possible_commands
116+
.keys()
117+
.filter(|c| cmd.starts_with(c.as_str()))
118+
.max_by_key(|c| c.len())
119+
{
120+
let actual_args = cmd.strip_prefix(suggested_command).unwrap();
121+
let args = if args.is_empty() {
122+
"".to_owned()
123+
} else {
124+
format!(
125+
" {}",
126+
args.into_iter().map(|os| os.to_string_lossy()).join(" ")
127+
)
128+
};
129+
format!("\n\tDid you mean the command `{suggested_command} {actual_args}{args}`")
130+
} else {
131+
"".to_owned()
132+
};
133+
let suggested_script = if let Some(suggested_script) = suggested_script(cmd) {
134+
format!("\n\tDid you mean the file `{suggested_script}`")
135+
} else {
136+
"".to_owned()
137+
};
138+
return Err(anyhow::anyhow!(
139+
"no such file or subcommand `{cmd}`{is_dir}{suggested_command}{suggested_script}"
140+
)
141+
.into());
142+
}
143+
(false, false) => {
144+
// HACK: duplicating the above for minor tweaks but this will all go away on
145+
// stabilization
146+
let possible_commands = crate::list_commands(config);
147+
let suggested_command = if let Some(suggested_command) = possible_commands
148+
.keys()
149+
.filter(|c| cmd.starts_with(c.as_str()))
150+
.max_by_key(|c| c.len())
151+
{
152+
let actual_args = cmd.strip_prefix(suggested_command).unwrap();
153+
let args = if args.is_empty() {
154+
"".to_owned()
155+
} else {
156+
format!(
157+
" {}",
158+
args.into_iter().map(|os| os.to_string_lossy()).join(" ")
159+
)
160+
};
161+
format!("\n\tDid you mean the command `{suggested_command} {actual_args}{args}`")
162+
} else {
163+
"".to_owned()
164+
};
165+
let suggested_script = if let Some(suggested_script) = suggested_script(cmd) {
166+
format!("\n\tDid you mean the file `{suggested_script}` with `-Zscript`")
167+
} else {
168+
"".to_owned()
169+
};
170+
return Err(anyhow::anyhow!(
171+
"no such subcommand `{cmd}`{suggested_command}{suggested_script}"
172+
)
173+
.into());
174+
}
102175
}
103176

104-
let manifest_path = Path::new(cmd);
105177
let manifest_path = root_manifest(Some(manifest_path), config)?;
106178

107179
// 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])
123195
cargo::ops::run(&ws, &compile_opts, args).map_err(|err| to_run_error(config, err))
124196
}
125197

198+
fn suggested_script(cmd: &str) -> Option<String> {
199+
let cmd_path = Path::new(cmd);
200+
let mut suggestion = Path::new(".").to_owned();
201+
for cmd_part in cmd_path.components() {
202+
let exact_match = suggestion.join(cmd_part);
203+
suggestion = if exact_match.exists() {
204+
exact_match
205+
} else {
206+
let possible: Vec<_> = std::fs::read_dir(suggestion)
207+
.into_iter()
208+
.flatten()
209+
.filter_map(|e| e.ok())
210+
.map(|e| e.path())
211+
.filter(|p| p.to_str().is_some())
212+
.collect();
213+
if let Some(possible) = closest(
214+
cmd_part.as_os_str().to_str().unwrap(),
215+
possible.iter(),
216+
|p| p.file_name().unwrap().to_str().unwrap(),
217+
) {
218+
possible.to_owned()
219+
} else {
220+
return None;
221+
}
222+
};
223+
}
224+
if suggestion.is_dir() {
225+
None
226+
} else {
227+
suggestion.into_os_string().into_string().ok()
228+
}
229+
}
230+
126231
fn to_run_error(config: &cargo::util::Config, err: anyhow::Error) -> CliError {
127232
let proc_err = match err.downcast_ref::<ProcessError>() {
128233
Some(e) => e,

src/bin/cargo/main.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,19 @@ fn execute_external_subcommand(config: &Config, cmd: &str, args: &[&OsStr]) -> C
178178
let command = match path {
179179
Some(command) => command,
180180
None => {
181+
let script_suggestion = if config.cli_unstable().script
182+
&& std::path::Path::new(cmd).is_file()
183+
{
184+
let sep = std::path::MAIN_SEPARATOR;
185+
format!("\n\tTo run the file `{cmd}`, provide a relative path like `.{sep}{cmd}`")
186+
} else {
187+
"".to_owned()
188+
};
181189
let err = if cmd.starts_with('+') {
182190
anyhow::format_err!(
183-
"no such command: `{}`\n\n\t\
191+
"no such command: `{cmd}`\n\n\t\
184192
Cargo does not handle `+toolchain` directives.\n\t\
185-
Did you mean to invoke `cargo` through `rustup` instead?",
186-
cmd
193+
Did you mean to invoke `cargo` through `rustup` instead?{script_suggestion}",
187194
)
188195
} else {
189196
let suggestions = list_commands(config);
@@ -192,7 +199,7 @@ fn execute_external_subcommand(config: &Config, cmd: &str, args: &[&OsStr]) -> C
192199
anyhow::format_err!(
193200
"no such command: `{cmd}`{did_you_mean}\n\n\t\
194201
View all installed commands with `cargo --list`\n\t\
195-
Find a package to install `{cmd}` with `cargo search cargo-{cmd}`",
202+
Find a package to install `{cmd}` with `cargo search cargo-{cmd}`{script_suggestion}",
196203
)
197204
};
198205

tests/testsuite/script.rs

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ error: no such command: `echo`
109109
110110
<tab>View all installed commands with `cargo --list`
111111
<tab>Find a package to install `echo` with `cargo search cargo-echo`
112+
<tab>To run the file `echo`, provide a relative path like `./echo`
112113
",
113114
)
114115
.run();
@@ -182,7 +183,7 @@ fn requires_nightly() {
182183
.with_stdout("")
183184
.with_stderr(
184185
"\
185-
error: running `echo.rs` requires `-Zscript`
186+
[ERROR] running the file `echo.rs` requires `-Zscript`
186187
",
187188
)
188189
.run();
@@ -200,7 +201,7 @@ fn requires_z_flag() {
200201
.with_stdout("")
201202
.with_stderr(
202203
"\
203-
error: running `echo.rs` requires `-Zscript`
204+
[ERROR] running the file `echo.rs` requires `-Zscript`
204205
",
205206
)
206207
.run();
@@ -591,30 +592,119 @@ args: []
591592
#[cargo_test]
592593
fn script_like_dir() {
593594
let p = cargo_test_support::project()
594-
.file("script.rs/foo", "something")
595+
.file("foo.rs/foo", "something")
595596
.build();
596597

597-
p.cargo("-Zscript -v script.rs")
598+
p.cargo("-Zscript -v foo.rs")
598599
.masquerade_as_nightly_cargo(&["script"])
599600
.with_status(101)
600601
.with_stderr(
601602
"\
602-
error: manifest path `script.rs` is a directory but expected a file
603+
[ERROR] no such file or subcommand `foo.rs`
604+
<tab>`foo.rs` is a directory
603605
",
604606
)
605607
.run();
606608
}
607609

608610
#[cargo_test]
609-
fn missing_script_rs() {
611+
fn non_existent_rs() {
610612
let p = cargo_test_support::project().build();
611613

612-
p.cargo("-Zscript -v script.rs")
614+
p.cargo("-Zscript -v foo.rs")
613615
.masquerade_as_nightly_cargo(&["script"])
614616
.with_status(101)
615617
.with_stderr(
616618
"\
617-
[ERROR] manifest path `script.rs` does not exist
619+
[ERROR] no such file or subcommand `foo.rs`
620+
",
621+
)
622+
.run();
623+
}
624+
625+
#[cargo_test]
626+
fn non_existent_rs_stable() {
627+
let p = cargo_test_support::project().build();
628+
629+
p.cargo("-v foo.rs")
630+
.masquerade_as_nightly_cargo(&["script"])
631+
.with_status(101)
632+
.with_stdout("")
633+
.with_stderr(
634+
"\
635+
[ERROR] no such subcommand `foo.rs`
636+
",
637+
)
638+
.run();
639+
}
640+
641+
#[cargo_test]
642+
fn did_you_mean_file() {
643+
let p = cargo_test_support::project()
644+
.file("food.rs", ECHO_SCRIPT)
645+
.build();
646+
647+
p.cargo("-Zscript -v foo.rs")
648+
.masquerade_as_nightly_cargo(&["script"])
649+
.with_status(101)
650+
.with_stdout("")
651+
.with_stderr(
652+
"\
653+
[ERROR] no such file or subcommand `foo.rs`
654+
<tab>Did you mean the file `./food.rs`
655+
",
656+
)
657+
.run();
658+
}
659+
660+
#[cargo_test]
661+
fn did_you_mean_file_stable() {
662+
let p = cargo_test_support::project()
663+
.file("food.rs", ECHO_SCRIPT)
664+
.build();
665+
666+
p.cargo("-v foo.rs")
667+
.masquerade_as_nightly_cargo(&["script"])
668+
.with_status(101)
669+
.with_stdout("")
670+
.with_stderr(
671+
"\
672+
[ERROR] no such subcommand `foo.rs`
673+
<tab>Did you mean the file `./food.rs` with `-Zscript`
674+
",
675+
)
676+
.run();
677+
}
678+
679+
#[cargo_test]
680+
fn did_you_mean_command() {
681+
let p = cargo_test_support::project().build();
682+
683+
p.cargo("-Zscript -v build--manifest-path=./Cargo.toml")
684+
.masquerade_as_nightly_cargo(&["script"])
685+
.with_status(101)
686+
.with_stdout("")
687+
.with_stderr(
688+
"\
689+
[ERROR] no such file or subcommand `build--manifest-path=./Cargo.toml`
690+
<tab>Did you mean the command `build --manifest-path=./Cargo.toml`
691+
",
692+
)
693+
.run();
694+
}
695+
696+
#[cargo_test]
697+
fn did_you_mean_command_stable() {
698+
let p = cargo_test_support::project().build();
699+
700+
p.cargo("-v build--manifest-path=./Cargo.toml")
701+
.masquerade_as_nightly_cargo(&["script"])
702+
.with_status(101)
703+
.with_stdout("")
704+
.with_stderr(
705+
"\
706+
[ERROR] no such subcommand `build--manifest-path=./Cargo.toml`
707+
<tab>Did you mean the command `build --manifest-path=./Cargo.toml`
618708
",
619709
)
620710
.run();

0 commit comments

Comments
 (0)