Explain how Windows batch newline variable hack works - batch-file

Can someone please explain how this works?
#echo off
REM Creating a Newline variable (the two blank lines are required!)
set NLM=^
set NL=^^^%NLM%%NLM%^%NLM%%NLM%
REM Example Usage:
echo There should be a newline%NL%inserted here.
emits:
There should be a newline
inserted here.
from How can you echo a newline in batch files?

The trick uses the behaviour of the caret.
Also explained at Long commands split over multiple lines in Windows Vista batch (.bat) file
The caret is an escape character for the next character, or at the line end it is used as multiline character, but this is nearly the same.
At the line end it simply escapes the next character, in this case the <Linefeed>, but there is a hidden feature, so if the escaped character is a <LF> it is ignored and the next character is read and escaped, but this charater will be always escaped, even if it is also a <LF>.
Now you can understand
set NLM=^
rem Two empty lines are required here
The NLM-Variable contains exactly one <LF> character.
But if you try to use it with echo Line1%NLM%Line2 it fails, as the parser stops parsing at a single <LF>.
But this works
echo Line1^
Line2
So you need to add an escaped linefeed into the line and that is the NL-Variable.
The NL-Variable consists of only three characters.
NL=^<LF><LF>
And if this is expanded, it creates only one escaped <LF> as the first <LF> after the caret will be ignored.
Btw. In my opinion, it is much easier to use linefeeds with delayed expansion, as there is no need to escape anything.
In this example I use %=EMPTY=% instead of an empty line (for self commenting), but as the variable =EMPTY= can't exists it will be expanded to an empty line.
setlocal EnableDelayedExpansion
(set NLM=^
%=EMPTY=%
)
echo Line1!NLM!Line2
EDIT: Append some hints for useful using the <LF>
1) Use it as newline in an echo
setlocal EnableDelayedExpansion
(set LF=^
%=EMPTY=%
)
echo Line1!LF!Line2
2) Use it to split commands in a parenthesis block
setlocal EnableDelayedExpansion
(set LF=^
%=EMPTY=%
)
(
echo Line1%LF%set x=4%LF%echo !x!%LF%
)
3) Create a (nearly) empty EOL-chararcter in a FOR /F loop,
as <LF> is the line delimiter an EOL of <LF> is the same than an empty one.
FOR /F ^"eol^=^
delims^=^" %%a in (myFile.php) do echo %%a
4) Use LF for splitting text in a FOR /F loop
setlocal EnableDelayedExpansion
(set LF=^
%=EMPTY=%
)
set "var=Content1;Content2"
FOR /F "delims=" %%a in ("%var:;=!LF!%") do (
echo %%a
)

There seems a way that also works with pipe:
(echo 1st line^
&echo 2nd line) | sort

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

Batch file to read the last line from .tmp even if it starts with ; character

