It's an Interpreter
If using complex mechanisms like subshelling (using commands enclosed in round brackets), keep in mind batch files are not really like using a programming language - batch language isn't that accurate, there are several flaws in the interpreter we have to take care of.
As cmd.exe being an interpreter, it reads line after line, and does some pattern replacing in advance to executing the line.
A "line" can be
* a single physical line
* several physical lines connected by a caret (^) at the end of each line
* several physical lines enclosed in round brackets
A single line is hence:
1: | @echo off |
1: | if 1 == 1 (echo yes) else (echo no) |
1: 2: | echo Writing a long ^ text here |
1: 2: 3: 4: 5: | if 1 == 1 ( echo yes ) else ( echo no ) |
Why should one know? As said already, it's an interpreter we use, and it's applying some string replacements.
If you are using environment variables, which is one of the things you will use every time, this gets most important.
E.g. the following code will not work as expected:
1: 2: 3: 4: 5: 6: 7: | set example=1 if %example% == 1 ( set example=2 echo %example% ) REM result: 1 REM expected: 2 |
In the first line, variable example is set to 1. In the next "line", containing IF up to the closing bracket, each occurance of %example% is replaced by the value set at that time, which is 1. That's why we almost always use Delayed Expansion. I will not further discuss that feature here; however, it might bring another headache in some (very advanced) cases:
1: 2: 3: 4: 5: | setlocal EnableDelayedExpansion set pwd=#^!pwd# echo !pwd! %pwd% set pwd=#^^^!pwd# echo !pwd! %pwd% |
Try it, and try to spot the exclamation mark ...
Escape!
That leads us to escaping. If you have to escape some control characters to override its special meaning, e.g. round brackets, pipe, percent sign, aso., you will have to escape them again for each interpreter go. Sometimes, that is. And that is the problem here, you will have to try your code ALWAYS for nested commands working as desired.
It's hard to construct examples for that, as they seem to follow no logic at all, so you will have to believe me. Working example which needs escaping is:
1: | for /F "tokens=*" %%F in ('dir /a:-d . /s/b ^| find "\temp\"') do del /f %%F
|
The pipe character needed escaping, because it is used for piping the output of one command to the input of another. If not escaped, cmd.exe would try to parse it when reading the complete line, which leads to a syntax error because of incomplete FOR.
Goto Issue
Coming back to variable expansion, oBdA (http://www.experts-exchan
1: 2: 3: 4: 5: 6: 7: 8: 9: | @echo off set Var=Value 1 if 1==1 ( echo Var when entering the block: %Var%; setting it to "Value 2" now ... set Var=Value 2 goto SomeLabel :SomeLabel echo Var is now: %Var% ) |
This will output Var as being 2, while without GOTO it results in 1, the value set before the block. This is because cmd.exe will reinterpret the code after it executes a GOTO. This is only a showcase, of course, noone would ever come to the conclusion having to use a goto in a block?!
The Case of the Missing Bracket
Another pitfall you might run into is accidently omitting a closing bracket. This will do NOTHING, and give you some brain work trying to debug. Example:
1: 2: 3: 4: | (echo Start for /L %%L in (1,1,100) do echo %%L echo End REM --- Missing closing ")" here |
Will not echo anything - because the line is never ended because of the missing ")", and the command is never executed ...
No Whitespace allowed!
Just another strange thing to note: If you try to break a line into two by using a caret, you can't use whitespace at the beginning of the line:
1: 2: | for %F in (*) do ^ echo %%F |
will give you errors that the command " " could not be found.
Block or one-liner?
Something which might puzzle you (I am puzzled at least) is the one-liner behaviour versus using blocks:
1: | if 1 == 1 echo yes & echo another yes |
This line will echo both commands. I.e. the line after IF is handled as one command. You could expect, as & is the command separator, that the IF is evaluated up to the ampersand, and after this is starting a new command. Wrong!
1: 2: | for %F in (*) do @echo %F > output.txt (for %F in (*) do @echo %F) > output.txt |
Line 1 will write the last result into output.txt. You could expect that the stdout redirection into a file (> output.txt) would apply to the FOR, but it's not, it's applied to the command (ECHO). And that's meaning that the file is overwritten in each go of the FOR loop. This also means that the file is opened and closed multiple times, which is a performance issue, so even if you use the append (>>) redirector it would be bad practice.
The third line will do what we want: collect all echo generated by the FOR loop, and writing it into the file all at once.
Redirecting output
Talking about redirection: Funny applications are
1: 2: 3: 4: 5: | >> output.txt echo yes >> output.txt if 1 == 1 echo This is a syntax error >> output.txt (if 1 == 1 echo yes, three) >> output.txt dir c:\* | findstr MyFiles >> output.txt (dir c:\* | findstr MyFiles) |
The first and third line actually work, the echoed text is appended to output.txt. After understanding the first line, the second seems to be logically correct - but instead, a syntax error is generated, stating IF is not expected at that position.
After adding brackets, as in third line, everything is fine.
Forth line doesn't what we want. DIR output is stored in the file, and FINDSTR is acting on a empty pipe. Hence the fifth line has to be used.
As a rule of thumb, if you want to use above syntax: Complex commands, like IF and FOR, which could or actually do require round brackets as part of there syntax, need to be enclosed in round brackets. Command chains (|, &, &&, ||) have to be enclosed, too.
The following syntax is not recommended, even if working as desired - it is obscuring the meaning and hard to read:
1: | dir c:\* | >> output.txt findstr MyFiles |
Very handy for dynamically generated scripts, e.g. for FTP, is this syntax
1: 2: 3: 4: 5: 6: 7: 8: 9: | @echo off call :genscript > script.cmd call script.cmd exit /b :genscript echo @echo off echo dir exit /b |
But be aware that piping does not work. If we change above call to
1: | call :genscript | findstr dir |
we get the same error as if we would try to use that call on commandline. I did not find any workaround to get this running yet.
This article is subject to occasional changes. I'll post a short comment about each addition.
For example:
You wrote:
<quote>
set example=1
if %example% == 1 (
set example=2
echo %example%
)
will not work as expected.
<end quote>
Better would have been
set example=1
if %example% == 1 (
set example=2
echo %example%
)
The expected result of this code is the output
2
The actual output of the code is
1
...