I'm trying to write a text adventure in batch, so I want to know how I can split variable like set userinput=take book and turn it into an array. I want to be able to write a program that will split strings into array items at each space. I've done this in a lot of other languages. There are several other questions like this, but I don't feel like they answer my question.
This task is probably much more difficult than you realize. There are lots of "simple" methods, but none of the simple ones are robust.
For example, a simple FOR loop can parse out individual tokens, but it will be broken by *, ?, and possibly ".
A robust solution requires a significant amount of code.
Here is my solution, with extensive comments:
#echo off
:: Start out with delayed expansion disabled, and define "user input"
:: I intentionally include "problem" characters
setlocal disableDelayedExpansion
set "userinput=take book! & ;MustPreserve EmptyLinesAreIgnored"
:: Initialize count
set "cnt=0"
:: Enable and use delayed expansion to protect against poison characters
setlocal enableDelayedExpansion
:: Substitute <LineFeed> for each <space>
(set parsed=!userinput: =^
%= Do not remove or alter this line =%
!)
:: Iterate each line, setting delims and eol to <space> to preserve all tokens
for /f "eol= delims= " %%A in ("!parsed!") do (
%= Return to delayed expansion disabled on first iteration, else ! is lost =%
if "!" equ "" endlocal
%= Increment the count =%
set /a cnt+=1
%= Temporarily enable delayed expansion to capture current count in FOR variable =%
setlocal enableDelayedExpansion
for %%N in (!cnt!) do (
endlocal
%= Save the array value =%
set "token.%%N=%%A"
)
)
::Print the results. Safe array access requires delayed expansion
setlocal enableDelayedExpansion
for /l %%N in (1 1 !cnt!) do echo token.%%N=!token.%%N!
It looks a bit better without all the comments
#echo off
setlocal disableDelayedExpansion
set "userinput=take book! & :MustPreserveColon ;MustPreserve EmptyLinesAreIgnored"
set "cnt=0"
setlocal enableDelayedExpansion
(set parsed=!userinput: =^
%= Do not remove or alter this line =%
!)
for /f "eol= delims= " %%A in ("!parsed!") do (
if "!" equ "" endlocal
set /a cnt+=1
setlocal enableDelayedExpansion
for %%N in (!cnt!) do (
endlocal
set "token.%%N=%%A"
)
)
setlocal enableDelayedExpansion
for /l %%N in (1 1 !cnt!) do echo token.%%N=!token.%%N!
And here is the output:
token.1=take
token.2=book!
token.3=&
token.4=;MustPreserve
token.5=EmptyLinesAreIgnored
Related
I cannot figure out how to correctly identify an empty/undefined variable having two semicolon one after one other or the line that starts with it.
This is the cycle:
for /F "delims=; tokens=1-7" %%m IN (testlist.txt) DO echo FUNCGROUP=%%r a=%%m b=%%n c=%%o d=%%p e=%%q f=%%s
I also tried adding "eol=;" and "......eol=" without success.
This is content of the first line of the file testlist.txt:
;xxxxxx;Active;;FALSE;con ter - dong;HWID000001;Item;sites/coll-
The result I need is, for the first cycle:
a=
b=xxxxxx
c=Active
d=
e=FALSE
f=con ter - dong
g=HWID000001
Thanks for any help.
To achieve that, you need to put a "NULL value" for empty fields in the lines of the file.
As there is no direct string substitution utilities with batch, you have to make substitutions beforehand to create "empty" fields. I suggest you to use a "NULL" character for empty fields like unbreakable space (Alt+0160).
In your case, this gives :
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
for /F "eol= tokens=*" %%l in (file.txt) DO (
SET "LINE=%%l"
SET "LINE=###!LINE:;;=; ;!###"
SET "LINE=!LINE:###;= ;!"
SET "LINE=!LINE:;###=; !"
SET "LINE=!LINE:###=!"
for /F "delims=; tokens=1-7" %%m in ("!LINE!") DO (
SET "RES=FUNCGROUP=%%r a=%%m b=%%n c=%%o d=%%p e=%%q f=%%s"
echo !RES: =!
)
)
Note that the SET "LINE=###!LINE:;;=; ;!###", SET "LINE=!LINE:###;= ;!" and the SET "LINE=!LINE:;###=; !" sections use the unbreakable space (Alt+0160) and replace beginning ";" with "Alt+0160;", the ending ";" with ";Alt+0160" and any following ";;" with ";Alt+0160;". The for loop parses then correctly the line, and next you just have to remove the unbreakable space to get "empty" variables.
EDIT: As brightly suggested by #jeb in the comments, you can also use quotes to handle empty fields. Each for loop variables can be then directly and simply unquoted.
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
for /F "eol= tokens=*" %%l in (file.txt) DO (
SET "LINE=%%l"
SET "LINE=###^"!LINE:;=^";^"!^"###"
SET "LINE=!LINE:###^";=^"^";!"
SET "LINE=!LINE:;^"###=;^"^"!"
SET "LINE=!LINE:###=!"
for /F "delims=; tokens=1-7" %%m in ("!LINE!") DO (
echo FUNCGROUP=%%~r a=%%~m b=%%~n c=%%~o d=%%~p e=%%~q f=%%~s
)
)
Unfortunately, you have not provided sufficient information for what you intend to do with your empty field, so this is only a demonstration to provide you with output similar to that which you've indicated in your question:
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
For /F "Delims==" %%G In ("(Set Field[) 2>NUL") Do Set "%%G="
Set "i=0"
For /F UseBackQ^ Delims^=^ EOL^= %%G In ("testlist.txt") Do Call :GetFields "%%G"
Pause
GoTo :EOF
:GetFields
Set "Record=%~1"
Set /A i += 1
SetLocal EnableDelayedExpansion
Set "Index=1"
Set "Field[!Index!]=%Record:;=" & Set /A Index +=1 & Set "Field[!Index!]=%"
Echo(&Echo Record !i!
For /L %%G In (1,1,!Index!) Do If Not Defined Field[%%G] (Echo Field[%%G]=) Else Set Field[%%G]
Exit /B
I need to escape "!" (and other special chars) in a for-loop where delayed variable expansion is enabled
I've tried manually escaping the loop-varible with string substitution ^^! in the loop-variable, %%a but with no luck at all. Is it already too late after the for has read them? If so, how the heck can I even accomplish this?
Below is a short function. The only relevant part here is the for-loop and the echo statement. That is printing out whole lines from a file every X'th line, and those lines are file-paths. They (sometimes) contain characters like "!" and other troublesome special characters. I just want echo here to pass it without interpreting it at all - but instead it ends up deleting my "!" chars.
For my use they need to be exactly correct or they are useless as they must correlate to actual files later on in what I use them for.
setlocal EnableDelayedExpansion
:SplitList
if [%3] == [] goto :SplitListUsage
set inputfile=%~1
set outputfile=%~2
set splitnumber=%~3
set skipLines=0
set skipLines=%~4
if %skipLines% GTR 0 (
set skip=skip=%skipLines%
) else (
set skip=
)
#echo off > %outputfile%
set lineNumber=0
for /f "tokens=* %skip% delims= " %%a in (%inputfile%) do (
set /a modulo="!lineNumber! %% !splitnumber!"
if "!modulo!" equ "0" (
echo %%a >> !outputfile!
)
set /a lineNumber+=1
)
exit /B 0
Quick solution:
if !modulo! equ 0 (
setlocal DisableDelayedExpansion
(echo %%a)>> "%outputfile%"
endlocal
)
Better solution:
Based on your code sample, there is no need to have delayed expansion enabled for the entire code,in fact you should keep it disabled to not mess with the file names or input strings which may contain !, and enabled it when necessary:
setlocal DisableDelayedExpansion
REM Rest of the code...
for /f "tokens=* %skip% delims= " %%a in (%inputfile%) do (
set /a "modulo=lineNumber %% splitnumber"
setlocal EnableDelayedExpansion
for %%m in (!modulo!) do (
endlocal
REM %%m is now have the value of modulo
if %%m equ 0 (
(echo %%a)>> "%outputfile%"
)
)
set /a lineNumber+=1
)
Side notes:
There are some other issues with your code which as you might have noticed, some of them are corrected in the above solutions. But to not distract you from main issue you had, I covered them here separately.
There is also room for improving the performance of the code when writing to the outputfile.
Here is the re-written code which covers the rest:
#echo off
setlocal DisableDelayedExpansion
:SplitList
if "%~3"=="" goto :SplitListUsage
set "inputfile=%~1"
set "outputfile=%~2"
set "splitnumber=%~3"
set "skipLines=0"
set /a "skipLines=%~4 + 0" 2>nul
if %skipLines% GTR 0 (
set "skip=skip=%skipLines%"
) else (
set "skip="
)
set "lineNumber=0"
(
for /f "usebackq tokens=* %skip% delims= " %%a in ("%inputfile%") do (
set /a "modulo=lineNumber %% splitnumber"
setlocal EnableDelayedExpansion
for %%m in (!modulo!) do (
endlocal
REM %%m is now have the value of modulo
if %%m equ 0 echo(%%a
)
set /a lineNumber+=1
)
)>"%outputfile%"
You didn't protect the variable assignments with double qoutes " e.g. set inputfile=%~1. If the now naked batch parameter %~1 contains spaces or special characters like & your batch files fails, either fatally with a syntax error or at execution time with incorrect data. The recommended syntax is to use set "var=value" which does not assign the quotes to the variable value but provides protection against special characters. It also protects the assignment against the accidental trailing spaces.
The %inputfile% may contain special characters or spaces, so it should be protected by double quoting it when using in the FOR /F's IN clause. When double quoting the file name in FOR /F the usebackq parameter must also be used.
With SET /A there is no need to expand the variable values: set /a modulo="!lineNumber! %% !splitnumber!". The variable names can be used directly, and it will work correctly with or without delayed expansion.
Using (echo %%a) >> "%outputfile%" inside a FOR loop introduces a severe performance penalty specially with a large number of iterations, because at each iteration the output file will be opened, written to and then closed. To improve the performance The whole FOR loop can redirected once. Any new data will be appended to the already opened file.
The odd looking echo( is to protect against the empty variable values, or when the variable value is /?. Using echo %%a may print the `ECHO is on/off' message if the variable is empty or may print the echo usage help.
In the main solutions, The reason I've used (echo %%a)>> "%outputfile%" instead of echo %%a >> "%outputfile%" is to prevent outputting the extra space between %%a and >>. Now you know the reason for using echo(, it is easy to understand the safer alternative: (echo(%%a)>> "%outputfile%"
I'm trying to extract every value for FileRef from a string I've already extracted out of a file. Unfortunately, the string is one line which makes it more difficult to use for /f "tokens=*".
The string is:
"<Cim:TrnTable_list><Cim:TrnTable Id="Root"><Cim:TrnElem Ref="3" FileRef="A1-FS.elt"/><Cim:TrnElem Ref="4" FileRef="A1-MS.elt"/><Cim:TrnElem Ref="9" FileRef="Product\Product-v1\Product-v1-MD.elt"/><Cim:TrnElem Ref="11" FileRef="Product\Product-v2\Product-v2-MD.elt"/><Cim:TrnElem Ref="12" FileRef="RunnerPart_Assembly#1.elt"/></Cim:TrnTable></Cim:TrnTable_list>"
How to get every value for FileRef inserted into a variable in the following format?:
A1-FS.elt?A1-MS.elt?Product\Product-v1\Product-v1-MD.elt?Product\Product-v2\Product-v2-MD.elt?RunnerPart_Assembly#1.elt
I mean, then I could loop trough them using for /f "delims=?" right?
Or is there a way to convert each ? in the above example to a 'new line' within one string, or maybe even better ideas to loop trough each FileRef-value?
Many thanks!
Squashman is right in his comment, use a language that is capable of handling XML data natively.
Anyway, if you insist on using pure Windows batch scripting, you could assemble a new string with ? symbols as separator like in the following script:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define constants here:
set "_FILE=%~dpn0.txt" & rem // (path to file containing the line of text)
(set ^"_LF=^
%= empty line =%
^") & rem // (this constitutes a new-line character)
rem // Initialise collection variable:
set "COLL=?"
rem // Read line from file:
for /F "usebackq delims=" %%L in ("%_FILE%") do (
set "LINE=%%~L"
setlocal EnableDelayedExpansion
rem // Replace `><` by `>` + line-break + `<`:
set ^"LINE=!LINE:^>^<=^>^%_LF%%_LF%^<!^"
rem // Read one tag enclosed within `<` and `>`:
for /F "delims=" %%I in ("!LINE!") do (
endlocal
set "ITEM=%%I"
rem // Extract string between ` FileRef` and `/>`:
setlocal EnableDelayedExpansion
set "ITEM=!ITEM:* FileRef=!"
set "ITEM=!ITEM:/>=!"
rem // Check for `=`-sign after `FileRef`:
if "!ITEM:~,1!"=="=" (
rem // Remove leading `=` and surrounding `""`:
for /F "delims=| eol=|" %%F in ("!ITEM:~1!") do (
endlocal
set "NAME=%%~F"
rem // Assemble return string using `?` as separator:
setlocal EnableDelayedExpansion
for /F "delims=| eol=|" %%J in ("!COLL!!NAME!?") do (
endlocal
set "COLL=%%J"
setlocal EnableDelayedExpansion
)
)
)
)
endlocal
)
rem // Return collection variable:
setlocal EnableDelayedExpansion
echo(!COLL:~1,-1!
endlocal
endlocal
exit /B
Toggling delayed expansion is done in order not to have trouble with ! symbols.
Better than collecting all values in a single variable is to just loop through them in my opinion.
This is one other way to brute force this. This code will put each FileRef into its own variable and sequence the variable name up.
#echo off
FOR /F "delims=" %%G IN (line.txt) do set "line=%%G"
set i=0
:loop
set /a i+=1
set "line=%line:*FileRef=%"
FOR /F "tokens=1* delims==/" %%G IN ("%line%") DO (
set "var%i%=%%~G"
set "line=%%H"
)
echo "%line%"|find /I "fileref" >nul 2>&1 &&GOTO loop
set var
pause
When executed it will output this.
C:\BatchFiles\SO\XML>bruteforce.bat
var1=A1-FS.elt
var2=A1-MS.elt
var3=Product\Product-v1\Product-v1-MD.elt
var4=Product\Product-v2\Product-v2-MD.elt
var5=RunnerPart_Assembly#1.elt
Press any key to continue . . .
If you don't want the data assigned into their own individual variables you can just use the %%G meta-variable directly inside the FOR command.
I have a version number 17.06.01.01 and I would like to know how many entries there are split by a period.
My last piece of code was;
setlocal ENABLEDELAYEDEXPANSION
for /F "tokens=1-10 delims=." %%a in ("17.09.01.04.03") do (
set /a "iCount+=1"
echo %%a, !iCount!
)
endlocal
I've tried a host of 'For' commands but seem to be getting further away each time.
replace every dot with a space (as a delimiter). Then count number of tokens:
#echo off
set "string=17.09.01.04.03"
set count=0
for %%a in (%string:.= %) do set /a count+=1
echo %count%
(May give false results, if there are other spaces, commas or tabs in the string, but should work nice for your example of version strings containing only numbers and dots)
#treintje:
echo off
set "string=1>7.0 9.0&1.04.0!3"
set count=0
:again
set "oldstring=%string%"
set "string=%string:*.=%"
set /a count+=1
if not "%string%" == "%oldstring%" goto :again
echo %count%
For the sample string you provided, Stephan's approach is perfectly sufficient.
However, if the string contains token separators (white-spaces, ,, ;, =), quotation marks ("), wild-cards (*, ?) or other special characters (^, &, (, ), >, <, |) it might probably fail. You could of course replace most of such characters by others in advance, and use delayed expansion rather than immediate one, but this still does not resolve all issues (for instance, you cannot replace all = or * characters due to the sub-string replacement syntax).
The following approach however can deal with every arbitrary combination of all such characters. Basically it replaces every period (.) with a new-line (line-feed) character and lets find /C /V "" count the total number of lines.
#echo off
set "string=17.09.01.04.03"
setlocal EnableDelayedExpansion
if defined string (set ^"strtmp=!string:.=^
%= empty string =%
!^") else set "strtmp="
for /F %%C in ('cmd /V /C echo(^^!strtmp^^!^| find /C /V ""') do set /A "count=%%C-1"
echo "!string!" contains %count% periods.
endlocal
The periods become replaced by line-feeds in advance, using delayed expansion in order not to fail if any special characters occur. The for /F loop executes the command line cmd /V /C echo(^^!string^^!| find /C /V "" and captures its output in a variable, reduced by one as find /C /V "" actually returns the number of lines, which are separated by one less line-feeds (hence periods), originally. The double-escaped exclamation marks ^^! are needed in order to ensure that the variable strtmp is actually expanded within the explicitly invoked inner-most cmd instance, because otherwise, the contained multi-line string is not correctly transported into the pipe (|).
A different approach comparing strLen before and after replacing the dots with nothing.
#Echo off&SetLocal EnableExtensions EnableDelayedExpansion
set "string=17.09.01.04.03"
set "str2=%string:.=%"
call :strlen string ret
call :strlen str2 ret2
Set /A "Dots=ret-ret2"
echo Number of dots in %string% is %Dots%
goto :Eof
:strLen string len
:$source http://www.dostips.com/?t=Function.strLen
(SETLOCAL ENABLEDELAYEDEXPANSION
set "str=A!%~1!"
set "len=0"
for /L %%A in (12,-1,0) do (set /a "len|=1<<%%A"
for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A")
)
ENDLOCAL&IF "%~2" NEQ "" SET /a %~2=%len%
EXIT /b
Test with strings from comments:
Pass: set "string=Tim says:"There is a cat and a dog""
Fail: set "string=1>7.0% 9.0&1.04.%0!3"
Pass: set "string=Tim says:"There is a cat. And a dog""
In for loop i have checked condition, if condition is true i have set the count value and skipped some lines in file,
#echo off
SetLocal EnableDelayedExpansion
set /a count=0
set for_parameters="skip=!count! delims="
for /f %for_parameters% %%a in ('list.txt') do (
echo %%a
if %%a==Exception: (
set /a count+=2
)
)
Endlocal
Its shows delims=" was unexpected at this time. Error
Can anyone help me to fix this problem .....
What jeb said about delayed expansion and FOR options is true - you can't use it - but that has nothing to do with the problem in your code.
You are using delayed expansion before you use the expression in your FOR statement, so there is no problem there.
Your problem is that you are attempting to set SKIP=0. The SKIP value must be >0 to be valid. The fix is simple: don't include the SKIP option if you don't want to skip any lines :-)
#echo off
SetLocal EnableDelayedExpansion
set /a count=0
set "skip="
if !count! gtr 0 set "skip=skip=!count!"
set for_parameters="!skip! delims="
for /f %for_parameters% %%a in ('list.txt') do (
echo %%a
if %%a==Exception: (
set /a count+=2
)
)
Endlocal
Expanding on jeb's point: you cannot do the following
for /f !for_parameters! %%a in ...
because FOR has special parsing rules. Most commands parse their options after delayed expansion. But FOR and IF parse their options before FOR variable expansion and delayed expansion take place. Neither FOR variables nor delayed expansion can be used to specify FOR or IF options.
In the for-options you can only use percent expansion, but not delayed expansion.
If your skip value itself is calculated in a block, then you need to extract the for loop into a function
You can use the More command to skip lines.
#echo off
:: By Elektro H#cker
SetLocal EnableDelayedExpansion
set /a count=2
for /F %%a in ('Type "list.txt" ^| MORE +!COUNT!') do (echo %%a)
Pause&exit