Come analizzo gli argomenti della riga di comando in Bash?


1922

Dì, ho uno script che viene chiamato con questa linea:

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

o questo:

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

Qual è il modo accettato di analizzare questo in modo tale che in ogni caso (o una combinazione delle due) $v, $fe $dsaranno tutti impostati truee $outFilesaranno pari a /fizz/someOtherFile?


1
Per zsh-utenti c'è una grande incorporato chiamato zparseopts che può fare: zparseopts -D -E -M -- d=debug -debug=de hanno entrambi -de --debugnella $debugserie echo $+debug[1]restituirà 0 o 1 se vengono utilizzati uno di quelli. Rif: zsh.org/mla/users/2011/msg00350.html
dezza

1
Tutorial davvero buono: linuxcommand.org/lc3_wss0120.php . Mi piace in particolare l'esempio "Opzioni della riga di comando".
Gabriel Staples,

Risposte:


2677

Metodo n. 1: utilizzo di bash senza getopt [s]

Due modi comuni per passare argomenti coppia chiave-valore sono:

Bash Space-Separated (ad es. --option argument) (Senza getopt [s])

uso 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

output da copia-incolla del blocco sopra:

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 (ad es. --option=argument) (Senza getopt [s])

uso 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

output da copia-incolla del blocco sopra:

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

Per comprendere meglio la ${i#*=}ricerca di "Rimozione sottostringa" in questa guida . Funzionalmente è equivalente a `sed 's/[^=]*=//' <<< "$i"`quale chiama un sottoprocesso inutile o `echo "$i" | sed 's/[^=]*=//'`che chiama due sottoprocessi inutili.

Metodo n. 2: utilizzo di bash con getopt [s]

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

Limitazioni di getopt (1) ( getoptversioni precedenti, relativamente recenti ):

  • non è in grado di gestire argomenti che sono stringhe vuote
  • impossibile gestire argomenti con spazi bianchi incorporati

Le getoptversioni più recenti non hanno queste limitazioni.

Inoltre, l'offerta shell POSIX (e altri) getoptsche non ha queste limitazioni. Ho incluso un getoptsesempio semplicistico .

uso 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

output da copia-incolla del blocco sopra:

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

I vantaggi di getoptssono:

  1. È più portatile e funzionerà in altre shell come dash.
  2. Può gestire più opzioni singole come -vf filenamenel tipico modo Unix, automaticamente.

Lo svantaggio di getoptsè che può gestire solo opzioni brevi ( -h, non --help) senza codice aggiuntivo.

C'è un tutorial getopts che spiega cosa significano tutte la sintassi e le variabili. In bash, c'è anche help getopts, che potrebbe essere informativo.


44
È davvero vero? Secondo Wikipedia esiste una versione GNU più recente di getoptcui include tutte le funzionalità getoptse poi alcune. man getoptsu Ubuntu 13.04 output getopt - parse command options (enhanced)come il nome, quindi presumo che questa versione avanzata sia standard ora.
Livven,

47
Che qualcosa sia in un certo modo sul tuo sistema è una premessa molto debole su cui basare le ipotesi di "essere standard".
szablica,

13
@Livven, che getoptnon è un'utilità GNU, fa parte di util-linux.
Stephane Chazelas,

4
Se lo usi -gt 0, rimuovi shiftafter after esac, aumenta tutto shiftdi 1 e aggiungi questo caso: *) break;;puoi gestire argomenti non opzionali. Esempio: pastebin.com/6DJ57HTc
Nicolas Lacombe,

2
Non fai eco –default. Nel primo esempio, noto che se –defaultè l'ultimo argomento, non viene elaborato (considerato come non optato), a meno che non while [[ $# -gt 1 ]]sia impostato come while [[ $# -gt 0 ]]
kolydart

562

