Windows is full of “features” that probably seemed like a good idea at the time but which turn out to be a mayor pain in certain situation. One of these is a feature in the handling of arguments passed to batch scripts: when you pass an argument that contains an equal sign or semicolon to a batch script, the argument gets split in two as if you had typed a space instead. Here is a simple example of a batch script that shows the first four arguments it was passed:
@ECHO OFF
ECHO Argument #1 = [ %1 ]
ECHO Argument #2 = [ %2 ]
ECHO Argument #3 = [ %3 ]
ECHO Argument #4 = [ %4 ]
ECHO Arguments = [ %1 %2 %3 %4 ]
And here is the output of this script, when executed with quotes, equal signs and semicolons in the command-line arguments:
C:\>test.cmd 1=not2;not3 " 2 " 3 4
Argument #1 = [ 1 ]
Argument #2 = [ not2 ]
Argument #3 = [ not3 ]
Argument #4 = [ " 2 " ]
Arguments = [ 1 not2 not3 " 2 " ]
This “feature” has existed since MS-DOS and apparently it got reported often enough to MS that they created a second KB article to tell you that they know it is a problem. Unfortunately, both KB articles offer no work-around. They also do not mention that this affects all versions of Windows to date as well.
If you use any of my tools, you may have noticed that I often allow you to specify options through command-line arguments in the form “–option=value”. If you are trying to write even the simplest batch wrapper script for any of these tools, you will immediately run into problems because of this “feature”: it is impossible for the script to know if the user typed “–option value details” or “–option=value;details” through conventional argument parsing.
Because I was frequently jumping through all kinds of flaming hoops to work around this “feature”, I decided to create a proper work-around to address this issue.
The solution
After reading through the “help” output for most commands, I found that “help call” explains the existence of “%*”:
C:\>help call
Calls one batch program from another.
<snip>
%* in a batch script refers to all the arguments (e.g. %1 %2 %3
%4 %5 ...)
<snip>
You may have noticed in my original example that “%*” retains the arguments passed to the script as is (without substituting equal signs, semicolons or any other characters for spaces). So, one solution would be to parse this string manually. In “help set” I found a way to extract a single character from a string as well as a way to create a counter that can be used as an index into the string:
C:\>help set
Displays, sets, or removes cmd.exe environment variables.
<snip>
Two new switches have been added to the SET command:
SET /A expression
SET /P variable=[promptString]
The /A switch specifies that the string to the right of the equal sign
is a numerical expression that is evaluated. The expression evaluator
is pretty simple and supports the following operations, in decreasing
order of precedence:
() - grouping
! ~ - - unary operators
* / % - arithmetic operators
+ - - arithmetic operators
<< >> - logical shift
& - bitwise and
^ - bitwise exclusive or
| - bitwise or
= *= /= %= += -= - assignment
&= ^= |= <<= >>=
, - expression separator
<snip>
May also specify substrings for an expansion.
%PATH:~10,5%
would expand the PATH environment variable, and then use only the 5
characters that begin at the 11th (offset 10) character of the expanded
result. If the length is not specified, then it defaults to the
remainder of the variable value. If either number (offset or length) is
negative, then the number used is the length of the environment variable
value added to the offset or length specified.
<snip>
In “help cmd” we can read about delayed environment variable expansion, which can be used to read/write environment variables at runtime:
C:\>help cmd
Starts a new instance of the Windows command interpreter
<snip>
/V:ON Enable delayed environment variable expansion using ! as the
delimiter. For example, /V:ON would allow !var! to expand the
variable var at execution time. The var syntax expands variables
at input time, which is quite a different thing when inside of a FOR
loop.
<snip>
And in “help setlocal” we can find that it is possible to enable delayed environment variable expansion without having to restart cmd.exe:
C:\>help setlocal
<snip>
ENABLEDELAYEDEXPANSION / DISABLEDELAYEDEXPANSION
enable or disable delayed environment variable
expansion. These arguments takes precedence over the CMD
/V:ON or /V:OFF switches. See CMD /? for details.
These modifications last until the matching ENDLOCAL command,
regardless of their setting prior to the SETLOCAL command.
<snip>
By combing these features, we can create a batch script “function” that parses the command line character by character, taking into consideration quotes, equal signs, semicolons, etc… and creating an environment variable for each argument, in quoted, unquoted and original form, as well as an environment variable that contains the number of arguments:
:PARSE_ARGV
SET PARSE_ARGV_ARG=[]
SET PARSE_ARGV_END=FALSE
SET PARSE_ARGV_INSIDE_QUOTES=FALSE
SET /A ARGC = 0
SET /A PARSE_ARGV_INDEX=1
:PARSE_ARGV_LOOP
CALL :PARSE_ARGV_CHAR !PARSE_ARGV_INDEX! "%%ARGV:~!PARSE_ARGV_INDEX!,1%%"
IF ERRORLEVEL 1 (
EXIT /B 1
)
IF !PARSE_ARGV_END! == TRUE (
EXIT /B 0
)
SET /A PARSE_ARGV_INDEX=!PARSE_ARGV_INDEX! + 1
GOTO :PARSE_ARGV_LOOP
:PARSE_ARGV_CHAR
IF ^%~2 == ^" (
SET PARSE_ARGV_END=FALSE
SET PARSE_ARGV_ARG=.%PARSE_ARGV_ARG:~1,-1%%~2.
IF !PARSE_ARGV_INSIDE_QUOTES! == TRUE (
SET PARSE_ARGV_INSIDE_QUOTES=FALSE
) ELSE (
SET PARSE_ARGV_INSIDE_QUOTES=TRUE
)
EXIT /B 0
)
IF %2 == "" (
IF !PARSE_ARGV_INSIDE_QUOTES! == TRUE (
EXIT /B 1
)
SET PARSE_ARGV_END=TRUE
) ELSE IF NOT "%~2!PARSE_ARGV_INSIDE_QUOTES!" == " FALSE" (
SET PARSE_ARGV_ARG=[%PARSE_ARGV_ARG:~1,-1%%~2]
EXIT /B 0
)
IF NOT !PARSE_ARGV_INDEX! == 1 (
SET /A ARGC = !ARGC! + 1
SET ARG!ARGC!=%PARSE_ARGV_ARG:~1,-1%
IF ^%PARSE_ARGV_ARG:~1,1% == ^" (
SET ARG!ARGC!_=%PARSE_ARGV_ARG:~2,-2%
SET ARG!ARGC!Q=%PARSE_ARGV_ARG:~1,-1%
) ELSE (
SET ARG!ARGC!_=%PARSE_ARGV_ARG:~1,-1%
SET ARG!ARGC!Q="%PARSE_ARGV_ARG:~1,-1%"
)
SET PARSE_ARGV_ARG=[]
SET PARSE_ARGV_INSIDE_QUOTES=FALSE
)
EXIT /B 0
To use it in a batch script, you should add the code to the end of the script and call it at the start like so:
@ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET ARGV=.%*
CALL :PARSE_ARGV
IF ERRORLEVEL 1 (
ECHO Cannot parse arguments
ENDLOCAL
EXIT /B 1
)
REM Main code goes here
ENDLOCAL
EXIT /B 0
:PARSE_ARGV
<snip>
Note: you should make sure your main code does not “fall through” into the PARSE_ARGV function by using “EXIT /B 0″ at the end of your code.
For each argument passed to the script, numbered environment variables will be created to store the arguments value “as is” and in quoted and unquoted form. Additionally, an environment variable will be created that contains the number of arguments supplied. Here is a list of the created environment variables and what they contain:
- !ARGC! – Contains the number of arguments,
- !ARGx! – Contains the value of the x-th argument as is,
- !ARGx_! – Contains the value of the x-th argument with any quotes removed,
- !ARGxQ! – Contains the value of the x-th argument with quotes added if not already present,
For example: the values for the first argument will be stored in !ARG1!, !ARG1_! and !ARG1Q!.
To make it easier to access any argument(s) by number, the following functions can be used:
To read the values of !ARGx!, !ARGx_! and !ARGxQ! for argument number x into environment variables !y!, !y_! and !yQ! use the below code and “CALL :GETARG x y”:
:GETARG
SET %2=!ARG%1!
SET %2_=!ARG%1_!
SET %2Q=!ARG%1Q!
EXIT /B 0
eg. “CALL :GETARG 1 FIRST_ARGUMENT” will set !FIRST_ARGUMENT!, !FIRST_ARGUMENT_! and !FIRST_ARGUMENTQ! to the values of !ARG1!, !ARG1_! and !ARG1Q! respectively.
To read the values of the numbered arguments x-y into environment variable !z! use the below code and “CALL :GETARGS x y z”:
:GETARGS
SET %3=
FOR /L %%I IN (%1,1,%2) DO (
IF %%I == %1 (
SET %3=!ARG%%I!
) ELSE (
SET %3=!%3! !ARG%%I!
)
)
EXIT /B 0
eg. “CALL :GETARGS 1 3 FIRST_THREE_ARGUMENT” will set !FIRST_THREE_ARGUMENTS! to the values of !ARG1!, !ARG2! and !ARG3! concatinated.
To conclude this post, here is an example that shows how you can use these functions to create a script that parses the command-line arguments correctly and greatly simplifies the handling of a variable number of arguments:
@ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET ARGV=.%*
CALL :PARSE_ARGV
IF ERRORLEVEL 1 (
ECHO Cannot parse arguments
ENDLOCAL
EXIT /B 1
)
ECHO Arguments count = !ARGC!
FOR /L %%I IN (1,1,!ARGC!) DO (
CALL :GETARG %%I ARGI
ECHO Argument #%%I = [ !ARGI! ]
)
CALL :GETARGS 1 !ARGC! ARGS
ECHO Arguments = [ !ARGS! ]
ENDLOCAL
EXIT /B 0
:GETARG
<snip>
:GETARGS
<snip>
:PARSE_ARGV
<snip>
You can download the example here. If put the output of this code for the initial test case and various numbers of arguments below:
C:\>test.cmd 1=not2;not3 " 2 " 3 4
Arguments count = 4
Argument #1 = [ 1=not2;not3 ]
Argument #2 = [ " 2 " ]
Argument #3 = [ 3 ]
Argument #4 = [ 4 ]
Arguments = [ 1=not2;not3 " 2 " 3 4 ]
C:\>test.cmd
Arguments count = 0
Arguments = [ ]
C:\>test.cmd 1
Arguments count = 1
Argument #1 = [ 1 ]
Arguments = [ 1 ]
C:\>test.cmd 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Arguments count = 18
Argument #1 = [ 1 ]
Argument #2 = [ 2 ]
Argument #3 = [ 3 ]
Argument #4 = [ 4 ]
Argument #5 = [ 5 ]
Argument #6 = [ 6 ]
Argument #7 = [ 7 ]
Argument #8 = [ 8 ]
Argument #9 = [ 9 ]
Argument #10 = [ 10 ]
Argument #11 = [ 11 ]
Argument #12 = [ 12 ]
Argument #13 = [ 13 ]
Argument #14 = [ 14 ]
Argument #15 = [ 15 ]
Argument #16 = [ 16 ]
Argument #17 = [ 17 ]
Argument #18 = [ 18 ]
Arguments = [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ]
Limitations/caveats
- This code depends on the Command Extensions feature as described in “help cmd”, if they are disabled and cannot be enabled or are unavailable, the code will not run. As far as I can tell, all versions of Windows since Windows XP at least support the feature and allow it to be enabled if it is disabled, so I do not expect this to be a problem.
- This code does not handle unclosed quotes in the arguments. If you execute a test script that uses this code with an unclosed quote in the arguments,as in [test.cmd "], an error message will be shown and the script will not run. Because I do not know of any valid use-case, I expect that this is often caused by an accidentally forgotten closing quote. I assume that the user benefits more from an error that allows him/her to fix the missing quote than from a program that tries to make assumptions about what the user wants.
I hope you’ll find this useful. Please use the comments to let me know if you do or if you have any suggestions!

