Jak parsować argumenty wiersza poleceń w Bash?

1919

Powiedzmy, że mam skrypt wywoływany za pomocą tej linii:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

albo ten:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

Co znajduje się w przyjętym sposobem analizowania tego taki, że w każdym przypadku (lub kombinacja tych dwóch) $v, $fi $dwszystko będzie ustawiona truei $outFilebędzie równa /fizz/someOtherFile?

Lawrence Johnston
źródło
1
Dla użytkowników zsh istnieje świetne narzędzie wbudowane o nazwie zparseopts, które może to zrobić: zparseopts -D -E -M -- d=debug -debug=dI mieć oba, -da --debugw $debugtablicy echo $+debug[1]zwróci 0 lub 1, jeśli jeden z nich zostanie użyty. Ref: zsh.org/mla/users/2011/msg00350.html
dezza
1
Naprawdę dobry tutorial: linuxcommand.org/lc3_wss0120.php . Szczególnie podoba mi się przykład „Opcje wiersza poleceń”.
Gabriel Staples

Odpowiedzi:

2673

Metoda nr 1: Używanie bash bez getopt [s]

Dwa typowe sposoby przekazywania argumentów para klucz-wartość to:

Bash Rozdzielone spacjami (np. --option argument) (Bez getopt [s])

Stosowanie demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi
EOF

chmod +x /tmp/demo-space-separated.sh

/tmp/demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

wynik z kopiowania i wklejenia powyższego bloku:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Bash Equals-Separated (np. --option=argument) (Bez getopt [s])

Stosowanie demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash

for i in "$@"
do
case $i in
    -e=*|--extension=*)
    EXTENSION="${i#*=}"
    shift # past argument=value
    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    shift # past argument=value
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi
EOF

chmod +x /tmp/demo-equals-separated.sh

/tmp/demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

wynik z kopiowania i wklejenia powyższego bloku:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Aby lepiej zrozumieć, ${i#*=}wyszukaj „Usuwanie podciągów” w tym przewodniku . Jest funkcjonalnie równoważny z tym, `sed 's/[^=]*=//' <<< "$i"`które wywołuje niepotrzebny podproces lub `echo "$i" | sed 's/[^=]*=//'`które wywołuje dwa niepotrzebne podprocesy.

Metoda nr 2: Używanie bash z getopt [s]

from: http://mywiki.wooledge.org/BashFAQ/035#getopts

ograniczenia getopt (1) (starsze, stosunkowo nowe getoptwersje):

  • nie obsługuje argumentów, które są pustymi łańcuchami
  • nie obsługuje argumentów za pomocą osadzonych białych znaków

Nowsze getoptwersje nie mają tych ograniczeń.

Dodatkowo oferta powłoki POSIX (i innych), getoptsktóra nie ma tych ograniczeń. Podałem uproszczony getoptsprzykład.

Stosowanie demo-getopts.sh -vf /etc/hosts foo bar

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar

wynik z kopiowania i wklejenia powyższego bloku:

verbose=1, output_file='/etc/hosts', Leftovers: foo bar

Zalety getoptsto:

  1. Jest bardziej przenośny i będzie działać w innych powłokach, takich jak dash.
  2. Może automatycznie obsługiwać wiele pojedynczych opcji, takich jak -vf filenametypowy system uniksowy.

Wadą getoptsjest to, że może obsługiwać tylko krótkie opcje ( -hnie --help) bez dodatkowego kodu.

Istnieje samouczek Getopts, który wyjaśnia, co oznacza cała składnia i zmienne. W bash jest też coś help getopts, co może być pouczające.

Bruno Bronosky
źródło
44
Czy to naprawdę prawda? Według Wikipedii istnieje nowsza ulepszona wersja GNU, getoptktóra zawiera wszystkie funkcje, getoptsa następnie niektóre. man getoptna wyjściach Ubuntu 13.04 getopt - parse command options (enhanced)jako nazwa, więc zakładam, że ta ulepszona wersja jest teraz standardem.
Livven
47
To, że coś jest pewnym sposobem w twoim systemie, jest bardzo słabą przesłanką do oparcia się na założeniach „bycia standardem”.
szablica
13
@Livven, który getoptnie jest narzędziem GNU, jest częścią util-linux.
Stephane Chazelas,
4
Jeśli używasz -gt 0, usuń shiftpo esac, zwiększ wszystkie shifto 1 i dodaj ten przypadek: *) break;;możesz obsłużyć argumenty nieopcjonalne. Np .: pastebin.com/6DJ57HTc
Nicolas Lacombe,
2
Nie echo –default. W pierwszym przykładzie zauważam, że jeśli –defaultjest ostatnim argumentem, nie jest przetwarzany (uważany za non-opt), chyba że while [[ $# -gt 1 ]]jest ustawiony jako while [[ $# -gt 0 ]]
kolydart
562

Brak odpowiedzi wspomina o ulepszonym getopt . A najczęściej głosowana odpowiedź jest myląca: ignoruje -⁠vfdkrótkie opcje stylu (wymagane przez PO) lub opcje po argumentach pozycyjnych (również wymagane przez OP); i ignoruje błędy parsowania. Zamiast:

  • Użyj ulepszonego getoptz util-linux lub wcześniej GNU glibc . 1
  • Działa z getopt_long()funkcją C GNU glibc.
  • Posiada wszystkie przydatne funkcje wyróżniające (inne ich nie mają):
    • obsługuje spacje, cytowanie znaków, a nawet binarne w argumentach 2 (nie getoptjest to możliwe bez rozszerzenia )
    • może obsłużyć opcje na końcu: script.sh -o outFile file1 file2 -v( getoptsnie robi tego)
    • Pozwala =-Style długie opcje: script.sh --outfile=fileOut --infile fileIn(pozwalając obu jest długa, jeśli własny parsowania)
    • pozwala na łączenie krótkich opcji, np. -vfd(prawdziwa praca, jeśli parsuje się)
    • pozwala na dotykanie argumentów opcji, np. -oOutfilelub-vfdoOutfile
  • Ma już 3 lata , więc nie brakuje w nim systemu GNU (np. Każdy Linux go ma).
  • Możesz sprawdzić jego istnienie za pomocą: getopt --test→ zwracanej wartości 4.
  • Inne getoptlub wbudowane powłoki getoptsmają ograniczone zastosowanie.

Następujące połączenia

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

wszyscy wracają

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

z następującymi myscript

#!/bin/bash
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 ulepszony getopt jest dostępny na większości „systemów bash”, w tym Cygwin; w systemie OS X spróbuj zainstalować gnu-getopt lubsudo port install getopt
2,exec() konwencjePOSIXnie mają niezawodnego sposobu na przekazanie binarnej wartości NULL w argumentach wiersza poleceń; te bajty przedwcześnie kończąpierwszą wersjęargumentu
3 wydaną w 1997 r. lub wcześniej (prześledziłem ją tylko do 1997 r.)

Robert Siemer
źródło
4
Dzięki za to. Właśnie potwierdzono z tabeli funkcji na stronie en.wikipedia.org/wiki/Getopts , jeśli potrzebujesz wsparcia dla długich opcji, a nie korzystasz z systemu Solaris, getoptjest to najlepsza droga.
johncip
4
Uważam, że jedynym zastrzeżeniem getoptjest to, że nie można go wygodnie używać w skryptach opakowujących, w których można mieć kilka opcji specyficznych dla skryptu opakowującego, a następnie przekazać opcje spoza skryptu opakowującemu plik wykonywalny w stanie nienaruszonym. Powiedzmy, że mam grepwywołanie otoki mygrepi mam opcję --foospecyficzną dla mygrep, wtedy nie mogę tego zrobić mygrep --foo -A 2, i mam to -A 2automatycznie przekazane grep; I trzeba to zrobić mygrep --foo -- -A 2. Oto moje wdrożenie na szczycie twojego rozwiązania.
Kaushal Modi
2
@ bobpaul Twoje zdanie na temat util-linux jest również błędne i wprowadza w błąd: pakiet jest oznaczony jako „niezbędny” na Ubuntu / Debian. Jako taki jest zawsze instalowany. - O których dystrybucjach mówisz (gdzie, jak mówisz, musi być celowo zainstalowany)?
Robert Siemer
3
Uwaga: nie działa to na komputerach Mac przynajmniej w bieżącym 10.14.3. Getopt, że statki to BSD getopt z 1999 roku ...
jjj
2
@transang Boolean negacja wartości zwracanej. I jego efekt uboczny: pozwól, aby polecenie zakończyło się niepowodzeniem (w przeciwnym razie errexit przerwałby program po błędzie). - Komentarze w skrypcie mówią więcej. W przeciwnym razie:man bash
Robert Siemer
144

Bardziej zwięzły sposób

script.sh

#!/bin/bash

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -d|--deploy) deploy="$2"; shift ;;
        -u|--uglify) uglify=1 ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

