Análisis de argumentos opcionales del archivo Windows Bat

84

Necesito que mi archivo bat acepte múltiples argumentos con nombre opcionales.

mycmd.bat man1 man2 -username alice -otheroption

Por ejemplo, mi comando tiene 2 parámetros obligatorios y dos parámetros opcionales (-username) que tiene un valor de argumento de alice y -otheroption:

Me gustaría poder introducir estos valores en variables.

Solo llamo a cualquiera que ya haya resuelto esto. Hombre, estas limas de murciélago son un fastidio.

galleta de pollo
fuente
12
¿Alguna razón por la que tiene que hacer esto en un archivo BAT y no decirlo en VBScript? Puede haber una forma de hacer esto en un archivo BAT, pero si se apega al enfoque del archivo BAT, "estás entrando en un mundo de dolor, hijo". :-)
Alek Davis
Aún mejor es usar PowerShell. Tiene una gestión de parámetros muy avanzada e incorporada.
Bill_Stewart
1
@AlekDavis, probablemente tengas razón, pero soy muy fóbico a VBScript. Si alguna vez tuviera que usar VBScript, creo que ya estaría en un mundo de dolor. Con mucho gusto escribiré yo mismo un archivo por lotes para automatizar algo, luego, tal vez, si quiero hacerlo más útil para las personas, agregaré algunos argumentos. A menudo, algunos de estos son opcionales. En ningún momento desde que dejé el mundo de la banca, he pensado o alguien me ha sugerido "querrás algo de VBScript para eso".
icc97
@chickeninabiscuit: Link ya no funciona. La versión de la máquina Wayback lo hace. No sé si puedo editar el comentario si Cambiará que se vea como lo hice el trabajo o no me voy a pegar aquí su lugar: http://web.archive.org/web/20090403050231/http://www.pcguide.com/vb/showthread.php?t=52323.
Brent Rittenhouse

Respuestas:

112

Aunque tiendo a estar de acuerdo con el comentario de @AlekDavis , hay varias formas de hacer esto en el shell de NT.

El enfoque que aprovecharía el comando SHIFT y la ramificación condicional IF , algo como esto ...

@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
ewall
fuente
1
SHIFT /2turnos comenzando en arg 2. No cambia dos veces. ¿No debería ser reemplazado por SHIFT & SHIFT?
dbenham
2
OK, veo por qué el código generalmente funciona: el SHIFT en el bucle finalmente mueve las opciones a arg 1 y no se hace daño (generalmente). Pero el SHIFT / 2 inicial es definitivamente un error que corromperá los resultados si el valor de arg 1 coincide con uno de los nombres de las opciones. Intenta correr mycmd.bat -username anyvalue -username myName -otheroption othervalue. El resultado debe ser user = myName, other = othervalue; pero el código da user = -username, other = othervalue. El error se corrige reemplazando SHIFT /2con SHIFT & SHIFT.
dbenham
2
@Christian - ewall es incorrecto acerca de ;- no se puede usar en lugar de &.
dbenham
1
Este es un gran comienzo, pero veo 3 problemas. Primero: a) Después de la línea 10, %1y %2han sido "Usados". b) El sencillo SHIFTen la línea 11 mueve el valor de %21 posición hacia abajo y luego está disponible (nuevamente) como %1, y el valor "No utilizado" de %3está disponible como %2. c) El IFen la línea 13 ve este valor ya "Usado" del %2que ahora está en %1. d) Si el "nombre de usuario" de la línea 10 se ve así "-otheroption", se usará nuevamente y otherse establecerá con el nuevo valor en %2, que probablemente era el nombre de la siguiente opción. --->
Kevin Fegan
2
El segundo problema: en la pregunta OP, el -otheroptionno fue seguido por un valor, por lo que probablemente se pensó como una -Flagopción de tipo y no debería consumir un segundo argumento. 3º: Si el argumento %1no es manejado por las IFdeclaraciones internas , se ignora silenciosamente. El código probablemente debería mostrar un mensaje que indique que no es una opción válida. Esto se puede hacer agregando las declaraciones requeridas SHIFTy GOTO :loopen las IFdeclaraciones internas , o agregando una ELSEdeclaración.
Kevin Fegan
74

La respuesta seleccionada funciona, pero podría mejorarse un poco.

  • Las opciones probablemente deberían inicializarse a los valores predeterminados.
  • Sería bueno conservar% 0 así como los argumentos obligatorios% 1 y% 2.
  • Se convierte en una molestia tener un bloque IF para cada opción, especialmente a medida que aumenta la cantidad de opciones.
  • Sería bueno tener una forma simple y concisa de definir rápidamente todas las opciones y valores predeterminados en un solo lugar.
  • Sería bueno admitir opciones independientes que sirvan como indicadores (sin valor después de la opción).
  • No sabemos si un argumento está entre comillas. Tampoco sabemos si se pasó un valor arg utilizando caracteres de escape. Es mejor acceder a un argumento usando% ~ 1 y encerrar la asignación entre comillas. Entonces, el lote puede confiar en la ausencia de comillas adjuntas, pero los caracteres especiales siguen siendo generalmente seguros sin escaparse. (Esto no es a prueba de balas, pero maneja la mayoría de situaciones)

Mi solución se basa en la creación de una variable OPTIONS que define todas las opciones y sus valores predeterminados. OPTIONS también se utiliza para probar si una opción proporcionada es válida. Se guarda una enorme cantidad de código simplemente almacenando los valores de las opciones en variables con el mismo nombre que la opción. La cantidad de código es constante independientemente de cuántas opciones se definan; solo tiene que cambiar la definición de OPCIONES.