Hell everyone, I have a weird question to you.
I have this .tmp file.
M l FNT;yriad_b
M l FNT;arial_bo
mm
zO
J
O R,P
H50,5,T
D -3.0,2.2
F98;arial_bo
F99;yriad_b
Sl1;0.0,0.0,100,100,100
P -1.8
T:Line01;6.5,6.5,0,98,pt23;XXX
G 0,8.5,0;L:70.5,0.7
T:Line04;5.5,14.2,0,99,pt18,q100;XXXX
T:Line05;5.5,18.2,0,99,pt9,q100;XXXX
T:Line88;5.5,26.0,0,99,pt21,q100;XXXX
T:Line08;5.5,68.0,0,99,pt21,q100;XXXX
B 5.5,70,9,code128, 7,.4;XXXX
T:Line99;5.5,61.8,0,99,pt11,q100;XXXX
B 5.5,85,5,code128, 7,.25;XXXX
T:Line09;5.5,82.8,0,99,pt17,q100;XXXX
T:Line10;5.5,33.3,0,99,pt17,q100;XXXX
B 5.5,35,2,code128, 7,.3;XXXX
T:Line11;5.5,48.1,0,99,pt18,q100;XX
T:Line19;16.0,48.1,0,99,pt18,q100;XX
B 5.5,50.0,0,code128, 7.0,.3;8
A 1
;ABC123
and I need to get the last line, exactly ABC123 without ; character.
I am currently using the batch script below, which works if I do not have the ; character before ABC123. I do not know why the batch script skips the lines stars with ; character.
Batch script.
for /f "delims=" %%x in (c:\print\pack2.tmp) do set Build=%%x
Can anybody help me ?
Thanks
It skips lines beginning with ; as this is the default EOL character, see also FOR /?.
To avoid this you could change the EOL to another character like
for /f "EOL=Y delims=" %%x in (c:\print\pack2.tmp) do set Build=%%x
If you know that a specific character can't be at this position, then you can choose it.
Or you use enclose the file with findstr, so that each line starts with a line number (then the FOR/F can grab also empty line).
setlocal EnableDelayedExpansion
for /f "delims=" %%L in ('findstr /n "^" c:\print\pack2.tmp') do (
set "line=%%L"
set "Build=!line:*:=!"
)
The line is skipped due to the eol option of for /F loops which defaults to the ; character.
Unlike the delims option, you cannot simply disable the eol option by stating the option string "eol=", because this would set it to the " character. A string like "eol= delims=" would also not work because this would take the SPACE as the eol character.
To disable the eol option there are several possibilities:
Hiding it behind the delims option:
for /F "usebackq eol=, delims=," %%L in ("file.txt") do set "Build=%%L"
Specifying an eol character to one of the given delims characters disables eol as delims seems to have the higher priority to for /F. The order of eol and delims does not matter.
Using the odd unquoted syntax:
for /F usebackq^ delims^=^ eol^= %%L in ("file.txt") do set "Build=%%L"
Every token separator (SPACE, TAB, ,, ;, = and also the non-break space 0xFF) must be escaped by preceding ^. Here, eol must be specified as the last option. The unescaped SPACE is not taken as the eol character, because it is consumed by the parser to separate the option string from the for variable reference %%L.
Using the odd line-break syntax:
for /F usebackq^ eol^=^
delims^= %%L in ("file.txt") do set "Build=%%L"
The empty line is mandatory here. The ^ after the eol^= part constitutes line continuation, so the first line break (CR+LF, carriage-return line-feed sequence) is ignored. The next character, an LF (the CR is already skipped by the parser when reading the line of code), is taken literally as the eol character. Since such cannot occur within lines of text as they mark the end of line, the eol option is finally disabled.
REM
#ECHO OFF
SETLOCAL
SET "sourcedir=U:\sourcedir"
SET "filename1=%sourcedir%\q40888302.txt"
FOR /f "usebackqtokens=1*eol=;delims=; " %%a IN ("%filename1%") DO (
SET var=%%~a
)
ECHO "%var%"
GOTO :EOF
You would need to change the setting of sourcedir to suit your circumstances.
I used a file named q40888302.txt containing your data for my testing.

Multiline text file, how to put into an environment variable

i have a file called file.txt which contains:
this is line one ^
this is line two ^
this is the last line
how can i put that into an env var?
i can do this from a batch file:
test.bat
set LF=^
[blank line]
[blank line]
rem two blank lines needed above
set multi=Line 1!LF!Line 2!LF!Last line
echo !multi!
this outputs three lines:
Line 1
Line 2
Last line
so how can i get file.txt into envvar inside a batch file?
As dbenham said, it can be done also with for/f but it's a bit more complicated.
The simple 80% solution is
setlocal EnableDelayedExpansion
set "var="
set LF=^
rem *** Two empty lines are required for the linefeed
FOR /F "delims=" %%a in (myFile.txt) do (
set "var=!var!!LF!%%a"
)
echo !var!
But it fails with:
- If a line is blank it will be skipped
- If a line begins with ; the EOL-character
- If a line contains ! (and carets)
But then you could use a bit more complex solution
#echo off
SETLOCAL DisableDelayedExpansion
set "all="
FOR /F "usebackq delims=" %%a in (`"findstr /n ^^ aux1.txt"`) do (
set "line=%%a"
SETLOCAL EnableDelayedExpansion
set "line=!line:#=#S!"
set "line=!line:*:=!"
for /F "delims=" %%p in ("!all!#L!line!") do (
ENDLOCAL
set "all=%%p"
)
)
SETLOCAL EnableDelayedExpansion
if defined all (
set "all=!all:~2!"
set ^"all=!all:#L=^
!"
set "all=!all:#S=#!"
)
echo !all!
What the code do?
First, the findstr /n ^^ will prepend each line with a line number and a colon, like
1:My first Line
2:; beginning with a semicolon
3:
4:there was an empty line
This solves the problem of empty lines and also the standard EOL-character ; can be ignored.
To get the content of the line, the value is set to a variable while delayed expansion is disabled, this solves the problem with ! and ^ characters.
To remove the line number and the colon, the delayed expansion will be enabled (no, a delim of : can't solve it).
Then all # are replaced with #S, this will be done first, as after the prefix removing the line could be empty and the replacement would fail.
But why I replace it?
That's because I can't insert the linefeeds here, as the following FOR/F would fail with embedded linefeeds,
so I only add linefeed marker (in this case I use #L), but the content of the file could contain also a #L, but by replacing all # with #S all markers are unique.
After the marker, there is the problem to close/disable the delayed expansion with an endlocal, but preserve the content of the modified all and line variable.
This is done with the FOR/F-endlocal trick, as the %%p can transport content behind the endlocal barrier.
Then after reading the complete file, I check if the all is defined, as it would be empty for an empty file.
Then the first linefeed marker #L will be removed, and all other markers are replaced with a real linefeed character.
Then the sharp safer #S will be reverted to #.
That's all, so even this solution is obviously...
You were almost there. You need to read each line of text and then append the line plus a line feed to the variable.
FOR /F could be used, but it doesn't play well with delayed expansion if the content contains ! characters. It is also awkward to preserve blank lines and awkward to disable the EOL option.
A simpler solution is to use SET /P to read the lines. The limitations with this technique are:
1) It trims trailing control characters from each line
2) The file must use Windows standard line terminators of carriage return, line feed. It will not work with Unix style line feed.
3) It is limited to reading 1023 bytes per line (not including the line terminator characters)
Bear in mind that an environment variable can only hold a little less than 8 kbytes of data. So you are limited to only loading a very small file into a variable with this technique.
#echo off
setlocal enableDelayedExpansion
set LF=^
:: Two blank lines above needed for definition of LF - do not remove
set file="test.txt"
set "var="
for /f %%N in ('find /c /v "" ^<%file%') do set lineCnt=%%N
<%file% (
for /l %%N in (1 1 %lineCnt%) do (
set "ln="
set /p "ln="
set "var=!var!!ln!!lf!"
)
)
set var

