Weird scope issue in .bat file - scope

I'm writing a simple .bat file and I've run into some weird behavior. There are a couple places where I have to do a simple if/else, but the code inside the blocks don't seem to be working correctly.
Here's a simple case that demonstrates the error:
#echo off
set MODE=FOOBAR
if "%~1"=="" (
set MODE=all
echo mode: %MODE%
) else (
set MODE=%~1
echo mode: %MODE%
)
echo mode: %MODE%
The output I'm getting is:
C:\>test.bat test
mode: FOOBAR
mode: test
Why is the echo inside the code block not getting the new value of the variable? In the actual code I'm writing I need to build a few variables and reference them within the scope of the if/else. I could switch this to use labels and gotos instead of an if/else, but that doesn't seem nearly as clean.
What causes this behavior? Is there some kind of limit on variables within code blocks?

You are running into the problem of cmd's static variable expansion. The MODE variable is only evaluated once. You can see this if you omit the #echo off line.
From the set /? documentation:
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%

setlocal EnableDelayedExpansion
will enable the /v flag

Additionally to already anwered here.
Long answer article:
https://rsdn.org/article/winshell/NTCommandProcessor.xml
The google translated variant is pretty broken, but you can at least read the raw text:
https://translate.google.com/?sl=ru&tl=en&text=https%3A%2F%2Frsdn.org%2F%3Farticle%2Fwinshell%2FNTCommandProcessor.xml&op=translate
Start read from Conditional block section.
Short answer:
The block operator (...) blocks a %-variable expansion until the top most scope. You have to exit the scope out to be able to use %-variable as is:
#echo off
set "MODE="
(
(
set MODE=all
echo MODE=%MODE%
)
echo MODE=%MODE%
)
echo MODE=%MODE%
Or use call prefix to reevaluate it in place:
#echo off
set "MODE="
(
(
set MODE=all
call echo MODE=%%MODE%%
)
)

Looks like the read and write use different scoping rules.
If you eliminate this line
set MODE=FOOBAR
it will work as expected. So you'll probably need to have a complex series if if/elses to get the variables populated as you'd like.

Related

What is the difference between "..." and x"..." in an IF condition in a Windows batch file?