Stosowanie:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify
Inanc Gumus
źródło
3
To właśnie robię. Muszę, while [[ "$#" > 1 ]]jeśli chcę wesprzeć zakończenie linii flagą logiczną ./script.sh --debug dev --uglify fast --verbose. Przykład: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
12
Łał! Prosty i czysty! Używam tego: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
2
O wiele ładniej jest wklejać do każdego skryptu niż zajmować się źródłem lub zastanawiać się, gdzie właściwie zaczyna się twoja funkcjonalność.
RealHandy
Ostrzeżenie: toleruje powielone argumenty, przeważa najnowszy argument. np. ./script.sh -d dev -d prodspowodowałoby deploy == 'prod'. I tak go użyłem: P :): +1:
yair
Używam tego (dzięki!), Ale zauważ, że pozwala on na pustą wartość argumentu, np. ./script.sh -dNie generowałby błędu, a po prostu ustawiał $deploypusty ciąg.
EM0
137

from: digitalpeer.com z niewielkimi modyfikacjami

Stosowanie myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

Aby lepiej zrozumieć, ${i#*=}wyszukaj „Usuwanie podciągów” w tym przewodniku . Jest funkcjonalnie równoważny z tym, `sed 's/[^=]*=//' <<< "$i"`które wywołuje niepotrzebny podproces lub `echo "$i" | sed 's/[^=]*=//'`które wywołuje dwa niepotrzebne podprocesy.

guneysus
źródło
4
Schludny! Chociaż to nie zadziała w przypadku argumentów rozdzielonych spacjami mount -t tempfs .... Prawdopodobnie można to naprawić za pomocą czegoś podobnego while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;itp.
Tobias Kienzler
3
To nie jest w stanie poradzić sobie z -vfdpołączonymi krótkimi opcjami stylu.
Robert Siemer,
105

getopt()/ getopts()to dobra opcja. Skradziono stąd :

Proste użycie „getopt” pokazano w tym mini-skrypcie:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

Powiedzieliśmy, że dowolne z -a, -b, -c lub -d będzie dozwolone, ale po tym -c następuje argument („c:” to mówi).

Jeśli nazwiemy to „g” i wypróbujemy:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

Zaczynamy od dwóch argumentów, a „getopt” rozbija opcje i umieszcza każdy w osobnym argumencie. Dodał także „-”.

Matt J
źródło
4
Używanie $*to nieprawidłowe użycie getopt. (Węży argumenty ze spacjami.) Zobacz moją odpowiedź dla właściwego użycia.
Robert Siemer
Dlaczego miałbyś to komplikować?
SDsolar,
@Matt J, pierwsza część skryptu (dla i) byłaby w stanie obsłużyć argumenty ze spacjami, jeśli użyjesz „$ i” zamiast $ i. Wydaje się, że getopts nie jest w stanie obsługiwać argumentów spacjami. Jaka byłaby przewaga używania getopt nad pętlą for i?
thebunnyrules
99

Na ryzyko dodania kolejnego przykładu do zignorowania, oto mój schemat.

  • uchwyty -n argi--name=arg
  • pozwala na zakończenie argumentów
  • pokazuje rozsądne błędy, jeśli coś jest źle napisane
  • kompatybilny, nie używa bashizmów
  • czytelny, nie wymaga utrzymywania stanu w pętli

Mam nadzieję, że komuś się przyda.

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done
bronson
źródło
4
Przepraszam za opóźnienie. W moim skrypcie funkcja handle_argument odbiera wszystkie argumenty niebędące opcjami. Możesz zamienić tę linię na cokolwiek chcesz, *) die "unrecognized argument: $1"lub zebrać argumenty do zmiennej *) args+="$1"; shift 1;;.
bronson
Niesamowity! Przetestowałem kilka odpowiedzi, ale to jedyna, która zadziałała dla wszystkich przypadków, w tym dla wielu parametrów pozycyjnych (zarówno przed, jak i po flagach)
Guilherme Garnier
2
ładny zwięzły kod, ale użycie -n i żaden inny argument nie powoduje nieskończonej pętli z powodu błędu shift 2, wydając shiftdwa razy zamiast shift 2. Sugerowana edycja.
lauksas
42

