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.
1 Answer
Reset to default 3The 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: becausecmd.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 asbatch.cmd 'http://example.?foo=1&bar=2'
from PowerShell breaks the batch file, because the latter receives argumenthttp://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.
- However, the workaround is actually easier with the broken behavior in effect (which as noted, still applies in 7.3+ by default):
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 asmsiexec.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\"\""
本文标签: windowsPassing through arguments from a cmd to a exeStack Overflow
版权声明:本文标题:windows - Passing through arguments from a .cmd to a .exe - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742318805a2452361.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论