From 0ce7cbb38a72fcb6917bcd0d0e671c3236baddf6 Mon Sep 17 00:00:00 2001 From: June Kim Date: Sat, 9 May 2026 10:09:31 -0700 Subject: [PATCH 1/3] fix: hint shell builtins in command-not-found error (#1944) When a user runs `fd -x cd` or similar shell builtins that lack standalone executables, the "Command not found" error now explains that the command is a shell builtin and suggests invoking it through a shell explicitly. --- src/exec/command.rs | 73 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/exec/command.rs b/src/exec/command.rs index 89522b029..fd728a869 100644 --- a/src/exec/command.rs +++ b/src/exec/command.rs @@ -98,13 +98,37 @@ pub fn execute_commands>>( ExitCode::Success } +/// Common shell builtins that typically do not exist as standalone executables. +/// When fd encounters a "command not found" error for one of these, it hints +/// that the user may be trying to use a shell builtin. +const SHELL_BUILTINS: &[&str] = &[ + ".", "alias", "bg", "bind", "cd", "command", "declare", "dirs", "eval", "exec", "exit", + "export", "fg", "hash", "help", "history", "jobs", "let", "local", "logout", "popd", "pushd", + "read", "readonly", "return", "set", "shift", "shopt", "source", "suspend", "times", "trap", + "type", "typeset", "unalias", "unset", "wait", +]; + +fn is_shell_builtin(program: &str) -> bool { + SHELL_BUILTINS.iter().any(|&b| b == program) +} + +fn command_not_found_message(program: &str) -> String { + if is_shell_builtin(program) { + format!( + "Command not found: {program}. Note: {program} is a shell builtin, \ + not a standalone program. To run shell builtins, invoke a shell explicitly, \ + e.g. fd -x sh -c '{program} ... \"$1\"' sh {{}}", + ) + } else { + format!("Command not found: {program}") + } +} + pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode { match (cmd, err) { (Some(cmd), err) if err.kind() == io::ErrorKind::NotFound => { - print_error(format!( - "Command not found: {}", - cmd.get_program().to_string_lossy() - )); + let program = cmd.get_program().to_string_lossy(); + print_error(command_not_found_message(&program)); ExitCode::GeneralError } (_, err) => { @@ -113,3 +137,44 @@ pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode { } } } + +#[cfg(test)] +mod builtin_tests { + use super::*; + + #[test] + fn detects_known_builtins() { + assert!(is_shell_builtin("cd")); + assert!(is_shell_builtin("export")); + assert!(is_shell_builtin("source")); + assert!(is_shell_builtin("eval")); + assert!(is_shell_builtin(".")); + } + + #[test] + fn rejects_non_builtins() { + assert!(!is_shell_builtin("grep")); + assert!(!is_shell_builtin("ls")); + assert!(!is_shell_builtin("")); + assert!(!is_shell_builtin("CD")); + // These typically exist as standalone executables + assert!(!is_shell_builtin("echo")); + assert!(!is_shell_builtin("printf")); + assert!(!is_shell_builtin("test")); + } + + #[test] + fn builtin_message_includes_hint() { + let msg = command_not_found_message("cd"); + assert!(msg.starts_with("Command not found: cd.")); + assert!(msg.contains("shell builtin")); + assert!(msg.contains("sh -c")); + } + + #[test] + fn non_builtin_message_is_plain() { + let msg = command_not_found_message("nonexistent"); + assert_eq!(msg, "Command not found: nonexistent"); + assert!(!msg.contains("shell builtin")); + } +} From 1fd9e70cc3e7fe75a45bdb8177ac0ff69a15b301 Mon Sep 17 00:00:00 2001 From: June Kim Date: Sat, 9 May 2026 10:15:57 -0700 Subject: [PATCH 2/3] changelog: add shell builtin hint entry for #1944 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08e0ed1e8..31f141785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ## Bugfixes - Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir). - Fire the "search pattern contains a path separator" diagnostic for any pattern containing `/`, not just patterns that happen to name an existing directory. Preserves the legacy Windows behaviour that also flags native `\` separators when the pattern resolves to a real directory. See #1873. +- Hint that a command is a shell builtin when `-x`/`-X` fails with "command not found", see #1944 (@kimjune01) # 10.4.2 From 1f27576d8e8b64a07a35a091b5aaf9fcb62335be Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 03:23:45 +0800 Subject: [PATCH 3/3] fix: use contains() for shell builtin check (clippy) Co-authored-by: Cursor --- src/exec/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exec/command.rs b/src/exec/command.rs index fd728a869..6907a5750 100644 --- a/src/exec/command.rs +++ b/src/exec/command.rs @@ -109,7 +109,7 @@ const SHELL_BUILTINS: &[&str] = &[ ]; fn is_shell_builtin(program: &str) -> bool { - SHELL_BUILTINS.iter().any(|&b| b == program) + SHELL_BUILTINS.contains(&program) } fn command_not_found_message(program: &str) -> String {