Przetwarzanie opcjonalnych argumentów w pliku Windows Bat

84

Mój plik bat musi akceptować wiele opcjonalnych nazwanych argumentów.

mycmd.bat man1 man2 -username alice -otheroption

Na przykład moje polecenie ma 2 obowiązkowe parametry i dwa opcjonalne parametry (-username), które ma wartość argumentu alicja i -otheroption:

Chciałbym móc wstawić te wartości do zmiennych.

Po prostu dzwonię do każdego, kto już to rozwiązał. Człowieku, te nietoperze są uciążliwe.

ciastko z kurczaka
źródło
12
Czy jest jakiś powód, dla którego musisz to zrobić w pliku BAT, a nie w VBScript? Może istnieć sposób, aby to zrobić w pliku BAT, ale trzymając się podejścia BAT, „wkraczasz w świat bólu, synu”. :-)
Alek Davis
Jeszcze lepiej jest użyć PowerShell. Posiada bardzo zaawansowane i wbudowane zarządzanie parametrami.
Bill_Stewart,
1
@AlekDavis, prawdopodobnie masz rację, ale bardzo fobię na VBScript. Gdybym kiedykolwiek musiał używać VBScript, myślę, że byłbym już w świecie bólu. Z przyjemnością napiszę sobie plik wsadowy, aby coś zautomatyzować, a może jeśli chcę, aby był bardziej użyteczny dla ludzi, dodam kilka argumentów. Często niektóre z nich są opcjonalne. W żadnym momencie, odkąd opuściłem świat bankowości, nie pomyślałem, czy ktoś nie zasugerował: „przydałby się do tego VBScript”.
icc97
@chickeninabiscuit: Link już nie działa. Wersja maszyny Wayback tak. Nie wiem, czy mogę edytować swój komentarz, jeśli będzie to zmienić, aby wyglądać jak ja pracę, czy nie jestem po prostu będzie go wkleić tutaj zamiast: http://web.archive.org/web/20090403050231/http://www.pcguide.com/vb/showthread.php?t=52323.
Brent Rittenhouse,

Odpowiedzi:

112

Chociaż zwykle zgadzam się z komentarzem @AlekDavis , jest kilka sposobów, aby to zrobić w powłoce NT.

Podejście skorzystałbym z polecenia SHIFT i rozgałęzienia warunkowego IF , coś takiego ...

@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
źródło
1
SHIFT /2przesunięcia rozpoczynające się od arg 2. Nie przesuwa się dwa razy. Czy nie należy go zastąpić SHIFT & SHIFT?
dbenham
2
OK - rozumiem, dlaczego kod zwykle działa - SHIFT w pętli ostatecznie przenosi opcje na arg 1 i nic się nie dzieje (zwykle). Ale początkowy SHIFT / 2 jest zdecydowanie błędem, który zepsuje wyniki, jeśli wartość arg 1 będzie pasować do jednej z nazw opcji. Spróbuj biegać mycmd.bat -username anyvalue -username myName -otheroption othervalue. Wynik powinien mieć postać user = myName, other = othervalue; ale kod daje user = -username, other = othervalue. Błąd jest ustalana poprzez zastąpienie SHIFT /2z SHIFT & SHIFT.
dbenham
2
@Christian - ewall jest niepoprawny ;- nie można go używać zamiast &.
dbenham
1
To świetny początek, ale widzę 3 problemy. Po pierwsze: a) Po linii 10 %1i %2zostały „użyte”. b) Pojedynczy SHIFTw linii 11 przesuwa wartość o %21 pozycję w dół i jest następnie dostępny (ponownie) jako %1, a wartość „Nieużywane” %3jest dostępna jako %2. c) W IFwierszu 13 widzimy tę już „Użytą” wartość, z %2której jest teraz w %1. d) Jeśli "nazwa użytkownika" z linii 10 będzie wyglądać "-otheroption", zostanie użyta ponownie i otherzostanie ustawiona na nową wartość w %2, która prawdopodobnie była nazwą następnej opcji. --->
Kevin Fegan,
2
Drugi problem: w pytaniu OP -otheroptionnie następowała wartość, więc prawdopodobnie była ona pomyślana jako -Flagopcja typu i nie powinna zużywać drugiego argumentu. Po trzecie: Jeśli argument %1nie jest obsługiwany przez instrukcje wewnętrzne IF, jest po cichu ignorowany. Kod powinien prawdopodobnie wyświetlić komunikat wskazujący, że nie była to prawidłowa opcja. Można to zrobić, dodając wymagane instrukcje SHIFTi GOTO :loopdo instrukcji wewnętrznych IFlub dodając ELSEinstrukcję.
Kevin Fegan,
74

