为什么文件的存在会影响批处理脚本中文件路径的解析方式?

3

关于批处理命令的 for,当使用 /f 和一个命令时,我有些不理解它的工作方式。

我试图循环输出一个可执行文件。然而,该可执行文件不存在,这没关系。

但是,与其得到预期的有关错误路径的错误信息,脚本似乎自发地错误地标记了该字符串。这让我思考是否格式化 for 循环时使用的引号或反引号不正确。

将一个可执行文件放在该位置可以解决问题,这使得路径字符串标记似乎依赖于该路径下实际存在一个文件。

为什么下面的批处理文件会发生这种情况:

@echo off
for /f "usebackq delims=" %%i in ( `"C:\Program Files\BOGUS_PATH\FAKE.exe"`) do (
    rem
)

为什么执行命令时会显示 'C:\Program' 不是内部或外部命令,也不是可运行的程序或批处理文件。 而不是 系统找不到指定的路径。


1
当你转义空格时,它可以工作:for /f "delims=" %%i in ("C:\Program^ Files\BOGUS_PATH\FAKE.exe") do echo %%i。(请不要问我为什么...) - Stephan
@Stephan - 这是完全不同的陈述 - 你的代码试图读取一个文件,并给出那个消息,因为找不到文件。转义没有任何作用 - 没有它,你可以得到相同的结果。OP的代码正在尝试执行一个命令。 - dbenham
1
@dbenham在编辑过程中丢失了'for /f "delims=" %%i in ('"C:\Program^ Files\BOGUS_PATH\FAKE.exe"') do echo %%i - Stephan
@Stephan - 哇,这真的很神奇。我感到震惊,竟然可以这样运作。你不需要FOR /F或引号。这个从命令行就行了:c:\program^ files\bogus_path\fake.exe。这是我见过的第一个可以这样转义标记分隔符的情况。但如果删除前面的“c:”,转义就不起作用了,在所有情况下都是我所期望的。今天我学到了东西,谢谢。 - dbenham
@Stephan - 在FOR循环中,如果没有引号,你需要使用for /f "delims=" %%I in ('c:\program^^^ files\bogus_path\fake.exe') do ...,因为有两轮解析,所以你需要转义转义符。 - dbenham
3个回答

5

首先,我有一个小抱怨,usebackq 不是必需的。更简单、功能等效的命令是:

@echo off
for /f "delims=" %%i in ('"C:\Program Files\BOGUS_PATH\FAKE.exe"') do (
    rem
)

只有当你试图读取一个文件,并且该文件的路径包含像空格这样的分隔符时,才需要使用usebackq
for /f "usebackq" %%A in ('"some file.txt"')

所有其他情况都可以使用“正常”表单。
for /f %%A in (fileName) do...
for /f %%A in ("string") do...
for /f %%A in ('someCommand') do...

现在回答你真正的问题 :-)

文件的存在或不存在实际上并不会改变解析。

首先,您需要了解在尝试从命令行执行不存在的程序时可能出现的错误消息。有三种可能性:

1)如果“命令”中包含冒号,除第二个位置以外的任何位置,那么您可能会得到以下消息:

c:\test\>abc:fake.exe
The filename, directory name, or volume label syntax is incorrect.

c:\test\>abc:\test\fake.exe
The filename, directory name, or volume label syntax is incorrect.

或者有时候你会得到下一个可能的错误信息。对于你会得到哪个错误信息的规则,我并不清楚,也觉得弄清楚确切的规则并不是很重要。

2) 如果“命令”包含一个表示路径的反斜杠,并且该路径无效,则会得到以下错误信息:

c:\test\>c:bogus\fake.exe
The system cannot find the path specified.

c:\test\>\bogus\fake.exe
The system cannot find the path specified.

c:\test\>abc:bogus\fake.exe
The system cannot find the path specified.

3) 如果找不到可执行文件,且“command”没有包含路径信息或提供的路径是有效的,则会出现以下错误:

C:\test\>fake.exe
'fake.exe' is not recognized as an internal or external command,
operable program or batch file.

C:\test>c:\test\fake.exe
'c:\test\fake.exe' is not recognized as an internal or external command,
operable program or batch file.

最后一个谜团是为什么"C:\Program Files\BOGUS_PATH\FAKE.exe"报错代码是3而不是2。

如果你在命令行中用引号执行,那么你会得到预期的结果:

C:\test>"C:\Program Files\BOGUS_PATH\FAKE.exe"
The system cannot find the path specified.

如果您在命令行中不使用引号执行,则会得到预期的结果:

