Używanie getopts do przetwarzania długich i krótkich opcji wiersza poleceń


410

Chciałbym mieć długie i krótkie formy opcji wiersza poleceń wywoływanych za pomocą mojego skryptu powłoki.

Wiem, że getoptsmożna tego użyć, ale podobnie jak w Perlu, nie byłem w stanie zrobić tego samego z powłoką.

Wszelkie pomysły, jak to zrobić, aby móc korzystać z opcji takich jak:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

W powyższym, oba polecenia oznaczają to samo dla mojej powłoki, ale używając getopts, nie byłem w stanie ich wdrożyć?


2
IMHO, zaakceptowana odpowiedź nie jest najlepsza. Nie pokazuje, jak używać getopts do obsługi argumentów „-” i „-”, co można zrobić, jak pokazał @Arvid Requate. Wstawiam inną odpowiedź przy użyciu podobnej koncepcji, ale dotyczy również błędu użytkownika polegającego na „zapomnieniu” w celu wstawienia wartości potrzebnych argumentów. Kluczowy punkt: getopty można uruchomić. Użytkownik powinien unikać używania „getopt” zamiast tego, jeśli wymagana jest przenośność między platformami. Ponadto getopts jest częścią standardu POSIX dla powłok, więc prawdopodobnie będzie przenośny.
pauljohn32

Odpowiedzi:


304

Można rozważyć trzy implementacje:

  • Wbudowane Bash getopts. Nie obsługuje to długich nazw opcji z prefiksem podwójnego myślnika. Obsługuje tylko opcje jednoznakowe.

  • Implementacja BSD UNIX samodzielnego getoptpolecenia (tego używa MacOS). To również nie obsługuje długich opcji.

  • Implementacja GNU autonomicznego getopt. GNU getopt(3)(używany w wierszu poleceń getopt(1)w systemie Linux) obsługuje długie opcje parsowania.


Niektóre inne odpowiedzi pokazują rozwiązanie używania wbudowanego basha getoptsdo naśladowania długich opcji. To rozwiązanie faktycznie stanowi krótką opcję, której znak to „-”. Otrzymujesz „-” jako flagę. Następnie wszystko, co następuje, staje się OPTARG, a testujesz OPTARG z zagnieżdżonym case.

Jest to sprytne, ale wiąże się z pewnymi zastrzeżeniami:

  • getoptsnie może egzekwować specyfikacji opt. Nie może zwrócić błędów, jeśli użytkownik poda nieprawidłową opcję. Podczas analizy OPTARG musisz wykonać własne sprawdzanie błędów.
  • OPTARG jest używany dla długiej nazwy opcji, co komplikuje użycie, gdy sama długa opcja ma argument. W końcu będziesz musiał sam to kodować jako dodatkowy przypadek.

Tak więc, chociaż można napisać więcej kodu, aby obejść brak obsługi długich opcji, jest to o wiele więcej pracy i częściowo odrzuca cel użycia parsera getopt w celu uproszczenia kodu.


18
Więc. Co to jest wieloplatformowe, przenośne rozwiązanie?
troelskn

6
GNU Getopt wydaje się być jedynym wyborem. Na Macu zainstaluj GNU getopt z Macports. W systemie Windows zainstalowałbym GNU getopt z Cygwin.
Bill Karwin

2
Widocznie , ksh getopts może obsługiwać długie opcje.
Tgr

1
@Bill +1, chociaż dość łatwo jest zbudować getopt ze źródła ( software.frodo.looijaard.name/getopt ) na Macu. Możesz także sprawdzić wersję getopt zainstalowaną w twoim systemie z poziomu skryptów za pomocą „getopt -T; echo $?”.
Chinasaur

8
@Bill Karwin: „Wbudowane narzędzie bash getopts nie obsługuje długich nazw opcji z prefiksem podwójnego myślnika”. Ale getopts mogą być przystosowane do obsługi długich opcji: patrz stackoverflow.com/a/7680682/915044 poniżej.
TomRoche,

305

getopti getoptssą różnymi zwierzętami, a ludzie wydają się mieć trochę niezrozumienia tego, co robią. getoptsto wbudowane polecenie do bashprzetwarzania opcji wiersza polecenia w pętli i przypisywania każdej znalezionej opcji i wartości kolejno do wbudowanych zmiennych, aby można było je dalej przetwarzać. getoptjest jednak zewnętrznym programem narzędziowym i tak naprawdę nie przetwarza twoich opcji tak, jak robią to np. bash getopts, Getoptmoduł Perl lub moduły Python optparse/ argparse. Wystarczy getoptkanonizować przekazane opcje - tzn. Przekonwertować je na bardziej standardową formę, aby skrypt powłoki mógł je łatwiej przetwarzać. Na przykład aplikacja getoptmoże konwertować następujące elementy:

myscript -ab infile.txt -ooutfile.txt

zaangażowany w to:

myscript -a -b -o outfile.txt infile.txt

Rzeczywiste przetwarzanie należy wykonać samodzielnie. Nie musisz wcale używać, getoptjeśli wprowadzasz różne ograniczenia w sposobie określania opcji:

  • umieść tylko jedną opcję na argument;
  • wszystkie opcje poprzedzają parametry pozycyjne (tj. argumenty nieopcji);
  • w przypadku opcji z wartościami (np. -opowyżej) wartość musi być oddzielnym argumentem (po spacji).

Dlaczego warto korzystać getoptzamiastgetopts ? Podstawowym powodem jest to, że tylko GNU getoptzapewnia obsługę opcji wiersza polecenia o długiej nazwie. 1 (GNU getoptjest domyślny w Linuksie. Mac OS X i FreeBSD są dostarczane z podstawowym i niezbyt przydatnym programemgetopt , ale wersję GNU można zainstalować; patrz poniżej.)

Na przykład, oto przykład użycia GNU getopt, z mojego skryptu o nazwie javawrap:

# NOTE: This requires GNU getopt.  On Mac OS X and FreeBSD, you have to install this
# separately; see below.
TEMP=`getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \
             -n 'javawrap' -- "$@"`

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"

VERBOSE=false
DEBUG=false
MEMORY=
DEBUGFILE=
JAVA_MISC_OPT=
while true; do
  case "$1" in
    -v | --verbose ) VERBOSE=true; shift ;;
    -d | --debug ) DEBUG=true; shift ;;
    -m | --memory ) MEMORY="$2"; shift 2 ;;
    --debugfile ) DEBUGFILE="$2"; shift 2 ;;
    --minheap )
      JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;;
    --maxheap )
      JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Pozwala to określić opcje takie jak --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt" lub podobne. Efektem wywołania getoptjest kanonizacja opcji, aby --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt"można było łatwiej je przetworzyć. Cytowanie wokół "$1"i "$2"jest ważne, ponieważ zapewnia, że ​​argumenty ze spacjami są poprawnie obsługiwane.

Jeśli usuniesz pierwsze 9 wierszy (wszystko wzdłuż eval setlinii), kod nadal będzie działał ! Jednak twój kod będzie znacznie bardziej wybitny pod względem rodzajów akceptowanych opcji: W szczególności będziesz musiał określić wszystkie opcje w opisanej powyżej „kanonicznej” formie. Za pomocą getoptjednak można grupować opcje jednoliterowe, stosować krótsze, niejednoznaczne formy długich opcji, używać albo --file foo.txtlub --file=foo.txt, używać albo -m 4096lub -m4096, mieszać opcje i nie-opcje w dowolnej kolejności itp. getopt wyświetla również komunikat o błędzie, jeśli znaleziono nierozpoznane lub niejednoznaczne opcje.

