Issue with output redirection in batch - cmd

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.

Related

Redirect output from a Windows batch file within the script

I am trying to make a Windows batch file (the main script) redirect all of it's output to a file while still displaying it on the screen. I cannot change all the code that calls the main script - I need to change the main script itself.
I have a solution that requires "bootstrapping" the main script by calling it again from another bat file (tee_start.bat).
What I'm sure I want to avoid is refactoring all the other scripts that call main.bat to use the tee command - it will be a much smaller, safer change if I can put some new code in main.bat.
Is there a way to do this that does not involve restarting the main file as I am doing below?
For example, is there a cmd.exe or powershell.exe command that says "Take my current STDOUT and tee it" - or does tee somehow support this behavior and I missed it?
Or, alternatively, should I settle for what I have implemented as the least invasive method?
I have implemented a solution as follows:
main.bat
REM this is an example of bootstrap mode - it will restart this main script with all output logged
call tee_start.bat %~dpnx0 %*
echo This output should appear both in the screen console and in a log file
tee_start.bat
REM in order to bootstrap the output redirection - this script gets called twice - once to initialize logging with powershell tee command
REM second time, it just echoes log information and returns without doing anything
if x%LOG_FILE% neq x (
echo Bootstrapped Process: %1%
echo Escaped Arguments: %ADDL_ARGS%
echo Log File: %LOG_FILE%
goto :EOF
)
set ADDL_ARGS=
:loop_start
shift
if x%1 == x goto :after_loop
REM preserve argument structure (quoted or not) from the input
set ADDL_ARGS=%ADDL_ARGS% \"%~1\"
goto loop_start
:after_loop
SET LOG_FILE=/path/to/some/logfile.log
powershell "%1% %ADDL_ARGS% 2>&1 | tee -Append %LOG_FILE%"
I suggest the following approach, which makes do without the aux. tee_start.bat file:
main.bat content (outputs to log.txt in the current folder; adjust as needed):
#echo off & setlocal
if defined LOGFILE goto :DO_IT
set "LOGFILE=log.txt"
:: Reinvoke with teeing
"%~f0" %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath \"%LOGFILE%\"; \"[output captured in: %LOGFILE%]\""
exit /b
:DO_IT
:: Sample output
echo [args: %*]
echo to stdout
echo to stderr >&2
echo [done]
Tee-Object uses a fixed character encoding, which is notably "Unicode" (UTF-16LE) in Windows PowerShell (powershell.exe), and (BOM-less UTF-8) in PowerShell (Core) 7+ (pwsh.exe).

Error detection and logging - Batch Files

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.

Why the return value of "echo" is always 1

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

How to exit a batch program upon error?

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
)

Redirect batch stderr to file

I have a batch file that executes a java application. I'm trying to modify it so that whenever an exception occurs, it'll write the STDERR out to a file.
It looks something like this:
start java something.jar method %1 %2 2>> log.txt
Is there a way I can write the arguments %1 and %2 to the log.txt file as well? I don't want to write it to the log file everytime this batch file gets called, only when an exception occurs.
I tried searching for a way to redirect STDERR into a variable, but I couldn't figure it out. Ideally I'd like the log file to look something like:
Batch file called with parameters:
- "first arg"
- "second arg"
Exception:
java.io.exception etc...
------------------------------------
Batch file called with parameters:
- "first arg"
- "second arg"
Exception:
java.io.exception etc...
Something like this might work:
javastart.cmd
#echo off
set params=%*
for /f "delims=" %%e in ('java something.jar method %1 %2 ^>nul') do (
echo Batch file called with parameters:>>log.txt
echo - Args[]: %*>>log.txt
echo - Arg[1]: %1>>log.txt
echo - Arg[2]: %2>>log.txt
echo Exception: %%e
)
I am not a java programmer myself, I cannot test this output/situation but:
1: %* means every parameters, from 1st to last (even %12 although it's not directly available.. ) whatever the formating. Might be better to use this than delimit those. In the even of bad spacing/quoting, you would have the full parameters too. I added a line in the log, to show the full line then the parameters. You can see where the problem was if it's a matter of bad arguments.
2: sending the stdout to null (^>nul) will only keep the stderr output, set into %%e
3: Whatever is in the do part will only happen is the for test statement actually has anything as an output, i.e. if %%e is set.
With those being said, you have to test the script and see if the exception is actually sent to stderr or, like some other softwares, to stdout even if an error occured.
I hope this helps.
The only working solution I see would be to redirect stderr to a temporary file
java blah.jar %1 %2 2>stderr
and afterwards looking whether something has been written to the file and writing to the log in that case.
for %%i in (stderr) do if %%~zi GTR 0 (
echo Parameters: %1 %2 >>log.txt
type stderr >> log.txt
)
If the batches aren't run in sequence but rather simultaneously you need to find something to uniquify the temp variable:
set file=%time::=%
set /a file=file
set file=%file%%random%
java blah.jar %1 %2 2>stderr_%file%
for %%i in (stderr) do if %%~zi GTR 0 (
echo Parameters: %1 %2 >>log.txt
type stderr >> log.txt
)
This will avoid clashes between multiple running batches. However, you are currently not using anything to lock writing to your log file and things may appear out of place when other things get written to it (with other batches you might get interleaving in the echo and type commands or, when you're redirecting output of java to that file as well, then it may get mixed up with regular output of the java program:
Parameters: foo bar
Some regular output
Parameters: foo2 bar2
More output
NullPointerException at Blah: What we really wanted to have right after the parameters
IOException at Blah: This exception belongs to the parameters foo2 bar2
You can use another file as semaphore for writing to the log to avoid the batch outputs getting mixed: create the file [copy nul file] when you want to write to the log, delete it afterwards and before you attempt to create it check whether it is actually there and wait until it disappears. You can't do anything about the stdout log being mixed into the file, though, except you use that temp file approach for stdout as well (and simply type the stdout log to log.txt when the Java program finished, but, again with using the semaphore.
I don't get exactly what you are asking, but here's some info that may help...
You can redirect stderr separately from stdout in a .cmd or .bat file. To redirect stderr, use something like this:
MyCommand.exe > command.stdout.txt 2> command.stderr.txt
Then, you can check the command.stderr.txt for content, and if any is present, concatenate it to the command.stdout.txt into your log file. Or you could concat it in any case. If you like you could also echo the command that you ran, into the final log file.
You can also check for the exit code in a batch file, using the %ERRORLEVEL% env var. Exe files are expected to set ERRORLEVEL in case of an error. I don't know if java.exe does this. It should, if it is a good citizen on Windows. This might be an alternative way of finding out if the Java app exited with an error condition. But this is no guarantee that stderr got nothing. For example, a Java app might print out an exception and stack trace, and then exit with code 0, which indicates success. In which case stderr would have gunk in it, but ERRORLEVEL would be zero.
EDIT: s/ERROR_LEVEL/ERRORLEVEL
How about something like this (untested):
#echo off
#echo Batch file called with parameters: >> log.txt
#echo - %1 >> log.txt
#echo - %2 >> log.txt
start java something.jar method %1 %2 2>> log.txt
Try using ERRORLEVEL
java something.jar method %1 %2 2>> log.txt
IF %ERRORLEVEL% NEQ 0 GOTO Error
Error:
#ECHO There was an error
#ECHO Arg 1: %1 >> log.txt
#ECHO Arg 2: %2 >> log.txt
A batch file like this should do the trick.
start_prog.cmd
CALL :Start_Prog arg1 arg2
GOTO :EOF
:Start_Prog
something.py %1 %2 2>&1 1>nul | "C:\Python26\python.exe" format_output.py %1 %2 >>log.txt
GOTO :EOF
Here I'm passing stderr via a pipe and passing arguments as arguments. The output is then appended to a log.txt file.
Throw away stdout
1>nul
Redirect stderr to stdout
2>&1
Pipe the stderr into a script to format the output, and pass arguments as arguments.
| "C:\Python26\python.exe" format_output.py %1 %2
Append output to a "log.txt".
>>log.txt
On my system I need to call python.exe otherwise the pipe doesn't work.
Here are the two files I used "something.py" and "format_output.py".
something.py
import sys
print >>sys.stdout, " ".join(sys.argv[1:])
print >>sys.stderr, "java.io.exception etc..."
format_output.py
import sys
print "Batch file called with parameters:"
for arg in sys.argv[1:]:
print '- "{0}"'.format(arg)
print "Exception:"
for line in sys.stdin:
print line
And finaly here's the output.
log.txt
Batch file called with parameters:
- "arg1"
- "arg2"
Exception:
java.io.exception etc...
The only thing missing is that something is always written to the "log.txt" file.
To tidy this up further I would move writing the log file into "format_output.py".
You could then add a check to see if the stderr from your program is blank.

Resources