I found a bug in Batch. How can I circumvent this bug? - for-loop

CMD is misinterpreting code on the false side of an if statement, resulting in a crash.
Here is some test code, which fails should the end user enter y or Y:
#Echo Off
Set "var="
Set "input="
:YorN
Set /P "input=Leave var empty? [Y(crash)|N]"
(Set input) 2>NUL | %SystemRoot%\System32\findstr.exe /I /L /X "input=Y input=N" 1>NUL
If ErrorLevel 1 GoTo YorN
If /I "%input%" == "n" Set "var=content1;content2;"
If Not "%var%" == "" (
For /F "Tokens=1,2 Delims=;" %%G In ("%var:~0,-1%") Do If Not "%%G" == "" Echo "%%G" "%%H"
) Else (
Echo As per your choosing, var is empty. Because of the if statement the "for" command didn't get interpreted and CMD didn't crash. You will not see this message.
)
Pause
Exit /B
This version however, with only one minor line break change works as intended.
#Echo Off
Set "var="
Set "input="
:YorN
Set /P "input=Leave var empty? [Y(crash)|N]"
(Set input) 2>NUL | %SystemRoot%\System32\findstr.exe /I /L /X "input=Y input=N" 1>NUL
If ErrorLevel 1 GoTo YorN
If /I "%input%" == "n" Set "var=content1;content2;"
If Not "%var%" == "" (
For /F "Tokens=1,2 Delims=;" %%G In ("%var:~0,-1%"
) Do If Not "%%G" == "" Echo "%%G" "%%H"
) Else (
Echo As per your choosing, var is empty. Because of the if statement the "for" command didn't get interpreted and CMD didn't crash. You will see this message.
)
Pause
Exit /B
Could somebody please explain to me what is causing this issue, or confirm that this is a bug in cmd.exe?

There is no bug, but the behavior is not obvious.
A minimal example shows the problem.
#echo off
set "var="
set "other=content"
echo First char of var is "%var:~0,1%" my other var=%other%
You get:
First char of var is "~0,1other
If you add any text to var it works as expected.
The variable var is undefined, not empty!
The problem is the expansion rule of undefined variables, the parser stops the variable expansion part, if it finds a double colon in an expression, but the variable is undefined.
In this case the parser ignores (and removes) the variable expansion after reading %var:.
But the the parser looks at the remaining line of ~0,1%" my other var=%other%.
It splits the line to
~0,1 -- Normal text
%" my other var=% -- This is a percent expansion of the variable with the name " my other var=, this variable is undefined, cmd.exe removes the complete part
other -- Normal text
% -- The trailing opening percent is removed, because there is no other percent sign
In your complicated example the line feed seems to solve the situation, because the part If Not "%%G" ... is on a separate line and the first percent of %%G is not used as the closing percent of the expression %") Do If Not "%.
In the failure case, it gets worse, because the closing parenthesis of the FOR block is removed and then the FOR command scans the rest of the file for a closing parenthesis and that ends in total rubbish.
For better understanding you could read:
SO: Percent Expansion Rules from #dbenham

