Skip to content

fix(shell): preserve quoted args on Windows cmd /C invocation (#1691)#1744

Closed
LeoLin990405 wants to merge 2 commits into
Hmbown:mainfrom
LeoLin990405:fix/1691-shell-arg-quoting
Closed

fix(shell): preserve quoted args on Windows cmd /C invocation (#1691)#1744
LeoLin990405 wants to merge 2 commits into
Hmbown:mainfrom
LeoLin990405:fix/1691-shell-arg-quoting

Conversation

@LeoLin990405

Copy link
Copy Markdown
Contributor

Summary / 概述

EN: Fixes #1691. On Windows, git commit -m "feat: complete sub-pages" run by the agent fails with error: pathspec 'sub-pages"' did not match any file(s) / error: pathspec 'feat:' did not match — the quoted -m message is split on spaces and a stray " leaks into a token. The same command works when typed in a terminal. This is not a DeepSeek‑TUI tokenizer bug; it is a Rust‑std ↔ cmd.exe escaping mismatch on the Windows shell‑invocation path.

中文: 修复 #1691。Windows 上,agent 执行 git commit -m "feat: complete sub-pages"error: pathspec 'sub-pages"' did not match any file(s) / error: pathspec 'feat:' did not match——带空格的 -m 引号消息被按空格切开、且尾引号 " 残留进了 token;同样命令在终端手敲却正常。这不是 DeepSeek‑TUI 自己的分词 bug,而是 Windows shell 调用路径上 Rust 标准库与 cmd.exe 的转义规则不匹配。

Root cause / 根因

EN: CommandSpec::shell() (crates/tui/src/sandbox/mod.rs) builds, on Windows, cmd /C "chcp 65001 >NUL & <command>". That payload is handed to std::process::Command at the spawn sites in crates/tui/src/tools/shell.rs. Rust std applies MSVCRT (CommandLineToArgvW) escaping, turning the embedded " into \". cmd.exe does not parse with MSVCRT rules — it treats \ literally and " as a bare quote toggle — so the quoting is destroyed and git (an MSVCRT program) receives feat:, complete, sub-pages" as separate argv entries. The Unix path (sh -c <command>, command string passed as a single argv slot) was already correct — verified empirically. There is no hand‑rolled tokenizer that splits quoted args anywhere in the exec path; shlex is used only for safety analysis and never feeds exec.

中文: CommandSpec::shell()crates/tui/src/sandbox/mod.rs)在 Windows 上构造 cmd /C "chcp 65001 >NUL & <command>",该 payload 在 crates/tui/src/tools/shell.rs 的 spawn 处交给 std::process::Command。Rust 标准库套用 MSVCRT(CommandLineToArgvW)转义,把内嵌 " 变成 \"cmd.exe 不按 MSVCRT 规则解析——它把 \ 当字面量、" 当裸引号开关——于是引号被破坏,git(MSVCRT 程序)收到 feat:completesub-pages" 三个独立参数。Unix 路径(sh -c <command>,命令串作为单个 argv 槽)本就正确(已实测)。exec 路径中不存在会切分引号参数的手写分词器;shlex 仅用于安全分析、绝不回灌 exec。

Fix / 修复

A cfg‑split push_shell_args(cmd, program, args) helper replaces cmd.args(args) at the 3 std spawn sites in shell.rs:

  • Windows: for the cmd /C <payload> shape only, pass /C and the payload via std::os::windows::process::CommandExt::raw_arg, suppressing std's per‑arg escaping so the string reaches cmd.exe verbatim — exactly as a terminal does. Any other program keeps normal (correct) escaping.
  • Non‑Windows: faithful pass‑through (cmd.args(args)) — byte‑for‑byte unchanged behavior.

portable_pty::CommandBuilder (tty path) is intentionally not touched: different type with no raw_arg, and the reported git commit path is non‑tty foreground std::Command. Out of scope for this fix.

中文: 新增按平台切分的 push_shell_args(cmd, program, args),替换 shell.rs 中 3 处 std spawn 的 cmd.args(args):Windows 仅对 cmd /C <payload> 形态用 CommandExt::raw_arg 透传,绕过 std 的逐参转义,让字符串原样抵达 cmd.exe(与终端一致),其他程序保持正常转义;非 Windows 为忠实透传,行为逐字节不变。pty 路径(portable_pty::CommandBuilder)刻意不动,超出范围。

Honest scope — please verify on Windows CI / 如实说明——请在 Windows CI 验证

EN: This is a well‑understood, documented std↔cmd.exe escaping fix, not speculative process‑control. The Unix/macOS path is provably unchanged (a test asserts push_shell_args is a pass‑through there). However, the corrupting code is #[cfg(windows)] and the runtime correctness of the raw_arg payload can only be verified on a Windows runner — it is not runtime‑testable on macOS/Linux. The added tests pin the contract (command stays one unsplit argv slot; Unix pass‑through) and pass on macOS, but please confirm the git commit -m "feat: complete sub-pages" round‑trip on Windows CI before merge. Per the v0.8.40 audit (#1736) Windows policy, flagging this explicitly rather than claiming a smoke‑tested fix.

中文: 这是一处机理清晰、有文档依据的 std↔cmd.exe 转义修复,并非臆测性的进程控制改动。Unix/macOS 路径可证明不变(有测试断言此处为透传)。但出错代码是 #[cfg(windows)]raw_arg payload 的运行时正确性只能在 Windows runner 上验证,macOS/Linux 无法运行时测试。新增测试锁定契约(命令保持单个不切分 argv 槽;Unix 透传)并在 macOS 通过,但合并前请在 Windows CI 确认 git commit -m "feat: complete sub-pages" 往返。遵循 v0.8.40 审计(#1736)的 Windows 策略,特此显式标注,不谎称已 smoke 验证。

Testing / 测试

cargo test -p deepseek-tui --bin deepseek-tui -- \
  issue_1691_quoted_commit_message_round_trips test_command_spec_shell
# 3 passed; 0 failed  (incl. pre-existing test_command_spec_shell — no regression)

New tests: sandbox::tests::test_command_spec_shell_quoted_arg_not_split, tools::shell::tests::issue_1691_quoted_commit_message_round_trips.

Refs #1691, #1736.

…#1691)

git commit -m "feat: complete sub-pages" failed on Windows with
'pathspec sub-pages" did not match' because the quoted -m message was
split on spaces. Root cause is not a tokenizer bug: CommandSpec::shell()
builds 'cmd /C "chcp 65001 >/dev/null & <command>"' and std::process::Command
applies MSVCRT escaping (" -> \"); cmd.exe does not use MSVCRT parsing,
so the quoting is destroyed and git receives feat:/complete/sub-pages"
as separate pathspecs. The Unix sh -c path was already correct.

Add a cfg-split push_shell_args() replacing cmd.args() at the 3 std
spawn sites in shell.rs. On Windows, for the cmd /C <payload> shape
only, pass /C and payload via CommandExt::raw_arg so the string reaches
cmd.exe verbatim (as a terminal does); other programs keep normal
escaping. Non-Windows is a faithful pass-through (byte-for-byte
unchanged). portable_pty path intentionally untouched (out of scope).

The Unix path is provably unchanged (tested); the Windows raw_arg
runtime correctness is only verifiable on a Windows runner -- flagged in
the PR for Windows CI verification per the Hmbown#1736 Windows policy.

Refs Hmbown#1691, Hmbown#1736.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR fixes issue #1691 where quoted shell arguments were incorrectly escaped on Windows by introducing a push_shell_args helper that uses raw_arg for cmd /C invocations. Feedback suggests improving the robustness of the cmd detection logic to handle absolute paths or extensions and strengthening test assertions to verify the actual content of the arguments.

Comment thread crates/tui/src/tools/shell.rs Outdated
Comment on lines +196 to +198
if program.eq_ignore_ascii_case("cmd")
&& args.len() == 2
&& args[0].eq_ignore_ascii_case("/C")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check for the cmd program name is currently a strict string comparison. If the program string contains a full path (e.g., C:\Windows\System32\cmd.exe) or includes the .exe extension, this check will fail, and the fix for quoted arguments will not be applied.

Consider using a more robust check that looks at the file stem of the program path to ensure the fix is applied regardless of how the shell is invoked.

    let is_cmd = Path::new(program)
        .file_stem()
        .and_then(|s| s.to_str())
        .map(|s| s.eq_ignore_ascii_case("cmd"))
        .unwrap_or(false);
    if is_cmd
        && args.len() == 2
        && args[0].eq_ignore_ascii_case("/C")

Comment thread crates/tui/src/tools/shell/tests.rs Outdated
);
let mut built = Command::new(&spec.program);
push_shell_args(&mut built, &spec.program, &spec.args);
assert_eq!(built.get_args().count(), 2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion only verifies the number of arguments, not their content. To ensure that the raw_arg call correctly preserved the payload (including quotes) and didn't introduce unexpected escaping, it would be better to verify the actual argument values against the expected specification.

        let got: Vec<String> = built
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_eq!(got, spec.args);

…bown#1744)

Address gemini-code-assist review on PR Hmbown#1744 (two MEDIUM):

- cmd detection used program.eq_ignore_ascii_case("cmd"), which fails
  for a full path (C:\Windows\System32\cmd.exe) or a .exe suffix, so the
  raw_arg quoting fix would not apply. Use Path::file_stem() instead
  (fully-qualified std::path::Path -> no unused import off-Windows).
- Strengthen the Windows block of issue_1691_quoted_commit_message_round_trips
  to assert argv content equals spec.args, not just arg count, so the
  raw_arg payload (quotes preserved, no extra escaping) is actually
  verified. sandbox/mod.rs already asserts content -- left untouched.

Windows paths are cfg-gated (compile-checked on macOS, executed on
Windows CI). macOS build + clippy clean.

Refs Hmbown#1691.
@LeoLin990405

Copy link
Copy Markdown
Contributor Author

Both points addressed in the latest push:

  • Robust cmd detection: replaced program.eq_ignore_ascii_case("cmd") with a std::path::Path::new(program).file_stem() check, so a full path (C:\Windows\System32\cmd.exe) or a .exe suffix still triggers the raw_arg path. Fully-qualified std::path::Path so there's no unused-import warning off-Windows.
  • Stronger assertion: the Windows block of issue_1691_quoted_commit_message_round_trips now asserts built.get_args() content equals spec.args (quotes preserved, no extra escaping), not just the arg count. sandbox/mod.rs already asserted content, so it was left untouched.

Windows paths are cfg-gated (compile-checked here, executed on Windows CI). macOS build + clippy clean.

@Hmbown Hmbown mentioned this pull request May 20, 2026
11 tasks
@Hmbown

Hmbown commented May 21, 2026

Copy link
Copy Markdown
Owner

Thanks for the Windows cmd quoting fix. This was harvested into v0.8.40 release PR #1823 as commits b3223cd and 0ce4150, and #1823 is now CI-green. Closing as superseded by the release branch; thank you for the careful Windows shell report and patch.

@Hmbown Hmbown closed this May 21, 2026
getong pushed a commit to getong/CodeWhale that referenced this pull request May 21, 2026
…bown#1744)

Address gemini-code-assist review on PR Hmbown#1744 (two MEDIUM):

- cmd detection used program.eq_ignore_ascii_case("cmd"), which fails
  for a full path (C:\Windows\System32\cmd.exe) or a .exe suffix, so the
  raw_arg quoting fix would not apply. Use Path::file_stem() instead
  (fully-qualified std::path::Path -> no unused import off-Windows).
- Strengthen the Windows block of issue_1691_quoted_commit_message_round_trips
  to assert argv content equals spec.args, not just arg count, so the
  raw_arg payload (quotes preserved, no extra escaping) is actually
  verified. sandbox/mod.rs already asserts content -- left untouched.

Windows paths are cfg-gated (compile-checked on macOS, executed on
Windows CI). macOS build + clippy clean.

Refs Hmbown#1691.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug Report: Git Commit Message with Spaces Causes Pathspec Parse Error

2 participants