Reading coordinates from xml file

I have this script, to read xml file. The file contains coordinates and I want to list the coordinates:
#echo off
setlocal EnableDelayedExpansion
FOR %%K IN (*.xml) DO (
SET K=%%K
SET K=!K:~0,-4!
SET "prep=0"
REM READ DATA
FOR /F "tokens=*" %%X IN (!K!.kml) DO (
if !prep! == 1 (
echo %%X
pause
FOR /F %%L IN ("%%X") DO (
SET L=%%L
IF NOT "!L:~0,1!" == "<" (
echo %%L
)
)
SET "prep=0"
)
if "%%X" == "<coordinates>" ( SET "prep=1" )
)
)
I got these result:
14.63778004128814,49.50141683426452,0 14.63696238385996,49.48348965654706,0 14.6
8840586504191,49.47901033971912,0 14.68589371304878,49.49939179836829,0 14.63778
004128814,49.50141683426452,0 </coordinates>
Press and key to continue...
14.63778004128814,49.50141683426452,0
Press and key to continue...
First you see the line with coordinates. Second, in the 3rd loop, there are coordinates printed. But I have only one pair of coordinates printed... If I will press a key again, the batch finishes without printing next columns. Can you help?
Edit
After the answer has been posted, I have question 1) could we use this:
SET LF=^
setlocal EnableDelayedExpansion
... (next code) ...
set "var=!var: =%LF%!"
So when there is no delayed LF variable, we could embed it. Or not?
And 2) why in your code
for %%L in ("!LF!") do set "X=!X: =%%~L!"
Did you use %%~L and not just %%L
Your immediate problem is that FOR /F does not iterate the tokens in a line. It simply parses each token that you ask for. If you don't specify a "tokens" option, then it defaults to "tokens=1" - it only parses the first token in the line.
However, FOR /F will treat a string as multiple lines if the string contains linefeed characters. It will then iterate each line like you want. The trick is to replace your space delimiter with a line feed character. There are multiple methods that can do the job, but I will show what I think is the easiest to work with.
First define a variable containing a single linefeed
set LF=^
::The two blank lines above are critical for the definition of the line feed
The next trick is to replace spaces in your variable with linefeeds. Normally substituion using a variable for the replacement would look something like set "var=!var:search=%replaceVar%!". But that won't work for the LF variable - it is difficult to work with the LF variable using normal expansion. It is much easier to use delayed expansion. We can't embed delayed expansion within delayed expansion, but we can transfer the value of LF to a simple FOR variable and use for %%L in ("!LF!") do set "var=!var: =%%~L!"
One thing about your code I do not understand - your initial FOR loop is iterating accross all the .KML files. You strip off the extension using a substring operation. There is a much easier way to do that without using an environment variable: %%~nK will give the base name of the file without the extension. But why do that at all when you turn around and append the extension again?
I used the %%K value directly - I added the USEBACKQ option and added quotes to allow for spaces in the file name.
Here is code that should do what you are expecting.
#echo off
setlocal EnableDelayedExpansion
::define a variable containing a linefeed character
set LF=^
::Above 2 blank lines are part of the LF definition, do not remove
for %%K in (*.kml) do (
set "prep=0"
for /f "usebackq tokens=*" %%X in ("%%K") do (
if !prep! == 1 (
echo %%X
pause
set "ln=%%X"
for %%L in ("!LF!") do set "ln=!ln: =%%~L!"
for /f %%L in ("!ln!") do (
set L=%%L
if not "!L:~0,1!" == "<" (
echo %%L
)
)
set "prep=0"
)
if "%%X" == "<coordinates>" ( set "prep=1" )
)
)
BUT - I think you have a bigger problem. I am worried that you are setting yourself up for a world of pain by using batch to parse XML. You are assuming the XML will always be layed out the same way. There are countless valid ways of adding or subtracting linefeeds and white space into the XML document that would break your algorithm. Can you be sure all your input files came from the same source and will always be formatted like you expect? I think you really should be using XSLT to parse and transform your XML document into a naked list of coordinates.
Answsers to additional questions
1) set "var=!var: =%LF%!" will not work - Regular expansion of LF requires escape sequences and multiple expansions. This will work: set "var=!var: =^%LF%LF%!"
The escape sequences for %LF% can get very tricky, so I try to avoid them.
2) Regarding for %%L in ("!LF!") do set "X=!X: =%%~L!", note that it is a simple FOR, not FOR /F. The !LF! must be quoted or else FOR will not read it. But the FOR statement preserves the quotes (unlike FOR /F), so I need %%~L to remove the enclosing quotes.
There is a very important distinction between FOR and FOR /F with regard to linefeeds. FOR will preserve quoted linefeeds, whereas FOR /F treats the linefeed as a line delimiter and iterates each line, so the linefeeds are not preserved.