Spóźniłem się z tym pytaniem około 4 lat, ale chcę się zrewanżować. Użyłem wcześniejszych odpowiedzi jako punktu wyjścia do uporządkowania mojej starej analizy adhoc param. Następnie dokonałem refaktoryzacji następującego kodu szablonu. Obsługuje zarówno długie, jak i krótkie parametry, używając argumentów = lub oddzielonych spacjami, a także wiele krótkich parametrów zgrupowanych razem. Wreszcie ponownie wstawia argumenty nieparametrowe z powrotem do zmiennych $ 1, $ 2 .. Mam nadzieję, że to się przyda.

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done
Shane Day
źródło
Kod ten nie może obsłużyć opcje z argumentów, takich jak ten: -c1. A użycie =oddzielenia krótkich opcji od ich argumentów jest niezwykłe ...
Robert Siemer
2
Z tym przydatnym fragmentem kodu napotkałem dwa problemy: 1) „shift” w przypadku „-c = foo” kończy się zjedzeniem następnego parametru; oraz 2) „c” nie powinno być uwzględniane we wzorze „[cfr]” dla możliwych do kombinacji krótkich opcji.
sfnd
36

Znalazłem problem z pisaniem przenośnego parsowania w skryptach tak frustrujący, że napisałem Argbash - generator kodu FOSS, który może generować kod parsujący argumenty dla twojego skryptu i ma kilka fajnych funkcji:

https://argbash.io

bubla
źródło
Dzięki za napisanie argbash, właśnie go użyłem i stwierdziłem, że działa dobrze. Głównie wybrałem argbash, ponieważ jest to generator kodu obsługujący starszą wersję bash 3.x znalezioną w systemie OS X 10.11 El Capitan. Jedynym minusem jest to, że podejście do generowania kodu oznacza całkiem sporo kodu w głównym skrypcie, w porównaniu do wywoływania modułu.
RichVel
Możesz faktycznie używać Argbash w taki sposób, że tworzy on specjalnie dostosowaną bibliotekę parsowania specjalnie dla Ciebie, którą możesz uwzględnić w skrypcie lub możesz mieć go w osobnym pliku i po prostu go pobrać. Dodałem przykład, aby to wykazać i jaśniej to wyjaśniłem również w dokumentacji.
bubla
Dobrze wiedzieć. Ten przykład jest interesujący, ale wciąż niezbyt jasny - być może możesz zmienić nazwę wygenerowanego skryptu na „parse_lib.sh” lub podobny i pokazać, gdzie wywołuje go główny skrypt (jak w sekcji skryptu opakowującego, która jest bardziej złożonym przypadkiem użycia).
RichVel
Problemy zostały rozwiązane w najnowszej wersji argbash: Dokumentacja została ulepszona, wprowadzono skrypt szybkiego uruchamiania argbash-init, a nawet można używać argbash online na argbash.io/generate
bubla
29

Moja odpowiedź w dużej mierze opiera się na odpowiedzi Brunona Bronosky'ego , ale w pewnym sensie zmiksowałem jego dwie implementacje czystego basha w jedną, z której dość często korzystam.

# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

To pozwala mieć zarówno opcje / wartości oddzielone spacjami, jak i równe zdefiniowane wartości.

Abyś mógł uruchomić skrypt za pomocą:

./myscript --foo -b -o /fizz/file.txt

jak również:

./myscript -f --bar -o=/fizz/file.txt

i oba powinny mieć ten sam efekt końcowy.

Plusy:

  • Pozwala zarówno na -arg = wartość, jak i -arg wartość

  • Działa z dowolną nazwą argumentu, której można użyć w bash

    • Znaczenie -a lub -arg lub --arg lub -arg lub cokolwiek innego
  • Pure Bash. Nie musisz się uczyć / używać getopt ani getopts

CONS:

  • Nie można łączyć argumentów

    • Czyli nie -abc. Musisz zrobić -a -b -c

Są to jedyne zalety / wady, które mogę wymyślić z góry

Ponyboy47
źródło
15

Myślę, że ten jest wystarczająco prosty w użyciu:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ $rc$opt == 0? ]&&exit 1;[ $rc == 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval $readopt
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

Przykład wywołania:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile
Alek
źródło
1
Przeczytałem wszystko i ten jest moim ulubionym. Nie lubię używać -a=1jako stylu argc. Wolę umieścić najpierw główną opcję - opcje, a później te specjalne z pojedynczymi odstępami -o option. Szukam najprostszego vs lepszego sposobu na czytanie argumentów.
m3nda
Działa naprawdę dobrze, ale jeśli przekażesz argument do opcji innej niż: wszystkie argumenty zostaną wzięte pod uwagę. Możesz sprawdzić ten wiersz ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFilewłasnym skryptem. -d opcja nie jest ustawiona jako d:
m3nda
15

Rozwijając doskonałą odpowiedź @ guneysus, oto poprawka, która pozwala użytkownikowi na użycie dowolnej składni, którą preferują, np.

command -x=myfilename.ext --another_switch 

vs

command -x myfilename.ext --another_switch

To znaczy, że równe można zastąpić białymi spacjami.

Ta „rozmyta interpretacja” może Ci się nie podobać, ale jeśli tworzysz skrypty, które są wymienne z innymi narzędziami (tak jak w przypadku mojego, który musi współpracować z ffmpeg), elastyczność jest przydatna.

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "$@"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done
niezsynchronizowane
źródło
13

Ten przykład pokazuje, jak używać getopti evali HEREDOCi shiftobsługiwać krótkich i długich parametrów bez wymaganej wartości, które obserwuje. Również instrukcja zamiany / sprawy jest zwięzła i łatwa do naśladowania.

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, dont change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<-EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

Najważniejsze wiersze powyższego skryptu to:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Krótko, do rzeczy, czytelny i obsługuje prawie wszystko (IMHO).

Mam nadzieję, że komuś pomoże.

phyatt
źródło
1
To jedna z najlepszych odpowiedzi.
Pan Polywhirl
11

Daję ci funkcję, parse_paramsktóra będzie analizować parametry z linii poleceń.

  1. Jest to czyste rozwiązanie Bash, bez dodatkowych narzędzi.
  2. Nie zanieczyszcza zasięgu globalnego.
  3. Bez wysiłku zwraca ci proste w użyciu zmienne, na których możesz zbudować dalszą logikę.
  4. Ilość myślników przed parametrami nie ma znaczenia ( --allrówna się -allrówna się all=all)

Poniższy skrypt jest działającą demonstracją kopiuj-wklej. Zobacz show_usefunkcję, aby zrozumieć, jak używać parse_params.

Ograniczenia:

  1. Nie obsługuje parametrów rozdzielanych spacjami ( -d 1)
  2. Nazwy Param stracą więc myślniki --any-parami -anyparambędą równoważne
  3. eval $(parse_params "$@")musi być używany wewnątrz funkcji bash (nie będzie działać w zakresie globalnym)

#!/bin/bash

# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4.1 (Jul-27-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
        _escaped=${1/\*/\'\"*\"\'}
        _escaped=${_escaped//\'/\\\'}
        _escaped=${_escaped//\"/\\\"}
        # If equals delimited named parameter
        nonspace="[^[:space:]]"
        if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key='$_val';"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
            # remove dashes
            local _key=${1//\-}
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "$@")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
Oleksii Chekulaiev
źródło
Aby użyć wersji demo do przeanalizowania parametrów show_use "$@"
zawartych w
Zasadniczo dowiedziałem się, że github.com/renatosilva/easyoptions robi to samo w ten sam sposób, ale jest nieco bardziej masywny niż ta funkcja.
Oleksii Chekulaiev
10

EasyOptions nie wymaga analizowania:

## Options:
##   --verbose, -v  Verbose mode
##   --output=FILE  Output filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "output file is ${output}"
    echo "${arguments[@]}"
fi
Renato Silva
źródło
Zajęło mi minutę, zanim uświadomiłem sobie, że komentarze u góry twojego przykładowego skryptu są analizowane w celu zapewnienia domyślnego ciągu pomocy użycia, a także specyfikacji argumentów. To genialne rozwiązanie i przykro mi, że uzyskało tylko 6 głosów w ciągu 2 lat. Być może to pytanie jest zbyt zatłoczone, aby ludzie mogli je zauważyć.
Metamorphic,
W pewnym sensie twoje rozwiązanie jest zdecydowanie najlepsze (oprócz @ OleksiiChekulaiev, który nie obsługuje „standardowej” składni opcji). Wynika to z faktu, że Twoje rozwiązanie wymaga tylko od pisarza skryptów jednokrotnego określenia nazwy każdej opcji . Fakt, że inne rozwiązania wymagają trzykrotnego określenia - w użyciu, we wzorcu „przypadku” i w ustawieniu zmiennej - ciągle mnie denerwuje. Nawet getopt ma ten problem. Jednak twój kod działa wolno na moim komputerze - 0,11 s dla implementacji Bash, 0,28 s dla Ruby. W porównaniu z 0.02s dla jawnego parsowania „while-case”.
Metamorphic,
Chcę szybszej wersji, być może napisanej w C. Również wersji kompatybilnej z zsh. Być może zasługuje to na osobne pytanie („Czy istnieje sposób na przeanalizowanie argumentów wiersza poleceń w powłokach typu Bash, które akceptują standardową składnię długich opcji i nie wymagają wpisywania nazw opcji więcej niż jeden raz?”).
Metamorphic,
10

getopts działa świetnie, jeśli # 1 masz go zainstalowanego i # 2 zamierzasz go uruchomić na tej samej platformie. Na przykład OSX i Linux zachowują się inaczej.

Oto rozwiązanie (nie getopts), które obsługuje flagi równości, nierówności i wartości logiczne. Na przykład możesz uruchomić skrypt w następujący sposób:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("$@")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done
Vangora
źródło
8

Tak właśnie robię w funkcji, aby uniknąć przerywania uruchamiania getopts jednocześnie gdzieś wyżej na stosie:

function waitForWeb () {
   local OPTIND=1 OPTARG OPTION
   local host=localhost port=8080 proto=http
   while getopts "h:p:r:" OPTION; do
      case "$OPTION" in
      h)
         host="$OPTARG"
         ;;
      p)
         port="$OPTARG"
         ;;
      r)
         proto="$OPTARG"
         ;;
      esac
   done
...
}
akostadinov
źródło
8

Rozwijając odpowiedź @ bruno-bronosky, dodałem „preprocesor” do obsługi niektórych popularnych formatowań:

  • Rozwija się --longopt=valw--longopt val
  • Rozwija się -xyzw-x -y -z
  • Obsługuje --wskazanie końca flag
  • Pokazuje błąd nieoczekiwanych opcji
  • Kompaktowy i łatwy do odczytania przełącznik opcji
#!/bin/bash

# Report usage
usage() {
  echo "Usage:"
  echo "$(basename $0) [options] [--] [file1, ...]"

  # Optionally exit with a status code
  if [ -n "$1" ]; then
    exit "$1"
  fi
}

invalid() {
  echo "ERROR: Unrecognized argument: $1" >&2
  usage 1
}

# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
  arg="$1"; shift
  case "${END_OF_OPT}${arg}" in
    --) ARGV+=("$arg"); END_OF_OPT=1 ;;
    --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
    --*) ARGV+=("$arg"); END_OF_OPT=1 ;;
    -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
    *) ARGV+=("$arg") ;;
  esac