Nessuna risposta menziona getopt migliorato . E la risposta più votata è fuorviante: o ignora le -⁠vfdopzioni brevi di stile (richieste dal PO) o le opzioni dopo argomenti posizionali (richiesti anche dal PO); e ignora gli errori di analisi. Anziché:

  • Usa potenziato getoptda util-linux o precedentemente GNU glibc . 1
  • Funziona con getopt_long()la funzione C di GNU glibc.
  • Ha tutte le utili funzioni distintive (le altre non le hanno):
    • gestisce spazi, virgolette e persino binari negli argomenti 2 (non potenziato getoptnon può farlo)
    • può gestire le opzioni alla fine: script.sh -o outFile file1 file2 -v( getoptsnon lo fa)
    • consente =opzioni lunghe in stile: script.sh --outfile=fileOut --infile fileIn(consentire entrambe è lungo se l'autoanalisi)
    • consente opzioni brevi combinate, ad es. -vfd(lavoro reale se autoanalisi)
    • consente di toccare argomenti-opzioni, ad esempio -oOutfileo-vfdoOutfile
  • È già così vecchio 3 che non manca questo sistema GNU (es. Qualsiasi Linux ce l'ha).
  • Puoi verificarne l'esistenza con: getopt --test→ restituisce il valore 4.
  • Altri getopto integrati nella shell getoptssono di uso limitato.

Le seguenti chiamate

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

tutti ritornano

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

con il seguente 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 getopt avanzato è disponibile sulla maggior parte dei "sistemi bash", incluso Cygwin; su OS X provare a installare gnu-getopt osudo port install getopt
2 leexec()convenzioniPOSIXnon hanno un modo affidabile per passare NULL binario negli argomenti della riga di comando; quei byte terminano prematuramente laprima versionedell'argomento
3 rilasciata nel 1997 o prima (l'ho rintracciata solo nel 1997)


4
Grazie per questo. È appena confermato dalla tabella delle funzionalità di en.wikipedia.org/wiki/Getopts , se hai bisogno di supporto per opzioni lunghe e non sei su Solaris, getoptè la strada da percorrere.
johncip,

4
Credo che l'unico avvertimento getoptsia che non può essere utilizzato convenientemente negli script wrapper in cui si potrebbero avere poche opzioni specifiche per lo script wrapper, quindi passare le opzioni non-wrapper-script all'eseguibile wrapping, intatte. Diciamo che ho un grepwrapper chiamato mygrepe ho un'opzione --foospecifica a mygrep, quindi non posso farlo mygrep --foo -A 2, e ho il -A 2passaggio automatico a grep; Ho bisogno di fare mygrep --foo -- -A 2. Ecco la mia implementazione in cima alla tua soluzione.
Kaushal Modi,

2
@bobpaul La tua affermazione su util-linux è sbagliata e fuorviante: il pacchetto è contrassegnato come "essenziale" su Ubuntu / Debian. Come tale, è sempre installato. - Di quali distro stai parlando (dove dici che deve essere installato apposta)?
Robert Siemer,

3
Nota che questo non funziona su Mac almeno fino all'attuale 10.14.3. Il getopt che spedisce è getopt BSD dal 1999 ...
jjj

2
@transang Negazione booleana del valore restituito. E il suo effetto collaterale: consentire a un comando di fallire (altrimenti errexit interromperebbe il programma in caso di errore). - I commenti nello script ti dicono di più. Altrimenti:man bash
Robert Siemer,

144

Modo più conciso

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"

Uso:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify

3
Questo è quello che sto facendo. Devo while [[ "$#" > 1 ]]se voglio sostenere la fine della linea con una bandiera booleana ./script.sh --debug dev --uglify fast --verbose. Esempio: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli

12
Wow! Semplice e pulito! Ecco come sto usando questo: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli

2
È molto più bello incollarlo in ogni script piuttosto che avere a che fare con l'origine o far sì che la gente si chieda dove inizia effettivamente la tua funzionalità.
RealHandy,

Attenzione: questo tollera argomenti duplicati, prevale l'ultimo argomento. ad es. ./script.sh -d dev -d prodsi tradurrebbe in deploy == 'prod'. L'ho usato comunque: P :): +1:
yair

Sto usando questo (grazie!) Ma nota che consente un valore di argomento vuoto, ad esempio ./script.sh -dnon genererebbe un errore ma si limiterebbe a impostare $deployuna stringa vuota.
EM0,

137

da: digitalpeer.com con lievi modifiche

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

Per comprendere meglio la ${i#*=}ricerca di "Rimozione sottostringa" in questa guida . Funzionalmente è equivalente a `sed 's/[^=]*=//' <<< "$i"`quale chiama un sottoprocesso inutile o `echo "$i" | sed 's/[^=]*=//'`che chiama due sottoprocessi inutili.


4
! Neat Anche se questo non funzionerà per argomenti separati da spazio mount -t tempfs .... Probabilmente si può risolvere il problema attraverso una cosa del genere while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;, ecc
Tobias Kienzler

3
Questo non può gestire le -vfdopzioni brevi combinate di stile.
Robert Siemer

105

getopt()/ getopts()è una buona opzione. Rubato da qui :

Il semplice uso di "getopt" è mostrato in questo mini-script:

#!/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

Ciò che abbiamo detto è che qualsiasi di -a, -b, -c o -d sarà permesso, ma che -c è seguito da un argomento (la "c:" dice che).

Se lo chiamiamo "g" e lo proviamo:

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

Iniziamo con due argomenti e "getopt" suddivide le opzioni e mette ciascuna nel proprio argomento. Ha anche aggiunto "-".


4
L'utilizzo $*è un utilizzo non funzionante di getopt. (Immerge argomenti con spazi.) Vedi la mia risposta per un uso corretto.
Robert Siemer,

Perché vorresti renderlo più complicato?
SDsolar,

@Matt J, la prima parte dello script (per i) sarebbe in grado di gestire argomenti con spazi in essi se usi "$ i" invece di $ i. Le getopts non sembrano essere in grado di gestire argomenti con spazi. Quale sarebbe il vantaggio di usare getopt sul ciclo for i?
thebunnyrules,

99

A rischio di aggiungere un altro esempio da ignorare, ecco il mio schema.

  • maniglie -n arg e--name=arg
  • consente argomenti alla fine
  • mostra errori sani se qualcosa è errato
  • compatibile, non usa basismi
  • leggibile, non richiede il mantenimento dello stato in un ciclo

Spero sia utile a qualcuno.

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

4
Scusa per il ritardo. Nel mio script, la funzione handle_argument riceve tutti gli argomenti non di opzione. Puoi sostituire quella riga con quello che desideri, magari *) die "unrecognized argument: $1"o raccogliere gli arg in una variabile *) args+="$1"; shift 1;;.
bronson,

Sorprendente! Ho testato un paio di risposte, ma questa è l'unica che ha funzionato per tutti i casi, inclusi molti parametri posizionali (sia prima che dopo le bandiere)
Guilherme Garnier

2
bel codice sintetico, ma l'uso di -n e nessun altro argomento provoca un ciclo infinito a causa di un errore shift 2, emettendo shiftdue volte invece di shift 2. Suggerito la modifica.
lauksas,

42

Sono in ritardo di circa 4 anni a questa domanda, ma voglio restituire. Ho usato le risposte precedenti come punto di partenza per riordinare la mia vecchia analisi ad hoc. Ho quindi riformulato il seguente codice modello. Gestisce sia parametri lunghi che brevi, usando argomenti separati da = o spazio, nonché più parametri brevi raggruppati insieme. Alla fine reinserisce tutti gli argomenti non param nelle variabili $ 1, $ 2 ... Spero sia utile

#!/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

Questo codice non può gestire le opzioni con argomenti come questo: -c1. E l'uso di =separare le brevi opzioni dai loro argomenti è insolito ...
Robert Siemer,

2
Mi sono imbattuto in due problemi con questo utile pezzo di codice: 1) lo "shift" nel caso di "-c = pippo" finisce per mangiare il prossimo parametro; e 2) 'c' non dovrebbe essere incluso nel modello "[cfr]" per opzioni brevi combinabili.
sfnd