UWAGA : W rzeczywistości istnieją dwie całkowicie różne wersje getopt, Basic getopti GNU getopt, z różnymi funkcjami i różnymi konwencjami wywoływania. 2 Podstawowy getoptjest dość zepsuty: nie tylko nie obsługuje długich opcji, nie może nawet obsługiwać spacji wewnątrz argumentów lub pustych argumentów, podczas gdy getoptsrobi to dobrze. Powyższy kod nie będzie działał w podstawowym języku getopt. GNU getoptjest instalowane domyślnie w systemie Linux, ale w Mac OS X i FreeBSD należy je instalować osobno. W systemie Mac OS X zainstaluj MacPorts ( http://www.macports.org ), a następnie zrób to, sudo port install getoptaby zainstalować GNU getopt(zwykle w /opt/local/bin) i upewnij się, że/opt/local/bin znajduje się on na ścieżce powłoki przed/usr/bin. Na FreeBSD zainstaluj misc/getopt.

Krótki przewodnik po modyfikowaniu przykładowego kodu dla własnego programu: z pierwszych kilku linii wszystko jest „płytką kotłową”, która powinna pozostać niezmieniona, z wyjątkiem linii, która wywołuje getopt. Powinieneś zmienić nazwę programu po -n, określić krótkie opcje po -oi długie opcje po --long. Umieść dwukropek po opcjach, które przyjmują wartość.

Wreszcie, jeśli widzisz kod, który właśnie setzamiast tego eval set, został napisany dla BSD getopt. Powinieneś to zmienić, aby użyć eval setstylu, który działa dobrze w obu wersjach getopt, podczas gdy zwykły setnie działa poprawnie z GNU getopt.

1 Faktycznie, getoptsw ksh93podporach długo nazwie opcje, ale ta skorupa nie jest używany tak często, jak bash. Wzsh użyj, zparseoptsaby uzyskać tę funkcjonalność.

2 Technicznie „GNU getopt” jest mylącą nazwą; ta wersja została napisana dla Linuksa, a nie dla projektu GNU. Jest jednak zgodny ze wszystkimi konwencjami GNU, a termin „GNU getopt” jest powszechnie używany (np. W FreeBSD).


3
Było to bardzo pomocne, pomysł użycia getopt do sprawdzenia opcji, a następnie przetworzenia ich w bardzo prostej pętli zadziałał naprawdę dobrze, gdy chciałem dodać opcje długiego stylu do skryptu bash. Dzięki.
ianmjones

2
getoptw Linuksie nie jest narzędziem GNU, a tradycyjne getoptnie pochodzi z BSD, ale z AT&T Unix. ksh93's getopts(również z AT&T) obsługuje długie opcje w stylu GNU.
Stephane Chazelas

@StephaneChazelas - edytowane w celu odzwierciedlenia twoich komentarzy. Nadal wolę termin „GNU getopt”, nawet jeśli jest to mylne określenie, ponieważ ta wersja jest zgodna z konwencjami GNU i ogólnie działa jak program GNU (np. Korzystający z POSIXLY_CORRECT), podczas gdy „getopt z rozszerzeniem Linux” błędnie sugeruje, że ta wersja istnieje tylko na Linux
Urban Vagabond

1
Pochodzi z pakietu util-linux, więc jest to tylko Linux, ponieważ ten pakiet oprogramowania jest przeznaczony tylko dla Linuksa (który getoptmożna łatwo przenieść na inne Uniksy, ale wiele innych programów util-linuxjest specyficznych dla Linuksa). Wszystkie programy inne niż GNU korzystające z GNU getopt (3) rozumieją $POSIX_CORRECT. Na przykład nie powiedziałbyś, że aplayto GNU tylko z tych powodów. Podejrzewam, że kiedy FreeBSD wspomina o GNU getopt, oznacza to interfejs API GNU getopt (3) C.
Stephane Chazelas

@StephaneChazelas - FreeBSD ma komunikat o błędzie „Zależność kompilacji: zainstaluj GNU getopt”, który jednoznacznie odnosi się do getoptutil, a nie getopt (3).
Urban Vagabond

202

Wbudowana funkcja getopts Bash może być używana do analizowania długich opcji poprzez wstawienie znaku myślnika, a następnie dwukropka w optspec:

#!/usr/bin/env bash 
optspec=":hv-:"
while getopts "$optspec" optchar; do
    case "${optchar}" in
        -)
            case "${OPTARG}" in
                loglevel)
                    val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
                    ;;
                loglevel=*)
                    val=${OPTARG#*=}
                    opt=${OPTARG%=$val}
                    echo "Parsing option: '--${opt}', value: '${val}'" >&2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h)
            echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
            exit 2
            ;;
        v)
            echo "Parsing option: '-${optchar}'" >&2
            ;;
        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done

Po skopiowaniu do pliku wykonywalnego name = getopts_test.shw bieżącym katalogu roboczym można wygenerować dane wyjściowe podobne do

$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'

Oczywiście getopts nie OPTERRsprawdza ani nie analizuje argumentów opcji dla długich opcji. Powyższy fragment skryptu pokazuje, jak można to zrobić ręcznie. Podstawowa zasada działa również w skorupie Debiana Almquist („dash”). Zwróć uwagę na szczególny przypadek:

getopts -- "-:"  ## without the option terminator "-- " bash complains about "-:"
getopts "-:"     ## this works in the Debian Almquist shell ("dash")

Zauważ, że jak zauważył GreyCat z http://mywiki.wooledge.org/BashFAQ , ta sztuczka wykorzystuje niestandardowe zachowanie powłoki, które pozwala na argument-opcja (tj. Nazwa pliku w „-f nazwa pliku”) do powiązania z opcją (jak w „-nazwa_pliku”). Standard POSIX mówi, że musi być między nimi spacja, która w przypadku „- longoption” przerwałaby parsowanie opcji i zamieniła wszystkie longoptions w argumenty niebędące opcjami.


2
Jedno pytanie: co to jest semantyka !w val="${!OPTIND}?
TomRoche,

2
@TomRoche it is Indirect Substitution: unix.stackexchange.com/a/41293/84316
ecbrodie

2
@ecbrodie: Jest tak, ponieważ faktycznie przetworzono dwa argumenty, a nie tylko jeden. Pierwszy argument to słowo „loglevel”, a następny to argument tego argumentu. Tymczasem getoptsautomatycznie zwiększamy tylko OPTINDo 1, ale w naszym przypadku musimy zwiększyć o 2, więc zwiększamy o 1 ręcznie, a następnie automatycznie getoptszwiększamy o 1.
Victor Zamanian

3
Ponieważ przechodzimy do równowagi Bash: Nagie nazwy zmiennych są dozwolone w wyrażeniach arytmetycznych, nie jest to $konieczne. OPTIND=$(( $OPTIND + 1 ))może być sprawiedliwy OPTIND=$(( OPTIND + 1 )). Co ciekawsze, możesz nawet przypisywać i zwiększać zmienne w wyrażeniu arytmetycznym, dzięki czemu można dalej go skracać : $(( ++OPTIND )), a nawet (( ++OPTIND ))brać pod uwagę, że ++OPTINDzawsze będzie dodatni, aby nie wywoływał uruchomienia powłoki z tą -eopcją. :-) gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html
clacke

3
Dlaczego nie --very-baddaje ostrzeżenia?
Tom Hale,

148

Wbudowany getopts polecenie jest nadal, AFAIK, ograniczone tylko do opcji jednoznakowych.

Istnieje (lub był) program zewnętrzny getopt, który reorganizowałby zestaw opcji, dzięki czemu łatwiej go było przeanalizować. Możesz dostosować ten projekt do obsługi długich opcji. Przykładowe użycie:

aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
    case "$1" in
    (-a) aflag=yes;;
    (-b) bflag=yes;;
    (-f) flist="$flist $2"; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*)  break;;
    esac
    shift
done

# Process remaining non-option arguments
...

Możesz użyć podobnego schematu z getoptlongpoleceniem.

Zauważ, że podstawową słabością getoptprogramu zewnętrznego jest trudność w obsłudze argumentów ze spacjami w nich oraz w ich dokładnym zachowaniu. Dlatego wbudowany getoptsjest lepszy, aczkolwiek ograniczony faktem, że obsługuje tylko opcje jednoliterowe.


11
getopt, z wyjątkiem wersji GNU (która ma inną konwencję wywoływania), jest zasadniczo uszkodzony. Nie używaj tego. Zamiast tego skorzystaj z ** getopts bash-hackers.org/wiki/doku.php/howto/getopts_tutorial
hendry

9
@hendry - z własnego linku: „Pamiętaj, że getopts nie jest w stanie analizować długich opcji w stylu GNU (--myoption) lub długich opcji w stylu XF86 (-myoption)!”
Tom Auger,

1
Jonathan - powinieneś przepisać przykład na eval setcytaty (zobacz moją odpowiedź poniżej), aby działał poprawnie z GNU getopt (domyślnie w Linuksie) i poprawnie obsługiwał spacje.
Urban Vagabond

@ UrbanVagabond: Nie jestem pewien, dlaczego powinienem to zrobić. Pytanie jest oznaczone jako Unix, a nie Linux. Celowo pokazuję tradycyjny mechanizm, który ma problemy z pustymi argumentami itp. Jeśli chcesz, możesz zademonstrować nowoczesną wersję przeznaczoną dla Linuksa, a twoja odpowiedź to robi. (I pamiętać, passim, że korzystanie z ${1+"$@"}. Jest osobliwy i kłóci się z tym, co jest konieczne w nowoczesnych pocisków a konkretnie z każdej muszli chcesz wybrać się na Linux Patrz Korzystanie $ 1: + „$ @”} w / bin / sh Przez omówienie tego zapisu.)
Jonathan Leffler

Powinieneś to zrobić, ponieważ eval setrobi to dobrze zarówno z GNU, jak i BSD getopt, podczas gdy zwykły setrobi to samo z BSD getopt. Równie dobrze możesz wykorzystać, eval setaby zachęcić ludzi do przyzwyczajenia się do robienia tego. BTW dzięki, nie zdawałem sobie sprawy, że ${1+"$@"}to już nie jest potrzebne. Muszę pisać rzeczy, które działają zarówno w systemie Mac OS X, jak i Linux - między nimi wymuszają dużą przenośność. Właśnie sprawdziłem i "$@"rzeczywiście postąpić właściwie na wszystko sh, bash, ksh, i zshpod Mac OS X; na pewno także pod Linuksem.
Urban Vagabond

78

Oto przykład, który faktycznie używa getopt z długimi opcjami:

aflag=no
bflag=no
cargument=none

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag="yes" ;;
    -b|--blong) bflag="yes" ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done

1
Powinieneś przepisać przykład, aby używać go eval setz cudzysłowami (patrz moja odpowiedź poniżej), aby działał poprawnie z GNU getopt (domyślnie w Linuksie) i poprawnie obsługiwał spacje.
Urban Vagabond

1
Jest to używane, getoptgdy chodzi o pytanie getopts.
Niklas Berglund,