Wybrana odpowiedź działa, ale przydałaby się pewna poprawa.

  • Opcje powinny prawdopodobnie zostać zainicjalizowane na wartości domyślne.
  • Byłoby dobrze zachować% 0, jak również wymagane argumenty% 1 i% 2.
  • Uciążliwe staje się posiadanie bloku IF dla każdej opcji, zwłaszcza gdy rośnie liczba opcji.
  • Byłoby miło mieć prosty i zwięzły sposób szybkiego definiowania wszystkich opcji i wartości domyślnych w jednym miejscu.
  • Byłoby dobrze wspierać samodzielne opcje, które służą jako flagi (brak wartości po opcji).
  • Nie wiemy, czy argument jest ujęty w cudzysłów. Nie wiemy też, czy wartość arg została przekazana przy użyciu znaków ucieczki. Lepiej uzyskać dostęp do argumentu za pomocą% ~ 1 i umieścić przypisanie w cudzysłowie. Wówczas partia może polegać na braku ujętych cudzysłowów, ale znaki specjalne są nadal ogólnie bezpieczne bez zmiany znaczenia. (To nie jest kuloodporne, ale radzi sobie w większości sytuacji)

Moje rozwiązanie polega na utworzeniu zmiennej OPTIONS, która definiuje wszystkie opcje i ich wartości domyślne. OPCJE są również używane do testowania, czy podana opcja jest prawidłowa. Ogromna ilość kodu jest zapisywana po prostu poprzez przechowywanie wartości opcji w zmiennych o takich samych nazwach jak opcja. Ilość kodu jest stała, niezależnie od liczby zdefiniowanych opcji; musi się zmienić tylko definicja OPTIONS.

EDYCJA - Ponadto kod pętli: musi ulec zmianie, jeśli zmieni się liczba obowiązkowych argumentów pozycyjnych. Na przykład, często wszystkie argumenty są nazwane, w takim przypadku chcesz przeanalizować argumenty zaczynające się od pozycji 1 zamiast 3. Zatem w pętli: wszystkie 3 stają się 1, a 4 staje się 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!

Naprawdę nie ma tak dużo kodu. Większość powyższego kodu to komentarze. Oto dokładnie ten sam kod, bez komentarzy.

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


To rozwiązanie zapewnia argumenty w stylu uniksowym w ramach wsadu systemu Windows. Nie jest to norma dla systemu Windows - batch zwykle ma opcje poprzedzające wymagane argumenty, a opcje są poprzedzone przedrostkiem /.

Techniki zastosowane w tym rozwiązaniu można łatwo dostosować do stylu opcji systemu Windows.

  • Pętla analizująca zawsze szuka opcji na %1i kontynuuje, dopóki arg 1 nie zacznie się od/
  • Zwróć uwagę, że przypisania SET muszą być umieszczone w cudzysłowach, jeśli nazwa zaczyna się od /.
    SET /VAR=VALUEnie
    SET "/VAR=VALUE"działa. I tak już to robię w moim rozwiązaniu.
  • Standardowy styl Windows wyklucza możliwość pierwszej wymaganej wartości argumentu zaczynającej się od /. To ograniczenie można wyeliminować, stosując niejawnie zdefiniowaną //opcję, która służy jako sygnał do wyjścia z pętli analizowania opcji. Nic nie zostanie zapisane dla //„opcji”.


Aktualizacja 2015-12-28: Wsparcie dla !wartości opcji

W powyższym kodzie każdy argument jest rozwijany, podczas gdy opóźnione rozwijanie jest włączone, co oznacza, że !najprawdopodobniej jest usuwany lub coś podobnego !var!jest rozwijane. Ponadto ^można go usunąć, jeśli !jest obecny. Następująca mała modyfikacja kodu bez komentarza usuwa takie ograniczenie !i ^jest zachowywana w wartościach opcji.

