为啥 ErrorLevel 只在 || 之后设置重定向失败时的操作员?

Posted

技术标签:

【中文标题】为啥 ErrorLevel 只在 || 之后设置重定向失败时的操作员?【英文标题】:Why is ErrorLevel set only after || operator upon a failed redirection?为什么 ErrorLevel 只在 || 之后设置重定向失败时的操作员? 【发布时间】:2016-12-25 01:08:16 【问题描述】:

在重定向失败时(由于文件不存在或文件访问权限不足),ErrorLevel 值似乎未设置(在以下示例中,文件 test.tmp 是写保护的,而文件 test.nil不存在):

>>> (call ) & rem // (reset `ErrorLevel`)

>>> > "test.tmp" echo Text
Access is denied.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> < "test.nil" set /P DUMMY=""
The system cannot find the file specified.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

但是,一旦失败的重定向之后是条件连接运算符||,它正在查询退出代码,ErrorLevel 就会意外地设置为1

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) || echo Fail
Access is denied.
Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") || echo Fail
The system cannot find the file specified.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

有趣的是,当使用运算符 &amp;&amp; 时,ErrorLevel 仍然是 0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) && echo Pass
Access is denied.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") && echo Pass
The system cannot find the file specified.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

ErrorLevel 仍然是 0 使用运算符 &amp;:

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) & echo Pass or Fail
Access is denied.
Pass or Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") & echo Pass or Fail
The system cannot find the file specified.
Pass or Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

如果条件连接运算符&amp;&amp;|| 都出现,ErrorLevel 也设置为1(如果|| 出现在&amp;&amp; 之前,则两个分支都像上一个示例一样执行,但是我认为这只是因为&amp;&amp; 评估了前面echo 命令的退出代码):

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) && echo Pass || echo Fail
Access is denied.
Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") || echo Fail && echo Pass
The system cannot find the file specified.
Fail
Pass

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

那么ErrorLevel 值和|| 运算符之间有什么联系,为什么ErrorLevel 会受到|| 的影响? || 是否将退出代码复制到 ErrorLevel?这一切是否只有通过(失败的)重定向才能实现,因为这些都是在执行任何命令之前处理的?

更奇怪的是,当正确恢复测试设置时(即,将 (call ) 替换为 (call) (最初将ErrorLevel设置为1),清除文件test.tmp的只读属性,创建文件test.nil(第一行不为空以避免set /PErrorLevel设置为1) ,并使用文件扩展名.bat 而不是.cmd 进行测试(避免set /PErrorLevel 重置为0)。

我在 Windows 7 和 Windows 10 上观察到所描述的行为。

【问题讨论】:

我正要问一个类似的问题时,我发现其中大部分已经是covered here by dbenham 在调试器中运行 CMD。在这里查看我的答案***.com/questions/38148571/vb6-debugging-compiled。 x cmd.* 显示了很多带有 parseerrorlevel 的函数。 如果您的问题是“为什么会发生这种情况?”,那么 Microsoft 只知道... 这种行为记录在 this answer,除了其他也以这种方式返回错误值的情况;查找“退出代码管理(表 5)”。顺便说一句,将错误级别重置为 0 的更简单方法是通过 ver 命令... 【参考方案1】:

大约 5 年前,我在 File redirection in Windows and %errorlevel% 首次发现了这种不合逻辑的行为。两个月后,我在batch: Exit code for "rd" is 0 on error as well 发现了与 RD (RMDIR) 命令相同的问题。最后一个问题的标题实际上具有误导性,因为失败 RD 的返回码不为零,但 ERRORLEVEL 与执行命令之前存在的任何值相比都没有改变。如果返回码真的为 0,那么|| 运算符将不会触发。

这一切是否只有通过(失败的)重定向才能实现,因为这些都是 在执行任何命令之前处理?

在执行命令之前重定向失败是正确的。而|| 正在响应重定向操作的非零返回码。如果重定向失败,则永远不会执行命令(在您的情况下为 ECHO)。

那么 ErrorLevel 值和 || 之间的联系是什么? 运算符,为什么 ErrorLevel 受 || 影响?是||复制出口 ErrorLevel 的代码?

必须跟踪两个不同的与错误相关的值 - 1) 任何给定的命令(或操作)返回代码(退出代码),以及 2) ERRORLEVEL。返回代码是暂时的 - 必须在每次操作后检查它们。 ERRORLEVEL 是一种 cmd.exe 方法,可以随着时间的推移保持“重要”错误状态。目的是检测所有错误,并相应地设置 ERRORLEVEL。但是如果每次成功操作后 ERRORLEVEL 总是被清零,那么对于批处理开发人员来说,ERRORLEVEL 将是无用的。因此 cmd.exe 的设计者试图对成功的命令何时清除 ERRORLEVEL 以及何时保留先前的值做出合乎逻辑的选择。我不确定他们的选择有多明智,但我已尝试在 Which cmd.exe internal commands clear the ERRORLEVEL to 0 upon success? 记录规则。

