2

I have a simple batch script test.bat that iterates thru command-line arguments:

@echo off
set prefix=C:\Program Files\MyProg
for %%x in (%*) do (
    echo %prefix%
)

this version of the script works without issue in cmd.exe:

C:\>test a b
C:\Program Files\MyProg
C:\Program Files\MyProg

However, if I modify the batch file to have a different prefix value, adding (x86):

set prefix=C:\Program Files (x86)\MyProg

the script fails:

C:\>test a b
\MyProg was unexpected at this time.

what a disaster! How can I resolve this issue?

Mike T
  • 34,456
  • 15
  • 128
  • 169
  • `set "prefix=C:\Program Files (x86)\MyProg"` or `set prefix=C:\Program Files (x86^)\MyProg`, though the first is preferred syntax. your issue is the closing parens, which interferes with `for` logic. +1 for your own solution – elzooilogico Feb 14 '20 at 06:54
  • @elzooilogico I am unsure how the caret escape on the closing parenthesis will solve the issue here? The only way the parenthesized block can not cause the error without using delayed expansion is when you double quote and escape the `)` i.e `set "prefix=C:\Program Files (x86^)\MyProg"` – Gerhard Feb 14 '20 at 08:05

3 Answers3

5

Enabling EnableDelayedExpansion with !prefix! solves this issue:

@echo off
set prefix=C:\Program Files (x86)\MyProg
setlocal EnableDelayedExpansion
for %%x in (%*) do (
    echo !prefix!
)
endlocal

!Unbelievable!

Mike T
  • 34,456
  • 15
  • 128
  • 169
  • +1 for using this site as it should be used. You had an issue you were struggling to understand, and requested assistance, providing a true MCVE, a 'before' and 'after', and actual paths etc. that all potential helpers would need to replicate your task. Then instead of hanging around waiting for somebody who isn't currently having the issue, to help you with, what is essentially, only your issue, you continued to try to help yourself. Then not only did you find a solution which worked for you, you posted it here with all the information required for future readers with similar needs. – Compo Feb 14 '20 at 03:56
  • 1
    Also for the purpose of this specific task, you only need to enable delayed expansion on the line above the `echo` then `endlocal` it on the line beneath. It seems as if it would be less resource intensive to enable it fewer times, but in my experience, I can say that I've had any noticeble degredation in performance even with a very high number of iterations. – Compo Feb 14 '20 at 04:00
2

Though you figured out that delayedexpansion fixes the issue inside of your parenthesized code block, given the example as is, you do not require the code block and can therefore get away without the parenthesis and without delayedexpansion. It is also suggested to double quote your set variable and value to eliminate possible creeping whitespace:

@echo off
set "prefix=C:\Program Files (x86)\MyProg"
for %%x in (%*) do echo %prefix%

I am also assuming that you wanted to use %%x in a way as well, therefore:

@echo off
set "prefix=C:\Program Files (x86)\MyProg"
for %%x in (%*) do echo %prefix%\%%~x
Gerhard
  • 18,114
  • 5
  • 20
  • 38
2

I recommend to first read How does the Windows Command Interpreter (CMD.EXE) parse scripts?

Windows command processor parses an entire code block starting with ( and ending with the matching ) before executing the command making use of the command block. During processing of a command block all environment variable references in the form %variable% are replaced by the current value of the environment variables or removed on an environment variable not existing at all. So the command block finally executed does not contain anymore any %variable%. That behavior is very good explained at Variables are not behaving as expected.

So the code

set prefix=C:\Program Files (x86)\MyProg
for %%x in (%*) do (
    echo %prefix%
)

results in executing FOR with

echo C:\Program Files (x86)\MyProg

The closing parenthesis ) is not inside an argument string being enclosed in ". For that reason the Windows command processor interprets it as end of the command block of command FOR.

It is syntactically not correct to specify one more command on same line after ) being interpreted as end of a command block without usage of an operator like & or && or ||. For that reason \MyProg is interpreted as syntactically invalid command after ) marking end of the command block.

The help output by running cmd /? in a command prompt window explains at end of last page that a file name containing a space or one of these characters &()[]{}^=;!'+,`~ must be enclosed in " to get all these characters interpreted literally with exception of ! if delayed environment variable expansion is enabled, too.