36

Ho trovato la questione di scrivere analisi portatili negli script così frustrante che ho scritto Argbash - un generatore di codice FOSS che può generare il codice di analisi degli argomenti per il tuo script e ha alcune belle caratteristiche:

https://argbash.io


Grazie per aver scritto argbash, l'ho appena usato e ho scoperto che funziona bene. Ho scelto principalmente argbash perché è un generatore di codice che supporta il vecchio bash 3.x trovato su OS X 10.11 El Capitan. L'unico aspetto negativo è che l'approccio del generatore di codice significa un sacco di codice nello script principale, rispetto alla chiamata di un modulo.
RichVel,

Puoi effettivamente utilizzare Argbash in modo da produrre librerie di analisi su misura solo per te che puoi aver incluso nel tuo script o puoi averlo in un file separato e semplicemente sorgente. Ho aggiunto un esempio per dimostrarlo e l'ho reso anche più esplicito nella documentazione.
bubla,

Buono a sapersi. Questo esempio è interessante ma ancora non molto chiaro - forse puoi cambiare il nome dello script generato in 'parse_lib.sh' o simile e mostrare dove lo script principale lo chiama (come nella sezione script di wrapping che è il caso d'uso più complesso).
RichVel,

I problemi sono stati risolti nella recente versione di argbash: la documentazione è stata migliorata, è stato introdotto uno script argbash-init di avvio rapido e puoi persino utilizzare argbash online su argbash.io/generate
bubla,

29

La mia risposta si basa in gran parte sulla risposta di Bruno Bronosky , ma in un certo senso ho mescolato le sue due implementazioni pure bash in una che uso abbastanza frequentemente.

# 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

Ciò consente di avere sia opzioni / valori separati da spazi, sia valori definiti uguali.

Quindi puoi eseguire il tuo script usando:

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

così come:

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

ed entrambi dovrebbero avere lo stesso risultato finale.

PROFESSIONISTI:

  • Consente sia -arg = value che -arg value

  • Funziona con qualsiasi nome arg che puoi usare in bash

    • Significato -a o -arg o --arg o -arg o altro
  • Bash puro. Non c'è bisogno di imparare / usare getopt o getopts

CONS:

  • Non riesco a combinare argomenti

    • Significa no -abc. Devi fare -a -b -c

Questi sono gli unici vantaggi / svantaggi che mi vengono in mente


15

Penso che questo sia abbastanza semplice da usare:

#!/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

Esempio di invito:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile

1
Ho letto tutto e questo è il mio preferito. Non mi piace usare -a=1come stile argc. Preferisco mettere prima l'opzione principale -opzioni e successivamente quelle speciali con spaziatura singola -o option. Sto cercando il modo più semplice contro migliore per leggere Argvs.
m3nda,

Funziona davvero bene ma se si passa un argomento a un'opzione non a: tutte le seguenti opzioni verrebbero prese come argomenti. Puoi controllare questa riga ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFilecon il tuo script. -d L'opzione non è impostata come d:
m3nda

15

Espandendo l'eccellente risposta di @guneysus, ecco una modifica che consente all'utente di utilizzare la sintassi che preferisce, ad es.

command -x=myfilename.ext --another_switch 

vs

command -x myfilename.ext --another_switch

Ciò significa che gli uguali possono essere sostituiti con spazi bianchi.

Questa "interpretazione confusa" potrebbe non essere di tuo gradimento, ma se stai realizzando script intercambiabili con altre utilità (come nel caso del mio, che deve funzionare con ffmpeg), la flessibilità è utile.

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

13

Questo esempio mostra come usare getopte evaled HEREDOCe shiftper gestire i parametri corti e lunghi con e senza un valore richiesto che segue. Anche l'istruzione switch / case è concisa e facile da seguire.

#!/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

Le righe più significative dello script sopra sono queste:

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

Breve, al punto, leggibile e gestisce quasi tutto (IMHO).

Spero che aiuti qualcuno.


1
Questa è una delle migliori risposte.
Mr. Polywhirl,

11

Ti do la funzione parse_paramsche analizzerà i parametri dalla riga di comando.

  1. È una soluzione Bash pura, senza utilità aggiuntive.
  2. Non inquina l'ambito globale.
  3. Ti restituisce senza sforzo variabili semplici da usare, su cui puoi costruire ulteriore logica.
  4. La quantità di trattini prima che i parametri non contano ( --alluguale a -alluguale all=all)

Lo script seguente è una dimostrazione funzionante di copia e incolla. Vedi la show_usefunzione per capire come usareparse_params .

limitazioni:

  1. Non supporta i parametri delimitati dallo spazio ( -d 1)
  2. I nomi dei parametri perderanno i trattini così --any-parame -anyparamsono equivalenti
  3. eval $(parse_params "$@")deve essere utilizzato all'interno della funzione bash (non funzionerà nell'ambito globale)