C:\test>C:\Program Files\BOGUS_PATH\FAKE.exe
'C:\Program' is not recognized as an internal or external command,
operable program or batch file.

您的脚本包含引号,因此您会期望前者,但实际上却得到了后者。

理解这个问题有两个方面。

首先,FOR /F通过执行以下命令来执行命令

C:\WINDOWS\system32\cmd.exe /c "C:\Program Files\BOGUS_PATH\FAKE.exe"

你可能认为你的“命令”被引用了,但是cmd.exe玩弄引号,这就是解释的第二部分。 cmd.exe引号规则在帮助文件中有描述:
c:\test\>help cmd
Starts a new instance of the Windows command interpreter
...
If /C or /K is specified, then the remainder of the command line after
the switch is processed as a command line, where the following logic is
used to process quote (") characters:

    1.  If all of the following conditions are met, then quote characters
        on the command line are preserved:

        - no /S switch
        - exactly two quote characters
        - no special characters between the two quote characters,
          where special is one of: &<>()@^|
        - there are one or more whitespace characters between the
          two quote characters
        - the string between the two quote characters is the name
          of an executable file.

    2.  Otherwise, old behavior is to see if the first character is
        a quote character and if so, strip the leading character and
        remove the last quote character on the command line, preserving
        any text after the last quote character.

除了最后一个条件——字符串没有指向有效的可执行文件,1.下的大多数条件都被满足了。2.规则被使用,因此命令变成了:

C:\Program Files\BOGUS_PATH\FAKE.exe

现在的结果非常清晰易懂 - "command" 在空格处被截断。
'C:\Program' is not recognized as an internal or external command,
operable program or batch file.

如果你正在尝试使用FOR /F执行一个必须用引号括起来的命令,那么你需要在整个命令外面再加一组引号。
for /f "delims=" %%A in ('""c:\some path\file.exe" "arg 1" arg2"') do ...

如果路径或带引号的参数中存在毒瘤字符,上述方法将会出现问题,因为额外的一组引号会打乱引用。你可以像下面这样转义毒瘤字符:
for /f "delims=" %%A in ('""c:\some path\file.exe" "this^&that" arg2"') do ...

但我发现这很尴尬,因为你在命令行中使用的命令与你在 FOR /F 中使用的命令不匹配。我更喜欢转义额外的引号,以便命令行上使用的任何内容也可在 FOR /F 中使用。

for /f "delims=" %%A in ('^""c:\some path\file.exe" "this&that" arg2^"') do ...

这确实解释了为什么当文件不存在时会出现“'C:\ Program'未被识别...”的错误消息。然而,这并不能解释为什么当文件存在时没有“'C:\ Program'未被识别...”的错误消息。 - cowlinator
@cowlinator - 实际上是这样的。如果文件存在,则使用规则集1,因此引号将被保留,文件将被找到并执行。 - dbenham
好的,简单来说:文件的存在不会改变解析过程。文件的存在会改变引号字符是被保留还是被剥离。等等,这不就是解析吗? - cowlinator

3
FOR 是 Windows 命令处理器 cmd.exe 的一个内部命令。 使用带有选项 /F 和要执行的命令的命令 FOR 会导致启动另一个命令进程,参数是以 %ComSpec% /c 和指定的命令行开头的。
所有输出到 STDOUT 处理程序的内容都由当前执行批处理文件的命令进程捕获,并在额外启动的 cmd.exe 终止后由 FOR 按行处理。
另外启动的命令进程的 STDERR 输出被重定向到当前命令进程的 STDERR 处理程序,从而导致在控制台窗口中显示它,如果 STDERR 不在额外启动的命令进程或当前命令进程中被重定向到不同的处理程序、文件或设备 NUL 中。
这可以通过使用免费的 Sysinternals(Microsoft)工具 Process Monitor 来查看。
  • 下载包含 Process Monitor 的 ZIP 文件。
  • 将 ZIP 文件解压缩到任何本地目录。
  • 用管理员身份运行 Procmon.exe,这是运行 Process Monitor 所需的。
  • 在显示的 Process Monitor Filter 对话框中首先 添加 条目 Process Name is cmd.exe,然后使用按钮 OK 关闭对话框。
  • 关闭工具栏上除了带有工具提示 Show File System Activity 的文件柜符号之外的最后五个选项。
  • 按下 Ctrl+X 清空列表。
  • 运行批处理文件。
  • 切换回 Process Monitor 并按下 Ctrl+E 停止捕获。
  • 确保在列 Process Name 中还看到列 PID。如果尚未显示 PID 列,请右键单击列表标头并左键单击上下文菜单项 Select Columns...,选中 Process ID 并使用 OK 关闭对话框窗口。我建议将 PID 列移动到列标头 Process Name 右侧。

向上滚动以开始捕获文件系统活动列表,并查找其中一个 cmd.exe 显示了不同进程标识符的行。那个具有不同 PID 的第二个 cmd.exe 是由 FOR 启动的 Windows 命令处理器实例。

右键单击此第二个 cmd.exe,在上下文菜单中左键单击属性,选择标签进程并查看显示的命令行

C:\Windows\system32\cmd.exe /c "C:\Program Files\BOGUS_PATH\FAKE.exe"

好的。现在我们知道了FORcmd.exe在运行命令以捕获该命令的输出时分别如何工作。关闭属性窗口。
查看列表,可以看到第二个cmd.exe所进行的文件系统访问,以查找指定的可执行文件,但在指定的现有目录中不存在。然后它会搜索C:\Program Files\BOGUS_PATH\FAKE.exe.*,同样也没有这样的文件。
现在将第一个参数字符串根据参数分隔符分解为多个部分,其中包括空格字符以及逗号或等号等其他字符。因此,第一个参数字符串C:\Program Files\BOGUS_PATH\FAKE.exe被再次分隔,导致现在将C:\Program解释为第一个参数字符串。 cmd.exe检查当前的目录路径C:\是否存在,结果为真,就像之前检查目录路径C:\Program Files\BOGUS_PATH一样。
接下来,检查是否存在与模式C:\Program.*匹配的文件,结果是没有这样的文件。然后cmd.exe检查是否存在文件C:\Program,同样的结果是没有这样的文件。
现在第二个cmd.exe放弃查找可执行文件,并输出错误消息。
确实令人困惑的是,已经根据需要用双引号括起来指定了完整路径的可执行文件,因此不会输出任何内容。

'C:\Program Files\BOGUS_PATH\FAKE.exe' 不是内部或外部命令,也不是可运行的程序或批处理文件。

这是可以预料的,但输出结果却是错误消息:

'C:\Program' 不是内部或外部命令,也不是可运行的程序或批处理文件。

我认为,如果用户使用命令(参数)运行cmd.exe,并且没有找到要执行的文件,则再次使用参数分隔符解析第一个参数字符串。
"C:\Programs\MyApplication.exe C:\Temp\FileToProcess.txt"

代替
"C:\Programs\MyApplication.exe" "C:\Temp\FileToProcess.txt"

所以问题是:
这种自动参数解析好还是不好?
嗯,这很难回答。至少在cmd.exe的帮助文件中有相关记录,正如dbenham他的回答中所述。

所以,你的意思是它首先搜索C:\Program Files\BOGUS_PATH\FAKE.exe.*而不尝试将其解析为参数;只有在找不到任何内容时,它才尝试使用空格作为参数分隔符将字符串解析为参数?因此,在文件不存在的情况下,它仅会给出“'C:\ Program'未被识别...”错误,因为它已经尝试评估整个字符串作为单个路径并失败了,但是第一个错误没有被打印。这就解释了为什么当文件不存在时解析行为似乎不同:它尝试了更多的解析选项。 - cowlinator
1
@cowlinator - 根据cmd.exe文档记录的规则,它无法在开始时知道是否应该剥离引号。所有规则集1的条件都得到满足,直到最后一个条件。因此,它必须搜索整个给定的带引号路径。如果找到了它,就会被执行。但是在找不到它时,cmd.exe会假定(按设计)应根据规则集2剥离引号。路径在空格处分割,c:\program既不是外部可执行文件也不是内部命令,并生成错误。 - dbenham

0
你可能需要做的是在可执行命令外面加上双引号:
@Echo Off
For /F Delims^=^ EOL^= %%A In ('""C:\Program Files\BOGUS_PATH\FAKE.exe""'
)Do Echo(%%A
Pause

内部双引号用于保护路径中已知的空格,而外部双引号用于在将内容传递给cmd.exe实例时保护内部双引号,以便运行括号内的命令。

然后为了防止任何错误消息从命令中输出,您应该将STDERR输出重定向到NUL

@Echo Off
For /F Delims^=^ EOL^= %%A In ('""C:\Program Files\BOGUS_PATH\FAKE.exe" 2>Nul"'
)Do Echo(%%A
Pause

如果可执行文件不存在,这将抑制错误消息:系统找不到指定的路径。 并且 Do 命令将不会运行,(因为没有要处理的 STDOUT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接