The usage of " around an argument string is not only required for file names, but for any argument string containing a space or one of these characters &()[]{}^=;!'+,`~ or the redirection operators <|> which should be also interpreted literally by cmd.exe.

So one solution would be:

set prefix=C:\Program Files (x86)\MyProg
for %%x in (%*) do (
    echo "%prefix%"
)

ECHO outputs the prefix with both ", but it is good practice to output file/folder names with ECHO always enclosed in double quotes.

Another solution is escaping ) with ^ to be interpreted as literal character. In this case it is important that the line with echo still has ^) after replacing %prefix% by the string value assigned to the environment variable prefix. For that reason it is necessary to define the prefix string with caret character interpreted as literal character which means two ^ are necessary left to ) on environment variable definition.

set prefix=C:\Program Files (x86^^)\MyProg
for %%x in (%*) do (
    echo %prefix%
)

The environment variable prefix is defined now with the string value:

C:\Program Files (x86^)\MyProg

This results in the FOR command block in getting ) interpreted as literal character.

It is also possible to define environment variable prefix with being enclosed in double quotes to get caret character ^ interpreted as literal character which avoids the need to double this character being otherwise interpreted as escape character.

set "prefix=C:\Program Files (x86^)\MyProg"
for %%x in (%*) do (
    echo %prefix%
)

One more solution is using delayed environment variable expansion:

@echo off
set prefix=C:\Program Files (x86)\MyProg
setlocal EnableDelayedExpansion
for %%x in (%*) do (
    echo !prefix!
)
endlocal

But there is a problem with this solution caused by double parsing of the command lines because of enabled delayed expansion as it can be seen on modifying the code to

@echo off
set prefix=C:\Program Files (x86)\MyProg
setlocal EnableDelayedExpansion
for %%x in (%*) do (
    echo !prefix!\%%~x
)
endlocal

and executing the batch file with the three arguments Hello! and Test and Start!. The exclamation marks are no longer interpreted as literal characters because of enabled delayed expansion. For that reason the output is:

C:\Program Files (x86)\MyProg\Hello

But the output should be:

C:\Program Files (x86)\MyProg\Hello!
C:\Program Files (x86)\MyProg\Test
C:\Program Files (x86)\MyProg\Start!

This output can be get with:

set "prefix=C:\Program Files (x86^)\MyProg"
for %%x in (%*) do (
    echo %prefix%\%%~x
)

However, really safe is printing the concatenated strings enclosed in double quotes.

set "prefix=%ProgramFiles(x86)%\MyProg"
for %%I in (%*) do (
    echo "%prefix%\%%~I"
)

The output is in this case for the above example with the three arguments Hello! and Test and Start!:

"C:\Program Files (x86)\MyProg\Hello!"
"C:\Program Files (x86)\MyProg\Test"
"C:\Program Files (x86)\MyProg\Start!"

See also my answer on Why is no string output with 'echo %var%' after using 'set var = text' on command line? It explains why the environment variable prefix is defined with using " left to variable name and at end of the string value assigned to the environment variable. That is the recommended syntax on definition of an environment variable.

Further, it is good practice not using as loop variable a case-sensitive interpreted letter for which a case-insensitive interpreted modifier exists as otherwise the results can be unexpected, especially on variable strings being concatenated dynamically.

Example:

@echo off
cls
echo Loop with %%~Xx:
for %%x in ("1" 2 3) do echo %%~Xx run.
echo Loop with %%~xX:
for %%X in ("1" 2 3) do echo %%~xX run.
echo Loop with %%~Ix:
for %%I in ("1" 2 3) do echo %%~Ix run.
echo Loop with %%~#x:
for %%# in ("1" 2 3) do echo %%~#x run.
pause

The output of this small batch file is:

Loop with %~Xx:
 run.
 run.
 run.
Loop with %~xX:
 run.
 run.
 run.
Loop with %~Ix:
1x run.
2x run.
3x run.
Loop with %~#x:
1x run.
2x run.
3x run.

The intention is here to get output the number with x appended and not to reference the not existing file extension of the three strings in set of command FOR.

Therefore the modifier letters ADFNPSTXZadfnpstxz should not be used as loop variable although it is possible in most cases.

Mofi
  • 38,783
  • 14
  • 62
  • 115