Assigning newline character to a variable in a batch script

The following is the batch script i have written
#echo off
setlocal enabledelayedexpansion
set finalcontent=
For /F "tokens=1-2* delims= " %%I in (abc.txt) do (
IF %%J EQU MAJORVER (
set currentline=%%I %%J %1
set finalcontent=!finalcontent!!currentline!
) ELSE IF %%J EQU MINORVER (
set currentline=%%I %%J %2
set finalcontent=!finalcontent!!currentline!
) ELSE IF %%J EQU BUILDNUM (
set currentline=%%I %%J %3
set finalcontent=!finalcontent!!currentline!
) ELSE (
set currentline=%%I %%J %%K%NL%
set finalcontent=!finalcontent!!currentline!
)
)
echo %finalcontent%>>xyz.txt
I want a newline character appended at the end of every occurence of the variable currentline. Can anyone guide me on this?
You can create a real newline character and assign it to a variable.
setlocal EnableDelayedExpansion
set LF=^
rem TWO empty lines are required
echo This text!LF!uses two lines
The newline best works with delayed expansion, you can also use it with the percent expansion, but then it's a bit more complex.
set LF=^
rem TWO empty lines are required
echo This text^%LF%%LF%uses two lines
echo This also^
uses two lines
How it works?
The caret is an escape character, it escapes the next character and itself is removed.
But if the next character is a linefeed the linefeed is also removed and only the next character is effectivly escaped (even if this is also an linefeed).
Therefore, two empty lines are required, LF1 is ignored LF2 is escaped and LF3 is neccessary to finish the "line".
set myLinefeed=^<LF1>
<LF2>
<LF3>
Hints:
It's often better to use a quite different format of the newline variable definition,
to avoid an inadvertently deletion of the required empty lines.
(SET LF=^
%=this line is empty=%
)
I have removed to often one of the empty lines and then I searched forever why my program didn't work anymore.
And the paranoid version checks also the newline variable for whitespaces or other garbage.
if "!LF!" NEQ "!LF:~0,1!" echo Error "Linefeed definition is defect, probably multiple invisble whitespaces at the line end in the definition of LF"
FOR /F "delims=" %%n in ("!LF!") do (
echo Error "Linefeed definition is defect, probably invisble whitespaces at the line end in the definition of LF"
)
You can define the newline as follows:
set newline=^& echo.
Then, you can use the newline in the echo-statement, like this: (not applicable in the outlined situation, however)
echo %finalcontent%%newline%
or you can use the newline in each set-statement, like this: (mind the extra caret)
set finalcontent=!finalcontent!!currentline!^!newline!
or similarly:
set currentline=%%I %%J %1^%newline%

Resources