Windows Bat file optional argument parsing
Asked Answered
T

6

103

I need my bat file to accept multiple optional named arguments.

mycmd.bat man1 man2 -username alice -otheroption

For example my command has 2 mandatory parameters, and two optional parameters (-username) that has an argument value of alice, and -otheroption:

I'd like to be able to pluck these values into variables.

Just putting out a call to anyone that has already solved this. Man these bat files are a pain.

Thora answered 20/10, 2010 at 0:5 Comment(4)
Any reason why you have to do this in a BAT file and not say in VBScript? There may be a way to do this in a BAT file, but sticking to the BAT file approach, you "you're entering a world of pain, son". :-)Politian
Even better is to use PowerShell. It has very advanced and built-in parameter management.Chlamys
@AlekDavis, you're probably right, but I'm highly VBScript-phobic. If I ever found myself having to use VBScript I think I'd already be in a world of pain. I'll happily write myself a batch file to automate something, then perhaps if I want to make it more useful for people then I'll add some arguments. Often some of these are optional. At no point since I left the world of banking have I thought, or has someone suggested "you'll want some VBScript for that".Jenkins
@chickeninabiscuit: Link no longer works. Wayback machine version does. I don't know if I edit your comment if It will change it to look like I did the work or not I'm just going t o paste it here instead: http://web.archive.org/web/20090403050231/http://www.pcguide.com/vb/showthread.php?t=52323.Bluish
M
132

Though I tend to agree with @AlekDavis' comment, there are nonetheless several ways to do this in the NT shell.

The approach I would take advantage of the SHIFT command and IF conditional branching, something like this...

@ECHO OFF

SET man1=%1
SET man2=%2
SHIFT & SHIFT

:loop
IF NOT "%1"=="" (
    IF "%1"=="-username" (
        SET user=%2
        SHIFT
    )
    IF "%1"=="-otheroption" (
        SET other=%2
        SHIFT
    )
    SHIFT
    GOTO :loop
)

ECHO Man1 = %man1%
ECHO Man2 = %man2%
ECHO Username = %user%
ECHO Other option = %other%

REM ...do stuff here...

