How to survive "delayed variable expansion" in a Windows batch script - windows

This is my script:
#echo off
setlocal
for /f %%i in ('echo aaa/') do set REPO=%%i
if "%REPO%"=="" (
echo No input
) else (
echo %REPO:~-1%
echo %REPO:~0,-1%
if %REPO:~-1%==/ set REPO=%REPO:~0,-1%
echo %REPO%
)
endlocal
Please, observe:
c:\dev\shunra\GlobalLibrary\Server>c:\Utils\hgbackup.cmd
/
aaa
aaa/
c:\dev\shunra\GlobalLibrary\Server>
What is going on?
EDIT
Note, that I am assigning to REPO something that evaluates to "aaa", hence I expect it to print "aaa", not "aaa/". It drives me crazy.
EDIT2
Apparently, here is the culprit (from help on the set command):
Finally, support for delayed environment variable expansion has been
added. This support is always disabled by default, but may be
enabled/disabled via the /V command line switch to CMD.EXE. See CMD /?
Delayed environment variable expansion is useful for getting around
the limitations of the current expansion which happens when a line
of text is read, not when it is executed. The following example
demonstrates the problem with immediate variable expansion:
set VAR=before
if "%VAR%" == "before" (
set VAR=after
if "%VAR%" == "after" #echo If you see this, it worked
)
would never display the message, since the %VAR% in BOTH IF statements
is substituted when the first IF statement is read, since it logically
includes the body of the IF, which is a compound statement. So the
IF inside the compound statement is really comparing "before" with
"after" which will never be equal. Similarly, the following example
will not work as expected:
set LIST=
for %i in (*) do set LIST=%LIST% %i
echo %LIST%
in that it will NOT build up a list of files in the current directory,
but instead will just set the LIST variable to the last file found.
Again, this is because the %LIST% is expanded just once when the
FOR statement is read, and at that time the LIST variable is empty.
So the actual FOR loop we are executing is:
for %i in (*) do set LIST= %i
which just keeps setting LIST to the last file found.
Delayed environment variable expansion allows you to use a different
character (the exclamation mark) to expand environment variables at
execution time. If delayed variable expansion is enabled, the above
examples could be written as follows to work as intended:
set VAR=before
if "%VAR%" == "before" (
set VAR=after
if "!VAR!" == "after" #echo If you see this, it worked
)
set LIST=
for %i in (*) do set LIST=!LIST! %i
echo %LIST%
But I tried using the ! sign, still it does not work for me. I use get ! printed on the screen or the wrong result again.

As has been discussed in the comments, and in your edited question, you need delayed expansion.
Delayed expansion must be enabled before you can use it. Within a batch script you can use setlocal enableDelayedExpansion
#echo off
setlocal enableDelayedExpansion
for /f %%i in ('echo aaa/') do set REPO=%%i
if "%REPO%"=="" (
echo No input
) else (
echo %REPO:~-1%
echo %REPO:~0,-1%
if %REPO:~-1%==/ set REPO=%REPO:~0,-1%
echo !REPO!
)
endlocal
EDIT
The above fails if the IN() clause is changed such that REPO is undefined. For example: in (echo.)
It fails because the entire IF/ELSE construct must have valid syntax, even it the ELSE clause will not be executed.
If REPO is undefined, then
if %REPO:~-1%==/ set REPO=%REPO:~0,-1%
expands to
if ~-1REPO:~0,-1
which is invalid syntax.
The problem again is solved by using delayed expansion.
#echo off
setlocal enableDelayedExpansion
for /f %%i in ('echo.') do set REPO=%%i
if "%REPO%"=="" (
echo No input
) else (
echo %REPO:~-1%
echo %REPO:~0,-1%
if !REPO:~-1!==/ set REPO=%REPO:~0,-1%
echo !REPO!
)
endlocal

Note, that I am assigning to REPO something that evaluates to "aaa"
Actually, you're conditionally assigning something. Have you testing whether the then-part is actually executing (for example, echo If Entered).

