BATCH: How to properly use a %% variable within a for loop - batch-file

This is the very first time i tried batch scripting so please bear with me.
I just wanted to read each line of my hosts file, and replace the line if it contains/matches a substring. I've seen a lot of answered questions about substrings here but I just can't make it work by using the provided solutions.
I have this code:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
set "hostspath=%SystemRoot%\System32\drivers\etc\hosts"
set "hostsbackuppath=c:\hosts"
>"%hostsbackuppath%.new" (
rem Parse the hosts file, skipping the already present hosts from our list.
rem Blank lines are preserved using findstr trick.
for /f "delims=: tokens=1*" %%a in ('%SystemRoot%\System32\findstr.exe /n /r /c:".*" "%hostspath%"') do (
set str1=%%b
if not x!str1:mydomainname=!==x!str1! (
rem Match found, replace this line.
echo "match!"
set matched=false
)
// Didn't match, do not replace
if not "!matched!"=="true" echo.%%b
)
)
I was trying out this solution to check for substring match among other else: Batch file: Find if substring is in string (not in a file)
Can someone help me? Thanks

SETLOCAL ENABLEDELAYEDEXPANSION
set "matched=true"
>"%hostsbackuppath%.new" (
for /f "delims=: tokens=1*" %%a in ('%SystemRoot%\System32\findstr.exe /n /r /c:".*" "%hostspath%"') do (
set "str1=%%b"
if not "!str1:mydomainname=!"=="!str1!" (
rem Match found, replace this line.
echo "match at %%b in line %%a"
set matched=false
)
// Didn't match, do not replace
if not "!matched!"=="true" echo.%%b
)
)
Hooley-dooley! Someone needs to learn to name variables appropriately.
First, you need to use setlocal enabledelayedexpansion - please see a thousand-and-one SO articles about delayed expansion.
Since str1 is varied within the loop, you need to use setlocal enabledelayedexpansion and !var! to access the varying value of var as %var% is the value at the time the for was encountered.
The syntax SET "var=value" (where value may be empty) is used to ensure that any stray trailing spaces are NOT included in the value assigned. set /a can safely be used "quoteless".
FOr the same reason, quoting each side of a comparison is preferred as it makes a single token of a string containing separators like spaces.
Then you have a comment "match found" after which you set matched to false ?? Therefore you need to initialise match (to true)
Now quite what you want to do is obscure. On re-reading, you probably want to set "matched=true" as the first line within the loop, not outside as I have it, so that the value is re-set to true for each line found and then set to false if a match is found.
All this negative logic is insane. I need a strong cup of coffee.

Related

batch for /f to iterate over all available drives to seach folder [duplicate]

