From 713ee63a51e490643b5b9a95d5e2188f4a2590f0 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:58:49 -0400 Subject: [PATCH 1/6] test(rust): pin MS C-runtime quoting for Windows argv --- packages/rust/src/command.rs | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs index ecef38e42..63d249147 100644 --- a/packages/rust/src/command.rs +++ b/packages/rust/src/command.rs @@ -244,6 +244,15 @@ pub(crate) fn build_argv( Ok(argv) } +/// Quote `arg` for a Windows command line by the MS C-runtime rules, always +/// wrapped in double quotes: an MSYS/Cygwin child globs, brace-expands and +/// unescapes any unquoted word, while an MSVCRT child parses the quoted form +/// back to `arg` exactly. +#[cfg_attr(not(windows), allow(dead_code))] +pub(crate) fn quote_windows_arg(arg: &str) -> String { + arg.to_string() +} + /// Replace every `{name}` in `token` with its placeholder value in a single /// left-to-right pass. Injected values are never re-scanned, so an untrusted /// value containing `{…}` is inert. Unknown `{…}` sequences are left literal. @@ -402,6 +411,47 @@ mod tests { assert_eq!(argv("run", &[Placeholder::new("path", "/tmp/x")]), ["run"]); } + #[test] + fn windows_quoting_wraps_a_plain_arg_in_quotes() { + assert_eq!(quote_windows_arg("hello"), r#""hello""#); + } + + #[test] + fn windows_quoting_keeps_an_empty_arg_as_an_empty_quoted_word() { + assert_eq!(quote_windows_arg(""), r#""""#); + } + + #[test] + fn windows_quoting_quotes_glob_and_brace_characters() { + assert_eq!(quote_windows_arg("[{}]"), r#""[{}]""#); + assert_eq!(quote_windows_arg("a?b*c"), r#""a?b*c""#); + } + + #[test] + fn windows_quoting_leaves_backslashes_not_before_a_quote_alone() { + assert_eq!( + quote_windows_arg(r"\\?\C:\Users\x.txt"), + r#""\\?\C:\Users\x.txt""# + ); + } + + #[test] + fn windows_quoting_doubles_trailing_backslashes_before_the_closing_quote() { + assert_eq!(quote_windows_arg(r"C:\dir\"), r#""C:\dir\\""#); + assert_eq!(quote_windows_arg(r"a\\"), r#""a\\\\""#); + } + + #[test] + fn windows_quoting_escapes_embedded_quotes() { + assert_eq!(quote_windows_arg(r#"[{"n":1}]"#), r#""[{\"n\":1}]""#); + } + + #[test] + fn windows_quoting_doubles_backslashes_before_an_embedded_quote() { + assert_eq!(quote_windows_arg(r#"a\"b"#), r#""a\\\"b""#); + assert_eq!(quote_windows_arg(r#"a\\"b"#), r#""a\\\\\"b""#); + } + #[test] fn extract_payload_takes_the_last_non_empty_line() { assert_eq!( From 009c015ffa36453da6d92ddec17b5b655b51372f Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:03:15 -0400 Subject: [PATCH 2/6] fix(rust): MS-quote every on-file argv element on Windows --- ...2026-10-02-windows-on-file-argv-quoting.md | 1 + packages/rust/src/command.rs | 39 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md diff --git a/packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md b/packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md new file mode 100644 index 000000000..0b504b260 --- /dev/null +++ b/packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md @@ -0,0 +1 @@ +**Fixed** On Windows, every argument of an `on-file` command is now passed double-quoted by the MS C-runtime rules, so an MSYS/Cygwin child (Git for Windows' `sh`, `printf`, `cat`) no longer glob- or brace-expands it. `on-file = "printf '[{}]'"` now yields its rows instead of none. diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs index 63d249147..285a2336f 100644 --- a/packages/rust/src/command.rs +++ b/packages/rust/src/command.rs @@ -119,8 +119,8 @@ pub fn run_command( let argv = build_argv(command, placeholders)?; // `build_argv` guarantees a non-empty argv. let mut cmd = Command::new(&argv[0]); - cmd.args(&argv[1..]) - .current_dir(cwd) + push_args(&mut cmd, &argv[1..]); + cmd.current_dir(cwd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(if stdin_payload.is_some() { @@ -193,6 +193,19 @@ pub fn run_command( } } +#[cfg(windows)] +fn push_args(cmd: &mut Command, args: &[String]) { + use std::os::windows::process::CommandExt; + for arg in args { + cmd.raw_arg(quote_windows_arg(arg)); + } +} + +#[cfg(not(windows))] +fn push_args(cmd: &mut Command, args: &[String]) { + cmd.args(args); +} + fn spawn_error(program: &str, source: std::io::Error, cwd: &Path) -> CommandError { // A missing cwd fails the child's chdir with `NotFound` too. if !searched_path(program, source.kind()) || !cwd.is_dir() { @@ -250,7 +263,27 @@ pub(crate) fn build_argv( /// back to `arg` exactly. #[cfg_attr(not(windows), allow(dead_code))] pub(crate) fn quote_windows_arg(arg: &str) -> String { - arg.to_string() + let mut out = String::with_capacity(arg.len() + 2); + out.push('"'); + let mut backslashes = 0; + for ch in arg.chars() { + match ch { + '\\' => backslashes += 1, + '"' => { + out.extend(std::iter::repeat_n('\\', backslashes * 2 + 1)); + out.push('"'); + backslashes = 0; + } + _ => { + out.extend(std::iter::repeat_n('\\', backslashes)); + out.push(ch); + backslashes = 0; + } + } + } + out.extend(std::iter::repeat_n('\\', backslashes * 2)); + out.push('"'); + out } /// Replace every `{name}` in `token` with its placeholder value in a single From d2e6cb09a48afa50f165ca6466233e0446dc7de7 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:13:37 -0400 Subject: [PATCH 3/6] e2e attestation for claude/1207-win-argv-quote --- .../e2e-attestations/claude-1207-win-argv-quote.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1207-win-argv-quote.json diff --git a/packages/python/e2e-attestations/claude-1207-win-argv-quote.json b/packages/python/e2e-attestations/claude-1207-win-argv-quote.json new file mode 100644 index 000000000..66256009f --- /dev/null +++ b/packages/python/e2e-attestations/claude-1207-win-argv-quote.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1790939617, + "exit_code": 0, + "commit": "009c015ffa36453da6d92ddec17b5b655b51372f", + "branch": "claude/1207-win-argv-quote" +} From a0f297e4118b4e6f5f8b217e4d422bc85f1bdbb5 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:14:49 -0400 Subject: [PATCH 4/6] e2e attestation for claude/1207-win-argv-quote --- .../ts/e2e-attestations/claude-1207-win-argv-quote.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1207-win-argv-quote.json diff --git a/packages/ts/e2e-attestations/claude-1207-win-argv-quote.json b/packages/ts/e2e-attestations/claude-1207-win-argv-quote.json new file mode 100644 index 000000000..7995af5a2 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1207-win-argv-quote.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1790939689, + "exit_code": 0, + "commit": "d2e6cb09a48afa50f165ca6466233e0446dc7de7", + "branch": "claude/1207-win-argv-quote" +} From 094015d85768e8aea01f5235d55d91c50049cdd1 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:18:04 -0400 Subject: [PATCH 5/6] refactor(rust): fold the Windows argv wiring into one fn --- packages/rust/src/command.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs index 285a2336f..90d250c0f 100644 --- a/packages/rust/src/command.rs +++ b/packages/rust/src/command.rs @@ -193,16 +193,17 @@ pub fn run_command( } } -#[cfg(windows)] +// One fn with cfg blocks rather than two cfg'd fns: cargo-mutants mutates +// the uncompiled twin too, and no test on a Linux gate can kill that mutant. fn push_args(cmd: &mut Command, args: &[String]) { - use std::os::windows::process::CommandExt; - for arg in args { - cmd.raw_arg(quote_windows_arg(arg)); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + for arg in args { + cmd.raw_arg(quote_windows_arg(arg)); + } } -} - -#[cfg(not(windows))] -fn push_args(cmd: &mut Command, args: &[String]) { + #[cfg(not(windows))] cmd.args(args); } From d4bdc556590a03e1176fc40bcb0988a86b038a9a Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:34:59 -0400 Subject: [PATCH 6/6] docs(rust): migration fragment for Windows on-file argv quoting --- ...2026-10-02-windows-on-file-argv-quoting.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md diff --git a/packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md b/packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md new file mode 100644 index 000000000..81c1102fd --- /dev/null +++ b/packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md @@ -0,0 +1,42 @@ +### Windows on-file arguments reach MSYS children unexpanded + +**Summary** + +On Windows, every argument of an `on-file` command is now passed double-quoted +by the MS C-runtime rules. An MSYS/Cygwin child (Git for Windows' `sh`, +`printf`, `cat`) previously glob- and brace-expanded any argument without a +space or tab; it now receives the argument as written. MSVCRT children parse +the same values as before. No signature changed, and Unix is unchanged. + +**Required changes** + +_None._ + +**Deprecations removed** + +_None._ + +**Behavior changes without code changes** + +| Surface (Windows, MSYS child) | Before | After | +|---|---|---| +| `on-file = "printf '[{}]'"` | child prints `[]`, zero rows | child prints `[{}]`, one row | +| an argument `*.txt` | expanded to matching files | the literal `*.txt` | + +A config that relied on an MSYS child expanding a glob in its argv must ask +for a shell explicitly: `sh -c 'cat *.txt'`. + +**Verification** + +```toml +[[table]] +name = "t" +ddl = "CREATE TABLE t (n INTEGER)" +glob = "*.txt" +on-file = "printf '[{\"n\":1}]'" +``` + +```bash +dirsql query "SELECT n FROM t LIMIT 1" -c .dirsql.toml +# [{"n":1}] +```