done

# Apply pre-processed options
set -- "${ARGV[@]}"

# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
  case "${END_OF_OPT}${1}" in
    -h|--help)      usage 0 ;;
    -p|--password)  shift; PASSWORD="$1" ;;
    -u|--username)  shift; USERNAME="$1" ;;
    -n|--name)      shift; names+=("$1") ;;
    -q|--quiet)     QUIET=1 ;;
    -C|--copy)      COPY=1 ;;
    -N|--notify)    NOTIFY=1 ;;
    --stdin)        READ_STDIN=1 ;;
    --)             END_OF_OPT=1 ;;
    -*)             invalid "$1" ;;
    *)              POSITIONAL+=("$1") ;;
  esac
  shift
done

# Restore positional parameters
set -- "${POSITIONAL[@]}"
jchook
źródło
6

Istnieje kilka sposobów parsowania argumentów cmdline (np. GNU getopt (nieprzenośny) vs BSD (OSX) getopt vs getopts) - wszystkie są problematyczne. To rozwiązanie jest

  • przenośny!
  • ma zerowe zależności, opiera się tylko na wbudowanych bashach
  • pozwala zarówno na krótkie, jak i długie opcje
  • obsługuje spacje między opcją a argumentem, ale może również używać =separatora
  • obsługuje łączony styl krótkich opcji -vxf
  • obsługuje opcję z opcjonalnymi argumentami (patrz przykład) i
  • nie wymaga rozdęcia kodu w porównaniu z alternatywami dla tego samego zestawu funkcji. Tj. Zwięzły, a zatem łatwiejszy w utrzymaniu

Przykłady: dowolny z

# flag
-f
--foo

# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"

# option with optional argument
--baz
--baz="Optional Hello"

#!/usr/bin/env bash

usage() {
  cat - >&2 <<EOF
NAME
    program-name.sh - Brief description

SYNOPSIS
    program-name.sh [-h|--help]
    program-name.sh [-f|--foo]
                    [-b|--bar <arg>]
                    [--baz[=<arg>]]
                    [--]
                    FILE ...

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option

  -b, --bar <arg>
          Option requiring an argument <arg>

  --baz[=<arg>]
          Option that has an optional argument <arg>. If <arg>
          is not specified, defaults to 'DEFAULT'
  --     
          Specify end of options; useful if the first non option
          argument starts with a hyphen

EOF
}