Czy (--, (-*i (*ważne wzory? Jak są one różne od --, -*i *?
Maëlan

@ Maëlan - wiodący otwarty nawias jest opcjonalny, więc (--)jest identyczny jak --)w casezwrotce. Dziwnie jest widzieć nierównomierne wcięcie i niespójne użycie tych opcjonalnych wiodących parenów, ale aktualny kod odpowiedzi wydaje mi się ważny.
Adam Katz

59

Długie opcje mogą być analizowane przez standardowe narzędzie getoptsjako „argumenty” do -„opcji”

Jest to przenośna i rodzima powłoka POSIX - nie są potrzebne żadne zewnętrzne programy ani bashizmy.

Ten przewodnik implementuje długie opcje jako argumenty dla -opcji, dlatego --alphajest postrzegany getoptsjako -z argumentem alphai --bravo=foojako -z argumentem bravo=foo. Prawdziwy argumentem mogą być zbierane z prostego zastąpienia: ${OPTARG#*=}.

W tym przykładzie -bi -c(i ich długie formy --bravooraz --charlie) mają obowiązkowe argumenty. Argumenty za długimi opcjami pojawiają się po znakach równości, np. --bravo=foo(Ograniczniki spacji dla długich opcji byłyby trudne do wdrożenia, patrz poniżej).

Ponieważ wykorzystuje to getoptswbudowane rozwiązanie, rozwiązanie to obsługuje takie jak cmd --bravo=foo -ac FILE(które łączy opcje -ai -cprzeplata długie opcje ze standardowymi opcjami), podczas gdy większość innych odpowiedzi tutaj walczy lub nie.

die() { echo "$*" >&2; exit 2; }  # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }

while getopts ab:c:-: OPT; do
  # support long options: https://stackoverflow.com/a/28466267/519360
  if [ "$OPT" = "-" ]; then   # long option: reformulate OPT and OPTARG
    OPT="${OPTARG%%=*}"       # extract long option name
    OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
    OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
  fi
  case "$OPT" in
    a | alpha )    alpha=true ;;
    b | bravo )    needs_arg; bravo="$OPTARG" ;;
    c | charlie )  needs_arg; charlie="$OPTARG" ;;
    ??* )          die "Illegal option --$OPT" ;;  # bad long option
    \? )           exit 2 ;;  # bad short option (error reported via getopts)
  esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list

Gdy opcją jest myślnik ( -), jest to opcja długa. getoptsbędzie parsował rzeczywistą długą opcję na $OPTARG, np. --bravo=foooryginalne zestawy OPT='-'i OPTARG='bravo=foo'. Te ifzestawy stanza $OPTdo zawartości $OPTARGprzed pierwszym znakiem równości ( bravow naszym przykładzie), a następnie usuwa, że od początku $OPTARG(uzyskując =foow tym kroku, lub pusty ciąg jeśli nie jest =). Wreszcie usuwamy wiodącą argumentację =. W tym momencie $OPTjest albo krótka opcja (jeden znak), albo długa opcja (ponad 2 znaki).

caseNastępnie dopasowuje albo krótkie lub długie opcje. W przypadku krótkich opcji getoptsautomatycznie narzeka na opcje i brakujące argumenty, dlatego musimy je replikować ręcznie za pomocą needs_argfunkcji, która kończy się, gdy $OPTARGjest pusta. ??*Warunek będzie pasować do każdego pozostałego długiej opcji ( ?dopasowuje pojedynczy znak i *zero lub więcej, więc ??*pasuje 2+ znaków), co pozwala nam wydać opcję „Illegal” błąd przed wyjściem.

(Uwaga na temat nazw zmiennych pisanych wielkimi literami: Ogólnie rzecz biorąc, radzę rezerwować zmienne pisane wielkimi literami do użytku systemowego. Zachowuję $OPTlitery pisane wielkimi literami, aby zachować zgodność $OPTARG, ale to łamie tę konwencję. Myślę, że pasuje, ponieważ jest to coś, co powinien był zrobić system, i powinno być bezpieczne, ponieważ nie ma żadnych standardów (afaik), które używają takiej zmiennej.)


Aby narzekać na nieoczekiwane argumenty na długie opcje, naśladuj to, co zrobiliśmy dla argumentów obowiązkowych: użyj funkcji pomocnika. Po prostu odwróć test, aby narzekać na argument, którego się nie oczekuje:

no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }

Starsza wersja tej odpowiedzi miał próbę przyjmowanie długich opcji z argumentami spacjami, ale to nie było wiarygodne; getoptsmoże przedwcześnie zakończyć działanie przy założeniu, że argument wykracza poza jego zakres, a ręczne zwiększanie $OPTINDnie działa we wszystkich powłokach.

Można to osiągnąć za pomocą jednej z następujących technik:

a następnie zakończył czymś takim [ $# -gt $OPTIND ] && OPTIND=$((OPTIND+1))


Bardzo fajne, samodzielne rozwiązanie. Jedno pytanie: skoro letter-cnie wymaga argumentu, czy nie wystarczyłoby użyć letter-c)? *wydaje się zbędny.
Philip Kearns

1
@Arne Argumenty pozycyjne są złe UX; są trudne do zrozumienia, a opcjonalne argumenty są dość niechlujne. getoptszatrzymuje się przy pierwszym argumencie pozycyjnym, ponieważ nie jest przeznaczony do radzenia sobie z nimi. Pozwala to na komendy podrzędne z własnymi argumentami, np. git diff --colorInterpretowałbym to command --foo=moo bar --baz wazjako --fooargument do commandi --baz wazjako argument (z opcją) do polecenia barpodrzędnego. Można to zrobić za pomocą powyższego kodu. Odrzucam --bravo -blahponieważ--bravo wymaga argumentu i nie jest jasne, -blahczy nie jest to inna opcja.
Adam Katz,

1
Nie zgadzam się co do UX: argumenty pozycyjne są użyteczne i łatwe, o ile ograniczysz ich liczbę (maksymalnie do 2 lub 1 plus N tego samego typu). Powinno być możliwe przeplatanie ich argumentami słów kluczowych, ponieważ użytkownicy mogą następnie budować polecenie krok po kroku (tj. Ls abc -la).
Arne Babenhauserheide

1
@AdamKatz: Napisałem mały artykuł z tym: draketo.de/english/free-software/shell-argument-parsing - zawiera wielokrotne czytanie pozostałych argumentów, aby złapać końcowe opcje.
Arne Babenhauserheide,

1
@ArneBabenhauserheide: Zaktualizowałem tę odpowiedź, aby obsługiwać argumenty rozdzielane spacjami. Ponieważ wymaga to evalpowłoki POSIX, jest wymieniony poniżej reszty odpowiedzi.
Adam Katz

33

Spójrz na shFlags, która jest przenośną biblioteką powłoki (czyli: sh, bash, dash, ksh, zsh w Linux, Solaris itp.).

To sprawia, że ​​dodawanie nowych flag jest tak proste, jak dodawanie jednej linii do skryptu, i zapewnia automatycznie generowaną funkcję użycia.

Oto prosty Hello, world!przy użyciu shFlag :

#!/bin/sh

# source shflags from current directory
. ./shflags

# define a 'name' command-line string flag
DEFINE_string 'name' 'world' 'name to say hello to' 'n'

# parse the command-line
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"

# say hello
echo "Hello, ${FLAGS_name}!"

W systemach operacyjnych z rozszerzoną funkcją getopt, która obsługuje długie opcje (np. Linux), możesz:

$ ./hello_world.sh --name Kate
Hello, Kate!

W pozostałym zakresie musisz użyć krótkiej opcji:

$ ./hello_world.sh -n Kate
Hello, Kate!

Dodanie nowej flagi jest tak proste, jak dodanie nowej DEFINE_ call.


2
To fantastyczne, ale niestety mój getopt (OS X) nie obsługuje spacji w argumentach: / zastanawiam się, czy istnieje alternatywa.
Alastair Stuart

@AlastairStuart - w OS X istnieje alternatywa. Użyj MacPorts, aby zainstalować GNU getopt (zwykle będzie instalowany w / opt / local / bin / getopt).
Urban Vagabond

3
@ UrbanVagabond - instalacja niesystemowych domyślnych narzędzi nie jest niestety nie do przyjęcia dla wystarczająco przenośnego narzędzia.
Alastair Stuart

@AlastairStuart - zobacz moją odpowiedź na przenośne rozwiązanie, które wykorzystuje wbudowane getopts zamiast GNU getopt. Jest to to samo, co podstawowe użycie getopts, ale z dodatkową iteracją dla długich opcji.
Adam Katz

31

Używanie getoptsz krótkimi / długimi opcjami i argumentami


Działa ze wszystkimi kombinacjami, np .:

  • foobar -f --bar
  • foobar --foo -b
  • foobar -bf --bar --foobar
  • foobar -fbFBAshorty --bar -FB --arguments = longhorn
  • foobar -fA "text shorty" -B --arguments = "text longhorn"
  • bash foobar -F --barfoo
  • sh foobar -B --foobar - ...
  • bash ./foobar -F --bar

Niektóre deklaracje dla tego przykładu

Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty

Jak wyglądałaby funkcja użycia

function _usage() 
{
  ###### U S A G E : Help and ERROR ######
  cat <<EOF
   foobar $Options
  $*
          Usage: foobar <[options]>
          Options:
                  -b   --bar            Set bar to yes    ($foo)
                  -f   --foo            Set foo to yes    ($bart)
                  -h   --help           Show this message
                  -A   --arguments=...  Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
                  -B   --barfoo         Set barfoo to yes ($barfoo)
                  -F   --foobar         Set foobar to yes ($foobar)
EOF
}