@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
źródło
Dzięki za bardzo eleganckie rozwiązanie, podoba mi się to bardziej niż to przyjęte jako odpowiedź. Jedyne, czego przegapiłeś, to powiedzieć, jak używać nazwanych parametrów w skrypcie wsadowym, na przykład echo% -username%
Roboblob
1
Bardzo eleganckie rozwiązanie. Jeśli rozważasz użycie tego, pamiętaj, że dostarczony kod zacznie analizować od argumentu 3. W ogólnym przypadku nie tego chcesz. Zamień „3” na „1”, aby to naprawić. @dbenham byłoby miło, gdybyś mógł to wyjaśnić w oryginalnym poście, prawdopodobnie nie jestem jedynym, który był zdezorientowany na początku.
ocroquette
@ocroquette - Słuszna uwaga. Nie miałem wielkiego wyboru, biorąc pod uwagę, że musiałem odpowiedzieć na konkretne pytanie. Ale kod musi zostać zmodyfikowany na wypadek zmiany liczby obowiązkowych argumentów pozycyjnych. Spróbuję zmienić moją odpowiedź, aby była jasna.
dbenham
Uwielbiam to. Dziękuję za rozwiązanie, nadal działa w 2019 całkiem czysto dla niektórych skryptów IDE!
Robert Smith
18

Jeśli chcesz używać argumentów opcjonalnych, ale nie nazwanych, to takie podejście zadziałało. Myślę, że jest to znacznie łatwiejszy kod do naśladowania.

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
źródło
IF "%1"==""zakończy się niepowodzeniem, jeśli argument zawiera sam cudzysłów. Po prostu użyj innego, nie specjalnego symbolu, na przykład ._ = [] # etc
Fr0sT,
2
Nie sądzę, żeby to działało, chyba że użytkownik wie, jak używać znaku „” zamiast argumentu. W przeciwnym razie, w jaki sposób mogę ustawić nazwę bazy danych, ale pozostawić serwer bazy danych pusty?
Sean Long
Jak inaczej widzisz inną odpowiedź, aby to osiągnąć? @SeanLong
sehe
@SeanLong Right, więc użytkownik musi wykonać rozkaz. Dlatego najbardziej wymagane argumenty należy umieścić na pierwszym miejscu.
Martin Braun
To była zdecydowanie najłatwiejsza opcja dla mnie. Dzięki za post :)
Steve Bauman
2

Tworzenie zmiennych dynamicznych

wprowadź opis obrazu tutaj

Plusy

  • Działa dla> 9 argumentów
  • Utrzymuje %1, %2... %*w takt
  • Działa zarówno na styl, jak /argi na -argstyl
  • Brak wcześniejszej wiedzy na temat argumentów
  • Wdrażanie jest oddzielone od głównej procedury

Cons

  • Stare argumenty mogą przeciekać do kolejnych przebiegów, dlatego użyj ich setlocaldo lokalnego określania zakresu lub napisz towarzyszącą :CLEAR-ARGSprocedurę!
  • Brak wsparcia dla aliasów (lubię --forceto -f)
  • Brak ""wsparcia dla pustych argumentów

Stosowanie

Oto przykład, jak następujące argumenty odnoszą się do zmiennych .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

Realizacja

@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
źródło
Możesz również przenieść zawartość :ARG-PARSERprocedury do zewnętrznego pliku .bat, tak arg-parser.bataby była dostępna także dla innych skryptów.
Simon Streicher
1

Kiedyś napisałem program, który obsługuje krótkie (-h), długie (--help) i nie będące opcjami argumenty w pliku wsadowym. Techniki te obejmują:

  • argumenty nie będące opcjami, po których następują argumenty opcji.

  • operator przesunięcia dla tych opcji, które nie mają argumentów, takich jak „--help”.

  • operator dwóch przesunięć czasowych dla tych opcji, które wymagają argumentu.

  • pętla po etykiecie do przetwarzania wszystkich argumentów wiersza poleceń.

  • Zamknij skrypt i zatrzymaj przetwarzanie dla tych opcji, które nie wymagają dalszych działań, takich jak „--help”.

  • Napisałem funkcje pomocy dla kierowania użytkownikami

Oto mój kod.

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
Rozwiązywanie równań
źródło
1

Oto parser argumentów. Możesz mieszać dowolne argumenty łańcuchowe (pozostawione nietknięte) lub opcje ze zmianą znaczenia (pojedyncze lub pary opcja / wartość). Aby to przetestować, odkomentuj ostatnie 2 instrukcje i uruchom jako:

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

Znak ucieczki jest zdefiniowany jako "_SEP_=/", w razie potrzeby przedefiniuj.

@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
źródło