Windows批处理文件可选参数解析

103

我需要让我的批处理文件接受多个可选的命名参数。

mycmd.bat man1 man2 -username alice -otheroption
例如,我的命令有两个必填参数和两个可选参数(-username)的参数值为alice,-otheroption:
我想能够将这些值提取到变量中。
只是向已经解决过这个问题的人发出一个请求。天啊,这些批处理文件真让人头疼。

14
为什么你一定要用BAT文件而不是VBScript呢?也许可以用BAT文件的方式实现,但如果坚持使用BAT文件的方法,那么“你会进入一个痛苦的世界,孩子”。 :-) - Alek Davis
更好的选择是使用PowerShell。它具有非常先进和内置的参数管理功能。 - Bill_Stewart
3
@AlekDavis,你可能是对的,但我非常害怕VBScript。如果我不得不使用VBScript,我想我已经处于痛苦之中了。我很乐意编写批处理文件来自动化某些操作,然后也许如果我想使它对人们更有用,我会添加一些参数。通常其中一些是可选的。自从我离开银行业以来,我从未想过或有人建议“你需要一些VBScript”。 - icc97
@chickeninabiscuit:链接已失效,但Wayback机器人版本还可以使用。我不知道如果我编辑了你的评论是否会让它看起来像是我做了这项工作,所以我只是在这里粘贴它:http://web.archive.org/web/20090403050231/http://www.pcguide.com/vb/showthread.php?t=52323 - Brent Rittenhouse
6个回答

132

虽然我倾向于同意@AlekDavis的评论,但在NT shell中仍有几种方法可以实现这一点。

我会利用SHIFT命令和IF条件分支的方法来解决问题,类似于以下内容...

@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

2
好的 - 我明白为什么代码通常有效 - 循环中的 SHIFT 最终将选项移动到 arg 1,通常不会有任何问题。但是初始的 SHIFT /2 明显是一个错误,如果 arg 1 的值恰好与其中一个选项名称匹配,则会破坏结果。尝试运行 mycmd.bat -username anyvalue -username myName -otheroption othervalue。结果应该是 user=myName,other=othervalue;但是代码给出的结果是 user=-username,other=othervalue。通过用 SHIFT & SHIFT 替换 SHIFT /2 来修复此错误。 - dbenham
2
@Christian - ewall关于的说法是不正确的- 它不能替代& - dbenham
2
第二个问题:在 OP 问题中,“-otheroption” 后面没有跟随一个值,因此它可能是一个“-Flag”类型的选项,不应该消耗第二个参数。第三个问题:如果参数“%1”没有被内部“IF”语句处理,它会被静默忽略。代码应该显示一个消息,指示它不是一个有效的选项。这可以通过将所需的“SHIFT”和“GOTO:loop”语句添加到内部“IF”语句中,或者添加一个“ELSE”语句来完成。 - Kevin Fegan
1
这是好的代码,我经过多次搜索才找到它。只是我认为有一个问题。当一次使用-username-otheroption时,变量会保留值,如果再次运行没有这些参数的代码,则可以看到先前UsernameOther的值。我认为必须在代码开头重置变量,使用以下代码:SET user= & SET other= - Nabi K.A.Z.
1
另外,我认为最好使用%~1%~2代替%1%2来忽略双引号(")两侧的值。 - Nabi K.A.Z.
显示剩余5条评论

82

选定的答案是有效的,但它可以改进一下。

  • 选项应该初始化为默认值。
  • 最好保留%0以及必需的参数%1和%2。
  • 每个选项都需要一个IF块,随着选项数量的增加,这将变得很麻烦。
  • 最好有一种简单而简洁的方式来快速定义所有选项和默认值。
  • 支持作为标志的独立选项(选项后没有值)也会很好。
  • 我们不知道参数是否用引号括起来。我们也不知道是否使用转义字符传递了参数值。最好使用%~1访问参数并将赋值语句放在引号内。然后批处理可以依赖于缺少括号引号,但特殊字符通常无需转义。(这不是绝对可靠的,但它可以处理大多数情况)

我的解决方案依赖于创建一个OPTIONS变量,该变量定义所有选项及其默认值。OPTIONS还用于测试提供的选项是否有效。通过简单地将选项值存储在与选项名称相同的变量中,可以节省大量代码。代码量不受定义的选项数量影响;只需更改OPTIONS定义即可。

编辑 - 此外,如果强制位置参数的数量发生更改,则必须更改:loop代码。例如,通常所有参数都是命名的,在这种情况下,您希望从位置1开始解析参数而不是3。因此,在:loop内,所有3变为1,4变为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!

实际上,代码并不多。上面的大部分代码都是注释。这里是完全相同的代码,没有注释。

@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!

该解决方案提供了Windows批处理中的Unix风格参数。这在Windows中并不是常见的方式——批处理通常在必需参数之前加上选项,并且选项以/为前缀。
此解决方案中使用的技术易于适应Windows风格的选项。
  • 解析循环总是查找%1处的选项,并持续到第一个参数不以/开头为止。
  • 请注意,如果名称以/开头,则必须将SET赋值括在引号中。
    SET /VAR=VALUE会失败
    SET "/VAR=VALUE"会成功。我已经在我的解决方案中这样做了。
  • 标准的Windows风格排除了第一个必需参数值以/开头的可能性。可以通过使用隐式定义的//选项来消除此限制,该选项用作退出选项解析循环的信号。对于//“选项”,不会存储任何内容。



更新2015-12-28:支持选项值中的!

在上面的代码中,启用延迟扩展时会扩展每个参数,这意味着!很可能会被删除,否则类似!var!的内容将被扩展。此外,如果存在!,则^也可能被删除。下面对未注释的代码进行了小修改,以消除限制,使选项值中保留!^

@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!

谢谢您提供的非常优雅的解决方案,我比接受的答案更喜欢这个。您唯一遗漏的是如何在批处理脚本中使用命名参数,例如echo %-username%。 - Roboblob
3
非常优雅的解决方案。如果您考虑使用它,请注意,提供的代码将从第3个参数开始解析。这在一般情况下并不是您想要的。请用“1”替换“3”来修复此问题。@dbenham如果您能在原始帖子中更明显地指出这一点,那就太好了,我可能不是唯一一个最初感到困惑的人。 - ocroquette
@ocroquette - 很好的观点。考虑到我必须回答具体问题,我没有太多选择。但是,当必需位置参数的数量发生变化时,代码必须进行修改。我会尝试编辑我的答案以让它更清晰明了。 - dbenham
我喜欢这个。谢谢你的解决方案,它在2019年仍然可以很干净地用于一些IDE脚本编写! - Robert Smith

24

如果您想使用可选参数,但不想使用命名参数,则这种方法对我很有效。我认为这是更易于理解的代码。

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%

2
我认为这只有在用户知道要使用""代替参数时才有效。否则,我怎么能设置一个数据库名称但留空数据库服务器呢? - Sean Long
你还有什么其他的想法,如何实现另一个答案呢?@SeanLong - sehe
这绝对是我最容易的选择。谢谢你的发布 :) - Steve Bauman
对于仍然需要处理老式Windows批处理文件的人,截至2022-11-07 uu985leastpain1667870138,这绝对是本帖中最不痛苦的解决方案。 - dreftymac
非常好的回答。谢谢。 - undefined
显示剩余2条评论

