diff --git a/src/commands.rs b/src/commands.rs index 11baafa..72111f2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,6 +6,7 @@ mod cd; mod echo; mod environment; mod exit; +mod introspection; mod pwd; mod source; mod status; @@ -55,11 +56,13 @@ pub(crate) fn run( ) -> Result> { let result = match name { "cd" => cd::run(shell, argv)?, + "command" => introspection::command(shell, argv), "pwd" => pwd::run()?, "exit" => exit::run(argv), "export" => environment::export(shell, argv, env_overlay), "unset" => environment::unset(shell, argv), "set" => environment::set(shell)?, + "type" => introspection::type_(shell, argv), "true" => status::true_(), "false" => status::false_(), "echo" => echo::run(argv)?, @@ -69,3 +72,21 @@ pub(crate) fn run( Ok(Some(result)) } + +pub(crate) fn is_builtin(name: &str) -> bool { + matches!( + name, + "." | "cd" + | "command" + | "echo" + | "exit" + | "export" + | "false" + | "pwd" + | "set" + | "source" + | "true" + | "type" + | "unset" + ) +} diff --git a/src/commands/introspection.rs b/src/commands/introspection.rs new file mode 100644 index 0000000..4801438 --- /dev/null +++ b/src/commands/introspection.rs @@ -0,0 +1,62 @@ +use crate::commands::{BuiltinResult, is_builtin}; +use crate::path::display_path; +use crate::runtime::Shell; + +pub(crate) fn command(shell: &Shell, argv: &[String]) -> BuiltinResult { + match argv.first().map(String::as_str) { + Some("-v") => command_v(shell, &argv[1..]), + Some(option) if option.starts_with('-') => BuiltinResult::stderr( + 2, + format!("command: unsupported option: {option}\n").into_bytes(), + ), + Some(_) => BuiltinResult::stderr( + 2, + b"command: only command -v is currently supported\n".to_vec(), + ), + None => BuiltinResult::success(), + } +} + +pub(crate) fn type_(shell: &Shell, argv: &[String]) -> BuiltinResult { + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut status = 0; + + for name in argv { + if is_builtin(name) { + stdout.extend_from_slice(format!("{name} is a shell builtin\n").as_bytes()); + } else if let Some(path) = shell.resolve_program(name) { + stdout.extend_from_slice(format!("{name} is {}\n", display_path(&path)).as_bytes()); + } else { + status = 1; + stderr.extend_from_slice(format!("type: {name}: not found\n").as_bytes()); + } + } + + BuiltinResult { + status, + stdout, + stderr, + } +} + +fn command_v(shell: &Shell, names: &[String]) -> BuiltinResult { + let mut stdout = Vec::new(); + let mut status = 0; + + for name in names { + if is_builtin(name) { + stdout.extend_from_slice(format!("{name}\n").as_bytes()); + } else if let Some(path) = shell.resolve_program(name) { + stdout.extend_from_slice(format!("{}\n", display_path(&path)).as_bytes()); + } else { + status = 1; + } + } + + BuiltinResult { + status, + stdout, + stderr: Vec::new(), + } +} diff --git a/src/runtime.rs b/src/runtime.rs index 15567a1..d16db5d 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -73,6 +73,10 @@ impl Shell { Ok((status, stdout)) } + pub(crate) fn resolve_program(&self, name: &str) -> Option { + resolve_program(name, &self.vars).ok() + } + fn run_pipeline(&mut self, pipeline: &Pipeline) -> Result { Ok(self.run_pipeline_inner(pipeline, false)?.status) } diff --git a/tests/cli.rs b/tests/cli.rs index c881890..702ef6b 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -63,3 +63,28 @@ fn failed_cd_does_not_run_argument_as_command() { assert_eq!(output.status.code(), Some(1)); assert!(output.stdout.is_empty()); } + +#[test] +fn command_v_reports_builtins_and_missing_commands() { + let output = Command::new(env!("CARGO_BIN_EXE_rysh")) + .args(["-c", "command -v cd definitely_missing_rysh_command"]) + .output() + .unwrap(); + + assert_eq!(output.status.code(), Some(1)); + assert_eq!(String::from_utf8_lossy(&output.stdout), "cd\n"); +} + +#[test] +fn type_reports_builtins() { + let output = Command::new(env!("CARGO_BIN_EXE_rysh")) + .args(["-c", "type cd"]) + .output() + .unwrap(); + + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "cd is a shell builtin\n" + ); +}