本节的其余部分是有根据的猜想。如果没有 cmd.exe 的原始开发人员的沟通,我认为不可能有明确的答案。但这给了我一个心理框架,可以成功地在 cmd.exe 错误行为的泥沼中导航。

我相信在 cmd.exe 中可能发生错误的任何地方,开发人员都应该检测返回代码,在发生错误时将 ERRORLEVEL 设置为非零,然后在运行时触发任何|| 代码。但在少数情况下,开发人员因不遵守规则而引入了错误。重定向失败或RD失败后,开发者成功调用||代码,但未能正确设置ERRORLEVEL。

我也相信|| 的开发者做了一些防御性编程。在执行|| 代码之前,ERRORLEVEL 应该已经设置为非零。但我认为 || 开发人员明智地不信任他/她的同行,并决定在 || 处理程序中设置 ERRORLEVEL。

至于使用什么非零值,|| 将原始返回码值转发给 ERRORLEVEL 似乎是合乎逻辑的。这意味着原始返回码必须存储在某个不同于 ERRORLEVEL 的临时存储区域中。我有两个证据支持这个理论:

1) The || operator sets at least 4 different ERRORLEVEL values when RD fails, depending on the type of error.

2)||设置的ERRORLEVEL与CMD /C设置的值相同,CMD /C只是转发最后一个命令/操作的返回码。

C:\test>(call )&rd .
The process cannot access the file because it is being used by another process.

C:\test>echo %errorlevel%
0

C:\test>(call )&rd . || rem
The process cannot access the file because it is being used by another process.

C:\test>echo %errorlevel%
32

C:\test>(call )&cmd /c rd .
The process cannot access the file because it is being used by another process.

C:\test>echo %errorlevel%
32

但是,有一个特点可能会使这一理论失效。如果您尝试运行不存在的命令,则会收到 9009 错误:

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

C:\test>echo %errorlevel%
9009

但是如果你使用|| 操作符,或者 CMD /C,那么 ERRORLEVEL 是 1 :-/

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

C:\test>echo %errorlevel%
1

C:\test>(call )

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

C:\test>echo %errorlevel%
1

我通过假设在没有|| 的情况下负责设置 9009 ERRORLEVEL 的代码必须执行某种类型的上下文敏感转换来生成 9009,从而解决了这个异常问题。但是 || 处理程序不知道翻译,所以它只是将本机返回代码转发到 ERRORLEVEL,覆盖已经存在的 9009 值。

我不知道有任何其他命令会根据是否使用 || 提供不同的非零 ERRORLEVEL 值。

更奇怪的是,我无法观察到相反的行为—— ErrorLevel 被 && -- 重置为 0,正确恢复 测试设置(即,将 (call ) 替换为 (call) (将 ErrorLevel 设置为 1最初),清除文件test.tmp的只读属性, 创建文件 test.nil (第一行不为空以避免设置 /P 设置 ErrorLevel 为 1),并使用文件扩展名 .bat 而不是 .cmd 测试(避免设置 /P 将 ErrorLevel 重置为 0))。

一旦您接受并非所有命令都会在成功时清除 ERRORLEVEL,那么这种行为就很合理了。如果 &amp;&amp; 要清除保留的错误,那么保留先前的错误不会有多大好处。

【讨论】:

【参考方案2】:

注意:本回答所有内容只是个人对cmd.exe的汇编代码/调试符号的解读,对产生汇编输出的源代码的推演。此答案中的所有代码只是一种伪代码,大致反映了cmd.exe 内部发生的情况,仅显示与问题相关的部分。

首先我们需要知道的是errorlevel的值是从GetEnvVar函数的内部变量_LastRetCode(至少这是调试信息符号中的名称)中检索出来的。

要知道的第二件事是,大多数cmd 命令在内部都与一组函数相关联。这些函数更改(或不更改)_LastRetCode 中的值,但它们还返回一个成功/失败代码(0/1 值),该代码在内部用于确定是否存在错误。

