I discovered something almost a year ago that has been bothering me, but I never got around to posting it before.
CMD.EXE Error handling for basic I/O is totally disfunctional.
I've edited the following paragraph in response to foxidrive's concerns. Hopefully the following will no longer mislead readers.
Error handling is generally inconsistent in the batch world, but normally you can detect when an internal command fails without undue burden. There are a few odd cases, for example:
- Many internal commands don't clear ERRORLEVEL upon success, but some do - It's a pain, but can be worked around. You can manually clear ERRORLEVEL before each command, or you can rely on && and ||.
- Redirection errors don't set the ERRORLEVEL - || to the rescue
- Failed RD does not set the ERRORLEVEL - || to the rescue
- DEL does not fire || or set ERRORLEVEL if a file was not deleted. This can be worked around by checking if a file still exists after issuing the DEL command.
- HELP return logic is backward. It returns ERRORLEVEL 1 (fires ||) if no argument is given, or if the argument is not a command recognized by HELP. It returns ERRORLEVEL 0 (fires &&) if an argument is given that is not recognized by HELP.
But then I decided to test I/O operations. What happens if redirection to a file is successful, but then the file is inaccessible midstream? Perhaps the device becomes full, or the device is disconnected.
There is nothing more basic than I/O, right? How bad can it be
The results are disturbing.
First I decided to test input. I have a USB read/write device on drive H:, so I wrote the following:
Warning - I recommend using a USB drive that is expendable - The tests involve removing the device while files are open, which is not a good thing. I had no ill effects, but...
testInput.bat
Code: Select all
@echo off
setlocal
:: Define input file on USB drive
set "in=H:\input.txt"
:: Create input file
>"%in%" (
echo Line 1
echo(
echo Line 3
)
3<"%in%" call :test
exit /b
:test
echo Reading Line 1:
set "ln="
(call )
<&3 set /p "ln=" && echo OK || echo FAIL
echo ERRORLEVEL=%errorlevel%
echo result=[%ln%]
echo(
echo Reading Line 2:
set "ln="
(call )
<&3 set /p "ln=" && echo OK || echo FAIL
echo ERRORLEVEL=%errorlevel%
echo result=[%ln%]
echo(
set /p "=Remove drive H: and press <Enter>"
echo(
echo Reading Line 3:
set "ln="
(call )
<&3 set /p "ln=" && echo OK || echo FAIL
echo ERRORLEVEL=%errorlevel%
echo result=[%ln%]
echo(
echo Reading Line 4:
set "ln="
(call )
<&3 set /p "ln=" && echo OK || echo FAIL
echo ERRORLEVEL=%errorlevel%
echo result=[%ln%]
echo(
For the first run, I ignore the prompt and allow the script to run to completion with the USB device plugged in.
Here are the results: EDIT - I corrected a copy/paste error that cut off the output
Code: Select all
C:\test>testInput
Reading Line 1:
OK
ERRORLEVEL=0
result=[Line 1]
Reading Line 2:
FAIL
ERRORLEVEL=1
result=[]
Remove drive H: and press <Enter>
Reading Line 3:
OK
ERRORLEVEL=0
result=[Line 3]
Reading Line 4:
FAIL
ERRORLEVEL=1
result=[]
Everything is as expected. It is "well known" that it is impossible to distinguish between empty input on line 2 vs. End Of File (EOF) on non-existent line 4.
Then for the next run I remove the USB drive when prompted:
Code: Select all
C:\test>testInput
Reading Line 1:
OK
ERRORLEVEL=0
result=[Line 1]
Reading Line 2:
FAIL
ERRORLEVEL=1
result=[]
Remove drive H: and press <Enter>
Reading Line 3:
FAIL
ERRORLEVEL=1
result=[]
Reading Line 4:
FAIL
ERRORLEVEL=1
result=[]
Ouch. EOF is never reached, but looking back at the first run, it is evident that empty input, EOF, and error reading input all yield the same result.
Now for the output tests.
testOutput.bat
Code: Select all
@echo off
setlocal enableDelayedExpansion
:: Define an output file on my USB drive
set "out=H:\output.txt"
:: Create a file needed for some tests on my local hard drive
>local.txt echo local.txt content
:: Define LF to contain a newline character
set ^"LF=^
%= empty line =%
^"
:: Call my test routine with stream 3 directed to my USB output file
call :test 3>"%out%"
:: Show the results in the output file
echo "%out%" content:
type "%out%"
exit /b
:test
set /p "=Remove drive H: and press <Enter>"
echo(
echo test ECHO output
(call )
>&3 echo echo test&&echo ECHO OK||echo ECHO FAIL
echo ERRORLEVEL = !errorlevel!
echo(
echo test SET /P output
set "var="
(call )
>&3 <local.txt set /p "var=set /p test!LF!"&&echo SET /P OK||echo SET /P FAIL
echo ERRORLEVEL = !errorlevel!
echo var=!var!
echo(
echo test SET output
(call )
>&3 set var&&echo SET OK||echo SET FAIL
echo ERRORLEVEL = !errorlevel!
echo(
echo test DIR
(call )
>&3 dir /b local.txt&&echo DIR OK||DIR FAIL
echo ERRORLEVEL = !errorlevel!
echo(
exit /b
For the first run I disregard the prompt and let the script go to completion with my USB drive plugged in. Here are the results:
Code: Select all
C:\test>testOutput
Remove drive H: and press <Enter>
test ECHO output
ECHO OK
ERRORLEVEL = 0
test SET /P output
SET /P OK
ERRORLEVEL = 0
var=local.txt content
test SET output
SET OK
ERRORLEVEL = 0
test DIR
DIR OK
ERRORLEVEL = 0
"H:\output.txt" content:
echo test
set /p test
var=local.txt content
local.txt
C:\test>echo !var!
!var!
All is as expected.
Now I run again, but this time remove my USB drive when prompted. And then... Shazam
Code: Select all
C:\test>testOutput
Remove drive H: and press <Enter>
test ECHO output
The volume for a file has been externally altered so that the opened file is no longer valid.
ECHO OK
ERRORLEVEL = 0
test SET /P output
SET /P OK
ERRORLEVEL = 0
var=local.txt content
test SET output
The volume for a file has been externally altered so that the opened file is no longer valid.
SET OK
ERRORLEVEL = 0
test DIR
There is not enough space on the disk.
C:\test>echo !var!
local.txt content
ECHO and SET both print out a good error message to stderr, but the error is nearly invisible to the code The only way to programmatically detect the error is to somehow capture the error message and take action accordingly - Yuck.
SET /P blithely ignores the fact that it failed to write out the prompt - the error is totally undetectable.
And then we have DIR
It gives a fatal error with an inappropriate error message. All batch processing immediately ceases, without the expected implicit ENDLOCAL. Control is returned to the command prompt with delayed expansion still enabled and the local variables still defined. The only way to safely run a DIR command if you think it is possible for output to fail midstream is to issue the command in a new CMD session. And then you would need some way to capture the fact that the output failed. I supposed it can be done, but again, Yuck
There are more internal commands to test for both input and output, but I've had enough of this insanity
Dave Benham