From 1a11d71f18bb6838ef82c8eaf5b3f3ca717d8de7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 15:11:21 +0000 Subject: [PATCH] Fix time command losing quoted arguments when re-parsing The `time` builtin joined its arguments with spaces and re-parsed them, which destroyed quoting information. For example, `time python -c "import torch"` would pass just `import` to Python instead of `import torch`, and `time python -c "print('ok')"` would fail to parse due to unquoted parentheses. Fix by shell-quoting arguments that contain whitespace or special characters before joining and re-parsing. https://claude.ai/code/session_01PQyPNXA8mZKTLkNfytRunv --- crates/shell/src/commands/time.rs | 23 ++++++++++++++++++++++- crates/tests/src/lib.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/shell/src/commands/time.rs b/crates/shell/src/commands/time.rs index 2838fae..f164fd4 100644 --- a/crates/shell/src/commands/time.rs +++ b/crates/shell/src/commands/time.rs @@ -97,7 +97,28 @@ async fn execute_time(context: &mut ShellCommandContext) -> Result<(), i32> { return Err(1); } - let command_line = context.args.join(" "); + let command_line = context + .args + .iter() + .map(|arg| { + if arg.is_empty() { + "''".to_string() + } else if arg.contains(|c: char| { + c.is_whitespace() + || matches!( + c, + '\'' | '"' | '\\' | '$' | '(' | ')' | '|' | '&' | ';' | '<' | '>' + | '`' | '!' | '{' | '}' | '[' | ']' | '*' | '?' | '#' | '~' + ) + }) { + // Wrap in single quotes, escaping any internal single quotes + format!("'{}'", arg.replace('\'', "'\\''")) + } else { + arg.clone() + } + }) + .collect::>() + .join(" "); #[cfg(unix)] let before_usage = get_resource_usage(); diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 22e42b1..3625288 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -2218,6 +2218,34 @@ async fn test_brace_group() { .await; } +#[tokio::test] +async fn time_preserves_quoted_args() { + // time should preserve quoted arguments when re-parsing the command + // Note: time's execute uses raw stdout, so we can't assert_stdout here. + // Instead we verify commands succeed (exit code 0) and stderr contains timing info. + TestBuilder::new() + .command(r#"time echo "hello world""#) + .assert_stderr_contains("real") + .assert_exit_code(0) + .run() + .await; + + TestBuilder::new() + .command(r#"time echo 'hello world'"#) + .assert_stderr_contains("real") + .assert_exit_code(0) + .run() + .await; + + // Arguments with special shell characters should be preserved + TestBuilder::new() + .command(r#"time echo "foo(bar)""#) + .assert_stderr_contains("real") + .assert_exit_code(0) + .run() + .await; +} + #[cfg(test)] fn no_such_file_error_text() -> &'static str { if cfg!(windows) {