为啥 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
有趣的是,当使用运算符 &&
时,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
?这一切是否只有通过(失败的)重定向才能实现,因为这些都是在执行任何命令之前处理的?
更奇怪的是,当正确恢复测试设置时(即,将 (call )
替换为 (call)
(最初将ErrorLevel
设置为1
),清除文件test.tmp
的只读属性,创建文件test.nil
(第一行不为空以避免set /P
将ErrorLevel
设置为1
) ,并使用文件扩展名.bat
而不是.cmd
进行测试(避免set /P
将ErrorLevel
重置为0
)。
我在 Windows 7 和 Windows 10 上观察到所描述的行为。
【问题讨论】:
我正要问一个类似的问题时,我发现其中大部分已经是covered here by dbenham 在调试器中运行 CMD。在这里查看我的答案***.com/questions/38148571/vb6-debugging-compiled。x cmd.*
显示了很多带有 parse
和 errorlevel
的函数。
如果您的问题是“为什么会发生这种情况?”,那么 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,那么这种行为就很合理了。如果 &&
要清除保留的错误,那么保留先前的错误不会有多大好处。
【讨论】:
【参考方案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
中。现在_LastRetCode
是1
eOr
调用Dispatch
执行eEcho
(运算符右侧)
Dispatch
调用GetFuncPtr
来检索eEcho
函数的地址。
Dispatch
调用返回的函数指针并返回返回值(eEcho
总是返回0
)
eOr
从Dispatch
返回返回值(0
)
这就是使用||
运算符时重定向操作失败返回的错误值 (return 1
) 存储在 _LastRetCode
中的方式。
eAnd
函数,即&&
运算符会发生什么?
eAnd( x )
ret = Dispatch( leftCommand )
if (ret != 0) return ret
ret = Dispatch( rightCommand )
return ret
_LastRetCode
没有任何变化,因此,如果调用的命令没有改变变量,errorlevel
也没有任何变化。
请记住,这只是对我所见的解释,实际代码的行为大致相同,但几乎肯定会有所不同。
【讨论】:
+1 - 您对“执行是...列表”的编辑是将您的答案整合成对我有意义的东西的关键部分 :-) 据我所知,我们的答案是兼容的,除非您的答案暗示&&
和||
的链是递归实现的,从右到左推入堆栈,因此最后一个(最左边)条件运算符的左侧首先执行。这是有道理的,而且很优雅。我的回答假设从左到右进行迭代处理,这也可以工作,但实现起来更复杂,显然不是现实:-)
@dbenham,当我向您指出这个答案时,我的意图是确认您成功了。据我所见(只是快速浏览,我将再次对其进行更好的测试)a && b || c
被处理为eOr(eAnd(a, b), c)
,是的,这意味着a
中的成功和b
中的失败意味着@ 987654400@被执行以上是关于为啥 ErrorLevel 只在 || 之后设置重定向失败时的操作员?的主要内容,如果未能解决你的问题,请参考以下文章
IE8:Div hover 只在设置背景色时有效,很奇怪,为啥?