Page 1 of 2

I/O error detection is broken

Posted: 01 Jan 2016 14:48
by dbenham
Happy New Year everyone. I haven't been active for a while, but I thought I should start the New Year right with a new post.

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.
In general, && and || are reliable ways to consistently detect success or failure of internal commands. It does not work properly for DEL, but can be worked around easily enough.

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. :evil:


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. :evil:

And then we have DIR :shock:
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 :evil:


There are more internal commands to test for both input and output, but I've had enough of this insanity :roll:


Dave Benham

Re: I/O error detection is broken

Posted: 01 Jan 2016 20:52
by dbenham
I managed to write routines that could more safely execute ECHO and DIR in a way that allows detection of write errors, and does not crash the batch file.

safeOutput.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

:: 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 call :safeEcho echo test&&echo ECHO OK||echo ECHO FAIL
echo ERRORLEVEL = !errorlevel!
echo(

echo test DIR
(call )
>&3 call :safeDir /b local.txt&&echo DIR OK||echo DIR FAIL
echo ERRORLEVEL = !errorlevel!
echo(

exit /b


:safeEcho
setlocal
set "err=%temp%\%~nx0.%time::=.%.temp"
2>"%err%" echo(%*
for %%F in ("%err%") do if %%~zF gtr 0 (
  >&2 type %%F
  del "%err%"
  exit /b 1
)
del "%err%"
exit /b 0


:safeDir
set "err=%temp%\%~nx0.%time::=.%.temp"
2>"%err%" cmd /c dir %*
for %%F in ("%err%") do if %%~zF gtr 0 (
  >&2 type %%F
  del "%err%"
  exit /b 1
)
del "%err%"
exit /b 0

As before, I first ignore the prompt and run to completion with the USB drive plugged in.

Code: Select all

C:\test>safeOutput
Remove drive H: and press <Enter>

test ECHO output
ECHO OK
ERRORLEVEL = 0

test DIR
DIR OK
ERRORLEVEL = 0

"H:\output.txt" content:
echo test
local.txt

All is as expected.

Now I run and disconnect the USB when prompted:

Code: Select all

C:\test>safeOutput
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 FAIL
ERRORLEVEL = 1

test DIR
There is not enough space on the disk.
DIR FAIL
ERRORLEVEL = 1

"H:\output.txt" content:
The system cannot find the path specified.

The error messages are properly sent to the console via stderr, and my code successfully detects the errors. The script does not crash, and the final output is the expected error message when I try to TYPE the output file. (remember that the output file on H: is no longer accessible)


There is room for improvement, but this enough for a proof of concept.

I could have written a similar safe routine for SET output.

But I don't think it is possible to detect SET /P input failure or SET /P output failure.


Dave Benham

Re: I/O error detection is broken

Posted: 01 Jan 2016 21:47
by foxidrive
dbenham wrote:
  • 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 ||


Happy noo yeer to you too mate!

I don't wanna hijack your thread Dave - just saying that I wondered about the last 1/2 dozen words you said above.
Is that in relation to internal commands only, or a blanket statement?

I'm wondering if a command line executable doesn't return errorlevels, then does && still function as a conditional operator?

Re: I/O error detection is broken

Posted: 01 Jan 2016 22:09
by dbenham
&& works properly, even if the internal command does not set ERRORLEVEL to 0.


Dave Benham

Re: I/O error detection is broken

Posted: 01 Jan 2016 23:42
by foxidrive
dbenham wrote:&& works properly, even if the internal command does not set ERRORLEVEL to 0.

Dave Benham


Ok. external commands aren't on topic.

Re: I/O error detection is broken

Posted: 01 Jan 2016 23:59
by dbenham
foxidrive wrote:
dbenham wrote:&& works properly, even if the internal command does not set ERRORLEVEL to 0.

Dave Benham


Ok. external commands aren't on topic.

Not so much that. It's just that I'm not aware of any external commands that fail to set the ERRORLEVEL upon success.

When many internal commands succeed, the && works properly, but the ERRORLEVEL is not cleared to 0. Instead the prior value of ERRORLEVEL is preserved (the value that existed before the internal command was run).

In contrast, when nearly any error occurs when running nearly any internal command, then the ERRORLEVEL is set to non-zero, and also the || works.

There are a few exceptions where the non-zero ERRORLEVEL is not set upon failure unless the || operator is used - The links in my first post address this point.

But this topic is about how internal commands fail to properly report I/O errors. Neither || nor ERRORLEVEL work when the tested internal commands fail to read or write to a file that has already been successfully opened via redirection.


Dave Benham

Re: I/O error detection is broken

Posted: 02 Jan 2016 01:34
by foxidrive
dbenham wrote:
  • or you can rely on && and ||


Fine. I did say I didn't want to hijack your thread, but you said the above and I am not certain it's 100% correct, and future readers is what I'm thinking of.

Re: I/O error detection is broken

Posted: 02 Jan 2016 08:29
by einstein1969
welcome back an happy new year Dave!

I have executed the first script (input) but the result is different for the first test.

Code: Select all

@echo off
setlocal

:: Define input file on USB drive
set "in=F:\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 F: 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(

pause

exit /b

results without removing USB device:

Code: Select all


Reading Line 1:
OK
ERRORLEVEL=0
result=[Line 1]

Reading Line 2:
FAIL
ERRORLEVEL=1
result=[]

Remove drive F: and press <Enter>

Reading Line 3:
OK
ERRORLEVEL=0
result=[Line 3]

Reading Line 4:
FAIL
ERRORLEVEL=1
result=[]


I'm testing the following....

Re: I/O error detection is broken

Posted: 02 Jan 2016 08:54
by dbenham
einstein1969 wrote:I have executed the first script (input) but the result is different for the first test.

Thanks.

I had a copy/paste error that accidentally cut off the output before the last test. I've edited that post to correct it.


Dave Benham

Re: I/O error detection is broken

Posted: 03 Jan 2016 13:31
by foxidrive

Code: Select all

c:\>echo %errorlevel%
0

c:\>pad file.txt
The system cannot find the file specified.

c:\>echo %errorlevel%
0

c:\>pad file.txt && echo yep
The system cannot find the file specified.
yep

Re: I/O error detection is broken

Posted: 03 Jan 2016 18:10
by dbenham
What the hell foxi :? :!: :?: :lol:

I am totally mystified as to what point you are trying to make.

Re: I/O error detection is broken

Posted: 03 Jan 2016 18:12
by Squashman
dbenham wrote:What the hell foxi :? :!: :?: :lol:

I am totally mystified as to what point you are trying to make.

I think he is trying to say the errorlevel should be a 1 and the conditional execution should not echo.

Re: I/O error detection is broken

Posted: 03 Jan 2016 18:36
by dbenham
And what is pad? My Win 7 system does not recognize it, and it properly sets ERRORLEVEL and fires the || conditional

Code: Select all

C:\test>(call )

C:\test>pad && echo OK || echo FAIL
'pad' is not recognized as an internal or external command,
operable program or batch file.
FAIL

C:\test>echo %errorlevel%
1

If PAD is an external program on his machine, than it is PAD's responsibility to return a non-zero exit code upon failure. If PAD were to return a non-zero exit code, then || would fire properly.


Dave Benham

Re: I/O error detection is broken

Posted: 03 Jan 2016 19:23
by Squashman
But if the "SYSTEM" says it cannot find file.txt, shouldn't it return an errorlevel 1?

Re: I/O error detection is broken

Posted: 03 Jan 2016 19:50
by dbenham
What system? The only way I can see that error message arising with that command string is if PAD is an external program that generated that message.

I guess I never stated this outright, but my interest is in how CMD.EXE and its internal commands handle error conditions. I at least expect internal commands to handle errors consistently. External commands are the wild wild west - especially when they are non Micorosoft.

But regardless whether the command is internal or external, if the command returns a 0 exit code, then && fires, if non-zero, then || fires. The really odd thing is that a few internal commands fire || upon error, yet fail to set ERRORLEVEL unless || is used. This is evidence that the exit code and ERRORLEVEL are not quite synonymous.

The I/O errors are interesting (tragic) in that they are virtually undetectable, except for the error message written to stderr.

The only other internal command I remember with this problem is DEL. If DEL fails because the file doesn't exist, or if the file is read only, then an error message is written to stderr, but || does not fire, and ERRORLEVEL is not set. Failing to return an error because a non-existent file couldn't be deleted is normally not a big deal, but failing to raise an error when unable to delete a read only file is tragic.

There are probably other internal commands with similar problems.