I recently found the post Find if substring is in string (not in a file) where it is stated that considering
#setlocal enableextensions enabledelayedexpansion
#echo off
set str1=%1
if not x%str1:bcd=%==x%str1% echo It contains bcd
endlocal
then
the x before the two sides of the equality is to ensure that the string bcd works okay. It also protects against certain "improper" starting characters.
However, I haven't found any explanation about the actual effect of this x. So what is the difference between x"%string%" and "%string%"?
That is simply a very bad coded string comparison. The x on both sides makes it possible to compare the two strings even if %str1:bcd=% or %str1% is substituted by Windows command processor on parsing entire command line by an empty string before execution of command IF.
But the batch file execution is nevertheless exited immediately by cmd.exe because of a syntax error in case of value of environment variable str1 contains a space character or "&<>|.
Enclosing an argument string in double quotes results in getting all characters except percent sign and with enabled delayed environment variable expansion also the exclamation mark interpreted as literal character including space which is outside a double quoted string interpreted as argument string separator.
So much better is:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
if "%~1" == "" goto EndBatch
set "str1=%~1"
if not "%str1:bcd=%" == "%str1%" echo It contains bcd
:EndBatch
endlocal
The first argument of the batch file is compared first without surrounding double quotes with an empty string. So if the batch file is started without any argument or with just "" as first argument string, Windows command processor executes the command GOTO resulting in restoring previous environment pushed on stack with command SETLOCAL and exits the batch file.
Otherwise the batch file is called really with an argument string. This argument string is assigned to environment variable str1 with removing surrounding double quotes if there are one. So on calling batch file with argument test the value test is assigned to environment variable str1 and on calling it with "another test" the value another test without the double quotes is assigned to str1. And even on calling the batch file with wrong coded argument string "bcd test (missing second ") just bcd test is assigned to the environment variable str1.
The IF condition compares the value of environment variable str1 with all occurrences of bcd removed with the unmodified variable value. The double quotes around the two strings make it possible to compare the two strings even on containing space or ampersand or the redirection operators <>|. The command IF includes the double quotes on comparing the two strings.
So is this code now safe?
No, it is not in case of somebody calls the batch file invalid with test_bcd" as argument string on which first double quote is missing. In this case the first IF command line executed by cmd.exe is:
if "test_bcd"" == "" goto EndBatch
The trailing " of the wrong specified argument string is not removed by cmd.exe and cause a syntax error on this command line on execution as it can be seen on running the batch file from within a command prompt window with first line modified to #echo on.
One solution without using delayed environment variable expansion is:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "str1=%~1"
if not defined str1 goto EndBatch
set "str1=%str1:"=%"
if not defined str1 goto EndBatch
if not "%str1:bcd=%" == "%str1%" echo It contains bcd
:EndBatch
endlocal
This code makes sure that str1 does not contain any double quote before executing the IF command comparing the strings.
Another solution is using delayed environment variable expansion:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
set "str1=%~1"
if not "!str1:bcd=!" == "!str1!" echo It contains bcd
endlocal
That looks better as the above code without usage of delayed environment variable expansion. But it does not work as expected if the the argument string is for example "!Hello!" because in this case the if not condition is also true and output is therefore the message It contains bcd although the string !Hello! does not contain bcd.
The solution is:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "str1=%~1"
setlocal EnableDelayedExpansion
if not "!str1:bcd=!" == "!str1!" echo It contains bcd
endlocal
endlocal
Delayed expansion is not enabled on assigning the argument string to environment variable str1 which results in getting the exclamation marks in string "!Hello!" interpreted as literal characters. Then delayed expansion is enabled for making the string comparison with using delayed environment variable expansion which avoids that the command line itself is modified by cmd.exe before execution of IF command.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /? ... explains %~1, not so good as done here.
echo /?
endlocal /?
goto /?
if /?
set /?
setlocal /?
See also:
How does the Windows Command Interpreter (CMD.EXE) parse scripts? ... a long answer every batch file writer should read carefully from top to bottom.
forfiles - FALSE vs. false (doesn't work with lower case?) ... this answer is about argument processing of command IF.
Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files ... this answer explains in detail how string comparisons are done by command IF.
Why is no string output with 'echo %var%' after using 'set var = text' on command line? ... this answer explains why set "variable=value" should be used in general and not other variants.
Single line with multiple commands using Windows batch file ... explains how & and || outside a double quoted argument string are interpreted by cmd.exe.
Microsoft article about Using command redirection operators explains how <>|& outside a double quoted argument string are interpreted by cmd.exe.
The addition of x (or any other alphabetic character) in front of a string ensures that the relational statement is syntactically valid even when/if the string is empty.
Suppose str1 is an empty string. Then the comparison %str1:bcd=%==%str1% after the substitution degenerates to ==, which is syntactically invalid.
However, with an x in front, the comparison becomes x==x and can be evaluated. Naturally, adding the same prefix to each of the two strings does not affect their (in)equality.

Batch string variables with variables in the string name

I have a batch file where I have a variable named t%num% and num is an integer and t%num%=#.
I need to set another variable called q equal to the contents of t%num%.
I tried set q=t%num% so that q would contain a #, but it did not work.
#ECHO OFF
SETLOCAL
SET num=5
SET t%num%=36
CALL SET q=%%t%num%%%
ECHO %q% %t5%
GOTO :EOF
Uses the idea that % escapes % so that the parser substitutes-in the value of num and the result is set q=%t5%
You can (ab)use delayed environment variable expansion for this purpose.
Let's say you have an environment variable t5 defined. The following batch script will assign the content of t5 to the variable q:
setlocal EnableDelayedExpansion
echo t5: %t5%
set num=5
set q=!t%num%!
echo q: %q%
However, be aware that enabling delayed environment variable expansion will make ! a special character not only in the batch script, but also inside the content of environment variables. Try setting the following content for t5 before executing the little script above:
set t5=Hello World!
You will notice that the exclamation mark at the end of "Hello World" simply disappears.
Or try this cheeky bit:
set t5=Guess what: !num!
Setting t5 like this and then executing the script, well... i leave it to you to find out what will happen :)

Why is processing of my batch file exited with a syntax error on string comparsion with IF command?