#!/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

Per utilizzare la demo per analizzare i parametri che entrano nel tuo script bash, fai semplicementeshow_use "$@"
Oleksii Chekulaiev,

Fondamentalmente ho scoperto che github.com/renatosilva/easyoptions fa lo stesso allo stesso modo ma è un po 'più massiccio di questa funzione.
Oleksii Chekulaiev,

10

EasyOptions non richiede alcun analisi:

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

Mi ci è voluto un minuto per capire che i commenti nella parte superiore dello script di esempio vengono analizzati per fornire una stringa di aiuto sull'utilizzo predefinita, nonché le specifiche dell'argomento. Questa è una soluzione geniale e mi dispiace che abbia ottenuto solo 6 voti in 2 anni. Forse questa domanda è troppo sommersa per essere notata dalla gente.
Metamorfico

In un certo senso la tua soluzione è di gran lunga la migliore (a parte quella di @ OleksiiChekulaiev che non supporta la sintassi delle opzioni "standard"). Questo perché la tua soluzione richiede al writer di script di specificare una sola volta il nome di ciascuna opzione . Il fatto che altre soluzioni richiedano che venga specificato 3 volte - nell'uso, nel modello "case" e nell'impostazione della variabile - mi ha continuamente infastidito. Anche getopt ha questo problema. Tuttavia, il tuo codice è lento sulla mia macchina - 0.11s per l'implementazione di Bash, 0.28s per il Ruby. Contro 0.02s per l'analisi esplicita "while-case".
Metamorfico

