Prevent extra whitespace when piping data in a Windows batch script - batch-file

I have to run an executable (written in C#) 42 times separately for 42 US states on Windows command line. I wrote a batch file to automate the process. The 1st user input to the executable is 1, and the 2nd one is the state abbreviation (AL, AZ, CT etc.). I have written the following script to do it:
#echo off
for /f "tokens=1 delims=" %%x in (CropHailStates.txt) do (
(echo 1 & echo %%x)|Z:\Models\LossCalc.exe
)
Each row in CropHailStates.txt file contains the state abbreviation as follows:
AL
AZ
CT
Now in manual mode, when 1 is entered at the 1st prompt and AL is entered at the 2nd, the C# program reads a file named "AL.Even.CropLoss.csv". But when I run the batch script to do it automatically, I get the error message saying that the file "AL .Even.CropLoss.csv" is not found. The problem is related to the extra whitespace after AL. It’s somehow adding the whitespace after inserting the state abbreviation. It’s like pressing the spacebar after writing the state abbreviation, but before pressing enter.
How can I get rid of that extra whitespace in the file name?

The problem is an artifact of the pipe parser when dealing with a parenthesized block of code.
The source of the problem is described by jeb at Why does delayed expansion fail when inside a piped block of code?.
Each side of the pipe is executed in a new CMD /C process.
When a multi-line parenthesized block gets piped, the parser must repackage the entire block into a single line so that it can be executed via CMD /C
(
echo line 1
echo line 2
) | findstr "^"
The left side of the pipe is executed as:
C:\WINDOWS\system32\cmd.exe /S /D /c" ( echo line 1 & echo line 2 )"
You can see the extra spaces that are introduced.
Even though your code is already on a single line, it still goes through the same parser that introduces those pesky spaces.
I know of three relatively simple solutions that eliminate the unwanted spaces without the need for a temporary file.
1) Add an extra CMD /C where you explicitly get the exact behavior you are looking for
#echo off
for /f "tokens=1 delims=" %%x in (CropHailStates.txt) do (
cmd /c "echo 1&echo %%x"|Z:\Models\LossCalc.exe
)
2) Store part of the command in a variable and delay expansion until execution of CMD /C
#echo off
setlocal
for /f "tokens=1 delims=" %%x in (CropHailStates.txt) do (
set "cmd=&echo %%x"
(echo 1%%cmd%%)|Z:\Models\LossCalc.exe
)
3) Introduce delayed expansion of a linefeed variable - a mind blowing technique developed by jeb that he describes in that same SO link that I provided
#echo off
setlocal
set ^"LF=^
%= This creates a linefeed character =%
"
for /f "tokens=1 delims=" %%x in (CropHailStates.txt) do (
(echo 1%%LF%%echo %%x%%LF%%)|Z:\Models\LossCalc.exe
)

(echo 1& echo %%x)>tempfile.txt
Z:\Models\LossCalc.exe <tempfile.txt
del tempfile.txt
should provide the data without the trailing spaces

To get rid of the last character (the whitespace) you can do:
#echo off
setlocal EnableDelayedExpansion
for /f "tokens=1 delims=" %%x in (CropHailStates.txt) do (
set "result=%%x"
(echo 1 & echo !result:~0,-1!)|Z:\Models\LossCalc.exe
)
Hope it helps.

Related

Writing to a file in batch