[ $# = 0 ] && _usage "  >>>>>>>> no options given "

getops z długimi / krótkimi flagami oraz długimi argumentami

while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
             --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done

Wynik

##################################################################
echo "----------------------------------------------------------"
echo "RESULT short-foo      : $sfoo                                    long-foo      : $lfoo"
echo "RESULT short-bar      : $sbar                                    long-bar      : $lbar"
echo "RESULT short-foobar   : $sfoobar                                 long-foobar   : $lfoobar"
echo "RESULT short-barfoo   : $sbarfoo                                 long-barfoo   : $lbarfoo"
echo "RESULT short-arguments: $sarguments  with Argument = \"$sARG\"   long-arguments: $larguments and $lARG"

Łącząc powyższe w spójny skrypt

#!/bin/bash
# foobar: getopts with short and long options AND arguments

function _cleanup ()
{
  unset -f _usage _cleanup ; return 0
}

## Clear out nested functions on exit
trap _cleanup INT EXIT RETURN

###### some declarations for this example ######
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty

function _usage() 
{
  ###### U S A G E : Help and ERROR ######
  cat <<EOF
   foobar $Options
   $*
          Usage: foobar <[options]>
          Options:
                  -b   --bar            Set bar to yes    ($foo)
                    -f   --foo            Set foo to yes    ($bart)
                      -h   --help           Show this message
                  -A   --arguments=...  Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
                  -B   --barfoo         Set barfoo to yes ($barfoo)
                  -F   --foobar         Set foobar to yes ($foobar)
EOF
}

[ $# = 0 ] && _usage "  >>>>>>>> no options given "

##################################################################    
#######  "getopts" with: short options  AND  long options  #######
#######            AND  short/long arguments               #######
while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
               --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done

Czy to nie działa z więcej niż jednym długim argumentem (-). Wydaje mi się, że czytam tylko pierwszy.
Sinaesthetic

@Sinaesthetic - Tak, bawiłem się evalpodejściem do argumentów z odstępami na długich opcjach i stwierdziłem, że jest niewiarygodne z niektórymi powłokami (choć spodziewam się, że będzie działać z bash, w którym to przypadku nie musisz używać eval). Zobacz moją odpowiedź, jak zaakceptować długie argumenty opcji =i moje zanotowane próby użycia spacji. Moje rozwiązanie nie wykonuje połączeń zewnętrznych, podczas gdy to wykorzystuje cutkilka razy.
Adam Katz

24

Inny sposób...

# translate long options to short
for arg
do
    delim=""
    case "$arg" in
       --help) args="${args}-h ";;
       --verbose) args="${args}-v ";;
       --config) args="${args}-c ";;
       # pass through anything else
       *) [[ "${arg:0:1}" == "-" ]] || delim="\""
           args="${args}${delim}${arg}${delim} ";;
    esac
done
# reset the translated args
eval set -- $args
# now we can process with getopt
while getopts ":hvc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        c)  source $OPTARG ;;
        \?) usage ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage
        ;;
    esac
done

1
Czy nie wymaga to miejsca w każdym $argsprzeniesieniu? Można to zrobić nawet bez bzdur, ale ten kod straci spacje w opcjach i argumentach (nie sądzę, że $delimsztuczka zadziała). Zamiast tego można uruchomić set wewnątrz tej forpętli, jeśli jesteś na tyle ostrożny, aby opróżnić go tylko na pierwszej iteracji. Oto bezpieczniejsza wersja bez szaleństw.
Adam Katz

18

Rozwiązałem w ten sposób:

# A string with command options
options=$@

# An array with all the arguments
arguments=($options)

# Loop index
index=0

for argument in $options
  do
    # Incrementing index
    index=`expr $index + 1`

    # The conditions
    case $argument in
      -a) echo "key $argument value ${arguments[index]}" ;;
      -abc) echo "key $argument value ${arguments[index]}" ;;
    esac
  done

exit;

Czy jestem głupi czy coś takiego? getopti getoptssą bardzo mylące.


1
Wydaje mi się, że to działa, nie wiem na czym polega problem z tą metodą, ale wydaje się prosty, więc musi istnieć powód, dla którego wszyscy inni go nie używają.
Billy Moon,

1
@Billy Tak, jest to proste, ponieważ nie używam żadnego skryptu do zarządzania moimi parametrami itp. Zasadniczo przekształcam ciąg argumentów ($ @) na tablicę i przeglądam ją. W pętli bieżąca wartość będzie kluczem, a następna będzie wartością. Proste.

1
@Theodore Cieszę się, że było to dla Ciebie pomocne! To było również dla mnie bolesne. Jeśli jesteś zainteresowany, możesz zobaczyć jego przykład w akcji tutaj: raw.github.com/rafaelrinaldi/swf-to-html/master/swf-to-html.sh

2
Zdecydowanie najłatwiejszy sposób, jaki widziałem. Zmieniłem to trochę, na przykład używając i = $ (($ i + 1)) zamiast expr, ale koncepcja jest hermetyczna.
Thomas Dignan

6
Wcale nie jesteś głupi, ale możesz nie mieć żadnej funkcji: getopt (s) może rozpoznać mieszane opcje (np .: -ltrlub -lt -rrównie dobrze -l -t -r). Zapewnia również obsługę błędów i łatwy sposób na przesunięcie leczonych parametrów po zakończeniu leczenia opcjami.
Olivier Dulac

14

Jeśli nie chcesz getoptzależności, możesz to zrobić:

while test $# -gt 0
do
  case $1 in

  # Normal option processing
    -h | --help)
      # usage and help
      ;;
    -v | --version)
      # version info
      ;;
  # ...

  # Special cases
    --)
      break
      ;;
    --*)
      # error unknown (long) option $1
      ;;
    -?)
      # error unknown (short) option $1
      ;;

  # FUN STUFF HERE:
  # Split apart combined short options
    -*)
      split=$1
      shift
      set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$@"
      continue
      ;;

  # Done with options
    *)
      break
      ;;
  esac

  # for testing purposes:
  echo "$1"

  shift
done

Oczywiście nie możesz użyć opcji długiego stylu za pomocą jednego myślnika. A jeśli chcesz dodać skrócone wersje (np. --Verbos zamiast --verbose), musisz dodać je ręcznie.

Ale jeśli chcesz uzyskać getoptsfunkcjonalność wraz z długimi opcjami, jest to prosty sposób na zrobienie tego.

Ten fragment kodu również umieszczam w treści .


Wydaje się, że działa to tylko z jedną długą opcją na raz, ale spełniło moją potrzebę. Dziękuję Ci!
kingjeffrey

W szczególnym przypadku --)wydaje się, że shift ;brakuje. W tej chwili --pozostanie jako pierwszy argument bez opcji.
dgw

Myślę, że tak naprawdę jest to lepsza odpowiedź, choć, jak wskazuje dgw, --opcja wymaga shifttam. Mówię to jest lepiej, bo alternatywy są albo platforma wersje Zależna getoptlub getopts_longczy trzeba wymusić zwarć opcje mają być używane tylko na początku polecenia (czyli - użyć getoptsnastępnie przetwarzać długie opcje później), a to daje każde zamówienie i pełną kontrolę.
Haravikk

Ta odpowiedź sprawia, że ​​zastanawiam się, dlaczego mamy wątek kilkudziesięciu odpowiedzi do wykonania zadania, które można wykonać jedynie za pomocą tego absolutnie jasnego i prostego rozwiązania, i czy istnieje jakiś powód dla miliardów przypadków użycia getopt (s) innych niż dowodzenie się.
Florian Heigl,

11

Wbudowane getoptsnie może tego zrobić. Istnieje zewnętrzny program getopt (1), który może to zrobić, ale można go uzyskać tylko w systemie Linux z pakietu util-linux . Jest wyposażony w przykładowy skrypt getopt-parse.bash .

Jest też getopts_longnapisana jako funkcja powłoki.


3
getoptZostała ujęta w FreeBSD w wersji 1.0 w 1993 roku i jest częścią FreeBSD od tamtego czasu. Jako taki został przyjęty z FreeBSD 4.x do włączenia do projektu Apple Darwin. Począwszy od OS X 10.6.8, strona podręcznika dołączona przez Apple pozostaje dokładnym duplikatem strony podręcznika FreeBSD. Tak, to jest zawarte w OS X i gobach innych systemów operacyjnych oprócz Linuksa. -1 w tej odpowiedzi za dezinformację.
ghoti

8
#!/bin/bash
while getopts "abc:d:" flag
do
  case $flag in
    a) echo "[getopts:$OPTIND]==> -$flag";;
    b) echo "[getopts:$OPTIND]==> -$flag";;
    c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
    d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
  esac
done

shift $((OPTIND-1))
echo "[otheropts]==> $@"

exit

.

#!/bin/bash
until [ -z "$1" ]; do
  case $1 in
    "--dlong")
      shift
      if [ "${1:1:0}" != "-" ]
      then
        echo "==> dlong $1"
        shift
      fi;;
    *) echo "==> other $1"; shift;;
  esac
done
exit

2
Wyjaśnienie byłoby miłe. Pierwszy skrypt akceptuje krótkie opcje tylko wtedy, gdy drugi skrypt ma błąd w długim analizowaniu argumentów opcji; jego zmienna powinna dotyczyć "${1:0:1}"argumentu 1, podłańcucha o indeksie 0, długości 1. Nie pozwala to na mieszanie krótkich i długich opcji.
Adam Katz

7

W ksh93 , getoptsobsługuje długie nazwy ...

while getopts "f(file):s(server):" flag
do
    echo "$flag" $OPTIND $OPTARG
done

Tak powiedziano w samouczkach, które znalazłem. Wypróbuj i przekonaj się.


4
To jest wbudowane getopts ksh93. Oprócz tej składni ma również bardziej skomplikowaną składnię, która umożliwia także długie opcje bez krótkiego ekwiwalentu i wiele więcej.
jilles

2
Rozsądna odpowiedź. OP nie określił CO.
ghoti

6

Piszę tylko od czasu do czasu skrypty powłoki i wypadam z praktyki, więc wszelkie uwagi są mile widziane.

Korzystając ze strategii zaproponowanej przez @Arvid Requate, zauważyliśmy błędy użytkowników. Użytkownik, który zapomni podać wartość, przypadkowo będzie traktował nazwę następnej opcji jako wartość:

./getopts_test.sh --loglevel= --toc=TRUE

spowoduje, że wartość „loglevel” będzie postrzegana jako „--toc = TRUE”. Można tego uniknąć.

Dostosowałem kilka pomysłów na temat sprawdzania błędów użytkownika w interfejsie CLI na podstawie http://mwiki.wooledge.org/BashFAQ/035 dyskusji na temat ręcznego analizowania. Włączyłem sprawdzanie błędów do obsługi argumentów „-” i „-”.

