Variable not being correctly evaluated when EnableDelayedExpansion is set - batch-file

I have a variable that apparently is not being correctly evaluated in a condition.
The scenario is the following: I've made a batch file that is set with SetLocal EnableExtensions, then it has a main routine that is set with SetLocal EnableExtensions EnableDelayedExpansion. The main routine calls a subroutine from within a nested if statement passing two parameters, and it gets a variable as a result. The subroutine being called is set with SetLocal EnableExtensions.
Problem is that when evaluating the variable that returns the subroutine, apparently the variable is not being correctly evaluated: a variable that one line before is echoed and confirmed to be equal to zero makes an if !variable! EQU 0 statement inexplicably return FALSE.
More explanations and details after the code below.
Sample code (Proof of Concept)
#echo off
SetLocal EnableExtensions
rem Initialize variables
rem -------------------------------------------------------------------------------
set ErrorStatus=0
set PreConditionOne=1
set PreConditionTwo=1
set ValueOne=50
set ValueTwo=50
rem - Main Routine
rem -------------------------------------------------------------------------------
SetLocal EnableExtensions EnableDelayedExpansion
rem Setting initial values for flag variables, value can be: 0=NO, 1=YES, 2=Not Checked.
set PreCheckFail=2
set ParamsNotEqual=2
rem Prechecks: Preconditions must be met in order to check variables.
echo Checking PreConditions.
if %PreConditionOne%==1 (
echo OK: First PreCondition is met.
echo Checking Second PreCondition.
if %PreConditionTwo%==1 (
echo OK: Second PreCondition is met.
echo Calling subroutine to check if values match.
call :Subroutine %ValueOne% %ValueTwo%
echo Back from subroutine.
echo InLoop: ErrorLevel: %ErrorLevel%, ErrorStatus: %ErrorStatus%, ParamsNotEqual: %ParamsNotEqual%
echo InLoopDelayed: ErrorLevel: !ErrorLevel!, ErrorStatus: !ErrorStatus!, ParamsNotEqual: !ParamsNotEqual!
rem This condition fails to correctly check ParamsNotEqual, it works fine with ErrorLevel.
if !ParamsNotEqual! EQU 0 (
echo OK Parameteres verified successfully.
set PreCheckFail=0
) else (
echo ERROR Parameter verification failed.
set PreCheckFail=1
)
) else (
echo ERROR: Second PreCondition is not met.
set PreCheckFail=1
)
) else (
echo ERROR: First PreCondition is not met.
set PreCheckFail=1
)
echo OutLoop: ErrorLevel: %ErrorLevel%, ErrorStatus: %ErrorStatus%, ParamsNotEqual: %ParamsNotEqual%
echo PreCheckFail: %PreCheckFail%
if %PreCheckFail% EQU 0 (echo "PreCheckFail IS ZERO") else (echo "PreCheckFail NOT ZERO")
endlocal
goto :Finish
:Subroutine
rem Subroutine
rem -------------------------------------------------------------------------------
SetLocal EnableExtensions
set ParamOne=%1
set ParamTwo=%2
echo This is the subroutine.
echo The first parameter passed is: %ParamOne%
echo The second parameter passed is: %ParamTwo%
if "%ParamOne%"=="%ParamTwo%" (
echo OK^! Both variables are equal.
set ParamsNotEqual=0
set ErrorStatus=0
) else (
echo ERROR^! Both variables are different.
set ParamsNotEqual=1
set ErrorStatus=1
)
echo EndSub: ErrorLevel: %ErrorLevel%, ErrorStatus: %ErrorStatus%, ParamsNotEqual: %ParamsNotEqual%
endlocal & set ParamsNotEqual=%ParamsNotEqual% & exit /b %ErrorStatus%
:Finish
endlocal
If you run this script, you will see that the subroutine correctly returns the ParamsNotEqual variable as 0, then the echo lines correctly show the values for both the normally parsed variables and the delayed ones:
InLoop: ErrorLevel: 0, ErrorStatus: 0, ParamsNotEqual: 2
InLoopDelayed: ErrorLevel: 0, ErrorStatus: 0, ParamsNotEqual: 0
But when the condition if !ParamsNotEqual! EQU 0 is evaluated, it just fails. Setting #echo on does not clarify anything, as the delayed variables are expanded on execution time so I cannot see what is really being evaluated there.
Test I've done so far
Things that have no effect so it keeps failing:
Set SetLocal EnableExtensions EnableDelayedExpansion for the whole script
Set SetLocal EnableExtensions EnableDelayedExpansion for the subroutine too.
Not setting previously the ParamsNotEqualvariable.
Evaluating the condition surrounding it with quotes, or brackets, or using == instead of EQU.
Evaluating if !ParamsNotEqual! EQU 2 (which is the initial value of the variable) also returns FALSE
Possible workarounds:
Using !ErrorLevel! instead of !ParamsNotEqual! works fine. This looks logic, as ErrorLevel is a system variable, not a user-defined one.
Not setting SetLocal for the subroutine, hence having the subroutine variables not isolated from the rest of the script, makes the main routine automagically work fine as it is. This has no sense to me but makes me think that this error may be due to some kind of restriction either when calling a subroutine from within a nested if when EnableDelayedExpansion is set, or with using nested SetLocal statements.
I would like to keep using the value of ParamsNotEqual instead of ErrorLevel, and also, I would like to keep the variables within the subroutine isolated with SetLocal.
So, my question: Is this strange behaviour due to something am I doing wrong or is this a restriction in the way the command processor handles variables when EnableDelayedExpansion is set?
Any hint will be greatly appreciated. If you need any further tests or clarification on the above don't hesitate on asking. A lot of thanks in advance.