I've set up an app for a couple of friends and me in batch with a auto-updating system, but I need to add a line of code in the auto-updating system. I decided to completely rewrite the file, it takes a long time to add 'echo' in from to every line and, '>>text.txt' at the end of every line and added '^' when needed, so I was wondering if there was an easier way of writing lot's of code to a file in batch.
Example:
#echo off
rem I need a way to do the following without adding 'echo' and '>>text.txt'
echo echo Checking for updates... >text.txt
echo echo 1.4 ^>^>new.txt >>text.txt
echo ping localhost -n 2 ^>nul >>text.txt
rem and so on and so on.
Or if there is a way to simply add a new line of code in a specific place in the file, that would also help!
Thanks in advance!
The following is how you can more easily and efficiently do what your current code does, by removing all of those individual write processes.
#( Echo #Echo Checking for updates...
Echo #(Echo 1.4^)^>^>"new.txt"
Echo #(%__AppDir__%ping.exe LocalHost -n 2^)^>NUL
)>"text.txt"
There are other possibilities, but at this time, based on the lack of information in your question, I'm reluctant to expand further at this time.
If I understand correctly, then you could do the following:
in the batch file, prepend each line of text that you want to output with :::: (this constitutes an invalid label that is going to be ignored);
then use the following code:
rem // Redirect to file:
> "text.txt" (
rem // Find all lines that begin with `::::` and loop over them:
for /F "delims=" %%T in ('findstr "^::::" "%~f0"') do (
rem // Store currently iterated line:
set "TEXT=%%T"
rem // Toggle delayed expansion to avoid loss of `!`:
setlocal EnableDelayedExpansion
rem // Remove `::::` prefix and output remaining line:
echo(!TEXT:*::::=!
endlocal
)
)
replace set "TEXT=%%T" by call set "TEXT=%%T" if you want to enable percent expansion within the returned text (so it could, for example, contain %~nx0, which would then be expanded to the file name of the script).
I am using this technique a lot (without the output redirection) for help systems in my batch files (/?).
Your asked
I need a way to do the following without adding echo and >>text.txt
The script takes advantage of the line continuation character, the caret ^.
The first character after the caret ^ is always escaped, so do linefeed characters:
#echo off
SETLOCAL EnableDelayedExpansion
call :init
>text.txt (
echo #echo off%NL%
Checking for updates...%NL%
>^>new.txt echo 1.4%NL%
>NUL ping localhost -n 2
)
ENDLOCAL
exit /b
:init
( set LF=^
%= 0X0D FORM FEED =%
)
::0X0A Carriage Return
FOR /F %%a in ('copy /Z "%~f0" nul') do set "CR=%%a"
::Create newline/line continuation character
set ^"NL=^^^%LF%%LF%^%LF%%LF%^^" %= Unix-Style Endings \n =%
::set ^"NL=%CR%^^^%LF%%LF%^%LF%%LF%^^" %= Windows-Style Endings \r\n =%
exit /b
The variable %LF% is a escaped linefeed, and %NL% is a escaped %LF% plus a escaped caret ^ for line continuation.
The code
>^>new.txt echo 1.4%NL%
>NUL ping localhost -n 2
might seem strange. Why isn't the first caret ^ escaped?
Because %NL% already escaped it.
Sources:
Explain how Windows batch newline variable hack works
https://stackoverflow.com/a/5642300/12861751
https://www.dostips.com/forum/viewtopic.php?t=6369

Not able to set the variable correctly in a batch script

I am reading a values from the xml file using batch ,
values are like Age="18"
When the value is extracted i don't want the double quotes
for /f "tokens=2 delims= " %%b in ('type <file_name> ^| FIND "info" ) do (
echo %%b
set string =%%b
string = Age="18" ( getting error here)
in the info tag
<info Name="xxx" Age="18"></info>
i need only the value 18 to be stored in a variable
Need assistance
This task can be done with:
for /F "tokens=5 delims==> " %%I in ('%SystemRoot%\System32\findstr.exe /R /C:"<info .* Age=\"[0123456789][0123456789]*\"" "XmlFile.xml"') do echo %%~I& set "string=%%~I"
FOR executes in this case in background one more command process with %ComSpec% /c and the command line between ' appended as additional arguments. So executed is in background with Windows installed to C:\Windows:
C:\Windows\System32\cmd.exe /c C:\Windows\System32\findstr.exe /R /C:"<info .* Age=\"[0123456789][0123456789]*\"" "XmlFile.xml"
FINDSTR runs a case-sensitive regular expression search for a line containing <info containing the attribute Age and the attribute value being a number. The found line is output to handle STDOUT (standard output) of the background command process captured by FOR.
After started cmd.exe closed itself after findstr.exe terminated, FOR processes the captured output line by line with skipping empty lines.
The line is split up into substrings using = and > and space as string delimiters because of delims==> . The fifth substring is the value of attribute Age enclosed in double quotes which is assigned to the specified loop variable I because of tokens=5. The double quotes are removed because of using modifier %~I.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
echo /?
findstr /?
for /?
set /?
Instead of choosing the 2nd token with   as the delimiter, you probably want the 4th token using the " as a delimiter:
#For /F Tokens^=4^ Delims^=^" %%G In ('^""%__AppDir%find.exe" /I "<info "^<"filename.txt"^"') Do #Set "age=%%G"
You may also wish, for robustness, to match a more unique pattern. You can do that by choosing findstr.exe instead of find.exe:
#For /F Tokens^=4^ Delims^=^" %%G In ('^""%__AppDir%findstr.exe" /IR "<info\ Name=\"[a-Z]*\"\ Age=\"[0-9]*\">" "filename.txt"^"') Do #Set "age=%%G"
Please note that the pattern I have used for the age, allows for superscript digits. However, this scenario is unlikely ever to be encountered, in your actual case, so I figured that this pattern should suffice.

Windows more command asks for user input

I have a batch file with more command to skip the first few lines of the file and print the rest. I am using more +6 file_name. I see that it opens some percentage of the file and asks the user to enter the prompt so that it can load the next portion. I tried redirecting the output of the more command to a file using the > operation to another file and still have the same problem.
https://ss64.com/nt/more.html
When MORE is used without any redirection symbols it will display the percent complete e.g.
MORE /E myfile.txt
--More (17%) --
Thanks,
Pavan.
The more command prompts for user input even when you redirect its output to a file, as soon as 64K lines have been encountered. In addition, more expands TABs to SPACEs.
A work-around is to use a for /F loop to skip the first 6 lines:
> "outfile.txt" (
for /F "usebackq skip=6 delims=" %%L in ("infile.txt") do #(
echo(%%L
)
)
This however limits the line length to 8K characters/bytes. Furthermore, this skips empty lines.
To keep empty lines, you could do this:
> "outfile.txt" (
for /F "skip=6 delims=" %%L in ('findstr /N "^" "infile.txt"') do #(
set "LINE=%%L"
setlocal EnableDelayedExpansion
echo(!LINE:*:=!
endlocal
)
)
This precedes every line by a line number and a colon using findstr intermittently, so no line appears empty to for /F; the prefix is removed when writing out the lines then. The line length is still limited though.
You may use this simpler method:
< myfile.txt (
rem Skip the first 6 lines
for /L %%i in (1,1,6) do set /P "="
rem Show the rest
findstr "^"
)
This method may fail if the skipped lines are longer than 1023 characters, that is the limit of set /P command.

Batch file log function for printing command output to both log file and screen

I found there are some topics similar to this problem but not exactly what I want, so I raise this topic.
I want to create a log function for printing message to both formatted log file and console output. The function is as below:
:LOGDEBUG
#echo DEBUG: %~1
if NOT EXIST %LOG_FILE% exit /b 1
#echo [%date - %time%] DEBUG: %~1 >> %LOG_FILE% 2>&1
exit /b 0
And I try to use it for printing the command execution output and if the output contains special character like "<" and ">", this function doesn't work well and prompt "The system cannot find the file specified". My code for executing a command is below:
for /f "usebackq delims=" %%a in (`dir c:\temp`) do (
CALL :LOGDEBUG "%%a"
)
However, when I use "echo" command directly instead of the log function, the output can be printed correctly on the console. Like the following code:
for /f "usebackq delims=" %%a in (`dir c:\temp`) do (
echo %%a
)
May I know what is the problem, and how can I print the output correctly by using the log function? Thanks
You have answered your question by own: when I use "echo" command directly...
#ECHO OFF
SETLOCAL EnableExtensions
set "LOG_FILE=D:\tempx\program.log" my testing value
rem type NUL>"%LOG_FILE%"
for /f "usebackq delims=" %%a in (`dir d:\temp 2^>NUL`) do (
CALL :LOGDEBUG "%%a"
)
rem type "%LOG_FILE%"
ENDLOCAL
exit /b
:LOGDEBUG
FOR %%A in ("%~1") do (
#echo DEBUG: %%~A
if NOT EXIST "%LOG_FILE%" exit /b 1
#echo [%date% - %time%] DEBUG: %%~A >> "%LOG_FILE%" 2>&1
)
exit /b 0
Resources (required reading):
(command reference) An A-Z Index of the Windows CMD command line
(additional particularities) Windows CMD Shell Command Line Syntax
(%~A etc. special page) Command Line arguments (Parameters)
(special page) EnableDelayedExpansion
Here is the batch code which should work.
#echo off
setlocal EnableDelayedExpansion
set "LOG_FILE=C:\program.log"
del "%LOG_FILE%" 2>nul
for /F "delims=" %%a in ('dir "C:\temp\*" 2^>nul') do call :LOGDEBUG "%%a"
endlocal
goto :EOF
:LOGDEBUG
set "StringToOutput=%~1"
echo DEBUG: !StringToOutput!
echo [%DATE% - %TIME%] DEBUG: !StringToOutput!>>"%LOG_FILE%"
goto :EOF
First delayed environment variable expansion is enabled and a copy of existing environment table is made. It is explained below why this is done.
Next the name of the log file with full path is assigned to an environment variable in local variable table. This path can be with or without 1 or more spaces in path. The log file is deleted in case of existing already from a previous run. This code can be removed if you want to append new lines to already existing file. But you should add in this case code to avoid that the log file permanently increases until no free storage space anymore.
The FOR command executes the command DIR and processes each line of the output of DIR written to stdout. Blank lines are skipped. The default delimiters are space, tab and newline characters. As wanted here are the entire lines of DIR, the default delimiter list is replaced by nothing which means only newline characters remain and loop variable %a gets assigned always an entire non blank line.
The output of command DIR contains < and > which are interpreted as redirection operators if found by command processor within a line not quoted. Therefore the line for DIR output is passed quoted to subroutine LOGDEBUG. Which characters must be usually quoted are listed on last help page printed into a command prompt window when executing cmd /? in a command prompt window.
When the loop has finished, the local environment table is deleted which means LOG_FILE and StringToOutput are also removed, and previous environment is restored which usually means the delayed expansion is turned off again before batch execution exits with a jump to predefined label to end of file.
The subroutine LOGDEBUG first assigns the passed string to an environment variable without surrounding quotes just needed because of special characters in line like < and >.
Next the line is written to console window without quotes using delayed expansion as otherwise < and > would be interpreted as redirecting operators and not literally.
The same line is written also to the log file with the difference of date and time inserted at beginning of line. You missed the percent sign after date in your code. Again delayed expansion is used to get the line with the characters < and > written to file without being interpreted as redirection operators.
Important is also that there is no space before >> as otherwise each line in log file would have a trailing space. 2>&1 is useless here as command echo does not write something to stderr.
The subroutine is exited with a jump to end of file resulting in command FOR processes next line.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /?
del /?
dir /?
for /?
goto /?
set /?
It would be of course possible to do all the output directly in body of command FOR without using a subroutine.
#echo off
setlocal EnableDelayedExpansion
set "LOG_FILE=C:\program.log"
del "%LOG_FILE%" 2>nul
for /F "delims=" %%a in ('dir "C:\temp\*" 2^>nul') do (
echo DEBUG: %%a
echo [!DATE! - !TIME!] DEBUG: %%a>>"%LOG_FILE%"
)
endlocal
Delayed variable expansion is nevertheless required here as otherwise %DATE% and %TIME% would be expanded by command processor like %LOG_FILE% already on parsing entire block defined by ( and ) before command FOR is executed at all which would result in same date and time written for all lines to the log file.

Windows 2003 batch script with pipe

I'm trying to write a batch script that will run every 15 minutes, check number of files in a directory and if that number is bigger then set limit, move all files to another directory. I would easily do this in bash script, but this is my first batch script.
I split this task in several steps:
Find number of files in directory.
I managed to do this with this command:
dir/b/a-d d:\test\test2 | find /v /c "::"
Next thing is to assign output of this command to some variable so I can compare it with my desired limit. This is where problem starts.
ECHO OFF
setlocal enableextensions
FOR /F "tokens=* USEBACKQ" %%a IN (`dir/b/a-d d:\test\test2 ^| find /v /c "::"`)
DO (#SET NUMFIL=%%a)
ECHO %NUMFIL%
endlocal
I'm getting: "| was unexpected at this time". Obviously, pipe is getting in the way. I found that it is special character and as such must be escaped with caret. After doing so, I'm getting: "The syntax of the command was incorrect." This is Windows server 2003.
3.After getting this problem solved, I plan to insert something like this:
IF %%NUMFIL%% > 20
(move "d:\test\test2\ti*" "d:\test\test2\dir\")
That would move all that files (all of them starts with "ti") to desired directory.
So my questions would be: what to do with #2 issue and will #3 work in this case?
Not sure ":: will work in your first case, since :: is unlikely to appear in a DIR output. A single colon would suffice, since the /c option of find counts the number of LINES in which the target string occurs.
The secret to the second problem is that the do keyword must occur on the same line as the closing-parenthesis of the IN clause. It is possible to break the structure into
FOR /F "tokens=* USEBACKQ" %%a IN (
' dir/b/a-d d:\test\test2 ^| find /v /c ":" '
) DO (SET NUMFIL=%%a)
Note the # is not required - it suppresses the command's being echoed, which is turned off by the initial #echo off (the # there suppresses the echoing of the ECHO OFF
Also, the parentheses around this set are not required. IF they are used, the open-parenthesis must occur on the same physical line as the do.
You also don't need to use usebackq since you have no need here to change the interpretation of quotes.
Third item - > is a redirector. For a comparison operator, use one of EQU GEQ LSS LEQ GTR NEQ depending on comparison required.
And again, the open-parenthesis must be on the same line as the if. With an else, the close-parenthsis before the ELSE , the ELSE keyword and the open-parenthesis after must all be on the same physical line.
I think this should work. Note that it does not use backticks.
ECHO OFF
setlocal enableextensions
FOR /F %%a IN (' dir /b /a-d "d:\test\test2" ^| find /c /v "" ') DO SET NUMFIL=%%a
ECHO %NUMFIL%
IF %NUMFIL% GTR 20 (move "d:\test\test2\ti*" "d:\test\test2\dir\")

Resources