This works for me (just an extract from my whole script)
choice /C 1234567H /M "Select an option or ctrl+C to cancel"
set _dpi=%ERRORLEVEL%
if "%_dpi%" == "8" call :helpme && goto menu
for /F "tokens=%_dpi%,*" %%1 in ("032 060 064 096 0C8 0FA 12C") do set _dpi=%%1
echo _dpi:%_dpi%:

Related

FOR will SET a variable in command line, but not inside a Batch file [duplicate]

Can someone give me an example of where a batch script would act differently with or without delayed expansion? Are there any situations where you would NOT want to use delayed expansion? Thanks.
Look at the following examples...
Example 1: The following code DOESN'T use delayed expansion, so the variables in the for loop are expanded only one time. This means that %Count% will always expand to 0 in each iteration of the loop, no matter what we do to it with the set command:
#echo off
set COUNT=0
for %%v in (1 2 3 4) do (
set /A COUNT=%COUNT% + 1
echo Count = %COUNT%
)
pause
So this script will output:
Count = 0
Count = 0
Count = 0
Count = 0
This is not how this loop is supposed to work.
Example 2: On the other hand, if we use delayed expansion, we have the following script, which will run as expected.
setlocal ENABLEDELAYEDEXPANSION
set COUNT=0
for %%v in (1 2 3 4) do (
set /A COUNT=!COUNT! + 1
echo Count = !COUNT!
)
pause
and, as expected, it will output:
Count = 1
Count = 2
Count = 3
Count = 4
When you use the ENABLEDELAYEDEXPANSION, and expand a variable using ! instead of %, the variable is re-expanded each time, and everything works as it's supposed to.
I wanted to add a great example on how "EnableDelayedExpansion" (EDE) can be useful outside of the ubiquitous FOR loop examples.
Here is a line of earthquake data that I wish to parse (I call it it 1line.txt)
ak_11574812 2015.04.29.193822 62.9525 -148.8849 1.0 9.5 1 49km S of Cantwell, Alaska
The problem I ran into was that last segment of this line does not always start at the same column number. So I needed to create a flexible SET command that will accurately pluck out the last segment of this line.
ECHO OFF
setlocal enableDelayedExpansion
set where=72
set /p line=<1line.txt
set locate=!line:~%where%,28!
echo %locate%
EDE allows me to place a variable (where) inside another variable (line). EDE will translate the variable bracketed by % first, then process the variable bracketed by ! and (in this case) push out the results into the "locate" variable.
Max's answer gives an example of where a batch script would act differently with or without delayed expansion.
For the sake of completeness, let's answer another part of the question and show a situation where you would NOT want to use delayed expansion when your data contain an exclamation mark ! (and show two ways of processing such data):
#ECHO OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
set "_auxFile=%temp%\%~n0.txt"
rem create multiline sample file
>"%_auxFile%" ( for /L %%G in (1,1,3) do echo line %%G is 100%% valid! Sure! Hurrah!)
rem create one-line sample file
>"%_auxFile%" echo this line is 100%% valid! Sure! Hurrah!
echo(
echo --- file content
type "%_auxFile%"
echo(
SETLOCAL EnableDelayedExpansion
echo --- enabled delayed expansion chokes down unescaped exclamation marks^^^! "^!"
for /F "usebackq delims=" %%G in ("%_auxFile%") do (
set "_auxLine=%%~G"
echo loop var=%%~G
echo _auxLine=!_auxLine!
)
ENDLOCAL
echo(
SETLOCAL DisableDelayedExpansion
echo --- toggled delayed expansion works although might be laborious!
for /F "usebackq delims=" %%G in ("%_auxFile%") do (
set "_auxLine=%%G"
echo loop var=%%G
SETLOCAL EnableDelayedExpansion
echo _auxLine=!_auxLine!
ENDLOCAL
)
ENDLOCAL
echo(
SETLOCAL DisableDelayedExpansion
echo --- keep delayed expansion DISABLED: use CALL command!
for /F "usebackq delims=" %%G in ("%_auxFile%") do (
set "_auxLine=%%G"
echo loop var=%%G
call :ProcessVar
)
ENDLOCAL
rem delete the sample file
del "%_auxFile%"
ENDLOCAL
goto :eof
:ProcessVar
echo _auxLine=%_auxLine%
echo WARNING: neither !_auxLine! nor %%G loop variable is available here!
goto :eof
Note that above script shows proper ways of escaping
% percent sign by %% doubling it (delayed expansion does not matter), and
! exclamation mark if delayed expansion is enabled:
"^!" if enclosed in a pair of double quotes, then use the cmd and batch-script general escape character ^ caret;
^^^! otherwise, use three ^ carets.
Output:
==> D:\bat\SO\10558316.bat
--- file content
this line is 100% valid! Sure! Hurrah!
--- enabled delayed expansion chokes down unescaped exclamation marks! "!"
loop var=this line is 100% valid Hurrah
_auxLine=this line is 100% valid Hurrah
--- toggled delayed expansion works although might be laborious!
loop var=this line is 100% valid! Sure! Hurrah!
_auxLine=this line is 100% valid! Sure! Hurrah!
--- keep delayed expansion DISABLED: use CALL command!
loop var=this line is 100% valid! Sure! Hurrah!
_auxLine=this line is 100% valid! Sure! Hurrah!
WARNING: !_auxLine! as well as %G loop variables are not available here!
==>
As pointed in the answer the main usage of the delayed expansion is the setting and accessing variables in brackets context.
Though it can be useful in another situations too.
Parametrizing substring and string substitution:
#echo off
setlocal enableDelayedExpansion
set "string=test string value"
set start=5
set get_next=6
echo #!string:~%start%,%get_next%!#
set "search_for=string"
set "replace_with=text"
echo #!string:%search_for%=%replace_with%!#
the output will be:
#string#
#test text value#
though this can be achieved with additional call this way is more performant
Using shift command within brackets parameterized argument access
#echo off
echo first attempt:
(
echo "%~1"
shift
echo "%~1"
)
::now the shift command will take effect
setlocal enableDelayedExpansion
echo second attempt:
(
set /a argument=1
call echo %%!argument!
shift
call echo %%!argument!
)
the output will be:
first attempt:
"first argument"
"first argument"
second attempt:
"second argument"
"third argument"
As you can see parameterized argument access can be done only with delayed expansion.
Using for tokens (or function arguments) for parameterization
One more approach for mixing !s and %s this could be useful for nested loops:
#echo off
setlocal enabledelayedexpansion
set begin=2
set end=2
set string=12345
for /f "tokens=1,2" %%A in ("!begin! !end!") do set "string2=!string:~%%A,%%B!"
echo !string2!
endlocal
as you can see now the for command tokens are used as parameters.
Several answers here answer the "How to use delayed expansion?" question or what happen if you don't use delayed expansion. However, the second question is "Are there any situations where you would NOT want to use delayed expansion?" and a couple answers take this question as "how to avoid the problems caused by using delayed expansion?"
My answer answers the question as I understand it: "In which situations is better to NOT use delayed expansion (instead of use it)?"
If you want to exchange the contents of two variables, the simplest way to perform this is using the %standard% variable expansion:
set "var1=%var2%" & set "var2=%var1%"
The way that the %standard% expansion works makes possible to achieve this replacemenmt without using any auxiliary variable. As far as I know, the Batch-file "programming language" is the only one that allows to perform this exchange in this way, that is, making good use of a language "feature" (not via a specialized "exchange" instruction/statement).

Cannot set argument value to a variable resulting empty in Windows batch script

I'm looping through all command-line arguments using SHIFT. I'm getting result of ECHO is off.. It is likely printing the empty variable.
:argLoopStart
SET paramName=
SET arg=%1
IF -%arg%-==-- GOTO argLoopEnd
IF %arg:~0,2%==-- (
SET paramName=%arg%
ECHO %arg%
ECHO %paramName%
)
SHIFT
GOTO argLoopStart
:argLoopEnd
By running the command fake-command --dbs=mydbname, I got this:
--dbs
ECHO is off.
According to the code above, ECHO %arg% prints --dbs and ECHO %paramName% prints ECHO is off. The line of SET paramName=%arg% is not working as I expected. %parameName% should print --dbs as well. However, it seems printing an empty variable.
You need to enable delayed expansion with SETLOCAL EnableDelayedExpansion at the top of your script:
Delayed Expansion will cause variables to be expanded at execution
time rather than at parse time, this option is turned on with the
SETLOCAL command. When delayed expansion is in effect variables may be
referenced using !variable_name! (in addition to the normal
%variable_name% )
#echo off
setlocal enabledelayedexpansion
:argLoopStart
set paramName=
set arg=%1
if -!arg!-==-- goto argLoopEnd
if %arg:~0,2%==-- (
set paramName=!arg!
echo !arg!
echo !paramName!
)
shift
goto argLoopStart
:argLoopEnd

SET command expantion substrings

When using the SET command in command prompt, what does % and ! mean, for example
set VAR=before
if "%VAR%" == "before" (
set VAR=after
if "!VAR!" == "after" #echo If you see this, it worked)
set LIST=
for %i in (*) do set LIST=!LIST! %i
echo %LIST%
Notice how there's %i and !VAR! what does this mean, %i cant be a variable right? as variables are written out like %variable%.
Any ideas what these are?. Also is the (*) just a literal?
Regards, S
In normal cases you can access variable value by enclosing its name with % :
set var=a
echo %var%
As when there is composition of commands with & or a few commands are put in brackets the set will take effect after all of them are executed.Then you need delayed expansion to access your variable and to enclose it with !
setlocal enableDelayedExpansion
if exist c:\ (
set var2=a
echo !var2!
)
Take a more close look at SET command.
in for loops you have a special variables that works only in context of for command - a tokens that change its values on each iteration of the loop:
for /f "delims=" %%# in ('set') do echo %%#
here are the letters that you can use as tokens.And be careful - you need to use double % in a script and a single in command prompt.
There's one more type of variables that has a % only at the beginning - arguments passed to the script (or a subroutine) - accessible with numbers (or rather digits) - from %0 to %9 where %0
is the name of the script itself.

Random variable not changing in "for" loop in windows batch file

I'm trying to print out a Random number multiple times but in the for loop I use, it doesn't reset the variable. Here's my code.
#echo off
for %%i in (*.txt) do (
set checker=%Random%
echo %checker%
echo %%i% >> backupF
)
echo Complete
There are 5 text files and so I want it to print 5 different random numbers but it just prints the same random number 5 times. Any help would be greatly appreciated. Thanks!
I'm not sure how you've been able to have it print even one random number. In your case, %checker% should evaluate to an empty string, unless you run your script more than once from the same cmd session.
Basically, the reason your script doesn't work as intended is because the variables in the loop body are parsed and evaluated before the loop executes. When the body executes, the vars have already been evaluated and the same values are used in all iterations.
What you need, therefore, is a delayed evaluation, otherwise called delayed expansion. You need first to enable it, then use a special syntax for it.
Here's your script modified so as to use the delayed expansion:
#echo off
setlocal EnableDelayedExpansion
for %%i in (*.txt) do (
set checker=!Random!
echo !checker!
echo %%i% >> backupF
)
endlocal
echo Complete
As you can see, setlocal EnableDelayedExpansion enables special processing for the delayed expansion syntax, which is !s around the variable names instead of %s.
You can still use immediate expansion (using %) where it can work correctly (basically, outside the bracketed command blocks).
Try by calling a method.
#echo off
pause
for %%i in (*.txt) do (
call :makeRandom %%i
)
echo Complete
pause
:makeRandom
set /a y = %random%
echo %y%
echo %~1 >> backupF
on my system I have to write
set checker=Random
instead of
set checker=!Random!

Why does this batch variable never change even when set?

#echo off
SET first=0
FOR %%N IN (hello bye) DO (
SET first=1
echo %first%
echo %%N
)
It seems that the variable "first" is always 0. Why?
With batch files, variables are expanded when their command is read - so that would be as soon as the for executes. At that point, it no longer says echo %first%, it literally says echo 0, because that was the value at the point of expansion.
To get around that, you need to use delayed expansion by surrounding your variable name with ! instead of % - so that would be echo !first!. This may require you to start cmd.exe with the /V parameter, or use setlocal enabledelayedexpansion in the beginning of your batch file (just after echo off).
If you type set /?, you'll see a much more detailed explanation of this at the end of the output.

Resources