What you have found (referring to revision 13 of your question) is something that I consider a bug (or at least a terrible design flaw) – but the problem is neither the if nor the for statement, it is the sub-string syntax:
If you follow the percent expansion rules very carefully, you may notice, that sub-string expansion (%VAR:~[integer][,[integer]]%) or sub-string substitution (%VAR:[*]search=[replace]%) behaves odd in case variable VAR is not defined. Here is an excerpt of that post with the most relevant sections highlighted:
Phase 1) Percent Expansion Starting from left, scan each character for % or <LF>. If found then
1.05 (truncate line at <LF>)
If the character is <LF> then
Drop (ignore) the remainder of the line from the <LF> onward
Goto Phase 2.0
Else the character must be %, so proceed to 1.1
1.1 (escape %) skipped if command line mode
[…]
1.2 (expand argument) skipped if command line mode
[…]
1.3 (expand variable)
[…]
Else if command extensions are enabled then[…]
If next character is % then[…]
Else if next character is : then
If VAR is undefined then
If batch mode thenRemove %VAR: and continue scan.
[…]
[…]
1.4 (strip %)
[…]
Applying this to your code portions, we can conclude the following:
First code portion:
If var is not defined, %var:~0,-1% becomes parsed to ~0,-1%, because of Remove %VAR: and continue scan in the above excerpt of Phase 1, leaving behind the remaining command line ~0,-1%") Do If Not "%%G" == "" Echo "%%G" "%%H", which is interpreted as:
the literal string ~0,1,
the (undefined) variable %") Do If Not "% (which becomes stripped),
the (undefined) variable %G" == "" Echo "% (which becomes stripped),
the (undefined) variable %G" "% (which becomes stripped),
and the remainder %H" (which the single %-sign becomes removed from),
constituting the command line For /F "Tokens=1,2 Delims=;" %G In ("~0,-1H" after Phase 1. The next line ) Else ( provides an expected closing parenthesis, but there is Do expected rather than Else. That is why the specific error message Else was unexpected at this time. arises.
Hence the %-sign in the fragment ~0,-1% is considered as an opening one for another (yet undefined) variable, impairing the whole remainder of the command line.
Second code portion:
If var is not defined, %var:~0,-1% also becomes parsed to ~0,-1%, because of Remove %VAR: and continue scan in the above excerpt of Phase 1, but leaving behind the remaining command line ~0,-1%", resulting in just the literal string ~0,-1" (with the %-sign stripped).
In this situation however, there is a line-break (<LF>) following, so Phase 1 is ended because of Goto Phase 2.0 and so, parsing newly begins with Phase 1 in the next line.
The key to all this is the statement and continue scan, meaning that from that point on, the next detected %-sign is recognised as an opening one.
How can I circumvent this bug?
To circumvent the described issue, you have got the following options:
To avoid command blocks in the problematic code section If Not "%var%" == "" ( … ) Else ( … ) by using the goto command and labels:
If Not Defined var GoTo UnDef
For /F "Tokens=1,2 Delims=;" %%G In ("%var:~0,-1%") Do If Not "%%G" == "" Echo "%%G" "%%H"
GoTo :Next
:UnDef
Echo As per your choosing, var is empty. Because of the "if" statement the "for" command didn't get interpreted and CMD didn't crash. You will see this message.
:Next
To utilise delayed variable expansion, which is applied to individual tokens rather than whole command lines, rendering them independent from each other; the section behind in in a for-loop constitutes such a token, according to the delayed expansion rules:
If Not "%var%" == "" (
SetLocal EnableDelayedExpansion
For /F "Tokens=1,2 Delims=;" %%G In ("!var:~0,-1!") Do EndLocal & If Not "%%G" == "" Echo "%%G" "%%H"
) Else (
Echo As per your choosing, var is empty. Because of the "if" statement the "for" command didn't get interpreted and CMD didn't crash. You will see this message.
)
Therein, delayed expansion is only active until parsing of the in clause because of the endlocal behind do (which is only executed once because there is only one loop iteration).

Related

How to set one variable equal to the sum of 2 other variables in cmd

My code is
FOR /F "skip=1 tokens=1-6" %%G IN ('WMIC Path Win32_LocalTime Get Day^,Hour^,Minute^,Month^,Second^,Year /Format:table') DO (
IF "%%~L"== "" goto s_done
Set _yyyy=%%L
Set _mm=00%%J
Set /a _nextmm=_mm+1
)
:s_done
What I want is this variable "_nextmm" = this variable "_mm" + 1
But when I run this code. It's result is
IF "2021" == "" goto s_done
Set _yyyy = 2021
Set _mm = 008
Set /a _nextmm = _mm+1
And I call echo %_nextmm%. The result is 1 instead of 009.
What did I do wrong in here?
I hope there are no SPACEs around the equal-to signs in your actual set command lines as they would harm. The best way is this syntax:
rem /* No spaces around `=`-sign, and quotes around the whole assignment expression,
rem which avoids unwanted training whitepsaces and protects special characters;
rem without assigning the quotes themselves to the variable value: */
set "VAR=Value"
Anyway, use:
set /A "_nextmm=%%J+1"
instead of set /A _nextmm=_mm+1, because (as I assume) %%J is 8, which is a correct decimal number, but _mm is 008, which is treated as an octal number due to the leading zeros (yes, I know, this is not intuitive, but take a look at this), which is invalid as there are only digits 0 to 7, hence 0 is taken for further arithmetic operations.
If you have your echo command line within the body of the for /F loop (so before the closing )), use:
for /F … (
…
call echo %%_nextmm%%
)
or use delayed variable expansion:
setlocal EnableDelayedExpansion
for /F … (
…
echo !_nextmm!
)
endlocal
If you have your echo command line outside of the body of the for /F loop (so after the closing )), normal or immediate expansion will work as expected:
for /F … (
…
)
echo %_nextmm%
TBH, I'm not sure of your logic, because if this was done in December 2021, then adding 1 would make the month incorrectly 13, and the year would remain as 2021.
For that reason, it would be much better if you just used PowerShell to assist you instead of WMIC. PowerShell sees dates as objects not strings, so you can perform the math directly in order to define the required values for your variables.
For /F "Tokens=1-2" %%G In ('
%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile
-Command "(Get-Date).AddMonths(1).ToString('yyyy MM')"') Do (Set "_yyyy=%%G"
Set "_MM=%%H")
If you wanted to do it without splitting the line up for better reading then:
For /F "Tokens=1-2" %%G In ('%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -Command "(Get-Date).AddMonths(1).ToString('yyyy MM')"') Do Set "_yyyy=%%G" & Set "_MM=%%H"