5

动态变量创建

enter image description here

优点

  • 适用于>9个参数
  • 保留%1%2,... %*
  • 适用于/arg-arg两种风格
  • 无需关于参数的先前了解
  • 实现与主要例程分离

缺点

  • 旧参数可能会泄漏到连续运行中,因此使用setlocal进行本地作用域或编写相应的:CLEAR-ARGS例程!
  • 尚未支持别名(例如将--force转换为-f
  • 不支持空的""参数

用法

以下是一个示例,说明以下参数与.bat变量的关系:

>> 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

实现

@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

1
你也可以将 :ARG-PARSER 程序的内容移动到一个外部的 .bat 文件中,例如 arg-parser.bat,以便其他脚本也可以使用它。 - Simon Streicher

2
这里是参数解析器。您可以混合任何字符串参数(保持不变)或转义选项(单个或选项/值对)。要测试它,请取消最后两个语句的注释并运行:
getargs anystr1 anystr2 /test$1 /test$2=123 /test$3 str anystr3

转义字符被定义为"_SEP_=/",如果需要重新定义,请进行修改。
@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

2

我曾经编写过一个程序,用于处理批处理文件中的短选项(-h)、长选项(--help)和非选项参数。

这个技巧包括:

  • 非选项参数后跟随选项参数。

  • 对于像“--help”这样没有参数的选项,使用shift运算符。

  • 对于需要参数的选项,使用两次shift运算符。

  • 循环遍历标签以处理所有命令行参数。

  • 对于不需要进一步操作的选项(如“--help”),退出脚本并停止处理。

  • 编写帮助函数以指导用户。

以下是我的代码。

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

以下是有关编程的内容,请将其翻译成中文。请只返回翻译后的文本: 这样更易读,谢谢 - oldpride

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接