I am writing a batch script to extract data from a server. If any command fails in the script below, I want to exit the batch program and record the error in an error log file.
Here is what I have so far:
call epmautomate login aaaa#a.com abcd123 https://www.website.com
epmautomate exportdata ABC_DATA_EXTRACT & epmautomate downloadfile ABC_DATA_EXTRACT.zip & MOVE "C:\doc\bin\ABC_DATA_EXTRACT.zip" C:\Users\Administrator\Documents\folder & cd C:\Users\Administrator\Documents\folder & unzip -n C:\Users\Administrator\Documents\folder\ABC_DATA_EXTRACT.zip & C:\Users\Administrator\Documents\folder\change_header.cmd
how can I add error detection and logging?
Before I discuss the logging and the errors I would like to point out what the reason was we wanted you to clean your code (you didn't do it as how we suggested):
there is no reason why you should use the & between your commands. Put each command on a separate line. & means "whatever happens execute also the following" but makes scripts unreadable. So if possible always put each command on a seperate line instead.
you use different ways to execute epmautomate. If it is a script use call each time you want to execute it else don't use call at all when you wish to execute epmautomate. Supposing your code works, I can assume it is not a script and the call isn't needed.
it is preferable to surround all your paths with double qoutes
Now the logging and error detection.
I know 2 different approaches for logging errors and making a batch-file exit on an error. The most important condition is that all commands (either personnal, built-in or 3rd party scripts/software) you use in your script must set the errorlevel correctly. That is the only condition if you want the cmd interpreter to know that an error occured during the execution of a command. Another condition this time for a proper log-file is that the commands you use should write proper error messages. In both approaches the error messages written to the error channel are appended to the log-file using the 2>> error redirection operator (the link also shows how to redirect both output and error messages if interested). I'll assume the path to the log-file without surrounding double quotes is available in the logfile variable. I've added the double quotes each time the variable is used: "%logfile%".
The first approach makes use of an IF statement. The IF ERRORLEVEL n will check if the errorlevel is greater or equal to n. So if we assume a command command1 sets errorlevels correctly (0 if successful, 1 or greater otherwise), the following should be able to stop your script if an errorlevel occured during its execution
command1 2>> "%logfile%"
IF ERRORLEVEL 1 (
REM some extra personal errormessage if needed
echo command1 failed, check the log-file for more info
REM Exit current script and set errorlevel on 1 (failure)
exit /b 1
)
You can exit with another strict positive integer if you want and use personal error codes.
The second approach makes use of conditional execution execution inside code blocks (groups of commands). command1 && command2 will execute command2 only if command1 was succesful. The errorlevel after command1 && command2 will reveal if both exited successfully or if one of them exited with an error. If you group commands together between ( ) like this:
(
command1
command2
command3
)
the cmd interpreter will just put all commands on one line and put && in between: it will end up executing command1 && command2 && command3. So to execute a group of commands and exit if an error occured during execution of one of the commands, one can use
(
command1
command2
command3
) 2>> "%logfile%"
IF ERRORLEVEL 1 (
REM some extra personal errormessage if needed
echo An error occured, consult the log-file for more info
REM Exit current script and set errorlevel on 1 (failure)
exit /b 1
)
There is one downside for this approach: the use of variables inside the code blocks is limited. As all commands inside the block are parsed and executed as one single command, you cannot give a variable a new value inside the block and use that new value inside the same block with the normal variable expansion (i.e. %var%). You'll have to use delayed expansion if you want to be able to use the new value.
Which approach you pick depends on the situation and on what you want to achieve. The first approach is a more general one. Thanks to the "high" variety of IF statements, it can be used for commands that don't set the errorlevel but use another way of communicating an error that occurred during execution. The first approach also allows a more accurate error analysis because you know which command caused the error and can add that info in the log-file easily. There is a problem though: you'll have some serious type work. You can try to solve that issue by using a function that executes all your commands and exit the batch script from within the function if needed but it's not that easy. I have another more easy option to abandon the script when using the function but I'll come back to it later.
The second approach has the disadvantage that you can't easily identify the command where the error occurred. You can solve that issue by printing a "success" message after each command to the log-file. After all, a good log-file should also contain what has been executed successfully.
#echo off
set logfile=C:\Users\path\to\logfile errors.txt
(
epmautomate login aaaa#a.com abcd123 https://www.website.com 2>> "%logfile%"
echo first epmautomate ok >> "%logfile%"
epmautomate exportdata ABC_DATA_EXTRACT 2>> "%logfile%"
echo second epmautomate ok >> "%logfile%"
epmautomate downloadfile ABC_DATA_EXTRACT.zip 2>> "%logfile%"
echo third epmautomate ok >> "%logfile%"
MOVE "C:\doc\bin\ABC_DATA_EXTRACT.zip" "C:\Users\Administrator\Documents\folder" 2>> "%logfile%"
echo move zip ok >> "%logfile%"
cd "C:\Users\Administrator\Documents\folder" 2>> "%logfile%"
echo cd to admin folder ok >> "%logfile%"
unzip -n "C:\Users\Administrator\Documents\folder\ABC_DATA_EXTRACT.zip" 2>> "%logfile%"
echo unzip zipfike ok >> "%logfile%"
call "C:\Users\Administrator\Documents\folder\change_header.cmd" 2>> "%logfile%"
echo call to change-header ok >> "%logfile%"
)
IF ERRORLEVEL 1 (
REM some extra personal errormessage if needed
echo An error occured, consult the log-file for more info
REM Exit current script and set errorlevel on 1 (failure)
exit /b 1
)
It does the job. You'll just have to write each thing you want in your log and move the error redirections inside the code block . If an error occurs you'll still have to look back in your code to know which command produced the last error messages in your log-file though but at least you'll know which command failed in your code-block thanks to the success messages.
For the completeness I'll add the solution for the first approach using a function to execute your commands as I said earlier (which I actually prefer). But because exiting a script from a function can be quite complex I would rather use the idea from the second approach (conditional execution inside a code-block) to abandon execution of the rest of the commands:
#echo off
set logfile=C:\Users\path\to\logfile_errors.txt
(
call :executeOwn epmautomate login aaaa#a.com abcd123 https://www.website.com
call :executeOwn epmautomate exportdata ABC_DATA_EXTRACT
call :executeOwn epmautomate downloadfile ABC_DATA_EXTRACT.zip
call :executeOwn MOVE "C:\doc\bin\ABC_DATA_EXTRACT.zip" "C:\Users\Administrator\Documents\folder"
call :executeOwn cd "C:\Users\Administrator\Documents\folder"
call :executeOwn unzip -n "C:\Users\Administrator\Documents\folder\ABC_DATA_EXTRACT.zip"
call :executeOwn call "C:\Users\Administrator\Documents\folder\change_header.cmd"
)
REM Exit current batch script with error status from last executed call
exit /b %ERRORLEVEL%
:executeOwn
%* 2>> "%logfile%"
IF ERRORLEVEL 1 (
REM Write command that was executing to log-file
echo FAILED : [ %* ] >> "%logfile%"
REM some extra personal errormessage if needed
echo An error occured, consult the log-file for more info
REM Exit current script and set errorlevel on 1 (failure)
exit /b 1
)
echo succeeded : [ %* ] >> "%logfile%"
exit /b 0
This method will even allow to solve the problem with the variable expansion on a way that doesn't require delayed expansion. You'll just have to use %%var%% instead of the usual %var% to expand variables inside the code block and the :executeOwn will be able to expand it to its newest value. Beware: special characters like ^&<> inside the code-block will have to be escaped with a caret ^^^&^<^> in order to be executed in the :executeOwn function except if they are part of double quoted string.
Related
I'm experimenting with Rust. I want to compile a program, and only if it succeeds, run it. So I'm trying:
rustc hello.rs && hello
But hello.exe always runs, even if compilation fails.
If I try
rustc hello.rs
echo Exit Code is %errorlevel%
I get "Exit Code is 101".
As I understand it, the only truthy value is 0 in cmd, which 101 is clearly not, and && is lazily evaluated, so why does it run hello?
rustc.bat looks like this:
#echo off
SET DIR=%~dp0%
cmd /c "%DIR%..\lib\rust.0.11.20140519\bin\rustc.exe %*"
exit /b %ERRORLEVEL%
Very curious this. Put a CALL in front and all should be fine.
call rustc hello.rs && hello
I don't totally understand the mechanism. I know that && and || do not read the dynamic %errorlevel% value directly, but operate at some lower level. They conditionally fire based on the outcome of the most recently executed command, regardless of the current %errorlevel% value. The || can even fire for a failure that does not set the %errorlevel%! See File redirection in Windows and %errorlevel% and batch: Exit code for "rd" is 0 on error as well for examples.
Your rustc is a batch file, and the behavior changes depending on if CALL was used or not. Without CALL, the && and || operators respond only to whether the command ran or not - they ignore the exit code of the script. With CALL, they properly respond to the exit code of the script, in addition to responding if the script failed to run (perhaps the script doesn't exist).
Put another way, batch scripts only notify && and || operators about the exit code if they were launched via CALL.
UPDATE
Upon reading foxidrive's (now deleted) answer more carefully, I realize the situation is more complicated.
If CALL is used, then everything works as expected - && and || respond to the ERRORLEVEL returned by the script. ERRORLEVEL may be set to 1 early on in the script, and as long as no subsequent script command clears the error, the returned ERRORLEVEL of 1 will be properly reported to && and ||.
If CALL is not used, then && and || respond to the errorcode of the last executed command in the script. An early command in the script might set ERRORLEVEL to 1. But if the last command is an ECHO statement that executes properly, then && and || respond to the success of the ECHO command instead of the ERRORLEVEL of 1 returned by the script.
The real killer is that EXIT /B 1 does not report the ERRORLEVEL to && or || unless the script was invoked via CALL. The conditional operators detect that the EXIT command executed successfully, and ignore the returned ERRORLEVEL!
The expected behavior can be achieved if the last command executed by the script is:
cmd /c exit %errorlevel%
This will properly report the returned ERRORLEVEL to && and ||, regardless whether the script was invoked by CALL or not.
Here are some test scripts that demonstrate what I mean.
test1.bat
#echo off
:: This gives the correct result regardless if CALL is used or not
:: First clear the ERRORLEVEL
(call )
:: Now set ERRORLEVEL to 1
(call)
test2.bat
#echo off
:: This only gives the correct result if CALL is used
:: First clear the ERRORLEVEL
(call )
:: Now set ERRORLEVEL to 1
(call)
rem This command interferes with && or || seeing the returned errorlevel if no CALL
test3.bat
#echo off
:: This only gives the correct result if CALL is used
:: First clear the ERRORLEVEL
(call )
:: Now set ERRORLEVEL to 1
(call)
rem Ending with EXIT /B does not help
exit /b %errorlevel%
test4.bat
#echo off
:: This gives the correct result regardless if CALL is used or not
:: First clear the ERRORLEVEL
(call )
:: Now set ERRORLEVEL to 1
(call)
rem The command below solves the problem if it is the last command in script
cmd /c exit %errorlevel%
Now test with and without CALL:
>cmd /v:on
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
>test1&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Failure with errorlevel=1
>test2&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Success, yet errorlevel=1
>test3&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Success, yet errorlevel=1
>test4&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Failure with errorlevel=1
>call test1&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Failure with errorlevel=1
>call test2&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Failure with errorlevel=1
>call test3&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Failure with errorlevel=1
>call test4&&echo Success, yet errorlevel=!errorlevel!||echo Failure with errorlevel=!errorlevel!
Failure with errorlevel=1
>
Quick demo to prove the point:
#ECHO OFF
SETLOCAL
CALL q24983584s 0&&ECHO "part one"
ECHO done one
CALL q24983584s 101&&ECHO "part two"
ECHO done two
GOTO :EOF
where q24983584s.bat is
#ECHO OFF
SETLOCAL
EXIT /b %1
GOTO :EOF
works as expected...
I have a script a.cmd that calls another script b.cmd, and redirects its output. the called script, starts an executable that is never terminated. The output of the executable is redirected to its own log file. Simplified code:
a.cmd:
[1] #ECHO OFF
[2] SET LOG_FILE_NAME="log.txt"
[3] REM Start the b.cmd redirecting all output
[4] CALL b.cmd >> %LOG_FILE_NAME% 2>&1
[5] ECHO returned to a.cmd >> %LOG_FILE_NAME% 2>&1
[6] EXIT /B 0
b.cmd:
[1] #ECHO OFF
[2] SET ANOTHER_LOG_FILE_NAME="log2.txt"
[4] ECHO RunForEver.exe redirecting all output
[5] START CMD /C "RunForEver.exe >> %ANOTHER_LOG_FILE_NAME% 2>&1"
[6] ECHO b.cmd execution complete
[7] EXIT /B 0
(Line numbers were added for convenience)
The problem I'm encountering is that line 4 in b.cmd seems to grab a handle on the initial log file (LOG_FILE_NAME) because all b.cmd output is redirected to it, and the handle is not released while the executable (and the cmd that launched it) are running.
I didn't except this behavior because I thought only the output of the start command itself will be redirected to the LOG_FILE_NAME log file, and the output from the other process that is actually running the RunForEver.exe executable will be written to the ANOTHER_LOG_FILE_NAME.
As a result, line 5 in a.cmd errors out with access denied to LOG_FILE_NAME.
Could someone explain what's going on? Is there a way to avoid this?
I tried doing the output redirection to LOG_FILE_NAME from inside b.cmd, but then I get the access denied error in line 2 of b.cmd.
Thanks in advance!
Wow! That is a fascinating and disturbing discovery.
I don't have an explanation, but I do have a solution.
Simply avoid any additional redirection to log.txt after the never ending process has started. That can be done by redirecting a parenthesized block of code just once.
#ECHO OFF
SET LOG_FILE_NAME="log.txt"
>>%LOG_FILE_NAME% 2>&1 (
CALL b.cmd
ECHO returned to a.cmd
)
EXIT /B 0
Or by redirecting the output of a CALLed subroutine instead.
#ECHO OFF
SET LOG_FILE_NAME="log.txt"
call :redirected >>%LOG_FILE_NAME% 2>&1
EXIT /B 0
:redirected
CALL b.cmd
ECHO returned to a.cmd
exit /b
If you need to selectively redirect output in a.cmd, then redirect a non-standard stream to your file just once, and then within the block, selectively redirect output to the non-standard stream.
#ECHO OFF
SET LOG_FILE_NAME="log.txt"
3>>%LOG_FILE_NAME% (
echo normal output that is not redirected
CALL b.cmd >&3 2>&1
ECHO returned to a.cmd >&3 2>&1
)
EXIT /B 0
Again, the same technique could be done using a CALL instead of a parenthesized block.
I've developed a simple, self contained TEST.BAT script that anyone can run to demonstrate the problem. I called it TEST.BAT on my machine.
#echo off
del log*.txt 2>nul
echo begin >>LOG1.TXT 2>&1
call :test >>LOG1.TXT 2>&1
echo end >>LOG1.TXT 2>&1
exit /b
:test
echo before start
>nul 2>&1 (
echo ignored output
start "" cmd /c "echo start result >LOG2.TXT 2>&1 & pause >con"
)
echo after start
pause >con
exit /b
Both the master and the STARTed process are paused, thus allowing me to choose which process finishes first. If the STARTed process terminates before the master, then everything works as expected, as evidenced by the following output from the main console window.
C:\test>test
Press any key to continue . . .
C:\test>type log*
LOG1.TXT
begin
before start
after start
end
LOG2.TXT
start result
C:\test>
Here is an example of what happens if I allow the main process to continue before the STARTed process terminates:
C:\test>test
Press any key to continue . . .
The process cannot access the file because it is being used by another process.
C:\test>type log*
LOG1.TXT
begin
before start
after start
LOG2.TXT
start result
C:\test>
The reason I find the behavior disturbing is that I can't fathom how the STARTed process has any relationship with LOG1.TXT. By the time the START command executes, all standard output has been redirected to nul, so I don't understand how the new process knows about LOG1.TXT, let alone how it establishes an exclusive lock on it. The fact that echo ignored output has no detectable output is proof that the standard output has been successfully redirected to nul.
After invoking the CMD "echo xxx" the %errorlevel% is always 1.
Even though "echo xxx" is executed successfully.
Yes, echo something has absolutely no effect on the error level and will not change errorlevel to 0 as that will just always succeed.
For example:
running a echo something > c:\somefile.txt, which will succeed actually creating the file, but not change errorlevel to 0.
c:\>copy nil c:
The system cannot find the file specified.
c:\>echo %errorlevel%
1
c:\>echo this.works > c:\test.txt
c:\>echo %errorlevel%
1
type c:\test.txt
this.works
External commands like FINDSTR and XCOPY are actually separate programs (FINDSTR.EXE, XCOPY.EXE). External commands set the ERRORLEVEL upon both success and failure. By convention, 0 indicates success, and non-zero indicates an error. But some programs may not follow that convention.
ECHO is an internal command, meaning that the command is built into the CMD.EXE program itself. No additional program is needed. Internal commands behave differently.
If used on the command line, or within a batch script with a .BAT extension, then most internal commands set ERRORLEVEL upon failure, but do nothing to the ERRORLEVEL upon success. However, there are some exceptions. Both VER and VOL do set the ERRORLEVEL to 0 upon success.
When used within a batch script with a .CMD extension, all internal commands set the ERRORLEVEL upon both success and failure, just like external commands.
The ERRORLEVEL of 1 that you see after ECHO must have come from a prior command that failed. I've never seen ECHO fail. The only way I can imagine it could fail is if stdout is successfully redirected to a file, but the storage device cannot be written to for some reason such as if the device is full.
echo is a curious command. Let's see how it behaves
When echo command works
If errorlevel is 0 before echo, after echo, errorlevel will be 0 (the obvious case)
If errorlevel is 1 before echo, after echo, errorlevel will be 1. Echo does not change errorlevel
When echo command "fails"
Can echo fail? Let's create a case where it "fails". Open two command windows on the same directory. In first one run pause > file.txt to generate a file and place a lock on it while the pause command is waiting a keypress. In second command window run echo something > file.txt. In this case, the echo command will fail, as the first command window hold a lock on the file, so the second one is not able to write to the file. Properly talking the echo has not failed, but the redirection does, but just to see what happens
If errorlevel is 1 before running echo, it is still 1 after the echo (the obvious case)
If errolevel is 0 before running echo, it is still 0 after the echo
So, it seems that the echo command behaves identically in the two cases
BUT if we change the way echo is executed to
echo something && echo works || echo fails
then the behaviour changes a bit
When echo command works
No difference. errorlevel will not change, keeping the value it had before running the echo command.
When echo command "fails"
Using the echo something > file && echo works || echo fails then, if errorlevel is 1 before running echo, it keeps its value.
But if errorlevel is 0 and the echo command fails, in this case, with this construct of the command, errorlevel will show the failure and change its value to 1
In bash "set -e" at the beginning of the script instructs bash to fail the whole script on first failure of any command inside.
How do I do the same for a Windows batch script?
Tuim's solution works, but it can be made even simpler.
The ERRORLEVEL is already set, so there is no need to GOTO a label that sets the ERRORLEVEL.
You can simply use
yourCommand || exit /b
Note that exit /b will only exit the current subroutine if you are in the middle of a CALL. Your script will have to exit each CALL, layer by layer, until it reaches the root of the script. That will happen automatically as long as you also put the test after each CALL statement
call :label || exit /b
It is possible to force a batch script to exit immediately from any CALL depth. See How can I exit a batch file from within a function? for more info. Be sure to read both answers. The accepted answer has a couple of potentially serious drawbacks.
Not directly but you can add the following to every line which has something to execute.
|| goto :error
And then define error, which stops the script.
:error
exit /b %errorlevel%
I've got a batch file that does several things. If one of them fails, I want to exit the whole program. For example:
#echo off
type foo.txt 2>> error.txt >> success.txt
mkdir bob
If the file foo.txt isn't found then I want the stderr message appended to the error.txt file, else the contents of foo.txt is appended to success.txt. Basically, if the type command returns a stderr then I want the batch file to exit and not create a new directory. How can you tell if an error occurred and decide if you need to continue to the next command or not?
use ERRORLEVEL to check the exit code of the previous command:
if ERRORLEVEL 1 exit /b
EDIT: documentation says "condition is true if the exit code of the last command is EQUAL or GREATER than X" (you can check this with if /?). aside from this, you could also check if the file exists with
if exist foo.txt echo yada yada
to execute multple commands if the condition is true:
if ERRORLEVEL 1 ( echo error in previous command & exit /b )
or
if ERRORLEVEL 1 (
echo error in previous command
exit /b
)