EDITAR - Además, el código de bucle: debe cambiar si cambia el número de argumentos posicionales obligatorios. Por ejemplo, muchas veces se nombran todos los argumentos, en cuyo caso desea analizar los argumentos que comienzan en la posición 1 en lugar de 3. Por lo tanto, dentro del ciclo:, los 3 se convierten en 1 y el 4 en 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!

Realmente no hay tanto código. La mayor parte del código anterior son comentarios. Aquí está exactamente el mismo código, sin los comentarios.

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


Esta solución proporciona argumentos de estilo Unix dentro de un lote de Windows. Esta no es la norma para Windows: por lo general, el lote tiene las opciones que preceden a los argumentos requeridos y las opciones tienen el prefijo /.

Las técnicas utilizadas en esta solución se adaptan fácilmente al estilo de opciones de Windows.

  • El ciclo de análisis siempre busca una opción en %1, y continúa hasta que arg 1 no comienza con/
  • Tenga en cuenta que las asignaciones SET deben estar entre comillas si el nombre comienza con /.
    SET /VAR=VALUEfalla
    SET "/VAR=VALUE"funciona. De todos modos, ya estoy haciendo esto en mi solución.
  • El estilo estándar de Windows excluye la posibilidad de que el primer valor de argumento requerido comience con /. Esta limitación se puede eliminar empleando una //opción definida implícitamente que sirve como señal para salir del ciclo de análisis de opciones. No se almacenará nada para la //"opción".


Actualización 2015-12-28: Soporte para !valores de opción in

En el código anterior, cada argumento se expande mientras la expansión retardada está habilitada, lo que significa que !lo más probable es que se eliminen, o que !var!se expanda algo similar . Además, ^también se puede quitar si !está presente. La siguiente pequeña modificación al código sin comentarios elimina la limitación de modo que !y ^se conservan en los valores de las opciones.

@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!
dbenham
fuente
Gracias por una solución muy elegante, me gusta más esta que la aceptada como respuesta. Lo único que se perdió es decir cómo usar los parámetros nombrados en el script por lotes, por ejemplo echo% -username%
Roboblob
1
Solución muy elegante. Si considera usar esto, tenga en cuenta que el código proporcionado comenzará a analizarse en el argumento 3. Esto no es lo que desea en el caso general. Reemplace el "3" por "1" para solucionar este problema. @dbenham sería bueno si pudieras hacer esto más obvio en la publicación original, probablemente no soy el único que estaba confundido al principio.
ocroquette
@ocroquette - Buen punto. No tuve muchas opciones, dado que tenía que responder la pregunta específica. Pero el código debe modificarse para cuando cambie el número de argumentos posicionales obligatorios. Intentaré editar mi respuesta para aclararla.
dbenham
Me encanta esto. Gracias por la solución, ¡todavía funciona en 2019 de manera bastante limpia para algunas secuencias de comandos IDE!
Robert Smith
18

Si desea utilizar argumentos opcionales, pero no argumentos con nombre, entonces este enfoque funcionó para mí. Creo que este es un código mucho más fácil de seguir.

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%
charlie.mott
fuente
IF "%1"==""fallará si el argumento contiene comillas. Simplemente use cualquier otro símbolo no especial en su lugar, como ._ = [] #, etc.
Viernes
2
No creo que esto funcione a menos que el usuario sepa usar "" en lugar de un argumento. De lo contrario, ¿cómo puedo establecer un nombre de base de datos pero dejar el servidor de base de datos en blanco?
Sean Long
¿De qué otra manera ves que otra respuesta logre eso? @SeanLong
sehe
@SeanLong Derecho, por lo que el usuario debe obedecer la orden. Por lo tanto, debe colocar los argumentos más requeridos en los primeros lugares.
Martin Braun
Esta fue definitivamente la opción más fácil para mí. Gracias por publicar :)
Steve Bauman
2

Creación de variables dinámicas

ingrese la descripción de la imagen aquí

Pros

  • Funciona para> 9 argumentos
  • Mantiene %1, %2... %*intacto
  • Funciona para ambos /argy -argestilo
  • Sin conocimiento previo sobre argumentos
  • La implementación está separada de la rutina principal

Contras

  • Los argumentos antiguos pueden filtrarse en ejecuciones consecutivas, por lo tanto, utilícelos setlocalpara el alcance local o escriba una :CLEAR-ARGSrutina adjunta .
  • Sin el apoyo del alias (como --forcea -f)
  • Sin ""soporte de argumento vacío

Uso

A continuación, se muestra un ejemplo de cómo se relacionan los siguientes argumentos con las variables .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

Implementación

@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_C%


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

Simon Streicher
fuente
También puede mover el contenido de la :ARG-PARSERrutina a un archivo .bat externo arg-parser.batpara que también esté disponible para otros scripts.
Simon Streicher
1

Una vez que había escrito un programa que maneja los argumentos cortos (-h), largos (--help) y sin opción en un archivo por lotes. Esta técnica incluye:

  • argumentos que no son opciones seguidos de argumentos de opción.

  • operador shift para aquellas opciones que no tienen argumentos como '--help'.

  • operador de dos turnos de tiempo para aquellas opciones que requieren un argumento.

  • recorrer una etiqueta para procesar todos los argumentos de la línea de comandos.

  • Salga de la secuencia de comandos y detenga el procesamiento de aquellas opciones que no requieran acciones adicionales como '--help'.

  • Funciones de ayuda escritas para la orientación del usuario

Aquí está mi código.

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
Solucionador de ecuaciones
fuente
1

Aquí está el analizador de argumentos. Puede mezclar cualquier argumento de cadena (que se mantenga intacto) o opciones de escape (pares únicos o de opción / valor). Para probarlo, descomente las últimas 2 declaraciones y ejecútelo como:

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

El carácter de escape se define como "_SEP_=/", redefinir si es necesario.

@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
Oscar
fuente