admin管理员组

文章数量:1332361

I have an executable that receives a number of command line arguments and I want to write a .cmd that performs a few checks and then passes on all the arguments to the executable.

As an example, take the executable echo_args.exe which is this Rust app:

use std::env;

fn main() {
    // Collect the command line arguments into a vector
    let args: Vec<String> = env::args().collect();

    // Iterate over the arguments and print each one
    for arg in args.iter() {
        println!("{}", arg);
    }
}

The fact that it's Rust has no bearing on the question, but this way you can reproduce my situation.

Calling this app directly from PowerShell:

echo_args.exe "hello `"`"world`"`""

Prints, as expected:

echo_args.exe
hello ""world""

Calling this app directly from Command Prompt:

echo_args.exe "hello """"world"""""

Prints, as expected:

echo_args.exe
hello ""world""

In both cases, a correct way of escaping double quotes is used, appropriate to the command processor.

However, if I write a test.cmd file, I run into the problem that it behaves differently when called from PowerShell or from Command Prompt.

The naive solution is:

@echo off
set exe=echo_args.exe
"%exe%" %*

And although that works when called from Command Prompt:

test "hello """"world"""""

It does not from PowerShell because:

test "hello `"`"world`"`""

Now prints:

echo_args.exe
hello "world"

Note that the double quotes get 'eaten' by the command processor running the .cmd (i.e. cmd.exe). So, Powershell passes on the hello ""world"" to cmd.exe (like it would to echo_args.exe) and cmd.exe dedupes the double quotes and passes hello "world" to the .exe.

Now, I realise that I can avoid this by also writing a test.ps1 that performs the same function, so that the .cmd does not get called from PowerShell, and for practical purposes that is fine, but my question is this: is there a way to write the .cmd so that it behaves correctly, regardless if it is started with cmd.exe automatically with PowerShell, or if it is found and executed from Command Prompt directly?

Detecting whether cmd.exe was launched from PowerShell seems error-prone and complicated. And I don't see a straightforward way of (re)constructing the arguments in the .cmd that avoids this problem (since the problem essentially happens before that code even runs). What am I missing? I've asked a few LLMs (ChatGPT 4o and Claude Sonnet 3.5), but they insist on proving their uselessness for problems that require some nuance and come up with an endless slew of non-solutions.

I have an executable that receives a number of command line arguments and I want to write a .cmd that performs a few checks and then passes on all the arguments to the executable.

As an example, take the executable echo_args.exe which is this Rust app:

use std::env;

fn main() {
    // Collect the command line arguments into a vector
    let args: Vec<String> = env::args().collect();

    // Iterate over the arguments and print each one
    for arg in args.iter() {
        println!("{}", arg);
    }
}

The fact that it's Rust has no bearing on the question, but this way you can reproduce my situation.

Calling this app directly from PowerShell:

echo_args.exe "hello `"`"world`"`""

Prints, as expected:

echo_args.exe
hello ""world""

Calling this app directly from Command Prompt:

echo_args.exe "hello """"world"""""

Prints, as expected:

echo_args.exe
hello ""world""

In both cases, a correct way of escaping double quotes is used, appropriate to the command processor.

However, if I write a test.cmd file, I run into the problem that it behaves differently when called from PowerShell or from Command Prompt.

The naive solution is:

@echo off
set exe=echo_args.exe
"%exe%" %*

And although that works when called from Command Prompt:

test "hello """"world"""""

It does not from PowerShell because:

test "hello `"`"world`"`""

Now prints:

echo_args.exe
hello "world"

Note that the double quotes get 'eaten' by the command processor running the .cmd (i.e. cmd.exe). So, Powershell passes on the hello ""world"" to cmd.exe (like it would to echo_args.exe) and cmd.exe dedupes the double quotes and passes hello "world" to the .exe.

Now, I realise that I can avoid this by also writing a test.ps1 that performs the same function, so that the .cmd does not get called from PowerShell, and for practical purposes that is fine, but my question is this: is there a way to write the .cmd so that it behaves correctly, regardless if it is started with cmd.exe automatically with PowerShell, or if it is found and executed from Command Prompt directly?

Detecting whether cmd.exe was launched from PowerShell seems error-prone and complicated. And I don't see a straightforward way of (re)constructing the arguments in the .cmd that avoids this problem (since the problem essentially happens before that code even runs). What am I missing? I've asked a few LLMs (ChatGPT 4o and Claude Sonnet 3.5), but they insist on proving their uselessness for problems that require some nuance and come up with an endless slew of non-solutions.

Share Improve this question edited Nov 21, 2024 at 2:30 Grismar asked Nov 21, 2024 at 1:47 GrismarGrismar 31.5k6 gold badges40 silver badges65 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 3

The sad reality in Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and last version is 5.1) as well as in (now obsolete) versions of PowerShell (Core) 7 (up to v7.2.x) is that an extra, manual layer of \-escaping of embedded " characters is required in arguments passed to external programs.

  • This is fixed in PowerShell v7.3+, with selective exceptions on Windows. Therefore, in PowerShell 7.3+, your calls work as intended, EXCEPT if you call a batch file, among other legacy programs.

  • The old, broken behavior is still available as an opt-in and by default still applies selectively to certain programs, notably batch files. You can avoid this by setting the $PSNativeCommandArgumentPassing preference variable to 'Standard', but this comes with caveats; see next section.

  • See this answer for details.

