我有一个可执行文件,它接收许多命令行参数,我想编写一个 .cmd 来执行一些检查,然后将所有参数传递给可执行文件。
echo_args.exe
以这个 Rust 应用程序的可执行文件为例:
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);
}
}
事实上,它是 Rust,这与问题无关,但通过这种方式你可以重现我的情况。
直接从 PowerShell 调用此应用程序:
echo_args.exe "hello `"`"world`"`""
打印结果与预期一致:
echo_args.exe
hello ""world""
直接从命令提示符调用此应用程序:
echo_args.exe "hello """"world"""""
打印结果与预期一致:
echo_args.exe
hello ""world""
在这两种情况下,都使用了适合命令处理器的正确的转义双引号的方法。
但是,如果我写入一个test.cmd
文件,我会遇到一个问题,即从 PowerShell 或命令提示符调用时它的行为会有所不同。
简单的解决方案是:
@echo off
set exe=echo_args.exe
"%exe%" %*
尽管从命令提示符调用时它可以工作:
test "hello """"world"""""
它不支持 PowerShell,因为:
test "hello `"`"world`"`""
现在打印:
echo_args.exe
hello "world"
请注意,双引号会被运行 .cmd 的命令处理器“吃掉”(即cmd.exe
)。因此,Powershell 会传递hello ""world""
to cmd.exe
(就像传递 to 一样echo_args.exe
),并对cmd.exe
双引号进行重复数据删除,然后传递hello "world"
给 .exe。
现在,我意识到我可以通过编写执行相同功能的代码来避免这种情况,这样test.ps1
.cmd 就不会从 PowerShell 调用,从实际目的来看这很好,但我的问题是:有没有办法编写 .cmd 使其正常运行,无论它是使用 PowerShell 自动启动cmd.exe
,还是直接从命令提示符中找到并执行?
检测是否cmd.exe
从 PowerShell 启动似乎容易出错且复杂。而且我没有看到一种简单的方法来(重新)构造 .cmd 中的参数以避免此问题(因为问题本质上发生在该代码运行之前)。我错过了什么?我问过几个法学硕士(ChatGPT 4o 和 Claude Sonnet 3.5),但他们坚持证明它们对于需要一些细微差别的问题毫无用处,并提出了无数非解决方案。
Windows PowerShell(旧的、随 Windows 一起提供的、仅适用于 Windows 的 PowerShell 版本,其最新版本是 5.1)以及PowerShell (Core) 7 (最高到 v7.2.x)(现已过时)版本中令人遗憾的现实是,在传递给外部程序的参数中需要对嵌入字符进行额外的手动转义
\
"
。此问题已在 PowerShell v7.3+中得到修复,但在 Windows 上存在选择性例外。因此,在 PowerShell 7.3+中,您的调用将按预期工作,除非您调用批处理文件和其他旧程序。
旧的、有问题的行为仍可作为可选行为,默认情况下仍选择性地应用于某些程序,特别是批处理文件。您可以通过将
$PSNativeCommandArgumentPassing
首选项变量设置为'Standard'
来避免这种情况,但这有注意事项;请参阅下一节。有关详细信息,请参阅此答案。
因此,在 Windows PowerShell(以及现已过时的 PowerShell 7.2 版本;在 PowerShell 7.3+ 中,调用的工作方式与
\
已删除的实例类似)中:另外,由于您的情况下不需要字符串插值,因此使用逐字字符串(
'...'
)可以避免PowerShell转义的需要:但是,即使需要可扩展(插值)字符串(
"..."
),也可以通过使用(总是多行的)here-string变体来避免 PowerShell 的转义。最后,为了完整起见,两个 PowerShell 版本 (所有版本) 都提供了
--%
所谓的停止解析令牌,它本质上将其后面的内容逐字复制到后台构建的进程命令行中。然而,这带来了严重的限制,特别是无法引用 PowerShell 变量 -有关详细信息,请参阅此答案的底部部分:警告:
调用批处理文件时有关 PowerShell 7.3+ 的警告:
由于默认情况下旧的、不完善的行为(
$PSNativeCommandArgumentPassing
默认为'Windows'
)仍然适用于批处理文件(以及其他旧式解释器),因此在 PowerShell 7.3+ 中仍然默认需要上述解决方法。如上所述,设置
$PSNativeCommandArgumentPassing = 'Standard'
将停用此行为并执行\"
嵌入"
字符的转义。对于后台的所有外部程序,包括批处理文件。但是,使用
\"
会在批处理文件中引发问题:因为cmd.exe
无法识别\"
为转义的,cmd.exe
所以嵌入在&
中的元字符总体上会导致语法错误;此外,在使用批处理语言处理单个参数时(而不是使用 将所有参数传递给另一个程序)只能识别 -escaping ;下面提到的建议可以在后台对批处理文件使用 -escaping 来避免这个问题。\"...\"
"..."
$*
""
""
还有另外一个陷阱
$PSNativeCommandArgumentPassing
,它与两个 PowerShell 版本的所有版本无关并且会影响它们:因为只有当要传递的逐字值在进程命令行中包含空格时"..."
,PowerShell 才会使用括起来 ,所以来自 PowerShell 的调用会破坏批处理文件,因为后者接收的参数未用引号括起来。batch.cmd 'http://example.org?foo=1&bar=2'
http://example.org?foo=1&bar=2
batch.cmd '"http://example.org?foo=1&bar=2"'
GitHub 问题 #15143是一项详细的提案,它本可以通过选择性地适应旧式解释器(例如)
cmd.exe
和非标准 CLI(例如)来避免今后出现这种混乱局面msiexec.exe
。不幸的是,它没有取得任何进展。至于您的观察和问题:
从上述内容可以看出,Windows PowerShell是罪魁祸首,因此您必须在调用时弥补其错误行为。
不,这并不是
cmd.exe
“吃掉”双引号,实际上,这是 Windows PowerShell 将根据其语法规则解析的逐字值放置在它用来在后台调用外部程序的进程命令行上的错误方式。按原样cmd.exe
传递它收到的任何内容,只进行最少的解释(在当前情况下没有任何解释)。具体来说,Windows PowerShell 构造的 - 损坏 - 进程命令行是:
这是错误的,因为 (Windows) PowerShell - 必须将双引号字符串传递给外部程序,因为只有这种形式的引用才能被 Windows CLI 理解 -忽略了对嵌入的 进行转义
"
。也就是说,为了逐字传递
hello ""world""
给外部程序,必须将"hello """"world"""""
或 更典型地"hello \"\"world\"\""
放在进程命令行上。PowerShell 7.3+ 现在确实执行了这种转义,使用
\"
(但如上所述,默认情况下不适用于批处理文件)。