:theend
Malformation answered 20/10, 2010 at 18:31 Comment(10)
SHIFT /2 shifts starting at arg 2. It does not shift two times. Shouldn't it be replaced with SHIFT & SHIFT?Shrieval
OK - I see why the code usually works - the SHIFT in the loop eventually moves the options to arg 1 and no harm is done (usually). But the initial SHIFT /2 is definitely a bug that will corrupt the results if arg 1 value happens to match one of the option names. Try running mycmd.bat -username anyvalue -username myName -otheroption othervalue. The result should be user=myName, other=othervalue; but the code gives user=-username, other=othervalue. The bug is fixed by replacing SHIFT /2 with SHIFT & SHIFT.Shrieval
@Malformation I'm surprised such a system could be accomplished with so little code. I've only got one question though, why do you need SHIFT & SHIFT?Commingle
@Christian: The two SET commands (SET man1=%1 and SET man2=%2) are learning the first and second arguments, which are positional and mandatory. The SHIFT & SHIFT just takes them out of the queue so that the later commands which look for --options don't see them. It could be two separate lines of just SHIFT, I chose the & to save space -- you could use a semicolon instead.Malformation
@Commingle - ewall is incorrect about ; - it cannot be used in place of &.Shrieval
This is a great start, but I see 3 problems. First: a) After line 10, %1 and %2 have been "Used". b) The single SHIFT in line 11 moves the value of %2 down 1 position and is then available (again) as %1, and the "Unused" value of %3 is available as %2. c) The IF in line 13 sees this already "Used" value from %2 which is now in %1. d) If the "user-name" from line 10 looks like "-otheroption", it will be used again and other will be set to the new value in %2, which was probably the name of the next option. --->Privy
e) The SHIFT in lines 11, 15, and 17 will remove 3 items from the argument list. f) The new value in %1 is probably the value that was meant to be stored in other, but it will silently be ignored. 2 ways to fix this: 1) At line 11, add another SHIFT and a GOTO :loop. Or 2) Change the 2 internal IF statements into IF --- ELSE IF, like: ) ELSE (IF "%1"=="-otheroption".... --->Privy
The 2nd problem: In the OP Question, the -otheroption was not followed by a value, so it was probably meant as a -Flag type option, and should not consume a 2nd argument. 3rd: If the argument %1 is not handled by the internal IF statements, it is silently ignored. The code should probably display a message indicating it was not a valid option. This could be done by adding the required SHIFT and GOTO :loop statements into the internal IF statements, or by adding an ELSE statement.Privy
This is good code, and I find it after many search. just I thinks there are one problem. When once time use -username and -otheroption, variables keep values, And if run again code without these params, you can see previous values for Username and Other. I thinks must be reset variables in first of code with this code: SET user= & SET other=Competition
And also, I thinks better use %~1 and%~2 instead of %1 and%2 for ignoring double quote (") both side values.Competition
S
82

The selected answer works, but it could use some improvement.

  • The options should probably be initialized to default values.
  • It would be nice to preserve %0 as well as the required args %1 and %2.
  • It becomes a pain to have an IF block for every option, especially as the number of options grows.
  • It would be nice to have a simple and concise way to quickly define all options and defaults in one place.
  • It would be good to support stand-alone options that serve as flags (no value following the option).
  • We don't know if an arg is enclosed in quotes. Nor do we know if an arg value was passed using escaped characters. Better to access an arg using %~1 and enclose the assignment within quotes. Then the batch can rely on the absence of enclosing quotes, but special characters are still generally safe without escaping. (This is not bullet proof, but it handles most situations)

My solution relies on the creation of an OPTIONS variable that defines all of the options and their defaults. OPTIONS is also used to test whether a supplied option is valid. A tremendous amount of code is saved by simply storing the option values in variables named the same as the option. The amount of code is constant regardless of how many options are defined; only the OPTIONS definition has to change.

EDIT - Also, the :loop code must change if the number of mandatory positional arguments changes. For example, often times all arguments are named, in which case you want to parse arguments beginning at position 1 instead of 3. So within the :loop, all 3 become 1, and 4 becomes 2.

@echo off
setlocal enableDelayedExpansion

:: Define the option names along with default values, using a <space>
:: delimiter between options. I'm using some generic option names, but 
:: normally each option would have a meaningful name.
::
:: Each option has the format -name:[default]
::
:: The option names are NOT case sensitive.
::
:: Options that have a default value expect the subsequent command line
:: argument to contain the value. If the option is not provided then the
:: option is set to the default. If the default contains spaces, contains
:: special characters, or starts with a colon, then it should be enclosed
:: within double quotes. The default can be undefined by specifying the
:: default as empty quotes "".
:: NOTE - defaults cannot contain * or ? with this solution.
::
:: Options that are specified without any default value are simply flags
:: that are either defined or undefined. All flags start out undefined by
:: default and become defined if the option is supplied.
::
:: The order of the definitions is not important.
::
set "options=-username:/ -option2:"" -option3:"three word default" -flag1: -flag2:"

:: Set the default option values
for %%O in (%options%) do for /f "tokens=1,* delims=:" %%A in ("%%O") do set "%%A=%%~B"

:loop
:: Validate and store the options, one at a time, using a loop.
:: Options start at arg 3 in this example. Each SHIFT is done starting at
:: the first option so required args are preserved.
::
if not "%~3"=="" (
  set "test=!options:*%~3:=! "
  if "!test!"=="!options! " (
    rem No substitution was made so this is an invalid option.
    rem Error handling goes here.
    rem I will simply echo an error message.
    echo Error: Invalid option %~3
  ) else if "!test:~0,1!"==" " (
    rem Set the flag option using the option name.
    rem The value doesn't matter, it just needs to be defined.
    set "%~3=1"
  ) else (
    rem Set the option value using the option as the name.
    rem and the next arg as the value
    set "%~3=%~4"
    shift /3
  )
  shift /3
  goto :loop
)

:: Now all supplied options are stored in variables whose names are the
:: option names. Missing options have the default value, or are undefined if
:: there is no default.
:: The required args are still available in %1 and %2 (and %0 is also preserved)
:: For this example I will simply echo all the option values,
:: assuming any variable starting with - is an option.
::
set -

:: To get the value of a single parameter, just remember to include the `-`
echo The value of -username is: !-username!

There really isn't that much code. Most of the code above is comments. Here is the exact same code, without the comments.

@echo off
setlocal enableDelayedExpansion

set "options=-username:/ -option2:"" -option3:"three word default" -flag1: -flag2:"

for %%O in (%options%) do for /f "tokens=1,* delims=:" %%A in ("%%O") do set "%%A=%%~B"
:loop
if not "%~3"=="" (
  set "test=!options:*%~3:=! "
  if "!test!"=="!options! " (
      echo Error: Invalid option %~3
  ) else if "!test:~0,1!"==" " (
      set "%~3=1"
  ) else (
      set "%~3=%~4"
      shift /3
  )
  shift /3
  goto :loop
)
set -

:: To get the value of a single parameter, just remember to include the `-`
echo The value of -username is: !-username!


This solution provides Unix style arguments within a Windows batch. This is not the norm for Windows - batch usually has the options preceding the required arguments and the options are prefixed with /.

The techniques used in this solution are easily adapted for a Windows style of options.

  • The parsing loop always looks for an option at %1, and it continues until arg 1 does not begin with /
  • Note that SET assignments must be enclosed within quotes if the name begins with /.
    SET /VAR=VALUE fails
    SET "/VAR=VALUE" works. I am already doing this in my solution anyway.
  • The standard Windows style precludes the possibility of the first required argument value starting with /. This limitation can be eliminated by employing an implicitly defined // option that serves as a signal to exit the option parsing loop. Nothing would be stored for the // "option".


Update 2015-12-28: Support for ! in option values

In the code above, each argument is expanded while delayed expansion is enabled, which means that ! are most likely stripped, or else something like !var! is expanded. In addition, ^ can also be stripped if ! is present. The following small modification to the un-commented code removes the limitation such that ! and ^ are preserved in option values.

@echo off
setlocal enableDelayedExpansion

set "options=-username:/ -option2:"" -option3:"three word default" -flag1: -flag2:"

for %%O in (%options%) do for /f "tokens=1,* delims=:" %%A in ("%%O") do set "%%A=%%~B"
:loop
if not "%~3"=="" (
  set "test=!options:*%~3:=! "
  if "!test!"=="!options! " (
      echo Error: Invalid option %~3
  ) else if "!test:~0,1!"==" " (
      set "%~3=1"
  ) else (
      setlocal disableDelayedExpansion
      set "val=%~4"
      call :escapeVal
      setlocal enableDelayedExpansion
      for /f delims^=^ eol^= %%A in ("!val!") do endlocal&endlocal&set "%~3=%%A" !
      shift /3
  )
  shift /3
  goto :loop
)
goto :endArgs
:escapeVal
set "val=%val:^=^^%"
set "val=%val:!=^!%"
exit /b
:endArgs

set -

:: To get the value of a single parameter, just remember to include the `-`
echo The value of -username is: !-username!
Shrieval answered 17/11, 2011 at 5:33 Comment(3)
Thanks for very ellegant solution, i like this more than the one accepted as answer. Only thing you missed is to say how to use the named parameters in the batch script, for example echo %-username%Cute
Very elegant solution. If you consider using this, be aware that the code as provided will start to parse at argument 3. This is not what you want in the general case. Replace the "3" by "1"s to fix this. @Shrieval it would be nice if you could make this more obvious in the original post, I am probably not the only one who was confused at first.Lively
@Lively - Good point. I didn't have much choice, given that I had to answer the specific question. But the code does have to be modified for when the number of mandatory positional arguments changes. I'll attempt to edit my answer to make it clear.Shrieval
M
25

If you want to use optional arguments, but not named arguments, then this approach worked for me. I think this is much easier code to follow.

REM Get argument values.  If not specified, use default values.
IF [%1]==[] ( SET "DatabaseServer=localhost" ) ELSE ( SET "DatabaseServer=%1" )
IF [%2]==[] ( SET "DatabaseName=MyDatabase" ) ELSE ( SET "DatabaseName=%2" )

REM Do work
ECHO Database Server = %DatabaseServer%
ECHO Database Name   = %DatabaseName%
Monicamonie answered 31/3, 2014 at 9:52 Comment(4)
IF "%1"=="" will fail if argument contains quotes itself. Just use any other non-special symbol instead, like ._=[]# etcBerceuse
I don't think this works unless the user knows to use "" in place of an argument. Otherwise, how can I ever set a DB name but leave the DB server blank?Gelatin
How else do you see another answer achieve that? @SeanLongGroth
@SeanLong Right, so the user must obey the order. So, you should put the most required arguments on first places.Jardiniere
B
8

Dynamic variables creation

enter image description here

Pros

  • Works for >9 arguments
  • Keeps %1, %2, ... %* in tact
  • Works for both /arg and -arg style
  • No prior knowledge about arguments
  • Implementation is separate from main routine

Cons

  • Old arguments may leak into consecutive runs, therefore use setlocal for local scoping or write an accompanying :CLEAR-ARGS routine!
  • No alias support yet (like --force to -f)
  • No empty "" argument support

Usage

Here is an example how the following arguments relate to .bat variables:

>> testargs.bat /b 3 -c /d /e /f /g /h /i /j /k /bar 5 /foo "c:\"

echo %*        | /b 3 -c /d /e /f /g /h /i /j /k /bar 5 /foo "c:\"
echo %ARG_FOO% | c:\
echo %ARG_A%   |
echo %ARG_B%   | 3
echo %ARG_C%   | 1
echo %ARG_D%   | 1

Implementation

@echo off
setlocal

CALL :ARG-PARSER %*

::Print examples
echo: ALL: %*
echo: FOO: %ARG_FOO%
echo: A:   %ARG_A%
echo: B:   %ARG_B%
echo: C:   %ARG_C%
echo: D:   %ARG_D%


::*********************************************************
:: Parse commandline arguments into sane variables
:: See the following scenario as usage example:
:: >> thisfile.bat /a /b "c:\" /c /foo 5
:: >> CALL :ARG-PARSER %*
:: ARG_a=1
:: ARG_b=c:\
:: ARG_c=1
:: ARG_foo=5
::*********************************************************
:ARG-PARSER
    ::Loop until two consecutive empty args
    :loopargs
        IF "%~1%~2" EQU "" GOTO :EOF

        set "arg1=%~1" 
        set "arg2=%~2"
        shift

        ::Allow either / or -
        set "tst1=%arg1:-=/%"
        if "%arg1%" NEQ "" (
            set "tst1=%tst1:~0,1%"
        ) ELSE (
            set "tst1="
        )

        set "tst2=%arg2:-=/%"
        if "%arg2%" NEQ "" (
            set "tst2=%tst2:~0,1%"
        ) ELSE (
            set "tst2="
        )


        ::Capture assignments (eg. /foo bar)
        IF "%tst1%" EQU "/"  IF "%tst2%" NEQ "/" IF "%tst2%" NEQ "" (
            set "ARG_%arg1:~1%=%arg2%"
            GOTO loopargs
        )

        ::Capture flags (eg. /foo)
        IF "%tst1%" EQU "/" (
            set "ARG_%arg1:~1%=1"
            GOTO loopargs
        )
    goto loopargs
GOTO :EOF
Bourassa answered 1/5, 2020 at 22:54 Comment(1)
You can also move the contents of the :ARG-PARSER routine to an external .bat file like arg-parser.bat to make it available to other scripts as well.Bourassa
A
2

Here is the arguments parser. You can mix any string arguments (kept untouched) or escaped options (single or option/value pairs). To test it uncomment last 2 statements and run as:

getargs anystr1 anystr2 /test$1 /test$2=123 /test$3 str anystr3

Escape char is defined as "_SEP_=/", redefine if needed.

@echo off

REM Command line argument parser. Format (both "=" and "space" separators are supported):
REM   anystring1 anystring2 /param1 /param2=value2 /param3 value3 [...] anystring3 anystring4
REM Returns enviroment variables as:
REM   param1=1
REM   param2=value2
REM   param3=value3
REM Leading and traling strings are preserved as %1, %2, %3 ... %9 parameters
REM but maximum total number of strings is 9 and max number of leading strings is 8
REM Number of parameters is not limited!

set _CNT_=1
set _SEP_=/

:PARSE

if %_CNT_%==1 set _PARAM1_=%1 & set _PARAM2_=%2
if %_CNT_%==2 set _PARAM1_=%2 & set _PARAM2_=%3
if %_CNT_%==3 set _PARAM1_=%3 & set _PARAM2_=%4
if %_CNT_%==4 set _PARAM1_=%4 & set _PARAM2_=%5
if %_CNT_%==5 set _PARAM1_=%5 & set _PARAM2_=%6
if %_CNT_%==6 set _PARAM1_=%6 & set _PARAM2_=%7
if %_CNT_%==7 set _PARAM1_=%7 & set _PARAM2_=%8
if %_CNT_%==8 set _PARAM1_=%8 & set _PARAM2_=%9

if "%_PARAM2_%"=="" set _PARAM2_=1

if "%_PARAM1_:~0,1%"=="%_SEP_%" (
  if "%_PARAM2_:~0,1%"=="%_SEP_%" (
    set %_PARAM1_:~1,-1%=1
    shift /%_CNT_%
  ) else (
    set %_PARAM1_:~1,-1%=%_PARAM2_%
    shift /%_CNT_%
    shift /%_CNT_%
  )
) else (
  set /a _CNT_+=1
)

if /i %_CNT_% LSS 9 goto :PARSE

set _PARAM1_=
set _PARAM2_=
set _CNT_=

rem getargs anystr1 anystr2 /test$1 /test$2=123 /test$3 str anystr3
rem set | find "test$"
rem echo %1 %2 %3 %4 %5 %6 %7 %8 %9

:EXIT
Adamantine answered 14/12, 2016 at 23:2 Comment(0)
G
2

Once I had written a program that handle the short (-h), long (--help) and non-option arguments in batch file. This techniques includes:

  • non-option arguments followed by a option arguments.

  • shift operator for those options that have no argument like '--help'.

  • two time shift operator for those options that require an argument.

  • loop through a label for processing all command line arguments.

  • Exit script and stop processing for those options that no need to require further action like '--help'.

  • Wrote help functions for user guidiness

Here is my code.

set BOARD=
set WORKSPACE=
set CFLAGS=
set LIB_INSTALL=true
set PREFIX=lib
set PROGRAM=install_boards

:initial
 set result=false
 if "%1" == "-h" set result=true
 if "%1" == "--help" set result=true
 if "%result%" == "true" (
 goto :usage
 )
 if "%1" == "-b" set result=true
 if "%1" == "--board" set result=true
 if "%result%" == "true" (
 goto :board_list
 )
 if "%1" == "-n" set result=true
 if "%1" == "--no-lib" set result=true
 if "%result%" == "true" (
 set LIB_INSTALL=false
 shift & goto :initial
 )
 if "%1" == "-c" set result=true
 if "%1" == "--cflag" set result=true
 if "%result%" == "true" (
 set CFLAGS=%2
 if not defined CFLAGS (
 echo %PROGRAM%: option requires an argument -- 'c'
 goto :try_usage
 )
 shift & shift & goto :initial
 )
 if "%1" == "-p" set result=true
 if "%1" == "--prefix" set result=true
 if "%result%" == "true" (
 set PREFIX=%2
 if not defined PREFIX (
 echo %PROGRAM%: option requires an argument -- 'p'
 goto :try_usage
 )
 shift & shift & goto :initial
 )

:: handle non-option arguments
set BOARD=%1
set WORKSPACE=%2

goto :eof


:: Help section

:usage
echo Usage: %PROGRAM% [OPTIONS]... BOARD... WORKSPACE
echo Install BOARD to WORKSPACE location.
echo WORKSPACE directory doesn't already exist!
echo.
echo Mandatory arguments to long options are mandatory for short options too.
echo   -h, --help                   display this help and exit
echo   -b, --boards                 inquire about available CS3 boards
echo   -c, --cflag=CFLAGS           making the CS3 BOARD libraries for CFLAGS
echo   -p. --prefix=PREFIX          install CS3 BOARD libraries in PREFIX
echo                                [lib]
echo   -n, --no-lib                 don't install CS3 BOARD libraries by default
goto :eof

:try_usage
echo Try '%PROGRAM% --help' for more information
goto :eof
Gormandize answered 19/12, 2017 at 12:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.