Potem zacząłem majstrować przy składni, więc wszelkie błędy tutaj są wyłącznie moją winą, a nie oryginalnymi autorami.

Moje podejście pomaga użytkownikom, którzy wolą długo wchodzić z lub bez znaku równości. Oznacza to, że powinna mieć taką samą odpowiedź na „--loglevel 9” jak „--loglevel = 9”. W metodzie - / space nie ma pewności, czy użytkownik zapomni argumentu, dlatego konieczne jest pewne odgadnięcie.

  1. jeśli użytkownik ma format długiego / równego znaku (--opt =), wówczas spacja po = wyzwala błąd, ponieważ nie podano argumentu.
  2. jeśli użytkownik ma argumenty długie / spacje (--opt), ten skrypt powoduje błąd, jeśli nie następuje żaden argument (koniec polecenia) lub jeśli argument zaczyna się od myślnika)

Jeśli zaczynasz od tego, istnieje interesująca różnica między formatami „--opt = wartość” i „--opt wartość”. W przypadku znaku równości argument wiersza poleceń jest postrzegany jako „opt = wartość”, a praca do przetworzenia polegająca na analizie ciągów znaków, w celu rozdzielenia na „=”. W przeciwieństwie do „--opt value” nazwa argumentu to „opt” i mamy problem z uzyskaniem następnej wartości podanej w wierszu poleceń. To tutaj @Arvid Requate zastosowało $ {! OPTIND}, odniesienie pośrednie. Wciąż tego nie rozumiem, a komentarze w BashFAQ wydają się ostrzegać przed tym stylem ( http://mywiki.wooledge.org/BashFAQ/006 ). BTW, nie sądzę, aby komentarze poprzedniego autora dotyczące ważności OPTIND = $ ((OPTIND + 1)) były poprawne. Mam na myśli powiedzieć

W najnowszej wersji tego skryptu flaga -v oznacza wydruk VERBOSE.

Zapisz go w pliku o nazwie „cli-5.sh”, włącz plik wykonywalny, a dowolny z nich będzie działał lub zawiedzie w pożądany sposób

./cli-5.sh  -v --loglevel=44 --toc  TRUE
./cli-5.sh  -v --loglevel=44 --toc=TRUE
./cli-5.sh --loglevel 7
./cli-5.sh --loglevel=8
./cli-5.sh -l9

./cli-5.sh  --toc FALSE --loglevel=77
./cli-5.sh  --toc=FALSE --loglevel=77

./cli-5.sh   -l99 -t yyy
./cli-5.sh   -l 99 -t yyy

Oto przykład danych wyjściowych sprawdzania błędów w intpu użytkownika

$ ./cli-5.sh  --toc --loglevel=77
ERROR: toc value must not have dash at beginning
$ ./cli-5.sh  --toc= --loglevel=77
ERROR: value for toc undefined

Powinieneś rozważyć włączenie opcji -v, ponieważ wypisuje ona elementy wewnętrzne OPTIND i OPTARG

#/usr/bin/env bash

## Paul Johnson
## 20171016
##

## Combines ideas from
## /programming/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
## by @Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035

# What I don't understand yet: 
# In @Arvid REquate's answer, we have 
# val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
# this works, but I don't understand it!


die() {
    printf '%s\n' "$1" >&2
    exit 1
}

printparse(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
    fi
}

showme(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'VERBOSE: %s\n' "$1" >&2;
    fi
}


VERBOSE=0
loglevel=0
toc="TRUE"

optspec=":vhl:t:-:"
while getopts "$optspec" OPTCHAR; do

    showme "OPTARG:  ${OPTARG[*]}"
    showme "OPTIND:  ${OPTIND[*]}"
    case "${OPTCHAR}" in
        -)
            case "${OPTARG}" in
                loglevel) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
                    printparse "--${OPTARG}" "  " "${val}"
                    loglevel="${val}"
                    shift
                    ;;
                loglevel=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        printparse "--${opt}" "=" "${val}"
                        loglevel="${val}"
                        ## shift CAUTION don't shift this, fails othewise
                    else
                        die "ERROR: $opt value must be supplied"
                    fi
                    ;;
                toc) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) #??
                    printparse "--${opt}" " " "${val}"
                    toc="${val}"
                    shift
                    ;;
                toc=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        toc=${val}
                        printparse "--$opt" " -> " "$toc"
                        ##shift ## NO! dont shift this
                    else
                        die "ERROR: value for $opt undefined"
                    fi
                    ;;

                help)
                    echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
                    exit 2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h|-\?|--help)
            ## must rewrite this for all of the arguments
            echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
            exit 2
            ;;
        l)
            loglevel=${OPTARG}
            printparse "-l" " "  "${loglevel}"
            ;;
        t)
            toc=${OPTARG}
            ;;
        v)
            VERBOSE=1
            ;;

        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done



echo "
After Parsing values
"
echo "loglevel  $loglevel" 
echo "toc  $toc"