When I run the following code, the batch processing is exited with a syntax error message output that it does not recognize the opening parenthesis.
if %errorlevel2%==s (
echo.
echo.
echo Press any key to continue...
echo.
echo.
pause>nul
start haxmenu\pin.bat
goto menu
)
Are my parentheses set wrong or is something about code inside the block which lets the script fail on execution?
The error message is:
( was unexpected at this time.
Introduction
So you have a code similar to this one:
#echo off
:menu
set /P "errorlevel2=Please enter your choice: "
if %errorlevel2%==s (
echo/
echo/
echo Press any key to continue...
echo/
echo/
pause>nul
start haxmenu\pin.bat
goto menu
)
First you have to take into consideration that Windows command processor replaces %errorlevel2% with the string of this variable before the line is parsed for a command.
So it depends on existence and value of environment variable errorlevel2 how the line with the IF condition looks like on execution of the batch file.
For example with errorlevel2 not defined at all and user on prompt just hits the key RETURN or ENTER the variable errorlevel2 is still not existing after user prompt and the IF condition line looks therefore as follows:
if ==s (
This IF condition is of course invalid and command processor exits batch processing because of the syntax error.
Using double quotes around both strings to compare is a first step to avoid the syntax error.
if "%errorlevel2%"=="s" (
Now the resulting line on execution of the batch file with nothing entered by the user is:
if ""=="s" (
This is a valid syntax.
But it does not prevent your batch code completely on exiting because of a syntax error as it can be seen by entering on prompt for example a single double quote. This would result in
if """=="s" (
which results again in an exit of batch processing because of a syntax error caused by the odd number of double quotes on left side of the equal operator.
Before thinking about a solution for this additional issue, we should also think about what happens when the user really just press RETURN or ENTER on prompt without entering anything. Run the following code:
#echo off
set /P "UserInput=Enter a number: "
echo UserInput=%UserInput%
set /P "UserInput=Hit key RETURN: "
echo UserInput=%UserInput%
pause
After entering on first prompt a number like 23 and hit on second prompt just RETURN as requested, the number 23 is output also a second time.
The current value of the environment variable is not modified if the user just hits RETURN on user prompt. This should be always taken into account when using set /P in a menu, especially if this menu is within a loop because in this case the variable has already a value after first choice made by the user which would be kept unmodified on next prompt if the user does not enter anything.
Solution 1: Remove all double quotes before string comparison
One solution is predefining the variable with a single double quote and remove all double quotes from input string.
#echo off
:menu
set "errorlevel2=""
set /P "errorlevel2=Please enter your choice: "
set "errorlevel2=%errorlevel2:"=%"
if /I "%errorlevel2%"=="s" (
echo/
echo/
echo Press any key to continue...
echo/
echo/
pause>nul
start haxmenu\pin.bat
goto menu
)
It is important that the value of variable errorlevel2 is predefined with a string containing at least one double quote for this solution if not one of the options the user should choose from is defined as default value because the variable errorlevel2 must exist with any value to avoid a syntax error on line removing all double quotes from string.
See Why is no string output with 'echo %var%' after using 'set var = text' on command line? for an explanation why using syntax set "variable=value" even with value containing a double quote.
Solution 2: Using delayed expansion on string comparison
A syntax error on string comparison because of string entered by the user could be avoided by using delayed expansion because the IF condition line itself is not modified in this case by the input string.
#echo off
setlocal EnableDelayedExpansion
:menu
set "errorlevel2="
set /P "errorlevel2=Please enter your choice: "
if "!errorlevel2!"=="s" (
echo/
echo/
echo Press any key to continue...
echo/
echo/
pause>nul
start haxmenu\pin.bat
goto menu
)
endlocal
The variable errorlevel2 is deleted now always before prompting the user.
Whatever the user enters does not modify the batch code line itself and therefore the string comparison always works even with a string entered with an odd number of double quotes.
Solution 3: Using command choice for simple menus
Windows offers the command choice (Microsoft article) for tasks where a user should select one of offered options by key. Using this command is nearly always better for single character choices than using set /P.
But how choice (SS64 article) can be used depends also on version of Windows respectively which version of choice is used as there are multiple versions with different syntax and capabilities. choice should be used for simple menus if compatibility of batch file with Windows versions prior Windows Vista is not needed anymore as the batch file is definitely never run on Windows XP or even older Windows.
In all probability, the problem is that errorlevel2 is not set (ie is set to nothing
You could get over this with
if "%errorlevel2%"=="s" (
but It's likely that you're expecting errorlevel2 to have been set in a block statement (ie within a parenthesised series of statements) and in that case, you'd need to look at about 10,000 aricles on SO relating to delayedexpansion.
Or, of course, show us a little more of your code - back to the if or do that's supposed to set errorlevel2

Batch String Manipulation

I am trying to write a batch program that writes more code based on what the user inputs. I am stuck on the string manipulation, as "!", ">", "&", and "%" all need to be escapeded before they are to be outputted to other files. This is what I have so far:
#echo off
set /p code=
set newcode=%code:^%=^%%,^>=^^>,!=^^!,^&=^^&%
echo %newcode%>>file.bat
All this escapinging escaped stuff is making my brain hurt, so can you please help me?
Thanks!
Since you haven't explained clearly what you are trying to do with the input, I am assuming you are trying to get the user to type something and then for that to be entered into a file. If that is the case te utilise copy in combination with con
#echo off
Echo Enter ^^Z (Ctrl + Z) and return it to end input
copy con file.txt
And that will allow the user to type WHATEVER they want, and it to be put in a file to be examined. Otherwise you're gonna have a lot of fun (And trouble) dealing with all the escaping you're gonna have to do (Rather ironic wasn't that?).
Mona.
Simply use delayed expansion in your case, as delayed expansion needs no further escaping for the content.
#echo off
setlocal EnableDelayedExpansion
set /p code=
echo(!code!
The echo( is for safe text output, even if the text is empty or is something like /?.
To the comment of #foxidrive:
When delayed expansion is enabled, then exclamation marks ! are altered, when they are visible before the delayed expansion is done, but the expanded content of delayed expanded variables will never be altered.
#echo off
set "var=Hello!"
setlocal EnableDelayedExpansion
echo "!var!" "Hello!"
This will result to: "Hello!" "Hello"

Batch file to read from filename and make directory

I have .txt files (1 or more) in a directory that I want my batch file to read their filename, get 20 characters starting from the 4th and creating a new directory in the folder. Here is my code:
for /f %%i in ('dir /b *.TXT') do (
set filename1=%%i
set folder1=%filename1:~4,20%
mkdir %folder1%
)
When I run this program the 1st time, I get a syntax not correct error for the line 3rd line (set folder1=....) and no folder is created. I tried running it a 2nd time and 2 folders were created (one named "~4" and one named "20"). On the 3rd run, the folder was correctly created! If I close the command prompt and open it again, it also needs to run 3 times before it creates the folder.
I've also tried using "for /r . $$i in (*.TXT) with no luck. As I understand, the problem is that the line with "set folder1=..." does not get the proper filename. I've also tried using %%~i or %%~ni, I've tried outputing the filename (which seems to always get the correct string) to a text file and then reading from that file, again with no luck. I don't know what else to try. Is it because %%i stores the file itself and not a string with the filename?
A sample file is named "REG_18004247K_20120208_A.TXT" and I want a folder to be created with the name "18004247K_20120208_A".
I am not at all familiar with batch programming (I'm only working with batch files for like 2 weeks) and I'm guessing the problem might be something really simple. Any help would be appreciated.
Kyriacos
%%i does stores a string with a filename. It is actually the only "variable" that works as expected here.
The key problem is that you are using environment variable substitution (of filename1 and of folder1) inside a loop, hoping that it will be expanded in each iteration of the loop.
However, environment variables are expanded before execution of the whole loop begins.
So, upon the first batch execution, filename1 is not defined and you get
an error, folder1 is not set, and you probably also see a folder
called %folder1% created.
However, the scripts defines filename1 at this time, and the second execution gets further to define folder1 correctly (although line 4 was
already expanded using the incorrect value and fun happens).
The third execution
finally gets to see the right value of folder1; but this would obviously not
work for more than one file, as the loop logic is dysfunctional.
This older answer explains this problem with great other examples and special cases.
Your batch can be fixed by setlocal enabledelayedexpansion at the beginning of the script, initializing the variables to empty strings before the loop, and switching to the !...! syntax when expanding them: !filename1:~4,20!, and !folder1!.
setlocal enabledelayedexpansion
for /r %%i in (*) do (
set filename1=%%i
set folder1=!filename1:~4,20!
mkdir !folder1!
)
Because ... delayed expansion takes some time to store actual variable.
!...! rather than %...% echoes fast allocating variables to DOS.

Resources