fatal() {
    for i; do
        echo -e "${i}" >&2
    done
    exit 1
}

# For long option processing
next_arg() {
    if [[ $OPTARG == *=* ]]; then
        # for cases like '--opt=arg'
        OPTARG="${OPTARG#*=}"
    else
        # for cases like '--opt arg'
        OPTARG="${args[$OPTIND]}"
        OPTIND=$((OPTIND + 1))
    fi
}

# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$@")  # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
    case "$optchar" in
        h) usage; exit 0 ;;
        f) foo=1 ;;
        b) bar="$OPTARG" ;;
        -) # long option processing
            case "$OPTARG" in
                help)
                    usage; exit 0 ;;
                foo)
                    foo=1 ;;
                bar|bar=*) next_arg
                    bar="$OPTARG" ;;
                baz)
                    baz=DEFAULT ;;
                baz=*) next_arg
                    baz="$OPTARG" ;;
                -) break ;;
                *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
            esac
            ;;
        *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
    esac
done

shift $((OPTIND-1))

if [ "$#" -eq 0 ]; then
    fatal "Expected at least one required argument FILE" \
    "See '${0} --help' for usage"
fi

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"
tmoschou
źródło
5

Chciałbym zaoferować moją wersję analizy składni opcji, która umożliwia:

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

Pozwala również na to (może być niepożądane):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

Przed użyciem musisz zdecydować, czy w opcji ma być użyte =, czy nie. Ma to na celu utrzymanie kodu w czystości (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done
galmok
źródło
1
co oznacza „+ x” na $ {key + x}?
Luca Davanzo
1
Jest to test sprawdzający, czy „klucz” jest obecny, czy nie. Dalej w dół wyłączam klawisz, a to przerywa wewnętrzną pętlę while.
galmok
5

Rozwiązanie, które zachowuje nieobsługiwane argumenty. Dema w zestawie.

Oto moje rozwiązanie. Jest BARDZO elastyczny i w przeciwieństwie do innych, nie powinien wymagać zewnętrznych pakietów i bez problemu radzi sobie z pozostałymi argumentami.

Zastosowanie to: ./myscript -flag flagvariable -otherflag flagvar2

Wszystko, co musisz zrobić, to edytować linię validflags. Przygotowuje łącznik i przeszukuje wszystkie argumenty. Następnie definiuje następny argument jako nazwę flagi, np

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

Główny kod (krótka wersja, pełne dalszych przykładów, także wersja z błędami):

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

Pełna wersja z wbudowanymi demami echa:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$@"
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
#   argval=$(echo $@ | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
$@"
shift $#
echo "post final clear args:
$@"
set -- $leftovers
echo "all post set args:
$@"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

Ostatni, ten popełni błąd, jeśli zostanie przekazany nieprawidłowy instrument.

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $@
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

Plusy: co robi, radzi sobie bardzo dobrze. Zachowuje nieużywane argumenty, których nie ma w wielu innych rozwiązaniach. Pozwala także na wywoływanie zmiennych bez ręcznego definiowania w skrypcie. Pozwala również na wstępne wypełnienie zmiennych, jeśli nie podano odpowiedniego argumentu. (Zobacz pełny przykład).

Wady: Nie można przeanalizować pojedynczego złożonego ciągu arg, np. -Xcvf przetworzyłby jako pojedynczy argument. Możesz jednak dość łatwo napisać dodatkowy kod do mojego, który dodaje tę funkcjonalność.


źródło
3

Zauważ, że getopt(1)był to krótkotrwały błąd AT&T.

getopt powstał w 1984 roku, ale został pochowany w 1986 roku, ponieważ tak naprawdę nie był użyteczny.

Dowodem na to, że getoptjest bardzo nieaktualna, jest to, że getopt(1)strona podręcznika nadal wspomina o tym, "$*"zamiast tego "$@", że została dodana do Bourne Shell w 1986 roku wraz zgetopts(1) wbudowaną powłoką, aby poradzić sobie z argumentami ze spacjami w środku.

BTW: jeśli jesteś zainteresowany analizowaniem długich opcji w skryptach powłoki, interesujące może być to, że getopt(3)implementacja z libc (Solaris) i ksh93obie dodały jednolitą długą implementację opcji, która obsługuje długie opcje jako aliasy dla krótkich opcji. To powoduje ksh93i Bourne Shellzaimplementowanie jednolitego interfejsu dla długich opcji poprzez getopts.

Przykład długich opcji ze strony podręcznika Bourne Shell:

getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"

pokazuje, jak długo aliasy opcji mogą być używane zarówno w Bourne Shell, jak i ksh93.

Zobacz stronę manuala ostatniej powłoki Bourne'a:

http://schillix.sourceforge.net/man/man1/bosh.1.html

oraz strona man dla getopt (3) z OpenSolaris:

http://schillix.sourceforge.net/man/man3c/getopt.3c.html

i na koniec strona podręcznika getopt (1) w celu weryfikacji nieaktualnej $ *:

http://schillix.sourceforge.net/man/man1/getopt.1.html

schily
źródło
3

Napisałem pomocnika bash, aby napisać ładne narzędzie bash

strona projektu: https://gitlab.mbedsys.org/mbedsys/bashopts

przykład:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

udzieli pomocy:

NAME:
    ./example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")

cieszyć się :)

