Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
586 views
in Technique[技术] by (71.8m points)

windows - Why is ErrorLevel set only after || operator upon a failed redirection?

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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:est>(call )&rd .
The process cannot access the file because it is being used by another process.

C:est>echo %errorlevel%
0

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

C:est>echo %errorlevel%
32

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

C:est>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:est>invalidCommand
'invalidCommand' is not recognized as an internal or external command,
operable program or batch file.

C:est>echo %errorlevel%
9009

But if you use the || operator, or CMD /C, then the ERRORLEVEL is 1 :-/

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

C:est>echo %errorlevel%
1

C:est>(call )

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

C:est>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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...