Voglio una versione più veloce, forse scritta in C. Inoltre, una versione compatibile con zsh. Forse questo merita una domanda separata ("Esiste un modo per analizzare gli argomenti della riga di comando in shell tipo Bash che accetta la sintassi standard delle opzioni lunghe e non richiede che i nomi delle opzioni vengano digitati più di una volta?").
Metamorfico

10

getopts funziona alla grande se il numero 1 è installato e il numero 2 si intende eseguirlo sulla stessa piattaforma. OSX e Linux (ad esempio) si comportano diversamente in questo senso.

Ecco una soluzione (non getopts) che supporta flag uguale, non uguale e booleano. Ad esempio, potresti eseguire il tuo script in questo modo:

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

8

Ecco come faccio in una funzione per evitare di interrompere l'esecuzione getopts nello stesso momento in un punto più alto dello stack:

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

8

Espandendo la risposta di @ bruno-bronosky, ho aggiunto un "preprocessore" per gestire alcune formattazioni comuni:

  • espande --longopt=val in--longopt val
  • espande -xyz in-x -y -z
  • supporti -- per indicare la fine delle bandiere
  • Mostra un errore per opzioni impreviste
  • Switch opzioni compatto e di facile lettura
#!/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[@]}"

6

Esistono diversi modi per analizzare gli argomenti cmdline (ad esempio GNU getopt (non portatile) vs BSD (OSX) getopt vs getopts) - tutti problematici. Questa soluzione è

  • portatile!
  • ha zero dipendenze, si basa solo su built-in bash
  • consente sia opzioni brevi che lunghe
  • gestisce gli spazi bianchi tra opzione e argomento ma può anche usare il =separatore
  • supporta lo stile di opzioni brevi concatenato -vxf
  • gestisce l'opzione con argomenti opzionali (vedi esempio) e
  • non richiede un eccesso di codice rispetto alle alternative per lo stesso set di funzionalità. Vale a dire succinta, e quindi più facile da mantenere

Esempi: uno qualsiasi di

# 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=${@}"

5

Vorrei offrire la mia versione dell'analisi delle opzioni, che consente quanto segue:

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

Lo consente anche (potrebbe essere indesiderato):

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