The result is correct as ParamsNotEqual isn't equal to 0, as you set it to 0<space>.
This line contains the problem.
endlocal & set ParamsNotEqual=%ParamsNotEqual% & exit /b %ErrorStatus%
You should always use the extended syntax of SET with surrounding quotes
endlocal & set "ParamsNotEqual=%ParamsNotEqual%" & ...

Related

Batch - How to replace exclamation mark with expression in if with delayed expension?

In order to return parameter values i need to change a string which requires to replace exclamation marks with an expression so that these exclamation marks are escaped later in the process. The current solution doesn't work inside an if. The following working script shows the issue quite well:
#ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET string="World^! wasserschutzpolizei^!"
REM REM Works quite well here!
REM SET "return1=!string:"=""!"
REM SET "return1=%return1:!=^^^!%"
REM SET "return1=!return1:""="!"
REM ECHO !return1!
IF NOT "!string!"=="" (
SET "return1=!string:"=""!"
REM This line doesn't work because of the % % inside the if...
REM Because it is one logical line.
SET "return1=%return1:!=^^^!%"
SET "return1=!return1:""="!"
ECHO !return1!
)
EXIT /B 0
ENDLOCAL
I had one non-working approach which uses CALL SET:
CALL SET "return1=%%return1:!=^^^!%%" but this doesn't work.
For an answer consider that I need delayed expension for other stuff too.
So, how do I need to change the code, espacially line SET "return1=%return1:!=^^^!%" so that it works inside the if?
Avoid using multi-line statements in batch scripts. It's not just difficult to get them right, they are hard to debug while developing the code. Delayed expansion is most useful when you need to use variables within variable replacement expressions. Here's your code the simple way:
#setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
#set prompt=$G
set input="World^! wasserschutzpolizei^!"
#rem Works quite well here!
#set "return1=!input:"=""!"
#set "return1=%return1:!=^^^!%"
#set "return1=!return1:""="!"
#echo %return1%
#if not "!input!"=="" call :DoIt
#echo %return1%
#exit /b 0
:DoIt
#set "return1=!input:"=""!"
#set "return1=%return1:!=^^^!%"
#set "return1=!return1:""="!"
#exit
Use subroutines when a short simple expression can't be used for if/loop bodies.
If you reversed your thinking and instead of using IF NOT …(, used IF …, you wouldn't have that problem as you wouldn't be setting variables within a block:
#ECHO OFF
SET "string=World! wasserschutzpolizei!"
IF "%string%"=="" EXIT /B 1
SET "return1=%string:"=""%"
SET "return1=%return1:!=^^^!%"
SET "return1=%return1:""="%"
ECHO %return1%
PAUSE
EXIT /B 0
The string will now have pre-escaped exclamation marks ready for your later process.
Like the answer of Compo, but this one works also for more than one return variable.
IF defined string (
SET "return1=!string:"=""!"
)
IF defined string (
SET "return1=%return1:!=^^^!%"
SET "return1=!return1:""="!"
)
It splits the code into two seperate blocks, therefore the percent expansion will work

Batch - Delayed expansion doesn't work with surrounding if clause

In the following script I call a subroutine with a string, the maximal length of a substring of that string and a 3rd non-existing variable to get the substring back.
The script should check if the next character after the string with the maximal substrings length is a space and if yes, then cut the string by that space (delete space to) and returning the substring and change the passed string by cutting the substring part. Example:
string: "Hello World Bla"
substringLength: 5
CALL :get_substring string substringLength substring
=> string: "World Bla" (no space), substringLength: 5 (no change), substring: "Hello"
This works fine without if clause but it doesn't when i use an if clause, even if i use delayed expansion.
Here is the working code without if statement:
#ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET string=Hello World wasserschutzpolizei
SET /A substringLength=5
CALL :get_substring string substringLength substring
ECHO !string!
ECHO !substring!
EXIT /B 0
ENDLOCAL
:get_substring
SETLOCAL ENABLEDELAYEDEXPANSION
SET "string=!%~1!"
SET "substringLength=!%2!"
SET nextChar=!string:~%substringLength%,1!
REM IF "!nextChar!"==" " (
SET substring=!string:~0,%substringLength%!
ECHO !substring!
SET /A cutSpaceCount=!substringLength!+1
SET string=!string:~%cutSpaceCount%!
ECHO !string!
ENDLOCAL & SET %1=%string% & SET %3=%substring% & EXIT /B 0
REM ) ELSE (
REM Some other case
REM )
EXIT /B 0
This doesn't work when I comment in the if statement:
IF "!nextChar!"==" " (
SET substring=!string:~0,%substringLength%!
ECHO !substring!
SET /A cutSpaceCount=!substringLength!+1
SET string=!string:~%cutSpaceCount%!
ECHO !string!
ENDLOCAL & SET %1=%string% & SET %3=%substring% & EXIT /B 0
) ELSE (
REM Some other case
)
Why does the script doesn't work with an if-statement?
How can fix it?
Be aware that my routine should also include an else statement which I cutted out because the problem is the same.
The problem is this line:
SET string=!string:~%cutSpaceCount%!
When this line is placed inside the IF command, then the value of cutSpaceCount is changed inside the code block (parentheses) of the IF, and hence it must be expanded via !cutSpaceCount! delayed expansion, not via a %cutSpaceCount% standard expansion.
You should use something like a "double delayed expansion", that is, similar to this construct:
SET string=!string:~!cutSpaceCount!!
Of course, this don't work, so the trick is use a for command to get the value of the first delayed expansion, and then use the FOR parameter to complete the second delayed expansion:
for /F %%c in ("!cutSpaceCount!") do SET "string=!string:~%%c!"
A similar problem happen when the final values in the subroutine are returned to the calling program. This is the final working code:
#ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET string=Hello World wasserschutzpolizei
SET /A substringLength=5
CALL :get_substring string substringLength substring
ECHO !string!
ECHO !substring!
EXIT /B 0
ENDLOCAL
:get_substring
SETLOCAL ENABLEDELAYEDEXPANSION
SET "string=!%~1!"
SET "substringLength=!%2!"
SET "nextChar=!string:~%substringLength%,1!"
IF "!nextChar!"==" " (
SET "substring=!string:~0,%substringLength%!"
ECHO !substring!
SET /A cutSpaceCount=!substringLength!+1
for /F %%c in ("!cutSpaceCount!") do SET "string=!string:~%%c!"
ECHO !string!
for /F "delims=" %%s in ("!string!") do for /F "delims=" %%b in ("!substring!") do (
ENDLOCAL & SET "%1=%%s" & SET "%3=%%b" & EXIT /B 0
)
) ELSE (
ECHO Some other case
)
PS - You don't need to expand variable values in SET /A command. Instead of:
SET /A cutSpaceCount=!substringLength!+1
you may simply use:
SET /A cutSpaceCount=substringLength+1
You seem to be very confused about the sequence of operations that occurs when delayed expansion has been invoked.
First, the value of var is substituted for %var%.
Then !var! is evaluated using the results.
The scope of this sequence of operations is one logical line, which may be one physical line or any number of physical lines continued with a terminal ^ or more commonly using a parenthesised sequence of lines.
In your mainline then,
CALL :get_substring string substringLength substring
ECHO !string!
ECHO !substring!
ENDLOCAL & SET %1=%string% & SET %3=%substring% & EXIT /B 0
Since these statements are not within the same logical line, they will be individually evaluated, so !var!==%var%.
Within your subroutine (non-IF version),
SET substring=!string:~0,%substringLength%!
ECHO !substring!
SET /A cutSpaceCount=!substringLength!+1
SET string=!string:~%cutSpaceCount%!
ECHO !string!
again are individual statements. The first set will first substitute for substringlength, and then execute SET substring=!string:~0,5! as a second operation.
Each of the echoes is a stand-alone statement, and the ! could (and preferably should) be replaced by %.
The set /a statement - well, set /a allows the current value of a variable to be used undecorated, so SET /A cutSpaceCount=substringLength+1 or SET /A cutSpaceCount=%substringLength%+1could be used here with no logical effect.
ENDLOCAL & SET %1=%string% & SET %3=%substring% & EXIT /B 0 will be evaluated according to the values established by the previous code-sequence.
However when you add the if, the code is parenthesised and thus becomes one logical statement and acts differently.
The echoes then require ! because you want to display the modified values within the code-block. Since cutSpaceCount is not set at the start of the code-block, SET string=!string:~%cutSpaceCount%! will be evaluated as SET string=!string:~!
and then ENDLOCAL & SET %1=%string% & SET %3=%substring% & EXIT /B 0 will duly substitute the values of the variables as they stood when the IFwas encountered
So, a replacement routine might be
:get_substring
SETLOCAL ENABLEDELAYEDEXPANSION
SET "string=!%~1!"
SET "substringLength=!%2!"
SET "substring=!string:~0,%substringLength%!"
SET "string=!string:~%substringLength%!"
IF "%string:~0,1%"==" " SET "string=%string:~1%"
ENDLOCAL & SET "%1=%string%" & SET "%3=%substring%"
EXIT /B 0
As others already explained, the problem is the line:
SET string=!string:~%cutSpaceCount%!
because you use immediate expansion (%) for variable cutSpaceCount which is changed in the same logical line/block of code.
A possible solution is to use call like this:
call set "string=%%string:~!cutSpaceCount!%%"
call introduces another variable expansion phase, so the sequence goes as follows:
immediate expansion phase where %% becomes %:
call set "string=%string:~!cutSpaceCount!%"
then delayed expansion occurs (let us assume a sample value of 5):
call set "string=%string:~5%"
another immediate expansion phase introduced by call to finally get %string:~5%.

Batch file quitting after doing "call :loop"

I have a bit of (Bad) Code for encrypting text, but to be able to decrypt it needs to have something inbetween the numbers. I want to fit random letters inbetween the numbers so it looks less obvious, this is where i got to:
#echo off
setlocal enableDelayedExpansion
set /p code=Text:
set chars=0123456789abcdefghijklmnopqrstuvwxyz
for /L %%N in (10 1 36) do (
for /F %%C in ("!chars:~%%N,1!") do (
Set _Alphanumeric=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
Set _count=0
set _RNDLen=%random%
Set /A _RNDLen=_RNDLen%%4
If !_count! leq %_RNDLen% call :loop
set "code=!code:%%C=-%%N!"
)
)
echo !code!
echo !_str!
pause
:loop
Set /a _count+=1
set _RND=%random%
Set /A _RND=_RND%%51
SET _str=!_str!!_Alphanumeric:~%_RND%,1!
EXIT /B %ERRORLEVEL%
The problem is that the program just quits before giving any output, even if i remove the exit /b statement. Thanks for help
I've no idea what principle you're using for your algorithm, but fundamentally you need to understand delayed expansion.
When your outer loop, for %%N is parsed, every %var% is replaced by the contents of that variable at that time, hence
set _RNDLen=%random%
If !_count! leq %_RNDLen% call :loop
are replaed by
set _RNDLen=a specific random number
If !_count! leq call :loop
The first line here will set _rndlen to the same number every time (for ny run) and since _rndlen is undefined at the start of the loop, it willl be replaced by nothing, hence the if statement has faulty syntax and hence cmd objects and would display a message.
You can use !random! with delayed expansion invoked to select a rendom number each time, and you need !_rndlen! to access the changed value of _rndlen (changed from its original value of nothing to some random value and then mod-4'd)
Personally, I'd assign _alphanumeric outside of the (outer) loop since its value isn't varied by the loop operation.
And naturally, you know that when you hit Return following the pause, the loop code will be executed before the routine terminates (by flow-through) and you should include a
goto :eof
line after the pause to skip this last operation.

Echo &variable% in a loop is delayed by 1 count. Echo is off error

Im trying to make a CMD batch script that will do the following.
Read the first line of a text file. The first line of the text file contains a date.
Delete the text file if the date is 3 months old from current date.
For illustration,
the first line of file A is Hello1, the first line of file B is Hello2
I want to get an output showing this
%counter% %first line of text file%,
so for my example it should look like this:
2 Hello2
1 Hello1
but instead, i am getting this:
2
1 Hello2
My current code is this.
set file.1=A.txt
set file.2=B.txt
set counter=2
SETLOCAL EnableDelayedExpansion
set counter=%counter%
:loop
if %counter% NEQ 0 (
set /p texte=<!file.%counter%!
echo %counter% %texte%
set /a counter=%counter%-1
gotop loop)
How do I fix this?
You have set it up for delayed expansion with your setlocal command (which should probably have a corresponding endlocal by the way) but you don't appear to be using delayed expansion in all the places it's needed.
Delayed expansion of variables requires the use of ! for expansion, not %.
Of course, once you do that, you're going to find issues with an expression like !file.!counter!! because cmd.exe is not the, err, greatest tool in the world :-)
However, that fact has produced some of the sneakiest coders in the world by forcing them to work around such limitations and you can do double-indirection of variables by using call as per the following program:
#setlocal enableextensions enabledelayedexpansion
#echo off
set file.1=A.txt
set file.2=B.txt
set counter=2
:loop
if !counter! NEQ 0 (
call :sneaky_set fspec file.!counter!
set /p texte=<!fspec!
echo !counter! !texte!
set /a counter=!counter!-1
goto loop
)
endlocal
goto :eof
:sneaky_set
set %1=!%2!
goto :eof
The call statement there passesfspec and file.N (the first level of indirection and where N is the current value in counter) to sneaky_set. It in turn executes:
set fspec=!file.N!
which is the second level of indirection and therefore sets file to the correct *.txt value.

what does !modified! do

I am trying to figure out what the following expression means in a bat file I am working with:
SET modified=!string:%SEARCHTEXT%=%REPLACETEXT%!
!modified! >> %outvar%
This is declared for execution right after a delimiting if statement for "_" and obtain arguments passed to the execution of a script then separate them as strings and write the strings in a file.
More specifically: what do the exclamations (!*!) do in this situation?
I've searched for like an hour but to no result. Can anyone give a hint?
The ! marks are the delayed expansion notation for variables (see SETLOCAL /? for documentation on this). Essentially, the ! marks tell the processor to evaluate the variable at the time the line is executed instead of when it is parsed, which is the behavior of %.
Take this simple example:
SET MyValue=This
IF "%MyValue%"=="This" (
SET NewValue=That
SET MyValue=%NewValue%
)
ECHO NewValue = %NewValue%
ECHO MyValue = %MyValue%
REM Outputs:
REM NewValue = That
REM MyValue =
MyValue does not have a value because when the IF statement was parsed, %NewValue% did not have a value assigned yet (because the SET line had not yet been processed).
Now consider this:
SETLOCAL EnableDelayedExpansion
SET MyValue=This
IF "%MyValue%"=="This" (
SET NewValue=That
SET MyValue=!NewValue!
)
ECHO NewValue = %NewValue%
ECHO MyValue = %MyValue%
REM Outputs:
REM NewValue = That
REM MyValue = That
ENDLOCAL
This works because the delayed expansion notation !, tells the processor to evaluate !NewValue! when the respective line is executed.
Additionally in your case, the SET line doing the replacement allows for variables to be used as the replacement parameters. Delayed expansion notation is much easier to work with instead of having to break it out into CALL statements.
The exclamation marks are used for delayed variable expansions, and are used in the same way as %-signs are for variables. This is mainly used inside parenthesis. This is because of the way batch executes loops and if-statements. consider this code:
#echo off
set "i=0"
if %i% equ 0 (
set "i=1"
echo %i%
)
pause
When executed, this doesn't echo 1, like expected, but echoes 0. That is because %i% get's replaced with 0 before the statements inside the if are executed. However, if you use !i! and setlocal EnableDelayedExpansion, like this:
#echo off
setlocal EnableDelayedExpansion
set "i=0"
if %i% equ 0 (
set "i=1"
echo !i!
)
echo %i%
pause
It will correctly echo 1. Also note that ! is only needed inside parenthesis, not outside of them.

Resources