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.
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:
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.org?foo=1&bar=2'
from PowerShell breaks the batch file, because the latter receives argument http://example.org?foo=1&bar=2
unquoted.
batch.cmd '"http://example.org?foo=1&bar=2"'
.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 passeshello "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\"\""