Therefore, in Windows PowerShell (and now-obsolete PowerShell 7.2- versions; in PowerShell 7.3+, the calls work analogously with the \ instances removed):

echo_args.exe "hello \`"\`"world\`"\`""

Alternatively, since no string interpolation is needed in your case, using a verbatim string ('...') obviates the need for PowerShell's escaping:

echo_args.exe 'hello \"\"world\"\"'

However, even when an expandable (interpolating) string ("...") is needed, there is a way to avoid the need for PowerShell's escaping by way of using the (invariably multiline) here-string variant

$addressee = 'world'
echo_args.exe @"
hello \"\"$addressee\"\"
"@

Finally, for the sake of completeness, both PowerShell editions (all versions) offer --%, the so-called stop-parsing token, which essentially copies what follows it verbatim to the process command line constructed behind the scenes. This entails severe limitations, however, notably the inability to reference PowerShell variables - see the bottom section of this answer for details:

# Use of the stop-parsing token, --%

# Both variations work, because most CLIs on Windows accept "" and \"
# interchangeably as an escaped "

echo_args.exe --% "hello """"world""""

echo_args.exe --% "hello \"\"world\"\""

Caveat:

  • While the above should work in PowerShell 7 as well, it doesn't as of v7.4.x, due to a long-standing bug: GitHub issue #18664.

Caveat re PowerShell 7.3+ when invoking a batch file:

  • Because the old, broken behavior by default ($PSNativeCommandArgumentPassing defaults to 'Windows') still applies to batch files (among other legacy interpreters), the above workarounds are still needed by default in PowerShell 7.3+.

    • As noted, setting $PSNativeCommandArgumentPassing = 'Standard' deactivates this behavior and performs \" escaping of embedded " chars. for all external programs behind the scenes, including batch files.

    • However, the use of \" can cause problems in batch files: because cmd.exe doesn't recognize a \" as escaped, cmd.exe metacharacters such as & inside \"...\" embedded in overall "..." can result in syntax errors; also, when processing individual arguments using the batch language (as opposed to just passing all arguments through to another program, with $*) only ""-escaping is recognized; the proposal mentioned below would have avoided this problem by using ""-escaping for batch files behind the scenes.

    • There is another pitfall, which is unrelated to $PSNativeCommandArgumentPassing and affects all versions of both PowerShell editions: Because PowerShell only employs "..." enclosure if the verbatim value to pass contains spaces on the process command line, a call such as batch.cmd 'http://example.?foo=1&bar=2' from PowerShell breaks the batch file, because the latter receives argument http://example.?foo=1&bar=2 unquoted.

      • However, the workaround is actually easier with the broken behavior in effect (which as noted, still applies in 7.3+ by default):
        batch.cmd '"http://example.?foo=1&bar=2"'.
        Again, the proposal mentioned next would have avoided this problem.
  • GitHub issue #15143 is a detailed proposal that would have avoided this whole mess going forward, by way of selective accommodations for legacy interpreters such as cmd.exe and nonstandard CLIs such as msiexec.exe. Unfortunately, it went nowhere.


As for your observations and questions:

is there a way to write the .cmd so that it behaves correctly

It follows from the above that Windows PowerShell is the culprit, so you must compensate for its buggy behavior there on invocation.

the double quotes get 'eaten' by the command processor running the .cmd (i.e. cmd.exe). So, Powershell passes on the hello ""world"" to cmd.exe (like it would to echo_args.exe) and cmd.exe dedupes the double quotes and passes hello "world" to the .exe.

No, it isn't cmd.exe that "eats" the double quotes, it is, in effect, the broken way in which Windows PowerShell places the verbatim value it has parsed according to its syntax rules on the process command line it uses to call external programs behind the scenes. cmd.exe passes whatever it receives on as-is, with only minimal interpretation (none in the case at hand).

Specifically, the - broken - process command line that Windows PowerShell constructs is:

# Windows PowerShell (and PowerShell 7.2-): BROKEN
# *Process* command line constructed behind the scenes, if
# echo_args.exe "hello `"`"world`"`"" is submitted: 
echo_args.exe "hello ""world"""

This is broken, because (Windows) PowerShell - which of necessity must pass a double-quoted string to external programs, as only this form of quoting can be expected to be understood by Windows CLIs - neglects to escape the embedded ".
That is, in order to pass verbatim hello ""world"" to an external program, either "hello """"world""""" or, more typically, "hello \"\"world\"\"" must be placed on the process command line.

PowerShell 7.3+ now does perform this escaping, using \" (but as noted, by default not for batch files).

# PowerShell 7.3+: OK
# *Process* command line constructed behind the scenes, if
# echo_args.exe "hello `"`"world`"`"" is submitted: 
echo_args.exe "hello \"\"world\"\""

本文标签: windowsPassing through arguments from a cmd to a exeStack Overflow