fix(shell): preserve quoted args on Windows cmd /C invocation (#1691)#1744
fix(shell): preserve quoted args on Windows cmd /C invocation (#1691)#1744LeoLin990405 wants to merge 2 commits into
Conversation
…#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.
There was a problem hiding this comment.
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.
| if program.eq_ignore_ascii_case("cmd") | ||
| && args.len() == 2 | ||
| && args[0].eq_ignore_ascii_case("/C") |
There was a problem hiding this comment.
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")| ); | ||
| let mut built = Command::new(&spec.program); | ||
| push_shell_args(&mut built, &spec.program, &spec.args); | ||
| assert_eq!(built.get_args().count(), 2); |
There was a problem hiding this comment.
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.
|
Both points addressed in the latest push:
Windows paths are cfg-gated (compile-checked here, executed on Windows CI). macOS build + clippy clean. |
…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.
Summary / 概述
EN: Fixes #1691. On Windows,
git commit -m "feat: complete sub-pages"run by the agent fails witherror: pathspec 'sub-pages"' did not match any file(s)/error: pathspec 'feat:' did not match— the quoted-mmessage 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.exeescaping 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 tostd::process::Commandat the spawn sites incrates/tui/src/tools/shell.rs. Rust std applies MSVCRT (CommandLineToArgvW) escaping, turning the embedded"into\".cmd.exedoes not parse with MSVCRT rules — it treats\literally and"as a bare quote toggle — so the quoting is destroyed andgit(an MSVCRT program) receivesfeat:,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;shlexis 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:、complete、sub-pages"三个独立参数。Unix 路径(sh -c <command>,命令串作为单个 argv 槽)本就正确(已实测)。exec 路径中不存在会切分引号参数的手写分词器;shlex仅用于安全分析、绝不回灌 exec。Fix / 修复
A cfg‑split
push_shell_args(cmd, program, args)helper replacescmd.args(args)at the 3 std spawn sites inshell.rs:cmd /C <payload>shape only, pass/Cand the payload viastd::os::windows::process::CommandExt::raw_arg, suppressing std's per‑arg escaping so the string reachescmd.exeverbatim — exactly as a terminal does. Any other program keeps normal (correct) escaping.cmd.args(args)) — byte‑for‑byte unchanged behavior.portable_pty::CommandBuilder(tty path) is intentionally not touched: different type with noraw_arg, and the reportedgit commitpath is non‑tty foregroundstd::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.exeescaping fix, not speculative process‑control. The Unix/macOS path is provably unchanged (a test assertspush_shell_argsis a pass‑through there). However, the corrupting code is#[cfg(windows)]and the runtime correctness of theraw_argpayload 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 thegit 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_argpayload 的运行时正确性只能在 Windows runner 上验证,macOS/Linux 无法运行时测试。新增测试锁定契约(命令保持单个不切分 argv 槽;Unix 透传)并在 macOS 通过,但合并前请在 Windows CI 确认git commit -m "feat: complete sub-pages"往返。遵循 v0.8.40 审计(#1736)的 Windows 策略,特此显式标注,不谎称已 smoke 验证。Testing / 测试
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.