Why is ErrorLevel set only after || operator upon

2019-01-25 10:15发布

问题:

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.