diff --git a/src/shell/commands/mod.rs b/src/shell/commands/mod.rs index 0c6dbdd..df3498e 100644 --- a/src/shell/commands/mod.rs +++ b/src/shell/commands/mod.rs @@ -98,6 +98,12 @@ pub fn builtin_commands() -> HashMap> { "false".to_string(), Rc::new(ExitCodeCommand(1)) as Rc, ), + // POSIX null command: discards its arguments and always exits 0. + // Commonly seen as `"test": ":"` in package.json no-op scripts. + ( + ":".to_string(), + Rc::new(ExitCodeCommand(0)) as Rc, + ), ( "unset".to_string(), Rc::new(unset::UnsetCommand) as Rc, diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 2a8f905..a471bdc 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -154,6 +154,24 @@ async fn boolean_logic() { .await; } +#[tokio::test] +async fn null_command() { + // POSIX `:` is the null command — always succeeds, discards args. + TestBuilder::new().command(":").run().await; + + TestBuilder::new() + .command(": ignored args && echo ok") + .assert_stdout("ok\n") + .run() + .await; + + TestBuilder::new() + .command(": && echo yes || echo no") + .assert_stdout("yes\n") + .run() + .await; +} + #[tokio::test] async fn exit() { TestBuilder::new()