OPTIND=$(( $OPTIND + 1 )): jest potrzebny za każdym razem, gdy „pożerasz” parametr OPTIND (na przykład: kiedy jeden użyty --toc value : wartość znajduje się w parametrze o numerze $ OPTIND. Po odzyskaniu go dla wartości toc powinieneś powiedzieć getopts, że następny parametr do przeanalizowania nie jest wartością, ale brakuje jednego po nim (stąd:. OPTIND=$(( $OPTIND + 1 )) i twój skrypt (jak również skrypt, do którego się odwołujesz), po zakończeniu: shift $(( $OPTIND -1 ))(gdy getopts zakończył pracę po analizie parametrów 1 do OPTIND-1, musisz je przesunąć, aby $@są teraz wszelkie pozostałe parametry „inne niż opcje”
Olivier Dulac

och, kiedy się przesuwasz, „przesuwasz” parametry pod getopts, więc OPTIND zawsze wskazuje właściwą rzecz ... ale uważam, że to bardzo mylące. Wierzę (nie mogę teraz przetestować skryptu), że nadal potrzebujesz zmiany $ (($ OPTIND - 1)) po pętli getopts while, aby 1 $ teraz nie wskazywał na pierwotny 1 $ (opcja), ale do pierwszego z pozostałych argumentów (tych, które pojawiają się po wszystkich opcjach i ich wartościach). np .: myrm -foo -bar = baz thisarg thethisone wtedyanother
Olivier Dulac

5

Wymyślenie kolejnej wersji koła ...

Ta funkcja jest (mam nadzieję) zgodnym z POSIX zamiennikiem powłoki bourne dla GNU getopt. Obsługuje krótkie / długie opcje, które mogą przyjmować argumenty obowiązkowe / opcjonalne / brak, a sposób ich określania jest prawie identyczny z GNU getopt, więc konwersja jest banalna.

Oczywiście jest to nadal znaczna część kodu do umieszczenia w skrypcie, ale jest to około połowa linii dobrze znanej funkcji powłoki getopt_long, i może być lepsza w przypadkach, gdy chcesz po prostu zastąpić istniejące zastosowania GNU getopt.

To całkiem nowy kod, więc YMMV (i zdecydowanie daj mi znać, jeśli z jakiegoś powodu nie jest on w rzeczywistości zgodny z POSIX - od samego początku chodziło o przenośność, ale nie mam przydatnego środowiska testowego POSIX).

Następujące użycie kodu i przykładu:

#!/bin/sh
# posix_getopt shell function
# Author: Phil S.
# Version: 1.0
# Created: 2016-07-05
# URL: http://stackoverflow.com/a/37087374/324105

# POSIX-compatible argument quoting and parameter save/restore
# http://www.etalabs.net/sh_tricks.html
# Usage:
# parameters=$(save "$@") # save the original parameters.
# eval "set -- ${parameters}" # restore the saved parameters.
save () {
    local param
    for param; do
        printf %s\\n "$param" \
            | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
    done
    printf %s\\n " "
}

# Exit with status $1 after displaying error message $2.
exiterr () {
    printf %s\\n "$2" >&2
    exit $1
}

# POSIX-compatible command line option parsing.
# This function supports long options and optional arguments, and is
# a (largely-compatible) drop-in replacement for GNU getopt.
#
# Instead of:
# opts=$(getopt -o "$shortopts" -l "$longopts" -- "$@")
# eval set -- ${opts}
#
# We instead use:
# opts=$(posix_getopt "$shortopts" "$longopts" "$@")
# eval "set -- ${opts}"
posix_getopt () { # args: "$shortopts" "$longopts" "$@"
    local shortopts longopts \
          arg argtype getopt nonopt opt optchar optword suffix

    shortopts="$1"
    longopts="$2"
    shift 2

    getopt=
    nonopt=
    while [ $# -gt 0 ]; do
        opt=
        arg=
        argtype=
        case "$1" in
            # '--' means don't parse the remaining options
            ( -- ) {
                getopt="${getopt}$(save "$@")"
                shift $#
                break
            };;
            # process short option
            ( -[!-]* ) {         # -x[foo]
                suffix=${1#-?}   # foo
                opt=${1%$suffix} # -x
                optchar=${opt#-} # x
                case "${shortopts}" in
                    ( *${optchar}::* ) { # optional argument
                        argtype=optional
                        arg="${suffix}"
                        shift
                    };;
                    ( *${optchar}:* ) { # required argument
                        argtype=required
                        if [ -n "${suffix}" ]; then
                            arg="${suffix}"
                            shift
                        else
                            case "$2" in
                                ( -* ) exiterr 1 "$1 requires an argument";;
                                ( ?* ) arg="$2"; shift 2;;
                                (  * ) exiterr 1 "$1 requires an argument";;
                            esac
                        fi
                    };;
                    ( *${optchar}* ) { # no argument
                        argtype=none
                        arg=
                        shift
                        # Handle multiple no-argument parameters combined as
                        # -xyz instead of -x -y -z. If we have just shifted
                        # parameter -xyz, we now replace it with -yz (which
                        # will be processed in the next iteration).
                        if [ -n "${suffix}" ]; then
                            eval "set -- $(save "-${suffix}")$(save "$@")"
                        fi
                    };;
                    ( * ) exiterr 1 "Unknown option $1";;
                esac
            };;
            # process long option
            ( --?* ) {            # --xarg[=foo]
                suffix=${1#*=}    # foo (unless there was no =)
                if [ "${suffix}" = "$1" ]; then
                    suffix=
                fi
                opt=${1%=$suffix} # --xarg
                optword=${opt#--} # xarg
                case ",${longopts}," in
                    ( *,${optword}::,* ) { # optional argument
                        argtype=optional
                        arg="${suffix}"
                        shift
                    };;
                    ( *,${optword}:,* ) { # required argument
                        argtype=required
                        if [ -n "${suffix}" ]; then
                            arg="${suffix}"
                            shift
                        else
                            case "$2" in
                                ( -* ) exiterr 1 \
                                       "--${optword} requires an argument";;
                                ( ?* ) arg="$2"; shift 2;;
                                (  * ) exiterr 1 \
                                       "--${optword} requires an argument";;
                            esac
                        fi
                    };;
                    ( *,${optword},* ) { # no argument
                        if [ -n "${suffix}" ]; then
                            exiterr 1 "--${optword} does not take an argument"
                        fi
                        argtype=none
                        arg=
                        shift
                    };;
                    ( * ) exiterr 1 "Unknown option $1";;
                esac
            };;
            # any other parameters starting with -
            ( -* ) exiterr 1 "Unknown option $1";;
            # remember non-option parameters
            ( * ) nonopt="${nonopt}$(save "$1")"; shift;;
        esac

        if [ -n "${opt}" ]; then
            getopt="${getopt}$(save "$opt")"
            case "${argtype}" in
                ( optional|required ) {
                    getopt="${getopt}$(save "$arg")"
                };;
            esac
        fi
    done

    # Generate function output, suitable for:
    # eval "set -- $(posix_getopt ...)"
    printf %s "${getopt}"
    if [ -n "${nonopt}" ]; then
        printf %s "$(save "--")${nonopt}"
    fi
}

Przykładowe użycie:

# Process command line options
shortopts="hvd:c::s::L:D"
longopts="help,version,directory:,client::,server::,load:,delete"
#opts=$(getopt -o "$shortopts" -l "$longopts" -n "$(basename $0)" -- "$@")
opts=$(posix_getopt "$shortopts" "$longopts" "$@")
if [ $? -eq 0 ]; then
    #eval set -- ${opts}
    eval "set -- ${opts}"
    while [ $# -gt 0 ]; do
        case "$1" in
            ( --                ) shift; break;;
            ( -h|--help         ) help=1; shift; break;;
            ( -v|--version      ) version_help=1; shift; break;;
            ( -d|--directory    ) dir=$2; shift 2;;
            ( -c|--client       ) useclient=1; client=$2; shift 2;;
            ( -s|--server       ) startserver=1; server_name=$2; shift 2;;
            ( -L|--load         ) load=$2; shift 2;;
            ( -D|--delete       ) delete=1; shift;;
        esac
    done
else
    shorthelp=1 # getopt returned (and reported) an error.
fi

4

Przyjęta odpowiedź bardzo dobrze pokazuje wszystkie niedociągnięcia wbudowanego basha getopts. Odpowiedź kończy się na:

Tak więc, chociaż można napisać więcej kodu, aby obejść brak obsługi długich opcji, jest to o wiele więcej pracy i częściowo odrzuca cel użycia parsera getopt w celu uproszczenia kodu.

I chociaż zasadniczo zgadzam się z tym stwierdzeniem, wydaje mi się, że liczba przypadków, w których wszyscy zaimplementowaliśmy tę funkcję w różnych skryptach, uzasadnia włożenie wysiłku w stworzenie „znormalizowanego”, dobrze przetestowanego rozwiązania.

Jako taki „zaktualizowałem” bash wbudowany getoptspoprzez implementację getopts_longw czystym bashu, bez zewnętrznych zależności. Korzystanie z funkcji jest w 100% zgodne z wbudowanym getopts.

Uwzględniając getopts_long(który jest hostowany na GitHub ) w skrypcie, odpowiedź na pierwotne pytanie można zaimplementować tak prosto, jak:

source "${PATH_TO}/getopts_long.bash"

while getopts_long ':c: copyfile:' OPTKEY; do
    case ${OPTKEY} in
        'c'|'copyfile')
            echo 'file supplied -- ${OPTARG}'
            ;;
        '?')
            echo "INVALID OPTION -- ${OPTARG}" >&2
            exit 1
            ;;
        ':')
            echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
            exit 1
            ;;
        *)
            echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
            exit 1
            ;;
    esac
done

shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift

3

Nie mam jeszcze wystarczającej liczby przedstawicieli, aby skomentować lub zagłosować na jego rozwiązanie, ale odpowiedź Sme działała dla mnie wyjątkowo dobrze. Jedynym problemem, na jaki natknąłem się, było to, że argumenty zostały zamknięte w pojedyncze cudzysłowy (więc mam je rozebrać).

Dodałem także przykładowe zastosowania i tekst POMOCY. Zamieszczę tutaj moją nieco rozszerzoną wersję:

#!/bin/bash

# getopt example
# from: /programming/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
HELP_TEXT=\
"   USAGE:\n
    Accepts - and -- flags, can specify options that require a value, and can be in any order. A double-hyphen (--) will stop processing options.\n\n

    Accepts the following forms:\n\n

    getopt-example.sh -a -b -c value-for-c some-arg\n
    getopt-example.sh -c value-for-c -a -b some-arg\n
    getopt-example.sh -abc some-arg\n
    getopt-example.sh --along --blong --clong value-for-c -a -b -c some-arg\n
    getopt-example.sh some-arg --clong value-for-c\n
    getopt-example.sh
"

aflag=false
bflag=false
cargument=""

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc:h\? -l along,blong,help,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag=true ;;
    -b|--blong) bflag=true ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    -h|--help|-\?) echo -e $HELP_TEXT; exit;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done

# to remove the single quotes around arguments, pipe the output into:
# | sed -e "s/^'\\|'$//g"  (just leading/trailing) or | sed -e "s/'//g"  (all)

echo aflag=${aflag}
echo bflag=${bflag}
echo cargument=${cargument}

while [ $# -gt 0 ]
do
    echo arg=$1
    shift

    if [[ $aflag == true ]]; then
        echo a is true
    fi

done

3

Tutaj możesz znaleźć kilka różnych metod analizy złożonych opcji w bash: http://mywiki.wooledge.org/ComplexOptionParsing

Stworzyłem następujący i myślę, że jest dobry, ponieważ jest to minimalny kod oraz działają długie i krótkie opcje. Długa opcja może mieć wiele argumentów przy takim podejściu.

#!/bin/bash
# Uses bash extensions.  Not portable as written.

declare -A longoptspec
longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default
optspec=":h-:"
while getopts "$optspec" opt; do
while true; do
    case "${opt}" in
        -) #OPTARG is name-of-long-option or name-of-long-option=value
            if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible
            then
                opt=${OPTARG/=*/}
                OPTARG=${OPTARG#*=}
                ((OPTIND--))    
            else #with this --key value1 value2 format multiple arguments are possible
                opt="$OPTARG"
                OPTARG=(${@:OPTIND:$((longoptspec[$opt]))})
            fi
            ((OPTIND+=longoptspec[$opt]))
            continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options
            ;;
        loglevel)
          loglevel=$OPTARG
            ;;
        h|help)
            echo "usage: $0 [--loglevel[=]<value>]" >&2
            exit 2
            ;;
    esac
break; done
done

# End of file

2

Pracuję nad tym tematem od dłuższego czasu ... i stworzyłem własną bibliotekę, którą będziecie musieli zdobyć w głównym skrypcie. Zobacz na przykład libopt4shell i cd2mpc . Mam nadzieję, że to pomoże !


2

Ulepszone rozwiązanie:

# translate long options to short
# Note: This enable long options but disable "--?*" in $OPTARG, or disable long options after  "--" in option fields.
for ((i=1;$#;i++)) ; do
    case "$1" in
        --)
            # [ ${args[$((i-1))]} == ... ] || EndOpt=1 ;;& # DIRTY: we still can handle some execptions...
            EndOpt=1 ;;&
        --version) ((EndOpt)) && args[$i]="$1"  || args[$i]="-V";;
        # default case : short option use the first char of the long option:
        --?*) ((EndOpt)) && args[$i]="$1"  || args[$i]="-${1:2:1}";;
        # pass through anything else:
        *) args[$i]="$1" ;;
    esac
    shift
done
# reset the translated args
set -- "${args[@]}"

function usage {
echo "Usage: $0 [options] files" >&2
    exit $1
}

# now we can process with getopt
while getopts ":hvVc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        V)  echo $Version ; exit ;;
        c)  source $OPTARG ;;
        \?) echo "unrecognized option: -$opt" ; usage -1 ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage -1
        ;;
    esac
done

shift $((OPTIND-1))
[[ "$1" == "--" ]] && shift

2

Być może łatwiej jest użyć ksh, tylko dla części getopts, jeśli potrzebujesz długich opcji wiersza poleceń, ponieważ można to łatwiej zrobić.

# Working Getopts Long => KSH