This question already has an answer here:
Variables are not behaving as expected
(1 answer)
Closed 3 years ago.
I am writing simple script to check if somefolder exists in available drives.
however %%G always shows empty string when concat with ":\somefolder"
if I just echo %%G it shows all the available drives.
I am new to batch scripting , not sure what am I missing here.
Thanks in advance.
#echo off
::parse the VER command
FOR /F "tokens=*" %%G IN ('wmic logicaldisk get caption') DO (
IF [%%G]==[] (
echo "empty string"
) ELSE (
SET var="%%G\somefolderpath"
IF EXIST %var% (
echo %var% found
) ELSE (
echo %var% not found
)
)
)
It's normal practice to have a setlocal command directly after the initial #echo off. This discards any changes made to the environment when the batch ends, so variables established by one batch file do not affect any further batches that may be run in the same session.
Within a block statement (a parenthesised series of statements), the entire block is parsed and then executed. Any %var% within the block will be replaced by that variable's value at the time the block is parsed - before the block is executed - the same thing applies to a FOR ... DO (block).
Hence, IF (something) else (somethingelse) will be executed using the values of %variables% at the time the IF is encountered.
Two common ways to overcome this are 1) to use setlocal enabledelayedexpansion (outside of the block or more usually after the initial #echo off) and use !var! in place of %var% to access the changed value of var or 2) to call a subroutine to perform further processing using the changed values.
In your case, setting var is not required - if exist "%%G\somefolderpath" would suffice.
Note also that assigning quoted strings to variables make the variables hard to combine logically. Inserting quotes as needed is far simpler. The syntax SET "var=value" (where value may be empty) is used to ensure that any stray trailing spaces are NOT included in the value assigned.
Here is another way to do it. The WMIC command is deprecated.
https://itknowledgeexchange.techtarget.com/powershell/wmic-deprecated/
As others have mentioned, setting and using a variable inside a FOR loop requires ENABLEDELAYED EXPANSION or CALL.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "delims=" %%G IN ('powershell -NoLogo -NoProfile -Command ^
"(Get-CIMInstance -ClassName Win32_LogicalDisk).DeviceID"') DO (
SET "VAR=%%G\somefolderpath"
IF EXIST "!VAR!" (
echo "!VAR!" found
) ELSE (
echo "!VAR!" not found
)
)
You can't set a variable and read it's contents in the loop unless you use delayed expansion or do a call echo.
Also, note that [%%G] will never = [] only a very specific scenario where you impropperly set up your For loop tokens so rhat they have ranges that overlap can create empty variolables.
In your loop it is not possible as you just select alll token, perfectly valid. For loops do not match lines that have no characters other than those that are considered delims (whitespace by default).
In any case,we can safely drop that portion.
Lets show some examples of how you might do this instead
To check directly:
#echo off
Set "_Dir=Some\Folder\Path"
FOR /F "tokens=*" %%G IN ('
wmic logicaldisk get caption
') DO (
IF EXIST "%%G\%_Dir%" (
ECHO."%%G\%_Dir%" Exists!
) ELSE (
ECHO."%%G\%_Dir%" Does Not Exist!
)
)
Set that varable and use it in the loop:
#(
SetLocal EnableDelayedExpansion
echo off
)
Set "_Dir=Some\Folder\Path"
FOR /F "tokens=*" %%G IN ('
wmic logicaldisk get caption
') DO (
SET "_Path=%%G\%_Dir%"
IF EXIST "!_Path!" (
ECHO."!_Path!" Exists!
) ELSE (
ECHO."!_Path!" Does Not Exist!
)
)
Notice that in order to use rhe variable in the loop we need to use ! instead of %

Batch File - Find two lines then copy everything between those lines

