10} How can I change the environment variable values within a FOR loop?
Unless
enabledelayedexpansion
is on, by default a for loop expands
its environment variables before the execution of the loop. Unlike
in programming with compilers the value of a variable that is
changed within a loop will not be set to its new value until the
loop is exited. (The same goes for any environment variable
expression on multiple lines within parentheses.)
For more information type SET /?
Also see items
#158
and
#38.
(Similar issues go for the statements within
IF conditions.)
Consider the following example of a typical, but partly
faulty attempt to count the number of lines in
a file
@echo off & setlocal enableextensions
:: Make a test file
for %%f in ("myfile.txt") do if exist %%f del %%f
for %%f in (first second third fourth) do (
echo This is the %%f line>>"myfile.txt")
::
set lineCount=0
::
for /f "delims=" %%r in ('type "myfile.txt"') do (
set /a lineCount+=1
echo
%lineCount
% %%r
)
echo The number of lines is %lineCount%
::
:: Clean up
for %%f in ("myfile.txt") do if exist %%f del %%f
endlocal & goto :EOF
The output will obviously not be quite what one would want:
D:\TEST>cmdfaq
0 This is the first line
0 This is the second line
0 This is the third line
0 This is the fourth line
The number of lines is 4
There are ways to amend. One is to put the
operations into a subroutine outside the loop as follows
@echo off & setlocal enableextensions
:: Make a test file
for %%f in ("myfile.txt") do if exist %%f del %%f
for %%f in (first second third fourth) do (
echo This is the %%f line>>"myfile.txt")
::
set lineCount=0
::
for /f "delims=" %%r in ('type "myfile.txt"') do (
call :WriteOneLine %%r
)
echo The number of lines is %lineCount%
::
:: Clean up
for %%f in ("myfile.txt") do if exist %%f del %%f
endlocal & goto :EOF
::
:: =============================================
:WriteOneLine
set /a lineCount+=1
echo
%lineCount
% %*
goto :EOF
The output will be
D:\TEST>cmdfaq
1 This is the first line
2 This is the second line
3 This is the third line
4 This is the fourth line
The number of lines is 4
The other option is to enable "delayed expansion"
@echo off & setlocal enableextensions enabledelayedexpansion
:: Make a test file
for %%f in ("myfile.txt") do if exist %%f del %%f
for %%f in (first second third fourth) do (
echo This is the %%f line>>"myfile.txt")
::
set lineCount=0
::
for /f "delims=" %%r in ('type "myfile.txt"') do (
set /a lineCount+=1
echo !lineCount! %%r
)
echo The number of lines is %lineCount%
::
:: Clean up
for %%f in ("myfile.txt") do if exist %%f del %%f
endlocal & goto :EOF
The value of an environment variable in delayed expansion in the
above is to be indicated by !variable! instead of the more familiar
%variable%. The output will be
D:\TEST>cmdfaq
1 This is the first line
2 This is the second line
3 This is the third line
4 This is the fourth line
The number of lines is 4
There is an interesting and sometimes useful twist using call.
Consider (note, no delayed expansion needed)
@echo off & setlocal enableextensions disabledelayedexpansion
:: Make a test file
for %%f in ("myfile.txt") do if exist %%f del %%f
for %%f in (first second third fourth) do (
echo This is the %%f line>>"myfile.txt")
::
set lineCount=0
::
for /f "delims=" %%r in ('type "myfile.txt"') do (
set /a lineCount+=1
call set line[%%lineCount%%]=%%r
)
echo The number of lines is %lineCount%
for /L %%i in (1,1,%lineCount%) do call echo %%i %%line[%%i]%%
::
:: Clean up
for %%f in ("myfile.txt") do if exist %%f del %%f
endlocal & goto :EOF
The output will be
C:\_D\TEST>cmdfaq
The number of lines is 4
1 This is the first line
2 This is the second line
3 This is the third line
4 This is the fourth line
Now,
which one should one choose? Depends on the particulars. The
advantage with the
disabledelayedexpansion
methods is that they are more tolerant of the poison characters
&()[]{}^=;!'+,`~
Note, incidentally, that if there are empty lines in myfile.txt, those
empty lines are ignored by the FOR /F loop. Also note the disappearance
of the exclamation mark !. For example
@echo off & setlocal enableextensions disabledelayedexpansion
:: Make a test file
for %%f in ("myfile.txt") do if exist %%f del %%f
for %%f in (first "second &()[]{}^=;
!'+,`~" third fourth) do (
echo This is the %%f line>>"myfile.txt")
echo.>>"myfile.txt"
for %%f in (sixth seventh) do (
echo This is the %%f line>>"myfile.txt")
::
type "myfile.txt"
pause
::
setlocal enableextensions enabledelayedexpansion
set lineCount=0
::
for /f "delims=" %%r in ('type "myfile.txt"') do (
set /a lineCount+=1
echo !lineCount! %%r
)
echo The number of lines is %lineCount%
::
:: Clean up
for %%f in ("myfile.txt") do if exist %%f del %%f
endlocal & goto :EOF
C:\_D\TEST>cmdfaq
This is the first line
This is the "second &()[]{}^=;!'+,`~" line
This is the third line
This is the fourth line
This is the sixth line
This is the seventh line
Press any key to continue . . .
1 This is the first line
2 This is the "second &()[]{}=;'+,`~" line
3 This is the third line
4 This is the fourth line
5 This is the sixth line
6 This is the seventh line
The number of lines is 6
Consider the following task. I needed to output the sequence
Item001.pdf
Item002.pdf
Item003.pdf
Item004.pdf
Item005.pdf
Item006.pdf
Item007.pdf
:
Item205.pdf
Item206.pdf
The solution is e.g.
@echo off & setlocal enableextensions enabledelayedexpansion
for /L %%i in (1,1,206) do (
set iii=000%%i
set iii=!iii:~-3!
echo Item!iii!.pdf)
endlocal & goto :EOF
Another example of a for loop, using this time the subroutine method
@echo off
::
:: A true life example of an XP script used in a repetitive task.
:: It well demonstrates how change the values of environment
:: variable WITHIN a FOR loop.
::
echo.+-------------------------------------------------------------+
echo ^| Convert Timo's IBM PC character text mail files into LATIN1 ^|
echo ^| By Prof. Timo Salmi, Last modified Tue 25-November-2003 ^|
echo +-------------------------------------------------------------+
echo.
::
:: Ask for confirmation before we go on, case insensitive
set /p ask_=Convert, are you sure [y/N]?
if /i not "%ask_%"=="y" if /i not "%ask_%"=="yes" goto :EOF
:: It is prudent practice to release the query variable immediately
:: (even if in this particular script it happens to be the only question)
set ask_=
::
:: Set the path for the origin and target files
:: The names of the files is meant to stay the same
set pathin_=C:\_D\MAIL
set pathout_=C:\Documents and Settings\ts\mail
::
:: Go trough all the relevant files
:: and call the pseudo-subroutine for each one
for %%f in (BATCH.MAI
NEWS0001.MAI
SCIWK001.MAI
TIMO.MAI
TIMO0001.MAI
TIMO0002.MAI
TIMO0003.MAI
TIMO0004.MAI
WORK0001.MAI
WORK0002.MAI) do call :_subru %%f
::
:: Finally clean up (instead of playing around with setlocal and endlocal)
for %%v in (pathin_ pathout_ filein_ fileout_) do set %%v=
:: Exit the script
goto :EOF
::
:: *** The pseudo-subroutine for each loop ***
::
:_subru
set filein_="%pathin_%\%1"
set fileout_="%pathout_%\%1"
::
:: Do the actual task
:: note that this way we can easily have redirection within the loop
:: IBM2LAT1 is an external (Turbo Pascal 7.01) filter program
:: But that is beside the point since we could have any task here
:: (Performs the same task as item #28)
IBM2LAT1 < %filein_% > %fileout_%
::
:: Show the relevant files at each loop, date/time size fullname
for /f "tokens=1 delims=" %%f in (%filein_%) do echo %%~tf %%~zf %%~ff
for /f "tokens=1 delims=" %%f in (%fileout_%) do echo %%~tf %%~zf %%~ff
echo.
goto :EOF
The next example is not about changing the values of the variables
within a
FOR loop, but it usefully
demonstrates the tricks and syntax within a
FOR
loop. It also demonstrates using re-usable script function modules,
more fully covered in
Item #18. It is the
familiar, common task of getting the current date and time components
into environment variables. At the same time using a Visual Basic
Script (VBScript) aided command line script for a portable solution is
covered by the example.
@echo off & setlocal enableextensions
:: Assign a value for the temporary folder variable temp_
call :AssignTemp temp_
::
:: Get the current date and time elements into environment variables
call :GetDateTime
::
endlocal & goto :EOF
::
:: Assign a temporary folder, use the default if not defined
:AssignTemp
setlocal
set return_=%temp%
if defined mytemp if exist "%mytemp%\" set return_=%mytemp%
endlocal & set "%1=%return_%" & goto :EOF
::
:GetDateTime
set vbs_=%temp_%\getdatetimenow.vbs
set skip=
findstr "'%skip%VBS1" "%~f0" > "%vbs_%"
for /f "tokens=1-6" %%a in ('
cscript //nologo "%vbs_%"') do (
set DD=%%a
set MM=%%b
set YYYY=%%c
set HH=%%d
set MI=%%e
set SS=%%f)
for %%f in ("%vbs_%") do if exist "%%~f" del "%%~f"
:: Show the current time for debugging or demonstration
setlocal enableextensions
enabledelayedexpansion
for %%a in (DD MM YYYY HH MI SS) do (echo %%a=!%%a!)
endlocal
goto :EOF
'
'........................................................................
'A Visual Basic Script to get the current date and time
'
DateTimeNow=Now
'VBS1
'
Wscript.StdOut.Write Right(0 & Day(DateTimeNow), 2) & " "
'VBS1
Wscript.StdOut.Write Right(0 & Month(DateTimeNow), 2) & " "
'VBS1
Wscript.StdOut.Write Year(DateTimeNow) & " "
'VBS1
'
Wscript.StdOut.Write Right(0 & DatePart("h", DateTimeNow), 2) & " "
'VBS1
Wscript.StdOut.Write Right(0 & DatePart("n", DateTimeNow), 2) & " "
'VBS1
Wscript.StdOut.WriteLine Right(0 & DatePart("s", DateTimeNow), 2) & " "
'VBS1
The output could be e.g.
C:\_D\TEST>cmdfaq
DD=17
MM=03
YYYY=2012
HH=20
MI=55
SS=21