#! /bin/ksh
# Getopts Long
USAGE="s(showconfig)"
USAGE+="c:(createdb)"
USAGE+="l:(createlistener)"
USAGE+="g:(generatescripts)"
USAGE+="r:(removedb)"
USAGE+="x:(removelistener)"
USAGE+="t:(createtemplate)"
USAGE+="h(help)"

while getopts "$USAGE" optchar ; do
    case $optchar in
    s)  echo "Displaying Configuration" ;;
        c)  echo "Creating Database $OPTARG" ;;
    l)  echo "Creating Listener LISTENER_$OPTARG" ;;
    g)  echo "Generating Scripts for Database $OPTARG" ;;
    r)  echo "Removing Database $OPTARG" ;;
    x)  echo "Removing Listener LISTENER_$OPTARG" ;;
    t)  echo "Creating Database Template" ;;
    h)  echo "Help" ;;
    esac
done

+1 - Zauważ, że jest to ograniczone do ksh93 - z projektu AST open source (AT&T Research).
Henk Langeveld,

2

Chciałem czegoś bez zewnętrznych zależności, ze ścisłą obsługą bash (-u) i potrzebowałem go do pracy nawet ze starszymi wersjami bash. To obsługuje różne typy parametrów:

  • krótkie boole (-h)
  • krótkie opcje (-i „image.jpg”)
  • długie boole (--help)
  • równa się opcjom (--file = "nazwa_pliku.ext")
  • opcje spacji (--file "nazwa_pliku.ext")
  • skonkatowane boole (-hvm)

Po prostu wstaw następujące elementy u góry skryptu:

# Check if a list of params contains a specific param
# usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ...
# the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable)
_param_variant() {
  for param in $1 ; do
    local variants=${param//\|/ }
    for variant in $variants ; do
      if [[ "$variant" = "$2" ]] ; then
        # Update the key to match the long version
        local arr=(${param//\|/ })
        let last=${#arr[@]}-1
        key="${arr[$last]}"
        return 0
      fi
    done
  done
  return 1
}

# Get input parameters in short or long notation, with no dependencies beyond bash
# usage:
#     # First, set your defaults
#     param_help=false
#     param_path="."
#     param_file=false
#     param_image=false
#     param_image_lossy=true
#     # Define allowed parameters
#     allowed_params="h|?|help p|path f|file i|image image-lossy"
#     # Get parameters from the arguments provided
#     _get_params $*
#
# Parameters will be converted into safe variable names like:
#     param_help,
#     param_path,
#     param_file,
#     param_image,
#     param_image_lossy
#
# Parameters without a value like "-h" or "--help" will be treated as
# boolean, and will be set as param_help=true
#
# Parameters can accept values in the various typical ways:
#     -i "path/goes/here"
#     --image "path/goes/here"
#     --image="path/goes/here"
#     --image=path/goes/here
# These would all result in effectively the same thing:
#     param_image="path/goes/here"
#
# Concatinated short parameters (boolean) are also supported
#     -vhm is the same as -v -h -m
_get_params(){

  local param_pair
  local key
  local value
  local shift_count

  while : ; do
    # Ensure we have a valid param. Allows this to work even in -u mode.
    if [[ $# == 0 || -z $1 ]] ; then
      break
    fi

    # Split the argument if it contains "="
    param_pair=(${1//=/ })
    # Remove preceeding dashes
    key="${param_pair[0]#--}"

    # Check for concatinated boolean short parameters.
    local nodash="${key#-}"
    local breakout=false
    if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then
      # Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h"
      local short_param_count=${#nodash}
      let new_arg_count=$#+$short_param_count-1
      local new_args=""
      # $str_pos is the current position in the short param string $nodash
      for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do
        # The first character becomes the current key
        if [ $str_pos -eq 0 ] ; then
          key="${nodash:$str_pos:1}"
          breakout=true
        fi
        # $arg_pos is the current position in the constructed arguments list
        let arg_pos=$str_pos+1
        if [ $arg_pos -gt $short_param_count ] ; then
          # handle other arguments
          let orignal_arg_number=$arg_pos-$short_param_count+1
          local new_arg="${!orignal_arg_number}"
        else
          # break out our one argument into new ones
          local new_arg="-${nodash:$str_pos:1}"
        fi
        new_args="$new_args \"$new_arg\""
      done
      # remove the preceding space and set the new arguments
      eval set -- "${new_args# }"
    fi
    if ! $breakout ; then
      key="$nodash"
    fi

    # By default we expect to shift one argument at a time
    shift_count=1
    if [ "${#param_pair[@]}" -gt "1" ] ; then
      # This is a param with equals notation
      value="${param_pair[1]}"
    else
      # This is either a boolean param and there is no value,
      # or the value is the next command line argument
      # Assume the value is a boolean true, unless the next argument is found to be a value.
      value=true
      if [[ $# -gt 1 && -n "$2" ]]; then
        local nodash="${2#-}"
        if [ "$nodash" = "$2" ]; then
          # The next argument has NO preceding dash so it is a value
          value="$2"
          shift_count=2
        fi
      fi
    fi

    # Check that the param being passed is one of the allowed params
    if _param_variant "$allowed_params" "$key" ; then
      # --key-name will now become param_key_name
      eval param_${key//-/_}="$value"
    else
      printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2
    fi
    shift $shift_count
  done
}

I użyj go w ten sposób:

# Assign defaults for parameters
param_help=false
param_path=$(pwd)
param_file=false
param_image=true
param_image_lossy=true
param_image_lossy_quality=85

# Define the params we will allow
allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality"

# Get the params from arguments provided
_get_params $*

1

Aby zachować zgodność między platformami i uniknąć polegania na zewnętrznych plikach wykonywalnych, przeniosłem trochę kodu z innego języka.

Uważam, że jest bardzo łatwy w użyciu, oto przykład:

ArgParser::addArg "[h]elp"    false    "This list"
ArgParser::addArg "[q]uiet"   false    "Supress output"
ArgParser::addArg "[s]leep"   1        "Seconds to sleep"
ArgParser::addArg "v"         1        "Verbose mode"

ArgParser::parse "$@"

ArgParser::isset help && ArgParser::showArgs

ArgParser::isset "quiet" \
   && echo "Quiet!" \
   || echo "Noisy!"

local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
   && echo "Sleep for $__sleep seconds" \
   || echo "No value passed for sleep"

# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"

Wymagana BASH jest trochę dłuższa niż mogłaby być, ale chciałem uniknąć polegania na tablicach asocjacyjnych BASH 4. Możesz go również pobrać bezpośrednio ze strony http://nt4.com/bash/argparser.inc.sh

#!/usr/bin/env bash

# Updates to this script may be found at
# http://nt4.com/bash/argparser.inc.sh

# Example of runtime usage:
# mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com

# Example of use in script (see bottom)
# Just include this file in yours, or use
# source argparser.inc.sh

unset EXPLODED
declare -a EXPLODED
function explode 
{
    local c=$# 
    (( c < 2 )) && 
    {
        echo function "$0" is missing parameters 
        return 1
    }

    local delimiter="$1"
    local string="$2"
    local limit=${3-99}

    local tmp_delim=$'\x07'
    local delin=${string//$delimiter/$tmp_delim}
    local oldifs="$IFS"

    IFS="$tmp_delim"
    EXPLODED=($delin)
    IFS="$oldifs"
}


# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
# Usage: local "$1" && upvar $1 "value(s)"
upvar() {
    if unset -v "$1"; then           # Unset & validate varname
        if (( $# == 2 )); then
            eval $1=\"\$2\"          # Return single value
        else
            eval $1=\(\"\${@:2}\"\)  # Return array
        fi
    fi
}

function decho
{
    :
}

function ArgParser::check
{
    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        matched=0
        explode "|" "${__argparser__arglist[$i]}"
        if [ "${#1}" -eq 1 ]
        then
            if [ "${1}" == "${EXPLODED[0]}" ]
            then
                decho "Matched $1 with ${EXPLODED[0]}"
                matched=1

                break
            fi
        else
            if [ "${1}" == "${EXPLODED[1]}" ]
            then
                decho "Matched $1 with ${EXPLODED[1]}"
                matched=1

                break
            fi
        fi
    done
    (( matched == 0 )) && return 2
    # decho "Key $key has default argument of ${EXPLODED[3]}"
    if [ "${EXPLODED[3]}" == "false" ]
    then
        return 0
    else
        return 1
    fi
}

function ArgParser::set
{
    key=$3
    value="${1:-true}"
    declare -g __argpassed__$key="$value"
}

function ArgParser::parse
{

    unset __argparser__argv
    __argparser__argv=()
    # echo parsing: "$@"

    while [ -n "$1" ]
    do
        # echo "Processing $1"
        if [ "${1:0:2}" == '--' ]
        then
            key=${1:2}
            value=$2
        elif [ "${1:0:1}" == '-' ]
        then
            key=${1:1}               # Strip off leading -
            value=$2
        else
            decho "Not argument or option: '$1'" >& 2
            __argparser__argv+=( "$1" )
            shift
            continue
        fi
        # parameter=${tmp%%=*}     # Extract name.
        # value=${tmp##*=}         # Extract value.
        decho "Key: '$key', value: '$value'"
        # eval $parameter=$value
        ArgParser::check $key
        el=$?
        # echo "Check returned $el for $key"
        [ $el -eq  2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" )
        [ $el -eq  0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments"        >&2 && ArgParser::set true "${EXPLODED[@]}"
        [ $el -eq  1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'"   >&2 && ArgParser::set "$2" "${EXPLODED[@]}" && shift
        shift
    done
}

function ArgParser::isset
{
    declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0
    return 1
}

function ArgParser::getArg
{
    # This one would be a bit silly, since we can only return non-integer arguments ineffeciently
    varname="__argpassed__$1"
    echo "${!varname}"
}

##
# usage: tryAndGetArg <argname> into <varname>
# returns: 0 on success, 1 on failure
function ArgParser::tryAndGetArg
{
    local __varname="__argpassed__$1"
    local __value="${!__varname}"
    test -z "$__value" && return 1
    local "$3" && upvar $3 "$__value"
    return 0
}

function ArgParser::__construct
{
    unset __argparser__arglist
    # declare -a __argparser__arglist
}

##
# @brief add command line argument
# @param 1 short and/or long, eg: [s]hort
# @param 2 default value
# @param 3 description
##
function ArgParser::addArg
{
    # check for short arg within long arg
    if [[ "$1" =~ \[(.)\] ]]
    then
        short=${BASH_REMATCH[1]}
        long=${1/\[$short\]/$short}
    else
        long=$1
    fi
    if [ "${#long}" -eq 1 ]
    then
        short=$long
        long=''
    fi
    decho short: "$short"
    decho long: "$long"
    __argparser__arglist+=("$short|$long|$1|$2|$3")
}

## 
# @brief show available command line arguments
##
function ArgParser::showArgs
{
    # declare -p | grep argparser
    printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )"
    printf "Defaults for the options are specified in brackets.\n\n";

    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        local shortname=
        local fullname=
        local default=
        local description=
        local comma=

        explode "|" "${__argparser__arglist[$i]}"

        shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide: 
        fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html
        test -n "$shortname" \
            && test -n "$fullname" \
            && comma=","

        default="${EXPLODED[3]}"
        case $default in
            false )
                default=
                ;;
            "" )
                default=
                ;;
            * )
                default="[$default]"
        esac

        description="${EXPLODED[4]}"

        printf "  %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default"
    done
}

function ArgParser::test
{
    # Arguments with a default of 'false' do not take paramaters (note: default
    # values are not applied in this release)

    ArgParser::addArg "[h]elp"      false       "This list"
    ArgParser::addArg "[q]uiet" false       "Supress output"
    ArgParser::addArg "[s]leep" 1           "Seconds to sleep"
    ArgParser::addArg "v"           1           "Verbose mode"

    ArgParser::parse "$@"

    ArgParser::isset help && ArgParser::showArgs

    ArgParser::isset "quiet" \
        && echo "Quiet!" \
        || echo "Noisy!"

    local __sleep
    ArgParser::tryAndGetArg sleep into __sleep \
        && echo "Sleep for $__sleep seconds" \
        || echo "No value passed for sleep"

    # This way is often more convienient, but is a little slower
    echo "Sleep set to: $( ArgParser::getArg sleep )"

    echo "Remaining command line: ${__argparser__argv[@]}"

}

if [ "$( basename "$0" )" == "argparser.inc.sh" ]
then
    ArgParser::test "$@"
fi

1

Jeśli wszystkie długie opcje mają unikalne i dopasowane, pierwsze znaki jako krótkie opcje, na przykład

./slamm --chaos 23 --plenty test -quiet

Jest taki sam jak

./slamm -c 23 -p test -q

Możesz użyć tego przed getopts, aby przepisać $ args:

# change long options to short options

for arg; do 
    [[ "${arg:0:1}" == "-" ]] && delim="" || delim="\""
    if [ "${arg:0:2}" == "--" ]; 
       then args="${args} -${arg:2:1}" 
       else args="${args} ${delim}${arg}${delim}"
    fi
done

# reset the incoming args
eval set -- $args

# proceed as usual
while getopts ":b:la:h" OPTION; do
    .....

Dzięki za mtvee za inspirację ;-)


Nie rozumiem tutaj znaczenia eval
przyjazny dla użytkownika

1

jeśli po prostu tak chcesz wywołać skrypt

myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ"

następnie możesz zastosować ten najprostszy sposób, aby to osiągnąć za pomocą getopt i --longoptions

spróbuj tego, mam nadzieję, że to się przyda

# Read command line options
ARGUMENT_LIST=(
    "input1"
    "input2"
    "input3"
)



# read arguments
opts=$(getopt \
    --longoptions "$(printf "%s:," "${ARGUMENT_LIST[@]}")" \
    --name "$(basename "$0")" \
    --options "" \
    -- "$@"
)


echo $opts

eval set --$opts

while true; do
    case "$1" in
    --input1)  
        shift
        empId=$1
        ;;
    --input2)  
        shift
        fromDate=$1
        ;;
    --input3)  
        shift
        toDate=$1
        ;;
      --)
        shift
        break
        ;;
    esac
    shift
done

0

getopts „może być użyty” do analizowania długich opcji, o ile nie oczekujesz, że będą miały argumenty ...

Oto jak:

$ cat > longopt
while getopts 'e:-:' OPT; do
  case $OPT in
    e) echo echo: $OPTARG;;
    -) #long option
       case $OPTARG in
         long-option) echo long option;;
         *) echo long option: $OPTARG;;
       esac;;
  esac
done

$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test

Jeśli spróbujesz użyć OPTIND do uzyskania parametru dla długiej opcji, getopts potraktuje go jako pierwszy brak opcjonalnego parametru pozycyjnego i przestanie analizować inne parametry. W takim przypadku lepiej będzie obsługiwać go ręcznie za pomocą prostego opisu sprawy.

To zawsze będzie działać:

$ cat >longopt2
while (($#)); do
    OPT=$1
    shift
    case $OPT in
        --*) case ${OPT:2} in
            long1) echo long1 option;;
            complex) echo comples with argument $1; shift;;
        esac;;

        -*) case ${OPT:1} in
            a) echo short option a;;
            b) echo short option b with parameter $1; shift;;
        esac;;
    esac
done


$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test

Chociaż nie jest tak elastyczny jak getopts i musisz sam zrobić wiele kodu sprawdzania błędów w instancjach sprawy ...

Ale to jest opcja.


Ale długie opcje często wymagają argumentów. I możesz zrobić więcej z - aby działało, nawet jeśli jest to coś w rodzaju hacka. W końcu można by argumentować, że jeśli nie natywnie wspierać go wtedy każdy sposób wykonania jest to coś hack ale mimo to można przedłużyć - też. I tak, shift jest bardzo przydatny, ale oczywiście jeśli oczekuje argumentu, może się okazać, że następny argument (jeśli nie zostanie określony przez użytkownika) jest częścią oczekiwanego argumentu.
Pryftan,

tak, jest to nazwa poc dla długich argumentów bez argumentów, aby rozróżnić między nimi potrzebujesz jakiejś konfiguracji, np. getops. Jeśli chodzi o zmianę, zawsze możesz „przywrócić” z setem. W każdym razie musi być konfigurowalny, jeśli parametr jest oczekiwany, czy nie. Możesz nawet użyć do tego trochę magii, ale wtedy zmusisz użytkowników do użycia - aby zasygnalizować, że magia się zatrzymuje i zaczynają się parametry pozycyjne, co jest gorsze imho.
estani

Słusznie. To więcej niż rozsądne. Tbh, nawet nie pamiętam, o co mi chodziło i zupełnie zapomniałem o tym pytaniu. Nawet niejasno pamiętam, jak to znalazłem. Twoje zdrowie. Aha i miej +1 za pomysł. Podjąłeś wysiłek i wyjaśniłeś, do czego zmierzasz. Szanuję ludzi, którzy starają się przekazywać innym pomysły itp.
Pryftan,

0

Wbudowanegetopts analizują tylko krótkie opcje (z wyjątkiem ksh93), ale nadal możesz dodać kilka wierszy skryptów, aby getopts obsługiwał długie opcje.

Oto część kodu znaleziona w http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts

  #== set short options ==#
SCRIPT_OPTS=':fbF:B:-:h'
  #== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
    [foo]=f
    [bar]=b
    [foobar]=F
    [barfoo]=B
    [help]=h
    [man]=h
)

  #== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
    #== translate long options to short ==#
    if [[ "x$OPTION" == "x-" ]]; then
        LONG_OPTION=$OPTARG
        LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
        LONG_OPTIND=-1
        [[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
        [[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
        OPTION=${ARRAY_OPTS[$LONG_OPTION]}
        [[ "x$OPTION" = "x" ]] &&  OPTION="?" OPTARG="-$LONG_OPTION"

        if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
            if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then 
                OPTION=":" OPTARG="-$LONG_OPTION"
            else
                OPTARG="$LONG_OPTARG";
                if [[ $LONG_OPTIND -ne -1 ]]; then
                    [[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
                    shift $OPTIND
                    OPTIND=1
                fi
            fi
        fi
    fi

    #== options follow by another option instead of argument ==#
    if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then 
        OPTARG="$OPTION" OPTION=":"
    fi

    #== manage options ==#
    case "$OPTION" in
        f  ) foo=1 bar=0                    ;;
        b  ) foo=0 bar=1                    ;;
        B  ) barfoo=${OPTARG}               ;;
        F  ) foobar=1 && foobar_name=${OPTARG} ;;
        h ) usagefull && exit 0 ;;
        : ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;;
        ? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;;
    esac
done
shift $((${OPTIND} - 1))

Oto test:

# Short options test
$ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello world
files=file1 file2

# Long and short options test
$ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello
files=file1 file2

W przeciwnym razie w najnowszej wersji Ksh Shell ksh93 getoptsmożna oczywiście analizować długie opcje, a nawet wyświetlać stronę podręcznika. (Zobacz http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options )


0

Th wbudowany w OS X (BSD) getopt nie obsługuje długich opcji, ale w wersji GNU robi: brew install gnu-getopt. Potem coś podobnego do: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt.


0

EasyOptions obsługuje krótkie i długie opcje:

## Options:
##   --verbose, -v   Verbose mode
##   --logfile=NAME  Log filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "log file: ${logfile}"
    echo "arguments: ${arguments[@]}"
fi
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.