可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Upon a failed redirection (due to a non-existent file or insufficient file access), the ErrorLevel
value seems not to be set (in the following examples, file test.tmp
is write-protected and file test.nil
does not exist):
>>> (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
However, as soon as the failed redirection is followed by the conditional concatenation operator ||
, which is querying the exit code, the ErrorLevel
becomes set to 1
, unexpectedly:
>>> (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
Interestingly, ErrorLevel
remains 0
when the operator &&
is used:
>>> (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
remains also 0
using the operator &
:
>>> (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
In case both conditional concatenation operators &&
and ||
appear, ErrorLevel
is set to 1
too (if ||
occurs before &&
, both branches are executed as in the last example, but I think this just because &&
evaluates the exit code of the preceding echo
command):
>>> (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
So what is the connection between the ErrorLevel
value and the ||
operator, why is ErrorLevel
affected by ||
? Is ||
copying the exit code to ErrorLevel
? Is all this only possible with (failed) redirections, because such are handled before any commands are executed?
Even more strangely, I could not observe the opposite behaviour -- ErrorLevel
being reset to 0
by &&
--, when correctly reverting the test setup (that is, replacing (call )
by (call)
(to set ErrorLevel
to 1
initially), clearing the read-only attribute of file test.tmp
, creating file test.nil
(first line not empty to avoid set /P
to set ErrorLevel
to 1
), and using file extension .bat
rather than .cmd
for testing (to avoid set /P
to reset ErrorLevel
to 0
)).
I observed the described behaviour on Windows 7 and Windows 10.
回答1:
I first discovered this illogical behavior nearly 5 years ago at File redirection in Windows and %errorlevel%. Two months later I discovered the same issue with the RD (RMDIR) command at batch: Exit code for "rd" is 0 on error as well. The title of that last question is actually misleading, because the return code of a failed RD is non-zero, but the ERRORLEVEL is unchanged from whatever value existed before the command was executed. If the return code were truly 0, then the ||
operator would not fire.
Is all this only possible with (failed) redirections, because such are
handled before any commands are executed?
You are correct that the redirection fails before the command is executed. And the ||
is responding to the non-zero return code of the redirection operation. The command (ECHO in your case) is never executed if the redirection fails.
So what is the connection between the ErrorLevel value and the ||
operator, why is ErrorLevel affected by ||? Is || copying the exit
code to ErrorLevel?
There are two different error related values that must be tracked - 1) any given command (or operation) return code (exit code), and 2) ERRORLEVEL. Return codes are transient - they must be checked after every single operation. ERRORLEVEL is the cmd.exe way to persist the "important" error states over time. The intent is for all errors to be detected, and the ERRORLEVEL is to be set accordingly. But ERRORLEVEL would be kind of useless to batch developers if it were always cleared to 0 after every successful operation. So the designers of cmd.exe attempted to make logical choices as to when a successful command clears the ERRORLEVEL, and when it preserves the prior value. I'm not sure how wise they were in their choices, but I have attempted to document the rules at Which cmd.exe internal commands clear the ERRORLEVEL to 0 upon success?.
The rest of this section is educated conjecture. I don't think a definitive answer is possible without communication from the original developers of cmd.exe. But this is what gives me a mental framework to successfully navigate the morass of cmd.exe error behavior.
I believe that wherever an error can occur within cmd.exe, the developers were supposed to detect the return code, set the ERRORLEVEL to non-zero upon error, and then fire any ||
code if it is in play. But in a few cases the developer introduced a bug by not playing by the rules. After failed redirection or failed RD, the developer(s) successfully invoked the ||
code, but failed to set the ERRORLEVEL properly.
I also believe that the developer(s) of ||
did some defensive programming. The ERRORLEVEL should already be set to non-zero before ||
code is executed. But I think the ||
developer wisely did not trust his/her peers, and decided to set the ERRORLEVEL within the ||
handler as well.
As to what non-zero value is used, it seems logical that ||
would forward the original return code value to ERRORLEVEL. This would mean that the original return code must have been stored in some temporary storage area that is distinct from ERRORLEVEL. I have two pieces of evidence that support this theory:
1) The ||
operator sets at least 4 different ERRORLEVEL values when RD fails, depending on the type of error.
2) The ERRORLEVEL set by ||
is the same value that CMD /C sets, and CMD /C simply forwards the return code of the last command/operation.
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
However, there is one peculiarity that threatens to invalidate this theory. If you attempt to run a non-existent command, then you get a 9009 error:
C:\test>invalidCommand
'invalidCommand' is not recognized as an internal or external command,
operable program or batch file.
C:\test>echo %errorlevel%
9009
But if you use the ||
operator, or CMD /C, then the ERRORLEVEL is 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
I resolve this anomaly in my mind by assuming that the code responsible for setting the 9009 ERRORLEVEL in the absense of ||
must be doing some type of context sensitive translation to generate 9009. But the ||
handler is not aware of the translation, so it simply forwards the native return code to ERRORLEVEL, overwriting the 9009 value that is already there.
I am not aware of any other commands that give different non-zero ERRORLEVEL values depending on whether ||
was used or not.
Even more strangely, I could not observe the opposite behaviour --
ErrorLevel being reset to 0 by && --, when correctly reverting the
test setup (that is, replacing (call ) by (call) (to set ErrorLevel to
1 initially), clearing the read-only attribute of file test.tmp,
creating file test.nil (first line not empty to avoid set /P to set
ErrorLevel to 1), and using file extension .bat rather than .cmd for
testing (to avoid set /P to reset ErrorLevel to 0)).
Once you accept that not all commands clear the ERRORLEVEL upon success, then this behavior makes perfect sense. It wouldn't do much good to preserve prior errors if the &&
were to wipe out the preserved error.
回答2:
note: All the content in this answer is just a personal interpretation of the assembler code/debug symbols of the cmd.exe
, a deduction of the source code that generates the assembler output. All the code in this answer is just a sort of pseudocode that roughly reflects what happens inside cmd.exe
, showing only the parts relevant to the question.
The first thing that we need to know is that errorlevel
value is retrieved from the internal variable _LastRetCode
(at least this is the name in the debug information symbols) from the GetEnvVar
function.
Second thing to know is that internally most of the cmd
commands are associated with a set of functions. Those functions change (or not) the value in the _LastRetCode
but they also return a sucess/failure code (a 0/1
value) that is internally used to determine if there is an error.
In the case of echo
command, the eEcho
function handles the output functionality, coded something like
eEcho( x ){
....
// Code to echo the required value
....
return 0
}
That is, echo
command has only one exit point and does not set/clear the _LastRetCode
variable (it will not change the errorlevel
value) and will always return an sucess code. echo
does not fail (from the batch point of view, it can fail and write to stderr
, but it will always return 0
and will never change _LastRetCode
).
But, how is this function called?, how is the redirection created?
There is a Dispatch
function that determines the command/function to call and that previously calls the SetDir
function (if needed) to create the required redirections. Once the redirection is set, the GetFuncPtr
function retrieves the address of the function to execute (the function associated to the cmd
command) calls it and returns the output of it to the caller.
Dispatch( x, x ){
....
if (redirectionNeeded){
ret = SetRedir(...)
if (ret != 0) return 1
}
....
func = GetFuncPtr(...)
ret = func(...)
return ret
}
While the return values from those functions reflect the presence of an error (they return 1
on failure, 0
on sucess), nor the Dispatch
not the SetDir
change the _LastRetCode
variable (while not shown here, none of them make any reference to the variable), so there is no errorlevel
change when a redirection fails.
What changes when using the ||
operator?
The ||
operator is handled inside the eOr
function that is coded, yes, again more or less
eOr( x ){
ret = Dispatch( leftCommand )
if (ret == 0) return 0
_LastRetCode = ret
ret = Dispatch( rightCommand )
return ret
}
It first executes the command on the left side. If it does not return an error there is nothing to do and exits with a sucess value. But if the left command fails, it stores the returned value inside the _LastRetCode
variable before calling the command on the right side. In the case in the question
(> "test.tmp" echo Text) || echo Fail
The execution is
Dispatch
(who has been called from the functions handling the batch execution and receives as a parameter what to execute) calls eOr
eOr
calls Dispatch
to execute eEcho
(the left side of the operator)
Dispatch
calls SetDir
to create the redirection
SetDir
fails and returns 1
(there is not any change to the _LastRetCode
)
Dispatch
returns 1
as SetDir
has failed
eOr
checks the return value and as it is not 0
the returned value is stored in _LastRetCode
. Now _LastRetCode
is 1
eOr
calls Dispatch
to execute eEcho
(the right side of the operator)
Dispatch
calls GetFuncPtr
to retrieve the address of the eEcho
function.
Dispatch
calls the returned function pointer and returns it return value (eEcho
always return 0
)
eOr
returns the return value from Dispatch
(0
)
That is how the error value returned (return 1
) on a failed redirection operation is stored in the _LastRetCode
when using the ||
operator.
What happens in the eAnd
function, that is, the &&
operator?
eAnd( x ){
ret = Dispatch( leftCommand )
if (ret != 0) return ret
ret = Dispatch( rightCommand )
return ret
}
There is not any change to the _LastRetCode
, so, if the called commands does not change the variable there is not any change to the errorlevel
.
Please, remember this is just an interpretation of what I see, real code will behave approximately the same but it will be almost surely different.