Devi decidere prima dell'uso se = deve essere usato su un'opzione o no. Questo per mantenere pulito il codice (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

1
qual è il significato di "+ x" su $ {chiave + x}?
Luca Davanzo,

1
È un test per vedere se 'chiave' è presente o meno. Più in basso ho disinserito il tasto e questo interrompe il ciclo while interno.
Galmok,

5

Soluzione che conserva argomenti non gestiti. Demo incluse.

Ecco la mia soluzione È MOLTO flessibile e diversamente da altri, non dovrebbe richiedere pacchetti esterni e gestisce gli argomenti rimanenti in modo pulito.

L'utilizzo è: ./myscript -flag flagvariable -otherflag flagvar2

Tutto quello che devi fare è modificare la riga dei flag validi. Anticipa un trattino e cerca tutti gli argomenti. Definisce quindi l'argomento successivo come il nome della bandiera, ad es

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

Il codice principale (versione corta, dettagliata con esempi più in basso, anche una versione con errori):

#!/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

La versione dettagliata con demo ecologiche integrate:

#!/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

L'ultimo, questo errore, se viene passato un oggetto non valido.

#!/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

Pro: cosa fa, gestisce molto bene. Conserva argomenti inutilizzati che molte altre soluzioni qui non fanno. Consente inoltre di chiamare le variabili senza essere definite manualmente nello script. Consente inoltre la prepopolazione delle variabili se non viene fornito alcun argomento corrispondente. (Vedi esempio dettagliato).

Contro: Non è possibile analizzare una singola stringa arg complessa, ad esempio -xcvf verrebbe elaborata come un singolo argomento. In qualche modo potresti facilmente scrivere codice aggiuntivo nel mio che aggiunge questa funzionalità.



3

Nota che getopt(1) stato un breve errore di vita da parte di AT&T.

getopt è stato creato nel 1984 ma già sepolto nel 1986 perché non era realmente utilizzabile.

Una prova del fatto che getoptè molto obsoleto è che la getopt(1)pagina man menziona ancora "$*"invece "$@", che è stata aggiunta alla Bourne Shell nel 1986 insieme algetopts(1) shell incorporata al fine di trattare argomenti con spazi all'interno.

A proposito: se sei interessato ad analizzare le opzioni lunghe negli script di shell, potrebbe essere interessante sapere che l' getopt(3)implementazione da libc (Solaris) ed ksh93entrambe hanno aggiunto un'implementazione di opzioni lunghe uniforme che supporta opzioni lunghe come alias per opzioni brevi. Ciò provoca ksh93e Bourne Shellimplementare un'interfaccia uniforme per lunghe opzioni tramitegetopts .

Un esempio di opzioni lunghe prese dalla pagina man di Bourne Shell:

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

mostra per quanto tempo gli alias delle opzioni possono essere utilizzati sia in Bourne Shell che in ksh93.

Vedi la pagina man di una recente Bourne Shell:

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

e la pagina man per getopt (3) di OpenSolaris:

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

e infine, la pagina man getopt (1) per verificare $ * obsoleto:

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


3

Ho scritto un aiutante bash per scrivere un bel strumento bash

casa del progetto: https://gitlab.mbedsys.org/mbedsys/bashopts

esempio:

#!/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

darà aiuto:

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:"")

godere :)


Ottengo questo su Mac OS X: `` `lib / bashopts.sh: riga 138: dichiarare: -A: opzione non valida dichiarare: uso: dichiarare [-afFirtx] [-p] [nome [= valore] ...] Errore in lib / bashopts.sh: 138. 'declare -x -A bashopts_optprop_name' è uscito con stato 2 Albero delle chiamate: 1: lib / controller.sh: 4 source (...) Uscita con lo stato 1 `` `
Josh Wulf,

Per usare questo è necessario Bash versione 4. Su Mac, la versione predefinita è 3. È possibile utilizzare la birra fatta in casa per installare bash 4.
Josh Wulf,

3

Ecco il mio approccio: usare regexp.

  • niente getopts
  • gestisce blocchi di parametri brevi -qwerty
  • gestisce parametri brevi -q -w -e
  • gestisce lunghe opzioni --qwerty
  • puoi passare l'attributo all'opzione corta o lunga (se stai usando il blocco di opzioni brevi, l'attributo è attaccato all'ultima opzione)
  • puoi usare gli spazi o =per fornire gli attributi, ma gli attributi corrispondono fino a incontrare il trattino + spazio "delimitatore", quindi in--q=qwe ty qwe ty è un attributo
  • gestisce il mix di tutto quanto sopra quindi -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributeè valido

script:

#!/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

Come questo. Forse basta aggiungere -e param all'eco con una nuova riga.
mauron85,

3

Supponiamo di creare uno script di shell chiamato test_args.shcome segue

#!/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"

Dopo aver eseguito il comando seguente:

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

L'output sarebbe:

year=2017 month=12 day=22 flag=true

5
Questo ha lo stesso approccio della risposta di Noè , ma ha meno controlli di sicurezza / garanzie. Questo ci consente di scrivere argomenti arbitrari nell'ambiente dello script e sono abbastanza sicuro che il tuo uso di eval qui possa consentire l'inserimento di comandi.
Will Barnwell,

2

Usa gli "argomenti" del modulo da bash- module

Esempio:

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

NAME="world"

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

info "Hello, $NAME!"

2

Combinazione di argomenti posizionali e basati su flag

--param = arg (uguale a delimitato)

Mescolando liberamente flag tra argomenti posizionali:

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

può essere realizzato con un approccio abbastanza conciso:

# 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 (spazio delimitato)

Di solito è più chiaro non mescolare --flag=valuee --flag valuestili.

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

Questo è un po 'rischioso da leggere, ma è ancora valido

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

fonte

# 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

2

Ecco una getopts che ottiene l'analisi con un codice minimo e ti consente di definire ciò che desideri estrarre in un caso usando eval con sottostringa.

Fondamentalmente 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}"
}

Dichiara le variabili come locali anziché globali come la maggior parte delle risposte qui.

Chiamato come:

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

$ {K: 3} è fondamentalmente una sottostringa per rimuovere il primo ---dalla chiave.


1

Anche questo può essere utile sapere, puoi impostare un valore e se qualcuno fornisce input, sovrascrivi il valore predefinito con quel valore.

myscript.sh -f ./serverlist.txt o semplicemente ./myscript.sh (e richiede impostazioni predefinite)

    #!/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}"

1

Un'altra soluzione senza getopt [s], POSIX, vecchio stile Unix

Simile alla soluzione che Bruno Bronosky ha pubblicato qui qui è uno senza l'uso di getopt(s).

La principale caratteristica di differenziazione della mia soluzione è che consente di concatenare le opzioni esattamente come tar -xzf foo.tar.gzè uguale a tar -x -z -f foo.tar.gz. E proprio come in tar,ps ecc. Il trattino principale è facoltativo per un blocco di opzioni brevi (ma questo può essere modificato facilmente). Sono supportate anche le opzioni lunghe (ma quando un blocco inizia con uno, sono richiesti due trattini iniziali).

Codice con opzioni di esempio

#!/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

Per l'utilizzo di esempio, vedere gli esempi più avanti.

Posizione delle opzioni con argomenti

Per quello che vale, le opzioni con argomenti non sono le ultime (solo le opzioni lunghe devono essere). Quindi, mentre ad esempio in tar(almeno in alcune implementazioni) le fopzioni devono essere ultime perché il nome del file segue ( tar xzf bar.tar.gzfunziona ma tar xfz bar.tar.gznon lo fa), non è questo il caso qui (vedere gli esempi successivi).

Molteplici opzioni con argomenti

Come ulteriore bonus, i parametri delle opzioni vengono consumati nell'ordine delle opzioni dai parametri con le opzioni richieste. Guarda l'output del mio script qui con la riga di comando abc X Y Z(o -abc X Y Z):

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

Opzioni lunghe concatenate pure

Inoltre puoi anche avere lunghe opzioni nel blocco opzioni dato che si verificano per ultime nel blocco. Quindi le seguenti righe di comando sono tutte equivalenti (incluso l'ordine in cui vengono elaborate le opzioni e i suoi argomenti):

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

Tutti questi portano a:

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

Non in questa soluzione

Argomenti opzionali

Le opzioni con argomenti opzionali dovrebbero essere possibili con un po 'di lavoro, ad esempio guardando avanti se c'è un blocco senza un trattino; l'utente dovrebbe quindi mettere un trattino davanti a ogni blocco che segue un blocco con un parametro che ha un parametro opzionale. Forse questo è troppo complicato per comunicare all'utente, quindi è meglio richiedere un trattino iniziale in questo caso.

Le cose diventano ancora più complicate con più parametri possibili. Vorrei sconsigliare di fare le opzioni cercando di essere intelligente determinando se l'argomento potrebbe essere adatto o meno (ad es. Con un'opzione prende solo un numero come argomento opzionale) perché questo potrebbe rompersi in futuro.

Personalmente preferisco opzioni aggiuntive invece di argomenti opzionali.

Argomenti delle opzioni introdotti con un segno di uguale

Proprio come con gli argomenti opzionali, non ne sono un fan (a proposito, c'è un thread per discutere i pro / contro dei diversi stili di parametro?) Ma se lo desideri potresti probabilmente implementarlo tu stesso come fatto su http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop con --long-with-arg=?*un'istruzione case e quindi eliminando il segno uguale (questo è BTW il sito che dice che è possibile effettuare la concatenazione dei parametri con un certo sforzo ma "l'ha lasciato come esercizio per il lettore "che mi ha fatto prendere le loro parole ma ho iniziato da zero).

Altre note

Conforme a POSIX, funziona anche con le configurazioni di Busybox antiche con cui dovevo fare i conti (ad es cut. headE getoptsmancante).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.