Skip to content

quoteCmdArg corrupts arguments containing a literal " on cmd surfaces #162

Open

Description

@dormouse-bot

quoteCmdArg corrupts arguments containing a literal " on cmd surfaces

When dor forwards a command tail (dor split -- …, dor ensure …) to a pane whose shell is cmd.exe, the host renders the argv with quoteCmdArg. For any argument that contains a double-quote, the output is malformed.

Trace

function quoteCmdArg(arg: string): string {
  if (arg === '') return '""';
  const escaped = arg
    .replace(/[%]/g, '%%')
    .replace(/([&|<>()^"])/g, '^$1');   // caret-escapes the "
  if (WINDOWS_SAFE_ARG.test(arg)) return escaped;
  return `"${escaped}"`;                 // ...then wraps the result in quotes
}

For arg = 'say "hi"' the function:

  1. caret-escapes each " → say ^"hi^", then
  2. wraps in surrounding quotes → "say ^"hi^"".

Inside a cmd double-quoted region, ^ is not an escape character — it is literal. So the embedded ^" does not produce an escaped quote; the first ^" is read as a literal ^ followed by a quote that closes the surrounding quoted region early, and the rest of the argument is re-parsed outside quotes. The argument the launched program receives is not say "hi".

The same belt-and-suspenders pattern (caret-escape and wrap in quotes) is what the existing test pins for a&b → "a^&b" (cli-output.test.mjs:204-206); for &/(/) the caret inside quotes is merely redundant (those chars are already literal inside quotes), so it's harmless-but-odd. For " it is actively wrong, and that case is untested.

Why this needs maintainer input rather than a drive-by fix

Correct cmd-line quoting for a literal " depends on how the receiving program parses its command line (the msvcrt/CommandLineToArgvW convention uses \" and ""; a bare cmd builtin differs). Picking the right escaping is a design decision that should be validated on an actual Windows + cmd.exe host, which this CI environment can't do. Flagging rather than guessing.

Suggested direction (needs Windows verification)

For the cmd kind, escape an embedded " by doubling it ("") or backslash-escaping (\") inside the wrapped form, and don't caret-escape characters that already sit inside the surrounding quotes. Add a cli-output.test.mjs case with an embedded " to pin whatever behavior is chosen.

Surfaced by the nightly code-quality survey.

Activity

  1. dormouse-bot commented on Jun 20, 2026

    @dormouse-bot
    CollaboratorAuthor

    Confirmed the reproduction by running the built function directly (buildShellCommandForKind('cmd', [...])):

    "say \"hi\"" => "\"say ^\"hi^\"\""
    "\""         => "\"^\"\""
    "x\"y"       => "\"x^\"y\""
    "a&b"        => "\"a^&b\""   # the existing, pinned case — caret here is harmless-but-redundant
    

    So say "hi" produces "say ^"hi^"", exactly as the trace describes. The ^ sits inside the surrounding cmd double-quoted region where it's literal, so the embedded ^" doesn't escape anything — the first " closes the quoted region early and the tail re-parses outside quotes. Every argument containing a " is affected, and that case is currently untested.

    I'm deferring the fix rather than guessing at it: correct cmd escaping for an embedded " depends on the receiving program's command-line parser (the msvcrt / CommandLineToArgvW convention uses \" / "", and the cmd.exe layer toggles quote state on every bare " regardless of a preceding ^). The two layers interact in a way that should be validated on a real Windows + cmd.exe host before committing, which this CI can't do — consistent with why this was filed as an issue rather than a PR.

    When the escaping is decided, the natural home for a pinning case is the cmd assertion in cli-output.test.mjs:204-206, alongside the existing a&b case — adding an argument with an embedded ".

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions