Why doesn't enabledelayedexpansion work with pipelined commands? - windows

I'm trying to use a Windows batch file to run a command, pipe its output to another command, and capture the exit status of the first command. But apparently, I can't do that, although I'll grant that Windows batch files are so arcane that I may have missed a technique. How can I both capture the exit status of a command and pipe its output?
It's the classic do_a_thing | tee logfile.txt usage, but I want to be able to check whether do_a_thing succeeded or not. After lots of fooling around, googling, and reading StackOverflow, I came up with the following:
setlocal enabledelayedexpansion
set rc=-1
( false & set rc=!errorlevel! & echo rc=!rc! ) | tee logfile.txt
echo %rc%
Which I had hoped would produce:
C:\>test.bat
C:\>setlocal enabledelayedexpansion
C:\>set rc=-1
C:\>(false & set rc=!errorlevel! & echo rc=!rc! ) | tee logfile.txt
rc=1
C:\>echo 1
1
But, alas, it did not:
C:\>test.bat
C:\>setlocal enabledelayedexpansion
C:\>set rc=-1
C:\>(false & set rc=!errorlevel! & echo rc=!rc! ) | tee logfile.txt
rc=!rc!
C:\>echo -1
-1
Removing the "| tee logfile.txt" produces the expected value of !rc!, but of course, it doesn't let me capture the log file.
C:\>test.bat
C:\>setlocal enabledelayedexpansion
C:\>set rc=-1
C:\>(false & set rc=!errorlevel! & echo rc=!rc! )
rc=1
C:\>echo 1
1

What you are trying to to do does not work because each side of the pipe is executed in its own command shell in a command line context with delayed expansion off by default. See Why does delayed expansion fail when inside a piped block of code? for more info. The accepted answer explains everything, and the other answers help give context to the accepted answer.
The easiest solution is to write the error code to a temp file and then read the file into a variable after the command has completed.
( false & call echo %%^^errorlevel%%>returnCode.txt ) | tee logfile.txt
set "rc="
<returnCode.txt set /p "rc="
del returnCode.txt
echo rc=%rc%
The CALL statement provides another level of parsing that occurs after the command has run. The trick is to delay the expansion OF %errorlevel% until after the command you are testing has run. In a command context you can simply use CALL ECHO %^ERRORLEVEL%. But the command line must also pass through a batch context parse before it reaches the command context, so the percents must be escaped as %% and the caret as ^^.

Related

I can't save cmd comand.bat to file [duplicate]

I am creating a batch file with some simple commands to gather information from a system. The batch file contains commands to get the time, IP information, users, etc.
I assembled all the commands in a batch file, and it runs, but I would like the batch file, when run to output the results to a text file (log). Is there a command that I can add to the batch that would do so?
Keep in mind I do not want to run the batch from cmd, then redirect output ; I want to redirect the output from inside the batch, if that is possible.
The simple naive way that is slow because it opens and positions the file pointer to End-Of-File multiple times.
#echo off
command1 >output.txt
command2 >>output.txt
...
commandN >>output.txt
A better way - easier to write, and faster because the file is opened and positioned only once.
#echo off
>output.txt (
command1
command2
...
commandN
)
Another good and fast way that only opens and positions the file once
#echo off
call :sub >output.txt
exit /b
:sub
command1
command2
...
commandN
Edit 2020-04-17
Every now and then you may want to repeatedly write to two or more files. You might also want different messages on the screen. It is still possible to to do this efficiently by redirecting to undefined handles outside a parenthesized block or subroutine, and then use the & notation to reference the already opened files.
call :sub 9>File1.txt 8>File2.txt
exit /b
:sub
echo Screen message 1
>&9 echo File 1 message 1
>&8 echo File 2 message 1
echo Screen message 2
>&9 echo File 1 message 2
>&8 echo File 2 message 2
exit /b
I chose to use handles 9 and 8 in reverse order because that way is more likely to avoid potential permanent redirection due to a Microsoft redirection implementation design flaw when performing multiple redirections on the same command. It is highly unlikely, but even that approach could expose the bug if you try hard enough. If you stage the redirection than you are guaranteed to avoid the problem.
3>File1.txt ( 4>File2.txt call :sub)
exit /b
:sub
etc.
if you want both out and err streams redirected
dir >> a.txt 2>&1
I know this is an older post, but someone will stumble across it in a Google search and it also looks like some questions the OP asked in comments weren't specifically addressed. Also, please go easy on me since this is my first answer posted on SO. :)
To redirect the output to a file using a dynamically generated file name, my go-to (read: quick & dirty) approach is the second solution offered by #dbenham. So for example, this:
#echo off
> filename_prefix-%DATE:~-4%-%DATE:~4,2%-%DATE:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%.log (
echo Your Name Here
echo Beginning Date/Time: %DATE:~-4%-%DATE:~4,2%-%DATE:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%.log
REM do some stuff here
echo Your Name Here
echo Ending Date/Time: %DATE:~-4%-%DATE:~4,2%-%DATE:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%.log
)
Will create a file like what you see in this screenshot of the file in the target directory
That will contain this output:
Your Name Here
Beginning Date/Time: 2016-09-16_141048.log
Your Name Here
Ending Date/Time: 2016-09-16_141048.log
Also keep in mind that this solution is locale-dependent, so be careful how/when you use it.
echo some output >"your logfile"
or
(
echo some output
echo more output
)>"Your logfile"
should fill the bill.
If you want to APPEND the output, use >> instead of >. > will start a new logfile.
#echo off
>output.txt (
echo Checking your system infor, Please wating...
systeminfo | findstr /c:"Host Name"
systeminfo | findstr /c:"Domain"
ipconfig /all | find "Physical Address"
ipconfig | find "IPv4"
ipconfig | find "Default Gateway"
)
#pause
Add these two lines near the top of your batch file, all stdout and stderr after will be redirected to log.txt:
if not "%1"=="STDOUT_TO_FILE" %0 STDOUT_TO_FILE %* >log.txt 2>&1
shift /1
There is a cool little program you can use to redirect the output to a file and the console
some_command ^| TEE.BAT [ -a ] filename
#ECHO OFF
:: Check Windows version
IF NOT "%OS%"=="Windows_NT" GOTO Syntax
:: Keep variables local
SETLOCAL
:: Check command line arguments
SET Append=0
IF /I [%1]==[-a] (
SET Append=1
SHIFT
)
IF [%1]==[] GOTO Syntax
IF NOT [%2]==[] GOTO Syntax
:: Test for invalid wildcards
SET Counter=0
FOR /F %%A IN ('DIR /A /B %1 2^>NUL') DO CALL :Count "%%~fA"
IF %Counter% GTR 1 (
SET Counter=
GOTO Syntax
)
:: A valid filename seems to have been specified
SET File=%1
:: Check if a directory with the specified name exists
DIR /AD %File% >NUL 2>NUL
IF NOT ERRORLEVEL 1 (
SET File=
GOTO Syntax
)
:: Specify /Y switch for Windows 2000 / XP COPY command
SET Y=
VER | FIND "Windows NT" > NUL
IF ERRORLEVEL 1 SET Y=/Y
:: Flush existing file or create new one if -a wasn't specified
IF %Append%==0 (COPY %Y% NUL %File% > NUL 2>&1)
:: Actual TEE
FOR /F "tokens=1* delims=]" %%A IN ('FIND /N /V ""') DO (
> CON ECHO.%%B
>> %File% ECHO.%%B
)
:: Done
ENDLOCAL
GOTO:EOF
:Count
SET /A Counter += 1
SET File=%1
GOTO:EOF
:Syntax
ECHO.
ECHO Tee.bat, Version 2.11a for Windows NT 4 / 2000 / XP
ECHO Display text on screen and redirect it to a file simultaneously
ECHO.
IF NOT "%OS%"=="Windows_NT" ECHO Usage: some_command ³ TEE.BAT [ -a ] filename
IF NOT "%OS%"=="Windows_NT" GOTO Skip
ECHO Usage: some_command ^| TEE.BAT [ -a ] filename
:Skip
ECHO.
ECHO Where: "some_command" is the command whose output should be redirected
ECHO "filename" is the file the output should be redirected to
ECHO -a appends the output of the command to the file,
ECHO rather than overwriting the file
ECHO.
ECHO Written by Rob van der Woude
ECHO http://www.robvanderwoude.com
ECHO Modified by Kees Couprie
ECHO http://kees.couprie.org
ECHO and Andrew Cameron
#echo OFF
[your command] >> [Your log file name].txt
I used the command above in my batch file and it works. In the log file, it shows the results of my command.
Adding the following lines at the bottom of your batch file will grab everything just as displayed inside the CMD window and export into a text file:
powershell -c "$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('^a')
powershell -c "$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('^c')
powershell Get-Clipboard > MyLog.txt
It basically performs a select all -> copy into clipboard -> paste into text file.
This may fail in the case of "toxic" characters in the input.
Considering an input like thisIsAnIn^^^^put is a good way how to get understand what is going on.
Sure there is a rule that an input string MUST be inside double quoted marks but I have a feeling that this rule is a valid rule only if the meaning of the input is a location on a NTFS partition (maybe it is a rule for URLs I am not sure).
But it is not a rule for an arbitrary input string of course (it is "a good practice" but you cannot count with it).

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

Redirecting Output from within Batch file

I am creating a batch file with some simple commands to gather information from a system. The batch file contains commands to get the time, IP information, users, etc.
I assembled all the commands in a batch file, and it runs, but I would like the batch file, when run to output the results to a text file (log). Is there a command that I can add to the batch that would do so?
Keep in mind I do not want to run the batch from cmd, then redirect output ; I want to redirect the output from inside the batch, if that is possible.
The simple naive way that is slow because it opens and positions the file pointer to End-Of-File multiple times.
#echo off
command1 >output.txt
command2 >>output.txt
...
commandN >>output.txt
A better way - easier to write, and faster because the file is opened and positioned only once.
#echo off
>output.txt (
command1
command2
...
commandN
)
Another good and fast way that only opens and positions the file once
#echo off
call :sub >output.txt
exit /b
:sub
command1
command2
...
commandN
Edit 2020-04-17
Every now and then you may want to repeatedly write to two or more files. You might also want different messages on the screen. It is still possible to to do this efficiently by redirecting to undefined handles outside a parenthesized block or subroutine, and then use the & notation to reference the already opened files.
call :sub 9>File1.txt 8>File2.txt
exit /b
:sub
echo Screen message 1
>&9 echo File 1 message 1
>&8 echo File 2 message 1
echo Screen message 2
>&9 echo File 1 message 2
>&8 echo File 2 message 2
exit /b
I chose to use handles 9 and 8 in reverse order because that way is more likely to avoid potential permanent redirection due to a Microsoft redirection implementation design flaw when performing multiple redirections on the same command. It is highly unlikely, but even that approach could expose the bug if you try hard enough. If you stage the redirection than you are guaranteed to avoid the problem.
3>File1.txt ( 4>File2.txt call :sub)
exit /b
:sub
etc.
if you want both out and err streams redirected
dir >> a.txt 2>&1
I know this is an older post, but someone will stumble across it in a Google search and it also looks like some questions the OP asked in comments weren't specifically addressed. Also, please go easy on me since this is my first answer posted on SO. :)
To redirect the output to a file using a dynamically generated file name, my go-to (read: quick & dirty) approach is the second solution offered by #dbenham. So for example, this:
#echo off
> filename_prefix-%DATE:~-4%-%DATE:~4,2%-%DATE:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%.log (
echo Your Name Here
echo Beginning Date/Time: %DATE:~-4%-%DATE:~4,2%-%DATE:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%.log
REM do some stuff here
echo Your Name Here
echo Ending Date/Time: %DATE:~-4%-%DATE:~4,2%-%DATE:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%.log
)
Will create a file like what you see in this screenshot of the file in the target directory
That will contain this output:
Your Name Here
Beginning Date/Time: 2016-09-16_141048.log
Your Name Here
Ending Date/Time: 2016-09-16_141048.log
Also keep in mind that this solution is locale-dependent, so be careful how/when you use it.
#echo off
>output.txt (
echo Checking your system infor, Please wating...
systeminfo | findstr /c:"Host Name"
systeminfo | findstr /c:"Domain"
ipconfig /all | find "Physical Address"
ipconfig | find "IPv4"
ipconfig | find "Default Gateway"
)
#pause
echo some output >"your logfile"
or
(
echo some output
echo more output
)>"Your logfile"
should fill the bill.
If you want to APPEND the output, use >> instead of >. > will start a new logfile.
Add these two lines near the top of your batch file, all stdout and stderr after will be redirected to log.txt:
if not "%1"=="STDOUT_TO_FILE" %0 STDOUT_TO_FILE %* >log.txt 2>&1
shift /1
There is a cool little program you can use to redirect the output to a file and the console
some_command ^| TEE.BAT [ -a ] filename
#ECHO OFF
:: Check Windows version
IF NOT "%OS%"=="Windows_NT" GOTO Syntax
:: Keep variables local
SETLOCAL
:: Check command line arguments
SET Append=0
IF /I [%1]==[-a] (
SET Append=1
SHIFT
)
IF [%1]==[] GOTO Syntax
IF NOT [%2]==[] GOTO Syntax
:: Test for invalid wildcards
SET Counter=0
FOR /F %%A IN ('DIR /A /B %1 2^>NUL') DO CALL :Count "%%~fA"
IF %Counter% GTR 1 (
SET Counter=
GOTO Syntax
)
:: A valid filename seems to have been specified
SET File=%1
:: Check if a directory with the specified name exists
DIR /AD %File% >NUL 2>NUL
IF NOT ERRORLEVEL 1 (
SET File=
GOTO Syntax
)
:: Specify /Y switch for Windows 2000 / XP COPY command
SET Y=
VER | FIND "Windows NT" > NUL
IF ERRORLEVEL 1 SET Y=/Y
:: Flush existing file or create new one if -a wasn't specified
IF %Append%==0 (COPY %Y% NUL %File% > NUL 2>&1)
:: Actual TEE
FOR /F "tokens=1* delims=]" %%A IN ('FIND /N /V ""') DO (
> CON ECHO.%%B
>> %File% ECHO.%%B
)
:: Done
ENDLOCAL
GOTO:EOF
:Count
SET /A Counter += 1
SET File=%1
GOTO:EOF
:Syntax
ECHO.
ECHO Tee.bat, Version 2.11a for Windows NT 4 / 2000 / XP
ECHO Display text on screen and redirect it to a file simultaneously
ECHO.
IF NOT "%OS%"=="Windows_NT" ECHO Usage: some_command ³ TEE.BAT [ -a ] filename
IF NOT "%OS%"=="Windows_NT" GOTO Skip
ECHO Usage: some_command ^| TEE.BAT [ -a ] filename
:Skip
ECHO.
ECHO Where: "some_command" is the command whose output should be redirected
ECHO "filename" is the file the output should be redirected to
ECHO -a appends the output of the command to the file,
ECHO rather than overwriting the file
ECHO.
ECHO Written by Rob van der Woude
ECHO http://www.robvanderwoude.com
ECHO Modified by Kees Couprie
ECHO http://kees.couprie.org
ECHO and Andrew Cameron
#echo OFF
[your command] >> [Your log file name].txt
I used the command above in my batch file and it works. In the log file, it shows the results of my command.
Adding the following lines at the bottom of your batch file will grab everything just as displayed inside the CMD window and export into a text file:
powershell -c "$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('^a')
powershell -c "$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('^c')
powershell Get-Clipboard > MyLog.txt
It basically performs a select all -> copy into clipboard -> paste into text file.
This may fail in the case of "toxic" characters in the input.
Considering an input like thisIsAnIn^^^^put is a good way how to get understand what is going on.
Sure there is a rule that an input string MUST be inside double quoted marks but I have a feeling that this rule is a valid rule only if the meaning of the input is a location on a NTFS partition (maybe it is a rule for URLs I am not sure).
But it is not a rule for an arbitrary input string of course (it is "a good practice" but you cannot count with it).

Windows command line: why environment variable is not available after &

Have a look at the following commands: why is the value of a not available immediately after the &?
C:\>set a=
C:\>set a=3&echo %a%
%a%
C:\>echo %a%
3
C:\>set a=3&echo %a%
3
But when I do
C:\>set a=
C:\>set a=3&set
a=3 is included in the listed variables!
I need this for a trick I learned here, to get the exit code of a command even output is piped:
Windows command interpreter: how to obtain exit code of first piped command
but I have to use it in a make script, that's why everything must be in one line!
This is what I am trying to do:
target:
($(command) & call echo %%^^errorlevel%% ^>$(exitcodefile)) 2>&1 | tee $(logfile) & set /p errorlevel_make=<$(exitcodefile) & exit /B %errorlevel_make%
But errorlevel_make is always empty (the file with the exit code exists and contains the correct exit code).
Is this a bug in cmd? Any ideas what I can do?
The reason for the observed behaviour is how the command line is processed.
In first case set a=3&echo %a%, when the line is interpreted BEFORE EXECUTING IT, all variables are replaced with their values. a has no value until line executes, so it can not be substituted in echo
In second case, set a=3&set, there is no variable substitution before execution. So, when set is executed, a has the value asigned.
It's much easier to create a seperate batch file to solve this, as it isn't obvious even for experts.
But in your case this should work
target:
($(command) & call echo %^^^^errorlevel% >$(exitcodefile)) 2>&1 | tee $(logfile) & set /p errorlevel_make=<$(exitcodefile) & call exit /B %^errorlevel_make%
A seperate batch could look like
extBatch.bat
#echo off
("%~1" & call echo %%^^errorlevel%% > "%~2") 2>&1 | tee "%~3" & set /p errorlevel_make=<"%~2"
exit /B %errorlevel_make%
Then you could start the batch from your make file
target:
extBatch.bat $(command) $(exitcodefile) $(logfile)
Inspired by the nice answer of jeb, I modified the code a little to be able to grab errorlevel for each side of the pipe, separately. For example you have the following pipe in your batch file:
CD non_exisitng 2>&1 | FIND /V ""
ECHO Errorlevel #right: %errorlevel%
The errorlevel above refers to the right-hand side of the pipe only, and it indicates if the FIND command succeeded of not.
However, to be able to get the errorlevel of both sides, we can change the above code to something like this:
(CD non_exisitng & CALL ECHO %%^^errorlevel%% >err) 2>&1 | FIND /V ""
ECHO Errorlevel #right: %errorlevel%
SET /P err=<err
ECHO Errorlevel #left: %err%
output:
The system cannot find the path specified.
errorlevel #right: 0
errorlevel #left: 1
#OP:
I realize this comes a bit late, but might be helpful for others. In addition to jeb's method of an external batch file, the original problem can also be solved as one-liner:
target:
($(command) & call echo %%^^errorlevel%% >$(exitcodefile)) 2>&1 | tee $(logfile) & set /p errorlevel_make=<$(exitcodefile) & cmd /c call exit /B %%errorlevel_make%%

How to return to execution of cmd command after ftp command "bye"?

I have .bat file. It looks like next
....many commands1
ftp -i -s:copy.txt
...many commands2
copy.txt contains next commands
open ...
login
password
get file
bye
When I execute my .bat file it works next
many commands1
ftp commands
But many commands2 remain don't excuted.
It sounds like one of your command2 commands may be calling (launching) a batch file without using the CALL command. (At the bottom are two Flowcharts explaining the consequences of using and not using CALL.) See **The consequences of using and not using CALL.
If my initial hunch is incorrect, you need to start taking steps to debug your batch files.
Comment-out any #ECHO OFF statements and put some PAUSE and maybe even ECHO statements in your batch files. This will help you locate your errors. Also, remember that you'll need to use the CALL command if you expect for your code to execute a batch file, then continue executing the CALLing batch file after it finishes executing the CALLed batch file. Also, realize that if you CALL any batch file containing an ECHO OFF statement, that ECHO OFF continues to be enforced even after control returns to the CALLing file. For instance:
:: For debugging porpuses we leave `ECHO` in it's default `ON` state
REM #echo off
:: Execute your leading commands:
commands1
:: If you prefer, because you know that your problem comes after this point;
:: You can, if you want, leave the leading `ECHO OFF` command alone, and
:: instead, put an `ECHO ON` statement here:
echo on
ftp -i -s:copy.txt
ECHO Back from FTPing...
pause
:: Calling command2.1.exe
command2.1.exe
echo Back from command2.1.exe
pause
:: Calling command2.2.bat
CALL command2.2.bat
:: Since Command2.2.bat contains an ECHO OFF statement
:: we'll turn `ECHO` back on again for debugging purposes.
ECHO ON
echo Back from command2.2.bat
pause
The consequences of using and not using CALL.
Assuming these two batch files:
RUN.BAT
#ECHO OFF
ECHO Starting: RUN.BAT
(Misc commands)
:: 'Calling' SECOND.BAT without useing `CALL`
SECOND.BATCH
:: Back from SECOND.BAT
ECHO Back from second.bat
SECOND.BAT
#echo off
ECHO Running: SECOND.BAT
The flow of execution without a CALL statement, will move like this:
C:\> RUN.BAT<kbd>ENTER</kbd>
|
\|/
v
#echo off
echo Running
(misc commands)
|
\|/
v
SECOND.BAT ----> #echo off
echo Running Seond.bat
(misc commands)
|
\|/
v
END
But if you change to SECOND.BAT line in the RUN.BAT file to CALL SECOND.BAT, the flow of execution will go like this:
C:\> RUN.BAT<kbd>ENTER</kbd>
|
\|/
v
#echo off
echo Running
(misc commands)
|
\|/
v
SECOND.BAT ----> #echo off
echo Running Seond.bat
(misc commands)
|
\|/
v
:: Back from SECOND.BAT <--+
|
\|/
v
ECHO Back from second.bat
|
\|/
v
END

Resources