Emeric Verschuur
źródło
Otrzymuję to w systemie Mac OS X: `` lib / bashopts.sh: linia 138: deklaruj: -A: nieprawidłowa opcja deklaruj: użycie: deklaruj [-afFirtx] [-p] [nazwa [= wartość] ...] Błąd w lib / bashopts.sh: 138. 'deklar -x -A bashopts_optprop_name' zakończony ze statusem 2 Drzewo połączeń: 1: lib / controller.sh: 4 źródło (...) Wyjście ze statusem 1 ``
Josh Wulf
Aby go użyć, potrzebujesz wersji Bash 4. Na Macu domyślną wersją jest 3. Możesz użyć domowego naparu do zainstalowania bash 4.
Josh Wulf
3

Oto moje podejście - użycie wyrażenia regularnego.

  • bez getoptów
  • obsługuje blok krótkich parametrów -qwerty
  • obsługuje krótkie parametry -q -w -e
  • obsługuje długie opcje --qwerty
  • możesz przekazać atrybut krótkiej lub długiej opcji (jeśli używasz bloku krótkich opcji, atrybut jest dołączany do ostatniej opcji)
  • możesz użyć spacji lub =do podania atrybutów, ale dopasowanie atrybutów do momentu napotkania łącznika + spacja „separator”, więc w --q=qwe ty qwe tyjest jeden atrybut
  • obsługuje wszystkie powyższe, więc -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributejest poprawny

scenariusz:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
options=$@

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done
a_z
źródło
Jak ten. Może po prostu dodaj -e param do echa z nową linią.
mauron85
3

Załóżmy, że tworzymy skrypt powłoki o nazwie test_args.shnastępująco

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

Po uruchomieniu następującego polecenia:

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

Dane wyjściowe będą:

year=2017 month=12 day=22 flag=true
Jan
źródło
5
Jest to takie samo podejście jak odpowiedź Noego , ale ma mniej kontroli bezpieczeństwa / zabezpieczeń. To pozwala nam zapisywać dowolne argumenty w środowisku skryptu i jestem prawie pewien, że użycie eval tutaj może pozwolić na zastrzyk poleceń.
Will Barnwell,
2

Użyj „argumentów” modułu z bash-modułów

Przykład:

#!/bin/bash
. import.sh log arguments

NAME="world"

parse_arguments "-n|--name)NAME;S" -- "$@" || {
  error "Cannot parse command line."
  exit 1
}

info "Hello, $NAME!"
Volodymyr M. Lisivka
źródło
2

Mieszanie argumentów pozycyjnych i opartych na flagach

--param = arg (równa się ograniczeniom)

Dowolne mieszanie flag między argumentami pozycyjnymi:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

można to osiągnąć dość zwięźle:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg (rozdzielany spacją)

Zwykle łatwiej jest nie mieszać --flag=valuei --flag valuestylizować.

./script.sh dumbo 127.0.0.1 --environment production -q -d

Jest to trochę ryzykowne do przeczytania, ale nadal jest ważne

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

Źródło

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2
Mark Fox
źródło
2

Oto getopts, który osiąga parsowanie przy minimalnym kodzie i pozwala zdefiniować, co chcesz wyodrębnić w jednym przypadku za pomocą eval z podciągiem.

Gruntownie eval "local key='val'"

function myrsync() {

        local backup=("${@}") args=(); while [[ $# -gt 0 ]]; do k="$1";
                case "$k" in
                    ---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include)
                        eval "local ${k:3}='${2}'"; shift; shift    # Past two arguments
                    ;;
                    *)  # Unknown option  
                        args+=("$1"); shift;                        # Past argument only
                    ;;                                              
                esac                                                
        done; set -- "${backup[@]}"                                 # Restore $@


        echo "${sourceurl}"
}

Deklaruje zmienne jako lokalne zamiast globalne, jak większość odpowiedzi tutaj.

Nazywany jako:

myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ... 

$ {K: 3} jest w zasadzie podciągiem do usunięcia pierwszego ---z klucza.

mmm
źródło
1

Warto również wiedzieć, że możesz ustawić wartość, a jeśli ktoś dostarczy dane wejściowe, zastąp wartość domyślną tą wartością.