I need to parse a text file.
I want to find the firstline in the text file
: the first line to find
set firstLine=------------------------------------------------------------------------------------------------------------------
and find the last line
:: the last line to find
set lastLine=*******************************************************************************************************************
Then I need to export to a new file everything between those two line.
echo >> M:\TESTING\Output.txt
I'm a beginner with this and I've searched for days, but am not finding how to do this.
I looked at for loops and if statements, but I'm still puzzled.
for /f "tokens=1 delims= " %%f in (M:\TESTING\*.txt) do (
:: sets then the line variable to the line just read
set line=%%f
:: the first line to find
set firstLine=------------------------------------------------------------------------------------------------------------------
:: the last line to find
set lastLine=*******************************************************************************************************************
Then if %line% = %fistLine% start the export.....
Any direction will be appreciated. thanks.
#DennisvanGils' approach is a good start and will do well in many cases.
However, it will not produce an exact copy of the text file content between the given lines:
leading whitespaces (SPACE and TAB) will be removed (due to tokens=* option),
lines starting with ; will be skipped (due to the default option eol=; of for /F), and
empty lines will be skipped as well (as for /F always skips such).
To get an exact copy of the text file portion, you could use the following code snippet:
#echo off
set "INFILE=M:\TESTING\input.txt"
set "OUTFILE=M:\TESTING\output.txt"
set "FIRSTLINE=------------------------------------------------------------------------------------------------------------------"
set "LASTLINE=*******************************************************************************************************************"
setlocal EnableExtensions DisableDelayedExpansion
set "FLAG="
> "%OUTFILE%" (
for /F "delims=" %%L in ('findstr /N "^" "%INFILE%"') do (
set "LINE=%%L"
setlocal EnableDelayedExpansion
set "LINE=!LINE:*:=!"
if "!LINE!"=="%FIRSTLINE%" (
endlocal
set "FLAG=TRUE"
) else if "!LINE!"=="%LASTLINE%" (
endlocal
goto :CONTINUE
) else if defined FLAG (
echo(!LINE!
endlocal
) else (
endlocal
)
)
)
:CONTINUE
endlocal
Core function here is findstr, which is configured so that every line in the file is returned, prefixed with a line number and a colon :. Its output is then parsed by a for /F loop. Because of the prefix, no line appears to be empty and therefore every one is iterated by the loop. In the body of the loop, the prefix is removed by the set "LINE=!LINE:*:=!" command for each line.
The variable FLAG is used to decide whether or not the current line is to be output; if it is defined, that is, a value is assigned, the command echo !LINE! is executed; otherwise it is not. FLAG is set if the current line matches the string in %FIRSTLINE%; if the line matches the string in %LASTLINE%, a goto command is executed which breaks the loop. This means also that only the first block between %FIRSTLINE% and %LASTLINE% matches is output.
If there might occur multiple %FIRSTLINE% and %LASTLINE% matches within the text file and you want to output every block, replace the goto command line by set "FLAG=".
Note that this approach does not check whether %FIRSTLINE% occurs before %LASTLINE%, nor does it even check for existence of %LASTLINE% (all remaining lines to the end of file are output in case). If all this is important, the logic need to be improved and even a second loop will be required most likely.
What you should do in this case is use a variable like a boolean to know if you encountered the startline and endline yet, and to know if you have to output the lines.
Also, you should use setlocal ENABLEDELAYEDEXPANSION with ! instead of % so you can change variables in loops and ifs (for more information about that, see this. The usage of () after if is not needed in this case, since the if is on one line, but they make things easier to read in my opinion. If you want to output the start and endline too, switch the checks for the start and endlines.
#echo off & setlocal ENABLEDELAYEDEXPANSION
set start=0
:: the first line to find
set firstLine=------------------------------------------------------------------------------------------------------------------
:: the last line to find
set lastLine=*******************************************************************************************************************
for /F "tokens=*" %%A in (TEST.txt) do (
:: sets then the line variable to the line just read
set line=%%A
if "!line!"=="!lastLine!" (set start=0)
if !start! equ 1 (echo !line!>>TESTOUTPUT.txt)
if "!line!"=="!firstLine!" (set start=1)
)
This should do what you want. Note that when you encounter a startline a second time it starts reading again.

Windows shell: get parameter value from file

I have a file C:\parameters.txt that contains different parameters, for example:
env_user=username123
env_pw=password123
env_url=example.com
Now I created a .cmd file that needs to get these values and put them in a variable, for example:
SET var_user=<Here I need 'username123'>
SET var_pw=<Here I need 'password123'>
SET var_url=<Here I need 'example.com'>
How do I write this in my cmd script to get the correct values for my variables?
You need to set a delimiter for = character so that words before/after = will be separated. Besides that, you need an array to set each of the parameters. You can do it like this:
#echo off
setlocal enabledelayedexpansion
set increment=0
for /f "tokens=1* delims==" %%a in (C:\parameters.txt) do (
set parameters_array[!increment!]=%%b
set /a increment+=1
)
echo %parameters_array[0]%
echo %parameters_array[1]%
echo %parameters_array[2]%
pause >nul
Keep in mind, array always starts from 0. You could change to set increment=1 if you prefer the array starts from 1.
Just a slight alternative to dark fang's solution, since your parameters.txt file's contents are already in the format of variable=value, you could
#echo off
setlocal
for /f "usebackq delims=" %%I in ("c:\parameters.txt") do set "%%I"
rem // display env_* variables
set env_
pause
The usebackq option allows you to quote the file name, which might be needed if you ever move c:\parameters.txt to a location containing spaces, ampersands, or other tricksy characters. It's a good habit to follow when reading the contents of text files with for /f.
Also, it's better not to use delayed expansion if you don't need it, as delayed expansion can sometimes corrupt values containing exclamation marks -- a situation that is reasonably possible when dealing with passwords.
I've found the solution thanks to different inputs.
#echo off
For /F "tokens=1* delims==" %%A IN (C:\parameters.txt) DO (
IF "%%A"=="env_user" set var_user=%%B
IF "%%A"=="env_pw" set var_pw=%%B
IF "%%A"=="env_url" set var_url=%%B
)
This will set the correct variables (not local) once the specific code name (before the = in parameters.txt) has been found.

Batch File: How to read only sections in a INI file

I am very much a novice at Batch Scripting and i'm trying to write a simple script to READ from an INI file based on the Parameters that is passed when the batch file is called.
This is an example for what the INI file would look like:
[SETTING1]
Value1=Key1
Value2=Key2
Value3=Key3
[SETTING2]
Value1=Key1
Value2=Key2
Value3=Key3
[SETTING3]
Value1=Key1
Value2=Key2
Value3=Key3
I am running into a problem when it comes to reading ONLY the section that is called. It will read from any section that matches the "Value" and "Key" and i don't know how to limit it to only read the section with the settings.
The file is being called with this Parameter: run.bat run.ini setting2. My code below is what I have so far and I feel as if i have officially hit a wall. Can anyone help with this? Any help would greatly be appreciated.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET INIFile="%~f1"
for /f "usebackq delims=" %%a in (!INIFile!) do (
if %%a==[%2] (
SET yesMatch=%%a
for /f "tokens=1,2 delims==" %%a in (!yesMatch!) do (
if %%a==Value1 set Key1=%%b
if %%a==Value2 set Key2=%%b
if %%a==Value3 set Key3=%%b
)
ECHO !yesMatch!
ECHO !Key1!
ECHO !Key2!
ECHO !Key3!
pause
)
)
pause
exit /b
#ECHO OFF
SETLOCAL
SET INIFile="%~f1"
SET "FLAG="
for /f "usebackq tokens=1,*eol=|delims==" %%a in (%INIFile%) do (
IF "%%b"=="" (
REM No "=" so section
IF /i "%%a"=="[%2]" (SET flag=Y) ELSE (SET "flag=")
) ELSE IF defined flag (
REM data line - only if FLAG is defined
REM set values defined
SET "%%a=%%b"
REM pick particular values
if /i "%%a"=="Value1" set "Key1=%%b"
if /i "%%a"=="Value2" set "Key2=%%b"
if /i "%%a"=="Value3" set "Key3=%%b"
)
)
SET key
SET val
GOTO :EOF
Here's a way to get your values.
The data in the file is either [section] or name=value so settling delims to = will assign either section-only to %%a or name to %%a and value to %%b.
The flag is only set (defined) after its appropriate section is encountered, and cleared on the next section. Only of it is defined will the assignment take place.
The advantage of the simple set %%a=%%b is that it results in setting whatever values are defined in the section - no changes to the code need to take place if new values are added. Your original version has the advantage of picking particular values and setting only those. You pays your money, you takes your choice.
Note that the /i switch on an if statement makes the comparison case-insensitive.
Nota also the use of set "value=string" which ensures that trailing spaces on a line are not included in the value assigned.
edit : By default, the end of line character is ; so any line starting ; is ignored by for/f. The consequence is that the values for the ;-commented-out section would override the values set for the previous section.
Setting eol to | should cure the problem. It really doesn't matter what eol is set to; it's exactly one character which may not appear anywhere in the INI-file (else that line would appear truncated.)
It is possible, if necessary, to set eol to control-Z but selecting an unused character is easier...
Consequently, a one-line change - the eol parameter is included in the for /f options.

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.

Resources