echo 命令的情况下,eEcho 函数处理输出功能,编码类似于

eEcho( x )
    ....
    // Code to echo the required value
    ....
    return 0

也就是说,echo 命令只有一个退出点,不会设置/清除_LastRetCode 变量(它不会改变errorlevel 的值),并且总是会返回一个成功代码。 echo 不会失败(从批处理的角度来看,它可以失败并写入stderr,但它总是会返回0,并且永远不会改变_LastRetCode)。

但是,这个函数是怎么调用的?,重定向是怎么创建的?

有一个Dispatch 函数确定要调用的命令/函数,并且之前调用SetDir 函数(如果需要)来创建所需的重定向。一旦设置了重定向,GetFuncPtr 函数就会检索要执行的函数的地址(与cmd 命令关联的函数)调用它并将其输出返回给调用者。

Dispatch( x, x )
    ....

    if (redirectionNeeded)
        ret = SetRedir(...)
        if (ret != 0) return 1
    

    ....

    func = GetFuncPtr(...)
    ret = func(...)
    return ret

虽然这些函数的返回值反映了错误的存在(它们在失败时返回 1,在成功时返回 0),Dispatch 不是 SetDir 也不会更改 _LastRetCode 变量(而此处未显示,它们都没有对变量进行任何引用),因此重定向失败时不会更改errorlevel

使用|| 运算符时有何变化?

|| 运算符在 eOr 编码函数内部处理,是的,或多或少

eOr( x )
    ret = Dispatch( leftCommand )
    if (ret == 0) return 0

    _LastRetCode = ret

    ret = Dispatch( rightCommand )
    return ret 

它首先执行左侧的命令。如果它没有返回错误,则无事可做,并以成功值退出。但是如果左边的命令失败,它会在调用右边的命令之前将返回值存储在_LastRetCode 变量中。在问题的情况下

(> "test.tmp" echo Text) || echo Fail

执行是

Dispatch(已从处理批处理执行的函数中调用并作为参数接收要执行的内容)调用eOr eOr 调用Dispatch 执行eEcho(运算符左侧) Dispatch 调用 SetDir 来创建重定向 SetDir 失败并返回1_LastRetCode 没有任何变化) Dispatch 返回 1,因为 SetDir 失败 eOr 检查返回值,因为它不是0,所以返回值存储在_LastRetCode 中。现在_LastRetCode1 eOr调用Dispatch执行eEcho(运算符右侧) Dispatch 调用GetFuncPtr 来检索eEcho 函数的地址。 Dispatch调用返回的函数指针并返回返回值(eEcho总是返回0eOrDispatch返回返回值(0)

这就是使用|| 运算符时重定向操作失败返回的错误值 (return 1) 存储在 _LastRetCode 中的方式。

eAnd 函数,即&amp;&amp; 运算符会发生什么?

eAnd( x )
    ret = Dispatch( leftCommand )
    if (ret != 0) return ret 

    ret = Dispatch( rightCommand )
    return ret

_LastRetCode 没有任何变化,因此,如果调用的命令没有改变变量,errorlevel 也没有任何变化。

请记住,这只是对我所见的解释,实际代码的行为大致相同,但几乎肯定会有所不同。

【讨论】:

+1 - 您对“执行是...列表”的编辑是将您的答案整合成对我有意义的东西的关键部分 :-) 据我所知,我们的答案是兼容的,除非您的答案暗示&amp;&amp;|| 的链是递归实现的,从右到左推入堆栈,因此最后一个(最左边)条件运算符的左侧首先执行。这是有道理的,而且很优雅。我的回答假设从左到右进行迭代处理,这也可以工作,但实现起来更复杂,显然不是现实:-) @dbenham,当我向您指出这个答案时,我的意图是确认您成功了。据我所见(只是快速浏览,我将再次对其进行更好的测试)a &amp;&amp; b || c 被处理为eOr(eAnd(a, b), c),是的,这意味着a 中的成功和b 中的失败意味着@ 987654400@被执行

以上是关于为啥 ErrorLevel 只在 || 之后设置重定向失败时的操作员?的主要内容,如果未能解决你的问题,请参考以下文章

findstr 退出代码/错误级别

scrapy 加入redis去重之后出现了如下报错,为啥

IE8:Div hover 只在设置背景色时有效,很奇怪,为啥?

为啥乘法只在一侧短路

Powershell 是不是忘记在解析器错误上设置 ERRORLEVEL?

第26课:返回值