myscript.sh -f ./serverlist.txt lub po prostu ./myscript.sh (i to zajmuje wartości domyślne)

    #!/bin/bash
    # --- set the value, if there is inputs, override the defaults.

    HOME_FOLDER="${HOME}/owned_id_checker"
    SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"

    while [[ $# > 1 ]]
    do
    key="$1"
    shift

    case $key in
        -i|--inputlist)
        SERVER_FILE_LIST="$1"
        shift
        ;;
    esac
    done


    echo "SERVER LIST   = ${SERVER_FILE_LIST}"
Mike Q
źródło
1

Inne rozwiązanie bez getopt [s], POSIX, stary styl uniksowy

Podobne do rozwiązania, które Bruno Bronosky opublikował tutaj, to jedno bez użycia getopt(s).

Główną cechą odróżniającą moje rozwiązanie jest to, że pozwala na łączenie opcji tak samo, jak tar -xzf foo.tar.gzjest równe tar -x -z -f foo.tar.gz. I podobnie jak w tar, psitp wiodącym myślnik jest opcjonalne dla bloku krótkich opcji (ale to można łatwo zmienić). Obsługiwane są również długie opcje (ale gdy blok zaczyna się od jednego, wówczas wymagane są dwa wiodące myślniki).

Kod z przykładowymi opcjami

#!/bin/sh

echo
echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from phk@[se.unix]"
echo

print_usage() {
  echo "Usage:

  $0 {a|b|c} [ARG...]

Options:

  --aaa-0-args
  -a
    Option without arguments.

  --bbb-1-args ARG
  -b ARG
    Option with one argument.

  --ccc-2-args ARG1 ARG2
  -c ARG1 ARG2
    Option with two arguments.

" >&2
}

if [ $# -le 0 ]; then
  print_usage
  exit 1
fi

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

    # we are at the beginning of a fresh block
    # remove optional leading hyphen and strip trailing whitespaces
    opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/')

  fi

  # get the first character -> check whether long option
  first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}')
  [ "$first_chr" = - ] && long_option=T || long_option=F

  # note to write the options here with a leading hyphen less
  # also do not forget to end short options with a star
  case $opt in

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

      if [ "$2" ]; then
        echo "Option BBB with argument '$2' activated!"
        shift
      else
        echo "BBB parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    c*|-ccc-2-args)

      if [ "$2" ] && [ "$3" ]; then
        echo "Option CCC with arguments '$2' and '$3' activated!"
        shift 2
      else
        echo "CCC parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

      if [ "$long_option" = T ]; then
        opt=$(echo "$opt" | awk '{print substr($1, 2)}')
      else
        opt=$first_chr
      fi
      printf 'Error: Unknown option: "%s"\n' "$opt" >&2
      print_usage
      exit 1
      ;;

  esac

  if [ "$long_option" = T ]; then

    # if we had a long option then we are going to get a new block next
    shift
    opt=

  else

    # if we had a short option then just move to the next character
    opt=$(echo "$opt" | awk '{print substr($1, 2)}')

    # if block is now empty then shift to the next one
    [ "$opt" ] || shift

  fi

done

echo "Doing something..."

exit 0

Aby zapoznać się z przykładem użycia, zobacz przykłady poniżej.

Pozycja opcji z argumentami

Na ile warto, opcje z argumentami nie są ostatnie (muszą być tylko długie opcje). Tak więc chociaż np. W tar(przynajmniej w niektórych implementacjach) fopcje muszą być ostatnie, ponieważ nazwa pliku następuje ( tar xzf bar.tar.gzdziała, ale tar xfz bar.tar.gznie działa), tutaj tak nie jest (patrz późniejsze przykłady).

Wiele opcji z argumentami

Jako kolejny bonus parametry opcji są zużywane w kolejności opcji według parametrów z wymaganymi opcjami. Wystarczy spojrzeć na wyniki mojego skryptu tutaj za pomocą wiersza poleceń abc X Y Z(lub -abc X Y Z):

Option AAA activated!
Option BBB with argument 'X' activated!
Option CCC with arguments 'Y' and 'Z' activated!

Połączono także długie opcje

Możesz także mieć długie opcje w bloku opcji, biorąc pod uwagę, że występują one na końcu bloku. Zatem wszystkie poniższe wiersze poleceń są równoważne (w tym kolejność przetwarzania opcji i jej argumentów):

  • -cba Z Y X
  • cba Z Y X
  • -cb-aaa-0-args Z Y X
  • -c-bbb-1-args Z Y X -a
  • --ccc-2-args Z Y -ba X
  • c Z Y b X a
  • -c Z Y -b X -a
  • --ccc-2-args Z Y --bbb-1-args X --aaa-0-args

Wszystko to prowadzi do:

Option CCC with arguments 'Z' and 'Y' activated!
Option BBB with argument 'X' activated!
Option AAA activated!
Doing something...

Nie w tym rozwiązaniu

Opcjonalne argumenty

Opcje z opcjonalnymi argumentami powinny być możliwe przy odrobinie pracy, np. Przez sprawdzenie, czy istnieje blok bez myślnika; użytkownik musiałby wtedy wstawić łącznik przed każdym blokiem po bloku z parametrem mającym parametr opcjonalny. Być może jest to zbyt skomplikowane, aby komunikować się z użytkownikiem, dlatego lepiej w tym przypadku po prostu wymagać łącznika wiodącego.

Sprawa staje się jeszcze bardziej skomplikowana dzięki wielu możliwym parametrom. Odradzałbym, aby opcje próbowały być sprytne, określając, czy argument może być za nim, czy nie (np. Z opcją po prostu bierze liczbę jako argument opcjonalny), ponieważ może się to złamać w przyszłości.

Osobiście preferuję dodatkowe opcje zamiast opcjonalnych argumentów.

Argumenty opcji wprowadzone ze znakiem równości

Podobnie jak w przypadku opcjonalnych argumentów, nie jestem tego fanem (BTW, czy istnieje wątek omawiania zalet / wad różnych stylów parametrów?), Ale jeśli tego chcesz, prawdopodobnie możesz go zaimplementować samodzielnie, tak jak na stronie http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop z --long-with-arg=?*instrukcją case, a następnie usuwając znak równości (jest to BTW strona, która mówi, że dokonanie konkatenacji parametrów jest możliwe z pewnym wysiłkiem, ale „pozostawiono [it] jako ćwiczenie dla czytelnika „co sprawiło, że uwierzyłem im na słowo, ale zacząłem od zera).

Inne notatki

POSIX, działa nawet na starożytnych BusyBox konfiguracji miałem do czynienia z (z np cut, headi getoptsbrakuje).

phk
źródło