为什么在管道中使用变量扩展的 `findstr` 命令会返回意外结果?

5

在试图为问题“为什么FindStr返回未找到”提供全面的答案时,我遇到了涉及管道的代码的奇怪行为。以下是基于原始问题的一些代码(在中执行):

rem // Set variable `vData` to literally contain `...;%main%\Programs\Go\Bin`:
set "vData=...;%%main%%\Programs\Go\Bin"
set "main=C:\Main"

echo/%vData%| findstr /I /C:"%%main%%\\Programs\\Go\\Bin"

这不返回匹配,因此没有任何回显,ErrorLevel 变成了 1
尽管当我逐步进行解析过程时,得出相反的结论,我期望有一个匹配和 ErrorLevel0,因为:
  1. at first, the whole line is parsed and immediate (%) expansion takes place, hence %vData% becomes expanded and %% become replaced by %, resulting in the following command line that is going to be executed:

    echo/...;%main%\Programs\Go\Bin| findstr /I /C:"%main%\\Programs\\Go\\Bin"
    
  2. each side of the pipe | is executed in its own new cmd instance by cmd /S /D c, both of which run in cmd context (which affects handling of %-expansion), resulting in these parts:

    • left side of the pipe:

      echo/...;C:\Main\Programs\Go\Bin
      
    • right side of the pipe:

      findstr /I /C:"C:\Main\\Programs\\Go\\Bin"
      

      (the search string that findstr finally uses is C:\Main\Programs\Go\Bin as the \ is used as escape character even in literal search mode, /C or /L)

正如您所看到的,最终搜索字符串实际上出现在回显字符串中,因此我期望匹配,但命令行没有返回结果。那么这里发生了什么,我错过了什么?


当我在执行管道命令行之前清除变量main时,我得到了预期的结果,这导致我得出结论,即变量main尽管我上面的假设,但仍未被扩展(请注意,在cmd上下文中,当变量为空时,% main%保留为字面值)。我是对的吗?

现在情况更加混乱了:当我把管道符号的右侧放在括号中时,会返回一个匹配,不管变量main是否被定义:

echo/%vData%| (findstr /I /C:"%%main%%\\Programs\\Go\\Bin")

有人能解释一下吗?这是否与findstr是外部命令有关,而echo则不同?(我的意思是,如果定义了mainbreak | echo/%vData%会按预期扩展main的值...)

这在 Windows 7 上如预期地返回匹配。 - Ben Personick
你的原始代码表现如预期(并且你也是这样期望的)。C:\Admin>echo/%vData%| findstr /I /C:"%%main%%\\Programs\\Go\\Bin" ...;%C:\Main%\Programs\Go\BinC:\Admin>echo/%vData% ...;%%main%%\Programs\Go\Bin - Ben Personick
这可能与Win10有关,以及允许更强大的终端仿真所做的更改吗?--我不确定如何,因为我认为Conhost仍然是cmd的默认设置,但我没有运行win 10,我有一些2012R2/2016/2019版本,如果你还没有测试,我明天会测试一下,至少看看哪些版本是一致的。 - Ben Personick
@BenPersonick,我的意思是要在[tag:batch-file]中执行代码(抱歉,在问题中我忘记提到这一点了--已更新),因为%%转义在[tag:cmd]-上下文中不起作用。我正在使用Windows 7... - aschipfl
这是一些非常有趣的东西,我认为你在主函数上使用 %% 除非它是一个打字错误并且你使用另一个变量来保存它,就像使用数据一样,我主要在 cmd 脚本中工作,并且通常在事后将它们转换为可粘贴的 CMD 脚本,所以现在我非常困惑,我不会预期 CLI 的行为方式与你期望脚本的行为方式相同,反之亦然。很好的发现,但对我来说,正好相反,这是一种把车放在马前的有趣感觉。 - Ben Personick
2个回答

4
我把你的例子简化成了:
@echo off

set "main=abc"
break | findstr /c:"111" %%main%%
break | echo findstr /c:"222" %%main%%

输出结果为:

FINDSTR: 无法打开 %main%。
findstr /c:"222" abc

这证明在管道中使用 exe 文件会导致不同的行为,与使用内部批处理命令不同。
只有对于内部命令才会创建新的 cmd.exe 实例。
这也是 findstr 不会展开百分号的原因。

这个令人困惑的行会被展开,因为括号强制创建一个新的 cmd.exe 实例。

break | (findstr /c:"111" %%main%%)

我将修改在5.3管道——Windows命令解释器(CMD.EXE)如何解析脚本的解释


1
非常感谢,这解释得很清楚!我刚刚验证了一下,管道不会为像 findstr 这样的外部命令启动新的 cmd 实例:type "hugefile.txt" | findstr /C:"text" 会创建两个新进程(在任务管理器中显示):cmd /S /D c" type ... "findstr ...(没有 cmd)。 - aschipfl
2
@aschipfl 感谢您发现这个好东西,我很惊讶之前没有人发现过。 - jeb
2
是的,这是一个非常有趣的发现!我已经更新了cmd.exe解析规则。有趣的是以下内容只需要一个转义符:echo "&" | findstr ^&。但是带括号的形式需要双重转义:echo "&" | (findstr ^^^&)。所有这些都在更新后的规则中意义明确。 - dbenham
1
更新cmd.exe解析规则是棘手的,因为有30,000个字符的限制。我不得不编辑其他一些部分,使其更加简洁,以腾出空间。也许你可以编辑你的答案为什么延迟扩展在代码块内部失败? - dbenham
2
这种行为仅适用于管道。我确认 for /f .... in ('someCommand') do ... 始终会调用 cmd.exe 来执行 someCommand,即使 someCommand 是外部命令。管道设计的一个好处是 - 当通过 cmd /c 显式执行命令时,不会有第二个隐式的 cmd /c。因此,像 break | cmd /v:on /c echo !cmdcmdline! 这样的命令是高效的。 - dbenham
显示剩余6条评论