Windows Batch script stops reading a file after a record has been written via IF in DO loop

Sometimes I think my relationship with the Windows command line is weighted in favour of Stockholms syndrome over anthing else. I'm hoping someone else in this world of punishing stuff has an answer for me!
Here's the background and the problem statement: I have a quite large script that does stuff with a for loop that steps through a file searching for a record number, and changing the record while writing the records around in a nice sequential batch way, ie records are written up to the target record; a changed record is written; the remaining records are written. I needed to add a new action verb to the existing set, which requires an additional IF clause. It suddenly stopped writing out remaining records after the found record had been changed. Couldn't find the answer.
So I started stripping away code until I reached this residual script. The IF clause in the FOR/ in / DO loop ... Leave it in, the script stalls after the written record as per the first sample below; leave it out, the FOR loop happily does it's thing, as per the second sample output. It gets stranger. Adding in script gives error messages, sometimes running through to completion, sometimes not ~ even a simple echo will give a parsing error. Placement in the script also seems to matter, which made debugging an absolute nightmare.
Clearly there is something that is triggering the command processor to quit the loop. SO I would pose the following two questions to the community:
What is causing the FOR /in /DO structure to stop processing records
What gives with the echo statements giving parsing errors? (echoing a variable name with a script line number, or just the number, for instance, is very low impact. )
Thanks. Code follows, any text file can be used as the second parameter, the first is a line number to action.
echo off
SETLOCAL enabledelayedexpansion
SET _filein=%~2
SET _line=%1
set _cnt=0
:: _filein = any file, _line = the line to be inspected, _cnt = loop counter
:: in the loop: _record = line value, _msg= display message if line is found
FOR /f "tokens=1 delims=" %%i IN ('type %_filein%') DO (
SET _record=%%i
SET /a _cnt+=1
IF [!_cnt!]==[%_line%] (
echo the line %_line% is !_record!
set _msg=the line is !_record! at position !_cnt!
goto sayrecord
)
echo not the line:
:sayrecord
echo !_cnt!- !_record!
)
:Xit
echo:
IF [%_msg%]==[] (
echo: left out
) ELSE (
echo: I'm in to see %_msg%
)
ENDLOCAL
exit /b
Test runs:
1. With the IF statement: (note parsing error statement)
H>_a 3 _files.txt
H>echo off
not the line:
1- notes.bat
not the line:
2- notesqa.bat
the line 3 is bulkfiledelete.bat
3- bulkfiledelete.bat
line was unexpected at this time.
H>
Without the IF:
H>_a 3 _files.txt
H>echo off
not the line:
1- notes.bat
not the line:
2- notesqa.bat
not the line:
3- bulkfiledelete.bat
not the line:
4- CheckAdminRights.bat
not the line:
: : :
not the line:
47- Reset connection 3 - renew.lnk
not the line:
48- ============
left out
H>
OK, so I have a code block that works thanks to #jwdonaahue for the initial clue; and #SomethingDark for the problem statement.
The :label is the problem here, although it works in many other production scripts it clearly is problematic. Instead of a script that drops to the bottom of the loop for processing, putting in an IF clause for each use case is what is needed.
#echo off
SETLOCAL enabledelayedexpansion
SET _filein=%~2
SET _line=%1
set _cnt=0
:: _filein = any file, _line = the line to be inspected, _cnt = loop counter
:: in the loop: _record = line value, _msg= display message if line is found
FOR /f "tokens=1 delims=" %%i IN ('type %_filein%') DO (
SET _record=%%i
SET /a _cnt+=1
IF [!_cnt!]==[%_line%] (
echo the line %_line% is !_record!
set "_msg=the line is !_record! at position !_cnt!"
echo not the line:
CALL :sayrecord
)
IF [!_cnt!] neq [%_line%] CALL :sayrecord
)
:Xit
echo:
IF [%_msg%]==[] (
echo: left out
) ELSE (
echo: I'm in to see %_msg%
)
ENDLOCAL
exit /b
:sayrecord
echo !_cnt!- !_record!
exit /b
i.e. to solve my problem,
add an IF clause for each action verb;
end with an `IF' clause which wraps the records that must be written unchanged
I'll also go through all the scripts and do a maint on them!
Given that your stated intent was to replace the content of %2 on line %1, you don't need delayed expansion or to set all of those variables:
#For /F "Tokens=1*Delims=]" %%I In ('Type "%~2"^|"%__AppDir__%find.exe" /V /N ""')Do #If "%%I"=="[%~1%" (Echo Inserted line content)Else Echo=%%J
#Pause
You would modify the content after Echo  and before ) to the replacement line content, and the last line is added just to ensure that you can read the output.
[Edit /]If you wanted to ask the end user for confirmation of the change before doing so, you could expand your code to incorporate that:
#For /F "Tokens=1*Delims=]" %%I In ('Type "%~2"^|"%__AppDir__%find.exe" /V /N ""')Do #If "%%I"=="[%~1" ("%__AppDir__%choice.exe" /M "Replace %%J with new content"&If ErrorLevel 2 (Echo=%%J)Else Echo Inserted line content)Else Echo=%%J
#Pause

Using FINDSTR to set variables in FOR Loop

(EDITED) How can I read data from a text file and assign certain parts of it to variables? In the lines below, for example, I want to assign the data to variables where indicated but ignore the header line, the preface word "DATA:" and any quotation marks.
HEADER (ignore)
DATA: "set text inside quotes to VAR1"
DATA: "set text inside quotes to VAR2"
DATA (etc.)
The code I'm using is:
setlocal enabledelayedexpansion
set COUNT=0
for /f "usebackq delims=" %%a in ("data.txt") do (
set "INPUT=%%a"
set /a COUNT=!COUNT!+1
echo !INPUT! | findstr /i /c:"HEADER" > nul
if errorlevel 1 (
set "INPUT=!INPUT:DATA:=!"
set "INPUT=!INPUT:"=!"
set VAR!COUNT!=!INPUT!
echo Variable !COUNT! = !VAR!!COUNT!
)
)
My expected output is
Variable 1 = set text inside quotes to VAR1
Variable 2 = set text inside quotes to VAR2
but what I get is
Variable 2 = 2
Variable 3 = 3
Variable 4 = 4
What am I doing wrong?
I took your code and fixed it up.
In particular:
I switched the findstr to find all lines that do not match HEADER
I used arrays to store the variables.
It may need some minor tweaks to get it exactly as you want it, but I think I've given you a working outline.
#echo off
setlocal enabledelayedexpansion
set COUNT=0
for /f "tokens=*" %%a in ('findstr /i /v HEADER data.txt') do (
set "INPUT=%%a"
set /A COUNT=!COUNT!+1
set "INPUT=!INPUT:DATA:=!"
set VAR[!COUNT!]=!INPUT!
call echo Variable !COUNT! = %%VAR[!COUNT!]%%
)
Output:
Variable 1 = "set text inside quotes to VAR1"
Variable 2 = "set text inside quotes to VAR2"
Variable 3 = DATA (etc.)
To match lines that have leading spaces, use switches:
findstr /V /R /C:"^ *HEADER"
The regular expression means:
^ : Beginning of line
[space] : a literal space
* : the previous character (space) repeated as many times as needed
HEADER : the text to find after leading spaces
The switches mean:
/R Use Regular Expressions
/C: Use this search-string
/V Show all lines that do NOT match this expression.

batch replace spaces between quotes in file and then remove all quotes

I want create a batch to replace spaces with a + sign if the space is in between quotes. Then I want to remove the quotes from a text file. How can I accomplish this?
So I want to change a line like this:
2016-01-11 14:45:09 Server 127.0.0.1 GET /global/images/logo_small.jpg - 80 - 173.252.120.117 "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)" "-" www.vietnam.ttu.edu 200 200 200 1868 0
To this line.
2016-01-11 14:45:09 Server 127.0.0.1 GET /global/images/logo_small.jpg - 80 - 173.252.120.117 facebookexternalhit/1.1+(+http://www.facebook.com/externalhit_uatext.php) - www.vietnam.ttu.edu 200 200 200 1868 0
Thanks
You could use JREPL.BAT to arrive at a very concise and efficient solution. JREPL is a pure script based (JScript/batch) regular expression text processing utility that runs on any version of Windows from XP onward.
jrepl "\q| " "q=!q;''|q?'+':' '" /j /x /t "|" /jbegln "q=false" /f test.txt /o -
For this solution I use the /T option, which is very similar to the unix tr utility, or the sed y command.
I define two search terms, the first for a quote (The \X option enables the \q escape sequence), and the second for a space.
The /J option treats replacement strings as JScript. The first replacement string for the quote toggles a "q" variable TRUE or FALSE, and replaces the quote with an empty string. The second replacement string conditionally replaces the space with a plus or space, depending on the state of the "q" variable.
The /JBEGLN option initializes the "q" variable to FALSE at the beginning of each line.
The /F option specifies the input file, and the /O - option specifies that the output overwrites the original file.
#ECHO OFF
SETLOCAL
SET "sourcedir=U:\sourcedir"
SET "filename1=%sourcedir%\q34732271.txt"
FOR /f "usebackqdelims=" %%a IN ("%filename1%") DO ECHO %%a&SET "line="&CALL :process %%a
GOTO :EOF
:process
SET "addition=%~1"
IF not DEFINED addition ECHO %line:~1%&GOTO :eof
IF "%~1"==%1 (
REM quoted
SET "line=%line% %addition: =+%"
) ELSE (
SET "line=%line% %addition%"
)
shift
GOTO process
You would need to change the setting of sourcedir to suit your circumstances.
I used a file named q34732271.txt containing your data for my testing.
The echo %%a shows your one line of data on the screen and the echo within the :process routine shows that line processed.
Batch is not an ideal language to process strings as it exhibits sensitivity to many symbols. This process should work provided you are happy to have space-strings compressed and the source string does not contain , ;,tab % or any other symbol that cmd treats specially.
Here is a pure batch-file solution that walks through the characters in each line in file line.txt, replaces all SPACEs in between a pair of quotation marks "" by + signs and stores the result in text_new.txt. The input string may contain any characters, even special ones:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem Define global constants here:
set "INFILE=line.txt"
set "OUTFILE=line_new.txt"
set "SEARCH= "
set "REPLACE=+"
set "KEEPQUOTES="
set "QUOTE="""
set "QUOTE=%QUOTE:~,1%"
set "QFLAG="
> "%OUTFILE%" (
for /F usebackq^ delims^=^ eol^= %%L in ("%INFILE%") do (
set "LINE=%%L"
call :SUB LINE
)
)
endlocal
exit /B
:SUB
setlocal EnableDelayedExpansion
set "LINE=!%1!"
set "LINENEW="
set /A "POS=0"
:LOOP
set "CHAR=!LINE:~%POS%,1!"
set /A "POS+=1"
if not defined CHAR (
echo(!LINENEW!
endlocal
exit /B
)
if "!CHAR!"=="!QUOTE!" (
if defined QFLAG (
set "QFLAG="
) else (
set "QFLAG=Quoted"
)
if defined KEEPQUOTES (
set "LINENEW=!LINENEW!!CHAR!"
)
) else if defined QFLAG (
if "!CHAR!"=="!SEARCH!" (
set "LINENEW=!LINENEW!!REPLACE!"
) else (
set "LINENEW=!LINENEW!!CHAR!"
)
) else (
set "LINENEW=!LINENEW!!CHAR!"
)
goto :LOOP

IF... OR IF... in a windows batch file

Is there a way to write an IF OR IF conditional statement in a windows batch-file?
For example:
IF [%var%] == [1] OR IF [%var%] == [2] ECHO TRUE
The zmbq solution is good, but cannot be used in all situations, such as inside a block of code like a FOR DO(...) loop.
An alternative is to use an indicator variable. Initialize it to be undefined, and then define it only if any one of the OR conditions is true. Then use IF DEFINED as a final test - no need to use delayed expansion.
FOR ..... DO (
set "TRUE="
IF cond1 set TRUE=1
IF cond2 set TRUE=1
IF defined TRUE (
...
) else (
...
)
)
You could add the ELSE IF logic that arasmussen uses on the grounds that it might perform a wee bit faster if the 1st condition is true, but I never bother.
Addendum - This is a duplicate question with nearly identical answers to Using an OR in an IF statement WinXP Batch Script
Final addendum - I almost forgot my favorite technique to test if a variable is any one of a list of case insensitive values. Initialize a test variable containing a delimitted list of acceptable values, and then use search and replace to test if your variable is within the list. This is very fast and uses minimal code for an arbitrarily long list. It does require delayed expansion (or else the CALL %%VAR%% trick). Also the test is CASE INSENSITIVE.
set "TEST=;val1;val2;val3;val4;val5;"
if "!TEST:;%VAR%;=!" neq "!TEST!" (echo true) else (echo false)
The above can fail if VAR contains =, so the test is not fool-proof.
If doing the test within a block where delayed expansion is needed to access current value of VAR then
for ... do (
set "TEST=;val1;val2;val3;val4;val5;"
for /f %%A in (";!VAR!;") do if "!TEST:%%A=!" neq "!TEST!" (echo true) else (echo false)
)
FOR options like "delims=" might be needed depending on expected values within VAR
The above strategy can be made reliable even with = in VAR by adding a bit more code.
set "TEST=;val1;val2;val3;val4;val5;"
if "!TEST:;%VAR%;=!" neq "!TEST!" if "!TEST:;%VAR%;=;%VAR%;"=="!TEST!" echo true
But now we have lost the ability of providing an ELSE clause unless we add an indicator variable. The code has begun to look a bit "ugly", but I think it is the best performing reliable method for testing if VAR is any one of an arbitrary number of case-insensitive options.
Finally there is a simpler version that I think is slightly slower because it must perform one IF for each value. Aacini provided this solution in a comment to the accepted answer in the before mentioned link
for %%A in ("val1" "val2" "val3" "val4" "val5") do if "%VAR%"==%%A echo true
The list of values cannot include the * or ? characters, and the values and %VAR% should not contain quotes. Quotes lead to problems if the %VAR% also contains spaces or special characters like ^, & etc. One other limitation with this solution is it does not provide the option for an ELSE clause unless you add an indicator variable. Advantages are it can be case sensitive or insensitive depending on presence or absence of IF /I option.
I don't think so. Just use two IFs and GOTO the same label:
IF cond1 GOTO foundit
IF cond2 GOTO foundit
ECHO Didn't find it
GOTO end
:foundit
ECHO Found it!
:end
A simple "FOR" can be used in a single line to use an "or" condition:
FOR %%a in (item1 item2 ...) DO IF {condition_involving_%%a} {execute_command}
Applied to your case:
FOR %%a in (1 2) DO IF %var%==%%a ECHO TRUE
Suppress executing twice
A comment pointed out that {execute_command} may be encountered twice. To avoid this, you can use a goto after the first encounter.
FOR %%a in (1 2) DO IF %var%==%%a (
ECHO TRUE
goto :continue
)
:continue
If you think there's a possibility that {execute_command} might be executed twice and you don't want that, you can just add && goto :eof:
FOR %%a in (1 2) DO IF %var%==%%a ECHO TRUE && goto :eof
Much simpler, and still on a single line.
Thanks for this post, it helped me a lot.
Dunno if it can help but I had the issue and thanks to you I found what I think is another way to solve it based on this boolean equivalence:
"A or B" is the same as "not(not A and not B)"
Thus:
IF [%var%] == [1] OR IF [%var%] == [2] ECHO TRUE
Becomes:
IF not [%var%] == [1] IF not [%var%] == [2] ECHO FALSE
Even if this question is a little older:
If you want to use if cond1 or cond 2 - you should not use complicated loops or stuff like that.
Simple provide both ifs after each other combined with goto - that's an implicit or.
//thats an implicit IF cond1 OR cond2 OR cond3
if cond1 GOTO doit
if cond2 GOTO doit
if cond3 GOTO doit
//thats our else.
GOTO end
:doit
echo "doing it"
:end
Without goto but an "inplace" action, you might execute the action 3 times, if ALL conditions are matching.
There is no IF <arg> OR or ELIF or ELSE IF in Batch, however...
Try nesting the other IF's inside the ELSE of the previous IF.
IF <arg> (
....
) ELSE (
IF <arg> (
......
) ELSE (
IF <arg> (
....
) ELSE (
)
)
The goal can be achieved by using IFs indirectly.
Below is an example of a complex expression that can be written quite concisely and logically in a CMD batch, without incoherent labels and GOTOs.
Code blocks between () brackets are handled by CMD as a (pathetic) kind of subshell. Whatever exit code comes out of a block will be used to determine the true/false value the block plays in a larger boolean expression. Arbitrarily large boolean expressions can be built with these code blocks.
Simple example
Each block is resolved to true (i.e. ERRORLEVEL = 0 after the last statement in the block has executed) / false, until the value of the whole expression has been determined or control jumps out (e.g. via GOTO):
((DIR c:\xsgdde /w) || (DIR c:\ /w)) && (ECHO -=BINGO=-)
Complex example
This solves the problem raised initially. Multiple statements are possible in each block but in the || || || expression it's preferable to be concise so that it's as readable as possible. ^ is an escape char in CMD batches and when placed at the end of a line it will escape the EOL and instruct CMD to continue reading the current batch of statements on the next line.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
(
(CALL :ProcedureType1 a b) ^
|| (CALL :ProcedureType2 sgd) ^
|| (CALL :ProcedureType1 c c)
) ^
&& (
ECHO -=BINGO=-
GOTO :EOF
)
ECHO -=no bingo for you=-
GOTO :EOF
:ProcedureType1
IF "%~1" == "%~2" (EXIT /B 0) ELSE (EXIT /B 1)
GOTO :EOF (this line is decorative as it's never reached)
:ProcedureType2
ECHO :ax:xa:xx:aa:|FINDSTR /I /L /C:":%~1:">nul
GOTO :EOF
It's possible to use a function, which evaluates the OR logic and returns a single value.
#echo off
set var1=3
set var2=5
call :logic_or orResult "'%var1%'=='4'" "'%var2%'=='5'"
if %orResult%==1 (
echo At least one expression is true
) ELSE echo All expressions are false
exit /b
:logic_or <resultVar> expression1 [[expr2] ... expr-n]
SETLOCAL
set "logic_or.result=0"
set "logic_or.resultVar=%~1"
:logic_or_loop
if "%~2"=="" goto :logic_or_end
if %~2 set "logic_or.result=1"
SHIFT
goto :logic_or_loop
:logic_or_end
(
ENDLOCAL
set "%logic_or.resultVar%=%logic_or.result%"
exit /b
)
If %x%==1 (
If %y%==1 (
:: both are equal to 1.
)
)
That's for checking if multiple variables equal value. Here's for either variable.
If %x%==1 (
:: true
)
If %x%==0 (
If %y%==1 (
:: true
)
)
If %x%==0 (
If %y%==0 (
:: False
)
)
I just thought of that off the top if my head. I could compact it more.
I realize this question is old, but I wanted to post an alternate solution in case anyone else (like myself) found this thread while having the same question. I was able to work around the lack of an OR operator by echoing the variable and using findstr to validate.
for /f %%v in ('echo %var% ^| findstr /x /c:"1" /c:"2"') do (
if %errorlevel% equ 0 echo true
)
While dbenham's answer is pretty good, relying on IF DEFINED can get you in loads of trouble if the variable you're checking isn't an environment variable. Script variables don't get this special treatment.
While this might seem like some ludicrous undocumented BS, doing a simple shell query of IF with IF /? reveals that,
The DEFINED conditional works just like EXIST except it takes an
environment variable name and returns true if the environment variable
is defined.
In regards to answering this question, is there a reason to not just use a simple flag after a series of evaluations? That seems the most flexible OR check to me, both in regards to underlying logic and readability. For example:
Set Evaluated_True=false
IF %condition_1%==true (Set Evaluated_True=true)
IF %some_string%=="desired result" (Set Evaluated_True=true)
IF %set_numerical_variable% EQ %desired_numerical_value% (Set Evaluated_True=true)
IF %Evaluated_True%==true (echo This is where you do your passing logic) ELSE (echo This is where you do your failing logic)
Obviously, they can be any sort of conditional evaluation, but I'm just sharing a few examples.
If you wanted to have it all on one line, written-wise, you could just chain them together with && like:
Set Evaluated_True=false
IF %condition_1%==true (Set Evaluated_True=true) && IF %some_string%=="desired result" (Set Evaluated_True=true) && IF %set_numerical_variable% EQ %desired_numerical_value% (Set Evaluated_True=true)
IF %Evaluated_True%==true (echo This is where you do your passing logic) ELSE (echo This is where you do your failing logic)
Never got exist to work.
I use
if not exist g:xyz/what goto h:
Else xcopy c:current/files g:bu/current
There are modifiers /a etc. Not sure which ones. Laptop in shop. And computer in office. I am not there.
Never got batch files to work above Windows XP
A much faster alternative I usually use is as follows, as I can "or" an arbitrary number of conditions that can fit in variable space
#(
Echo off
Set "_Match= 1 2 3 "
)
Set /a "var=3"
Echo:%_Match%|Find " %var% ">nul || (
REM Code for a false condition goes here
) && (
REM code for a true condition goes here.
)
it's quite simple, just use below
IF %var% == 1 (
ECHO TRUE)
IF %var% == 2 (
ECHO TRUE)
Another option is to display the current environment variables and exploit the default behaviour of FINDSTR:
FINDSTR "hello there" x.y searches for "hello" or "there" in file x.y.
So
SET | FINDSTR /I /X "var=1 var=2" >NUL
IF %ERRORLEVEL% EQU 0 (
ECHO TRUE
) ELSE (
ECHO FALSE
)
Where
/I Specifies that the search is not to be case-sensitive.
/X Prints lines that match exactly.
If regular expressions are preferred, use FINDSTR /R /I "^var=1$ ^var=2$" >NUL instead.
Edit: FINDSTR /R should be used if the variable string includes a space, e.g., FINDSTR /R /I "^var=1 a$ ^var=2 b$" >NUL.
Edit: If the variable string includes spaces, a literal search string should be used. E.g., FINDSTR /I /X /C:"var=1 a" /C:"var=2 b" >NUL.
There is no OR operator but you can write (the pseudocode)
IF [%var%] == [1] OR IF [%var%] == [2] ECHO TRUE
like
IF "%var%" == "1" SET "match=y"
IF "%var%" == "2" SET "match=y"
IF DEFINED match ECHO TRUE
Note that the double quotes prevents a syntax error from being triggered if var is undefined.
I took bogdan's solution to the next level by building an extern function that is a callable and clean abstraction of IF, so it can be used in inline blocks. Don't look further, if you build a batch library anyways.
lib.cmd
#ECHO OFF
SETLOCAL ENABLEEXTENSIONS
SHIFT & GOTO:%1
: Common batch extension library.
:::
: Performs conditional processing in batch programs. Is callable for inline use.
: Arguments:
: %1 - /I for case-insensitive comparison on strings, can be skipped for case-sensitive comparison.
: %2 - NOT to negate the result, can be skipped.
: %3 - EXIST for file checks or first string to compare with, also supports "string1"=="string2" (full condition).
: %4 - If EXIST is specified, path to the directory or file.
: Otherwise if %3 is not a full condition, this argument has to be one of:
: - == Compares both strings to be equal using lstrcmpW or lstrcmpiW (case-insensitive).
: - EQU Converts both strings to numbers and checks if they are equal.
: - NEQ Converts both strings to numbers and checks if they are not equal.
: - LSS Converts both strings to numbers and checks if the first is lesser than the second.
: - LEQ Converts both strings to numbers and checks if the first is lesser or equal than the second.
: - GTR Converts both strings to numbers and checks if the first is greater than the second.
: - GEQ Converts both strings to numbers and checks if the first is greater or equal than the second.
: If a string cannot be parsed to a number, its numeric representation will be used.
: This argument can be skipped, so == will be used.
: %5 - If %3 is not a full condition, this argument has to be the second string to compare.
: Outputs:
: Nothing
: Returns:
: 0, if the condition is met, 1 otherwise
:::
:test-if
IF "%~1"=="/I" SET "I=/I " & SHIFT
IF "%~1"=="/i" SET "I=/I " & SHIFT
IF "%~1"=="NOT" SET "NOT=NOT " & SHIFT
IF "%~1"=="not" SET "NOT=NOT " & SHIFT
IF "%~1"=="EXIST" SET "EXIST=EXIST " & SHIFT
IF "%~1"=="exist" SET "EXIST=EXIST " & SHIFT
SET "string1=%~1%"
IF "%~3"=="" (
SET "comp=^=^="
SET "string2=%~2"
) ELSE (
SET "comp=%~2"
SET "string2=%~3"
)
IF %I%%NOT%%EXIST% "%string1%" %comp% "%string2%" (
ENDLOCAL & EXIT /B 0
)
ENDLOCAL & EXIT /B 1
Usage
( ( CALL lib test-if "%1" == "foo" ) || ( CALL lib test-if "%1" == "bar" ) ) && (
ECHO "Argument is foo or bar"
)
lib is the path to the lib.cmd, the suffix .cmd is not mandatory for cmd-files
Any IF syntax is compatible with this abstraction, so you can also do things like test-if EXIST "path" or test-if not 300 LSS 200
The == in test-if "%1" == "foo" will be stripped away by batch and I address this fact in my case, but this causes test-if "%1" "foo" to be valid as well, it's not the standard though.
Realizing this is a bit of an old question, the responses helped me come up with a solution to testing command line arguments to a batch file; so I wanted to post my solution as well in case anyone else was looking for a similar solution.
First thing that I should point out is that I was having trouble getting IF ... ELSE statements to work inside of a FOR ... DO clause. Turns out (thanks to dbenham for inadvertently pointing this out in his examples) the ELSE statement cannot be on a separate line from the closing parens.
So instead of this:
FOR ... DO (
IF ... (
)
ELSE (
)
)
Which is my preference for readability and aesthetic reasons, you have to do this:
FOR ... DO (
IF ... (
) ELSE (
)
)
Now the ELSE statement doesn't return as an unrecognized command.
Finally, here's what I was attempting to do - I wanted to be able to pass several arguments to a batch file in any order, ignoring case, and reporting/failing on undefined arguments passed in. So here's my solution...
#ECHO OFF
SET ARG1=FALSE
SET ARG2=FALSE
SET ARG3=FALSE
SET ARG4=FALSE
SET ARGS=(arg1 Arg1 ARG1 arg2 Arg2 ARG2 arg3 Arg3 ARG3)
SET ARG=
FOR %%A IN (%*) DO (
SET TRUE=
FOR %%B in %ARGS% DO (
IF [%%A] == [%%B] SET TRUE=1
)
IF DEFINED TRUE (
SET %%A=TRUE
) ELSE (
SET ARG=%%A
GOTO UNDEFINED
)
)
ECHO %ARG1%
ECHO %ARG2%
ECHO %ARG3%
ECHO %ARG4%
GOTO END
:UNDEFINED
ECHO "%ARG%" is not an acceptable argument.
GOTO END
:END
Note, this will only report on the first failed argument. So if the user passes in more than one unacceptable argument, they will only be told about the first until it's corrected, then the second, etc.

Resources