为什么ErrorLevel仅在||之后设置操作员重定向失败?

时间:2016-12-25 01:08:17

标签: windows batch-file cmd io-redirection exit-code

重定向失败(由于文件不存在或文件访问不足),似乎没有设置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

有趣的是,当使用运算符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仍然&

>>> (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

如果出现条件连接运算符&&||ErrorLevel也设置为1(如果||出现在&&之前,两个分支都按照上一个示例执行,但我认为这只是因为&&计算了前面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?所有这些只能用于(失败的)重定向,因为这些是在执行任何命令之前处理的吗?

更奇怪的是,在正确恢复测试设置(即替换{{}时,我无法观察到相反的行为 - ErrorLevel0重置为&& {1}} (call ) {最初设置(call)ErrorLevel),清除文件1的只读属性,创建文件test.tmp(首先行不为空以避免test.nilset /P设置为ErrorLevel),并使用文件扩展名1而不是.bat进行测试(以避免.cmdset /P重置为ErrorLevel))。

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

2 个答案:

答案 0 :(得分:5)

我差不多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对于批处理开发人员来说是没用的,如果它在每次成功操作后总是被清除为0。因此,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.但是|| handler不知道翻译,因此它只是将原生返回代码转发给ERRORLEVEL,覆盖已存在的9009值。

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

  

更奇怪的是,我无法观察到相反的行为 -   错误级别由&amp;&amp;重置为0 - ,正确还原时   测试设置(即,通过(调用)替换(调用)(将ErrorLevel设置为   最初1),清除文件test.tmp的只读属性,   创建文件test.nil(第一行不为空以避免设置/ P)   ErrorLevel为1),并使用文件扩展名.bat而不是.cmd   测试(避免设置/ P将ErrorLevel重置为0))。

一旦你接受并非所有命令在成功时都清除了ERRORLEVEL,那么这种行为就非常有意义了。如果&&要消除保留的错误,保留先前的错误并不会有多大帮助。

答案 1 :(得分:4)

note :本答案中的所有内容只是对cmd.exe的汇编代码/调试符号的个人解释,是对生成汇编器输出的源代码的推导。这个答案中的所有代码都只是一种伪代码,大致反映了cmd.exe中发生的事情,只显示与问题相关的部分。

我们需要知道的第一件事是从errorlevel中的内部变量_LastRetCode(至少这是调试信息符号中的名称)检索GetEnvVar值功能

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

对于echo命令,eEcho函数处理输出功能,编码类似

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

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

但是,如何调用此函数?,如何创建重定向?

有一个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 
}

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

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

执行是

  • Dispatch(已从处理批处理执行的函数调用并作为参数接收执行的内容)调用eOr
  • eOr致电Dispatch执行eEcho(运营商左侧)
  • Dispatch调用SetDir来创建重定向
  • SetDir失败并返回1_LastRetCode没有任何变更)
  • Dispatch返回1SetDir失败
  • eOr检查返回值,因为它不是0,返回的值会存储在_LastRetCode中。现在_LastRetCode1
  • eOr调用Dispatch来执行eEcho(运营商的右侧)
  • Dispatch调用GetFuncPtr来检索eEcho函数的地址。
  • Dispatch调用返回的函数指针并返回返回值(eEcho始终返回0
  • eOr返回Dispatch0
  • 的返回值

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

eAnd函数中发生了什么,即&&运算符?

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

    ret = Dispatch( rightCommand )
    return ret
}

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

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