1
在CMD脚本中改变事情。
我对原因的快速而简单的理解:
批处理按预期运行,即使在管道的另一侧,也没有必要在“main”上加倍百分号,因为没有发生扩展,它是一个普通的变量。
也就是说,您编写的操作顺序是正确的,但是每行都在单独进行,我认为您只是离问题太近了,没有注意到或者我没有理解问题?
我不确定这是否清楚地说明了,因此我计划逐步审查我认为正在发生的事情:
“set“vData =...;%%main%%\Programs\Go\Bin”“
存储在内存中的结果:“...;%main%\Programs\Go\Bin”
“ECHO”%vData%“产生的结果为:“...;%main%\Programs\Go\Bin”
添加主要部分

set "main=C:\Main"

存储在内存中的结果为: "C:\Main"

ECHO %main% 的结果为: "C:\Main"

ECHO '%%main%%' 的结果为: "%C:\Main%"

现在定义了 Main,让我们再次 Echo vData:

存储在内存中的结果仍然是: "...;%main%\Programs\Go\Bin"

ECHO "%vData%" 的结果为: "...;%main%\Programs\Go\Bin"

CALL ECHOing "%vData%" 的结果为: "...;C:\Main\Programs\Go\Bin"

因此,这一侧依赖于扩展,但 FindStr 侧不依赖于扩展,因为 %main% 已经被定义,所以它在第一次传递时就被扩展了。

当被 CMD 解释器解析时,我认为顺序将如下所示:

Given your original Find String:

echo/%vData%| findstr /I /C:"%%main%%\\Programs\\Go\\Bin"

Becomes:

echo/...;%main%\Programs\Go\Bin| findstr /I /C:"%C:\Main%\\Programs\\Go\\Bin"

Becomes:

echo/...;C:\Main\Programs\Go\Bin | (findstr /I /C:"%C:\Main%\\Programs\\Go\\Bin")

或者,当被CMD解释器解析时,我认为顺序将如下:

Given the modified Regex String:

echo/%vData%| findstr /I /C:"%main%\\Programs\\Go\\Bin"

Becomes:

echo/...;%main%\Programs\Go\Bin| findstr /I /C:"C:\main\\Programs\\Go\\Bin"

Becomes:

echo/...;C:\Main\Programs\Go\Bin | (findstr /I /C:"C:\Main\\Programs\\Go\\Bin")

如果您的担忧是实际上在一个变量中存储了%%Main%%,比如说%_Regex%变量:

那么一般来说,您需要将管道符号另一侧的语句用括号括起来或者按我的经验调用findStr。

例如:

@(
  SETLOCAL
  ECHO OFF
)
rem // Set variable `vData` to literally contain `...;%main%\Programs\Go\Bin`:
set "vData=...;%%main%%\Programs\Go\Bin"
rem // Set variable `_Regex` to literally contain `%main%\\Programs\\Go\\Bin`:
SET "_Regex=%%main%%\\Programs\\Go\\Bin"
rem // Set variable `_Regex` to literally contain `C:\Main`:
set "main=C:\Main"

ECHO(&ECHO(Variable Contents After Main Set:&ECHO(
     echo(Normal:   vData = "%vData%"
     echo(Normal:  _Regex = "%_Regex%"
     echo(Normal:    main = "%main%"
CALL echo(CALLed:   vData = "%vData%"
CALL echo(CALLed:  _Regex = "%_Regex%"


ECHO(&ECHO(Testing the Results of The FindString Methods:
ECHO(==============================================&ECHO(
ECHO( Original ^%%^%%main^%%^%% Method:
echo/%vData%| findstr /I /C:"%%main%%\\Programs\\Go\\Bin"
ECHO(==============================================&ECHO(
ECHO( Using the ^%%_Regex^%% Stored Variable:
echo/%vData%| findstr /I /C:"%_Regex%"
ECHO(==============================================&ECHO(
ECHO( Using just ^%%main^%%:
echo/%vData%| findstr /I /C:"%main%\\Programs\\Go\\Bin"
ECHO(==============================================&ECHO(
ECHO( Using the CALL ^%%_Regex^%% Stored Variable:
echo/%vData%| CALL findstr /I /C:"%_Regex%"
ECHO(==============================================&ECHO(
ECHO( Using the (^%%_Regex^%%) Stored Variable:
echo/%vData%| ( findstr /I /C:"%_Regex%" )
ECHO(==============================================&ECHO(

结果:

C:\Admin>C:\Admin\TestFindStr.cmd

Variable Contents After Main Set:

Normal:   vData = "...;%main%\Programs\Go\Bin"
Normal:  _Regex = "%main%\\Programs\\Go\\Bin"
Normal:    main = "C:\Main"
CALLed:   vData = "...;C:\Main\Programs\Go\Bin"
CALLed:  _Regex = "C:\Main\\Programs\\Go\\Bin"

Testing the Results of The FindString Methods:
==============================================

 Original %%main%% Method:
==============================================

 Using the %_Regex% Stored Variable:
==============================================

 Using just %main%:
...;C:\Main\Programs\Go\Bin
==============================================

 Using the CALL %_Regex% Stored Variable:
...;C:\Main\Programs\Go\Bin
==============================================

 Using the (%_Regex%) Stored Variable:
...;C:\Main\Programs\Go\Bin
==============================================


这正是我根据CMD的经验所期望的。

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