Wie analysiere ich Befehlszeilenargumente in Bash?


1921

Angenommen, ich habe ein Skript, das mit dieser Zeile aufgerufen wird:

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

oder dieses:

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

Was ist die akzeptierte Möglichkeit , diese , so dass (oder eine Kombination aus beiden) jeweils zu parsen $v, $fund $dwerden alle eingestellt werden trueund $outFilegleich sein wird /fizz/someOtherFile?


1
Für zsh-Benutzer gibt es eine großartige integrierte Funktion namens zparseopts, die Folgendes kann: zparseopts -D -E -M -- d=debug -debug=dUnd beide -dund --debugim $debugArray echo $+debug[1]geben 0 oder 1 zurück, wenn eine davon verwendet wird. Ref: zsh.org/mla/users/2011/msg00350.html
dezza

1
Wirklich gutes Tutorial: linuxcommand.org/lc3_wss0120.php . Besonders gut gefällt mir das Beispiel "Befehlszeilenoptionen".
Gabriel Staples

Antworten:


2676

Methode 1: Verwenden von bash ohne getopt [s]

Zwei gebräuchliche Methoden zum Übergeben von Schlüssel-Wert-Paar-Argumenten sind:

Bash Space-Separated (zB --option argument) (ohne getopt [s])

Verwendungszweck 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

Ausgabe vom Einfügen des obigen Blocks:

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 (zB --option=argument) (ohne getopt [s])

Verwendungszweck 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

Ausgabe vom Einfügen des obigen Blocks:

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

Zum besseren Verständnis ${i#*=}suchen Sie in diesem Handbuch nach "Entfernen von Teilzeichenfolgen" . Es ist funktional äquivalent zu `sed 's/[^=]*=//' <<< "$i"`dem , was einen unnötigen Unterprozess `echo "$i" | sed 's/[^=]*=//'`aufruft oder der zwei unnötige Unterprozesse aufruft .

Methode 2: Bash mit getopt [s] verwenden

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

Einschränkungen für getopt (1) (ältere, relativ aktuelle getoptVersionen):

  • kann keine Argumente verarbeiten, die leere Zeichenfolgen sind
  • Argumente mit eingebettetem Leerzeichen können nicht verarbeitet werden

Neuere getoptVersionen haben diese Einschränkungen nicht.

Darüber hinaus bietet die POSIX-Shell (und andere) getoptsdiese Einschränkungen nicht an. Ich habe ein vereinfachtes getoptsBeispiel beigefügt .

Verwendungszweck 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

Ausgabe vom Einfügen des obigen Blocks:

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

Die Vorteile von getoptssind:

  1. Es ist tragbarer und funktioniert in anderen Shells wie dash.
  2. Es kann -vf filenameautomatisch mehrere einzelne Optionen wie auf typische Unix-Weise verarbeiten.

Der Nachteil von getoptsist, dass es nur kurze Optionen ( -hnicht --help) ohne zusätzlichen Code verarbeiten kann.

Es gibt ein getopts-Tutorial, in dem erklärt wird, was alle Syntax und Variablen bedeuten. In Bash gibt es auch help getopts, was informativ sein könnte.


44
Ist das wirklich wahr? Laut Wikipedia gibt es eine neuere GNU-erweiterte Version, getoptdie alle Funktionen von getoptsund noch einige mehr enthält. man getoptUnter Ubuntu 13.04 wird getopt - parse command options (enhanced)der Name ausgegeben, daher gehe ich davon aus, dass diese erweiterte Version jetzt Standard ist.
Livven

47
Dass etwas auf Ihrem System eine bestimmte Art ist, ist eine sehr schwache Voraussetzung, um die Annahme, "Standard zu sein", zu begründen.
Szablica

13
@Livven, das getoptist kein GNU-Dienstprogramm, es ist Teil von util-linux.
Stephane Chazelas

4
Wenn Sie verwenden -gt 0, entfernen Sie Ihre shiftnach dem esac, erweitern Sie alle shiftum 1 und fügen Sie diesen Fall hinzu: *) break;;Sie können nicht optionale Argumente verarbeiten. Beispiel: pastebin.com/6DJ57HTc
Nicolas Lacombe

2
Du hallst nicht –default. Im ersten Beispiel –defaultwhile [[ $# -gt 1 ]]while [[ $# -gt 0 ]]
stelle

562

Keine Antwort erwähnt verbesserte getopt . Und die Antwort mit der höchsten Bewertung ist irreführend: Sie ignoriert entweder -⁠vfdStil-Kurzoptionen (vom OP angefordert) oder Optionen nach Positionsargumenten (auch vom OP angefordert). und es ignoriert Parsing-Fehler. Stattdessen:

  • Verwenden Sie Enhanced getoptvon Util-Linux oder früher GNU Glibc . 1
  • Es funktioniert mit getopt_long()der C-Funktion von GNU glibc.
  • Hat alle nützlichen Unterscheidungsmerkmale (die anderen haben sie nicht):
    • behandelt Leerzeichen, Anführungszeichen und sogar Binärdateien in Argument 2 (nicht erweitert getoptkann dies nicht)
    • es kann Optionen am Ende verarbeiten: script.sh -o outFile file1 file2 -v( getoptstut dies nicht)
    • erlaubt =-style lange Optionen: script.sh --outfile=fileOut --infile fileIn(beide zuzulassen ist langwierig, wenn sie selbst analysiert werden)
    • ermöglicht kombinierte kurze Optionen, z. B. -vfd(echte Arbeit, wenn selbst analysiert)
    • erlaubt das Berühren von Optionsargumenten, zB -oOutfileoder-vfdoOutfile
  • Ist schon so alt 3, dass dies keinem GNU-System fehlt (zB hat es irgendein Linux).
  • Sie können seine Existenz testen mit: getopt --test→ Rückgabewert 4.
  • Andere getoptoder Shell-eingebaute getoptssind von begrenztem Nutzen.

Die folgenden Aufrufe

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

Alle kehren zurück

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

mit den folgenden 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 erweitertes getopt ist auf den meisten "Bash-Systemen" verfügbar, einschließlich Cygwin; Unter OS X versuchen Sie, Brew Gnu-Getopt odersudo port install getopt
2 zu installieren. Die POSIX-exec()Konventionen haben keine zuverlässige Möglichkeit, binäres NULL in Befehlszeilenargumenten zu übergeben. Diese Bytes beenden vorzeitig dieerste Version vonArgument
3, die 1997 oder früher veröffentlicht wurde (ich habe sie nur bis 1997 zurückverfolgt).


4
Danke dafür. Gerade aus der Feature-Tabelle unter en.wikipedia.org/wiki/Getopts bestätigt , wenn Sie Unterstützung für lange Optionen benötigen und nicht mit Solaris arbeiten, getoptist der richtige Weg.
Johncip

4
Ich glaube, dass die einzige Einschränkung darin getoptbesteht, dass es nicht bequem in Wrapper-Skripten verwendet werden kann, in denen möglicherweise nur wenige Optionen für das Wrapper-Skript verfügbar sind, und dann die Nicht-Wrapper-Skript-Optionen an die umschlossene ausführbare Datei intakt übergeben werden. Nehmen wir an, ich habe einen grepWrapper namens mygrepund ich habe eine --foospezifische Option für mygrep, dann kann ich das nicht mygrep --foo -A 2und habe die -A 2automatisch an übergeben grep. Ich brauche zu tun mygrep --foo -- -A 2. Hier ist meine Implementierung zusätzlich zu Ihrer Lösung.
Kaushal Modi

2
@bobpaul Ihre Aussage zu util-linux ist falsch und auch irreführend: Das Paket ist unter Ubuntu / Debian als "wesentlich" gekennzeichnet. Als solches ist es immer installiert. - Über welche Distributionen sprechen Sie (wo Sie sagen, dass sie absichtlich installiert werden müssen)?
Robert Siemer

3
Beachten Sie, dass dies auf dem Mac zumindest bis zum aktuellen 10.14.3 nicht funktioniert. Die getopt, die versendet wird, ist BSD getopt von 1999 ...
jjj

2
@transang Boolesche Negation des Rückgabewerts. Und sein Nebeneffekt: Lassen Sie einen Befehl fehlschlagen (andernfalls würde errexit das Programm bei einem Fehler abbrechen). - Die Kommentare im Skript sagen mehr. Ansonsten:man bash
Robert Siemer

144

Prägnanter Weg

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"

Verwendungszweck:

./script.sh -d dev -u

# OR:

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

3
Das mache ich. Muss, while [[ "$#" > 1 ]]wenn ich das Beenden der Zeile mit einem booleschen Flag unterstützen möchte ./script.sh --debug dev --uglify fast --verbose. Beispiel: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli

12
Beeindruckend! Einfach und sauber! So benutze ich das: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli

2
Dies ist viel besser in jedes Skript einzufügen, als sich mit der Quelle zu befassen oder sich fragen zu lassen, wo Ihre Funktionalität tatsächlich beginnt.
RealHandy

Warnung: Dies toleriert doppelte Argumente, das neueste Argument hat Vorrang. zB ./script.sh -d dev -d prodwürde dazu führen deploy == 'prod'. Ich habe es trotzdem benutzt: P :): +1:
yair

Ich benutze dies (danke!), Aber beachte, dass es einen leeren Argumentwert zulässt, z. B. ./script.sh -dkeinen Fehler erzeugen würde, sondern nur $deployeine leere Zeichenfolge setzen würde.
EM0

137

von: digitalpeer.com mit geringfügigen Änderungen

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

Zum besseren Verständnis ${i#*=}suchen Sie in diesem Handbuch nach "Entfernen von Teilzeichenfolgen" . Es ist funktional äquivalent zu `sed 's/[^=]*=//' <<< "$i"`dem , was einen unnötigen Unterprozess `echo "$i" | sed 's/[^=]*=//'`aufruft oder der zwei unnötige Unterprozesse aufruft .


4
Ordentlich! Dies funktioniert jedoch nicht für durch Leerzeichen getrennte Argumente à la mount -t tempfs .... Man kann dies wahrscheinlich über so etwas wie while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;etc beheben
Tobias Kienzler

3
Dies kann nicht mit -vfdkombinierten Stiloptionen umgehen .
Robert Siemer

105

getopt()Ich getopts()bin eine gute Option. Von hier gestohlen :

Die einfache Verwendung von "getopt" wird in diesem Mini-Skript gezeigt:

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

Was wir gesagt haben ist, dass jedes von -a, -b, -c oder -d erlaubt ist, aber dass -c von einem Argument gefolgt wird (das "c:" sagt das).

Wenn wir dies "g" nennen und es ausprobieren:

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

Wir beginnen mit zwei Argumenten, und "getopt" bricht die Optionen auseinander und fügt jedes in ein eigenes Argument ein. Es wurde auch "-" hinzugefügt.


4
Verwenden $*ist gebrochene Verwendung von getopt. (Argumente werden mit Leerzeichen versehen.) Siehe meine Antwort für die ordnungsgemäße Verwendung.
Robert Siemer

Warum sollten Sie es komplizierter machen wollen?
SDsolar

@Matt J, der erste Teil des Skripts (für i) kann Argumente mit Leerzeichen verarbeiten, wenn Sie "$ i" anstelle von $ i verwenden. Die getopts scheinen nicht in der Lage zu sein, Argumente mit Leerzeichen zu verarbeiten. Was wäre der Vorteil der Verwendung von getopt gegenüber der for i-Schleife?
Thebunnyrules

99

Auf die Gefahr hin, ein weiteres zu ignorierendes Beispiel hinzuzufügen, ist hier mein Schema.

  • Griffe -n argund--name=arg
  • erlaubt Argumente am Ende
  • zeigt vernünftige Fehler an, wenn etwas falsch geschrieben ist
  • kompatibel, verwendet keine Bashismen
  • lesbar, erfordert keine Aufrechterhaltung des Status in einer Schleife

Hoffe, es ist nützlich für jemanden.

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
Entschuldigung für die Verspätung. In meinem Skript empfängt die Funktion handle_argument alle Argumente ohne Option. Sie können diese Zeile durch eine beliebige ersetzen *) die "unrecognized argument: $1"oder die Argumente in einer Variablen zusammenfassen *) args+="$1"; shift 1;;.
Bronson

Tolle! Ich habe ein paar Antworten getestet, aber dies ist die einzige, die in allen Fällen funktioniert hat, einschließlich vieler Positionsparameter (sowohl vor als auch nach Flaggen)
Guilherme Garnier,

2
netter prägnanter Code, aber die Verwendung von -n und keinem anderen Argument führt aufgrund eines Fehlers zu einer Endlosschleife shift 2, die shiftzweimal statt ausgegeben wird shift 2. Schlug die Bearbeitung vor.
Lauksas

42

Ich bin ungefähr 4 Jahre zu spät bei dieser Frage, möchte aber etwas zurückgeben. Ich habe die früheren Antworten als Ausgangspunkt verwendet, um meine alte Ad-hoc-Parameteranalyse aufzuräumen. Ich habe dann den folgenden Vorlagencode überarbeitet. Es verarbeitet sowohl lange als auch kurze Parameter mit = oder durch Leerzeichen getrennten Argumenten sowie mehrere kurze Parameter, die zusammen gruppiert sind. Schließlich werden alle nicht parametrischen Argumente wieder in die Variablen $ 1, $ 2 .. eingefügt. Ich hoffe es ist nützlich.

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

Dieser Code kann keine Optionen mit folgenden Argumenten verarbeiten : -c1. Und die Verwendung von =, um kurze Optionen von ihren Argumenten zu trennen, ist ungewöhnlich ...
Robert Siemer

2
Ich bin auf zwei Probleme mit diesem nützlichen Codeabschnitt gestoßen: 1) Die "Verschiebung" im Fall von "-c = foo" führt dazu, dass der nächste Parameter gegessen wird. und 2) 'c' sollte nicht in das "[cfr]" - Muster für kombinierbare Short-Optionen aufgenommen werden.
6.

36

Ich fand die Angelegenheit, tragbares Parsing in Skripten zu schreiben, so frustrierend, dass ich Argbash geschrieben habe - einen FOSS-Codegenerator, der den Argument-Parsing-Code für Ihr Skript generieren kann und einige nette Funktionen hat:

https://argbash.io


Danke, dass du Argbash geschrieben hast. Ich habe es gerade benutzt und festgestellt, dass es gut funktioniert. Ich habe mich hauptsächlich für Argbash entschieden, weil es ein Codegenerator ist, der die ältere Bash 3.x unterstützt, die unter OS X 10.11 El Capitan zu finden ist. Der einzige Nachteil ist, dass der Code-Generator-Ansatz im Vergleich zum Aufruf eines Moduls ziemlich viel Code in Ihrem Hauptskript bedeutet.
RichVel

Sie können Argbash tatsächlich so verwenden, dass eine maßgeschneiderte Analysebibliothek nur für Sie erstellt wird, die Sie in Ihr Skript aufgenommen haben können, oder Sie können sie in einer separaten Datei haben und sie einfach als Quelle verwenden. Ich habe ein Beispiel hinzugefügt , um dies zu demonstrieren, und ich habe es auch in der Dokumentation expliziter gemacht.
Bubla

Gut zu wissen. Dieses Beispiel ist interessant, aber immer noch nicht wirklich klar. Vielleicht können Sie den Namen des generierten Skripts in 'parse_lib.sh' oder ähnliches ändern und zeigen, wo das Hauptskript es aufruft (wie im Abschnitt mit dem Wrapping-Skript, der einen komplexeren Anwendungsfall darstellt).
RichVel

Die Probleme wurden in der jüngsten Version von argbash behoben: Die Dokumentation wurde verbessert, ein Schnellstart-Argbash-Init-Skript wurde eingeführt, und Sie können argbash sogar online unter argbash.io/generate
bubla

29

Meine Antwort basiert größtenteils auf der Antwort von Bruno Bronosky , aber ich habe seine beiden reinen Bash-Implementierungen zu einer zusammengefasst, die ich ziemlich häufig verwende.

# 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

Auf diese Weise können Sie sowohl durch Leerzeichen getrennte Optionen / Werte als auch gleich definierte Werte festlegen.

So können Sie Ihr Skript ausführen mit:

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

ebenso gut wie:

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

und beide sollten das gleiche Endergebnis haben.

PROS:

  • Ermöglicht sowohl -arg = Wert als auch -arg Wert

  • Funktioniert mit jedem Arg-Namen, den Sie in Bash verwenden können

    • Bedeutung -a oder -arg oder -arg oder -arg oder was auch immer
  • Pure Bash. Keine Notwendigkeit, getopt oder getopts zu lernen / zu verwenden

Nachteile:

  • Args können nicht kombiniert werden

    • Bedeutet nein -abc. Sie müssen -a -b -c tun

Dies sind die einzigen Vor- und Nachteile, die mir auf den ersten Blick einfallen


15

Ich denke, dieser ist einfach genug zu benutzen:

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

Aufrufbeispiel:

./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
Ich habe alles gelesen und dieses ist mein bevorzugtes. Ich mag es nicht, -a=1als Argc-Stil zu verwenden. Ich bevorzuge es, zuerst die Hauptoptionsoptionen und später die speziellen Optionen mit einfachem Abstand zu setzen -o option. Ich bin auf der Suche nach dem einfachsten gegen besseren Weg, um Argumente zu lesen.
m3nda

Es funktioniert wirklich gut, aber wenn Sie ein Argument an eine Nicht-a: -Option übergeben, werden alle folgenden Optionen als Argumente verwendet. Sie können diese Zeile ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFilemit Ihrem eigenen Skript überprüfen . -d Option ist nicht als d:
m3nda

15

Um die hervorragende Antwort von @guneysus zu erweitern, finden Sie hier eine Optimierung, mit der der Benutzer die von ihm bevorzugte Syntax verwenden kann, z

command -x=myfilename.ext --another_switch 

vs.

command -x myfilename.ext --another_switch

Das heißt, die Gleichheit kann durch Leerzeichen ersetzt werden.

Diese "Fuzzy-Interpretation" gefällt Ihnen vielleicht nicht, aber wenn Sie Skripte erstellen, die mit anderen Dienstprogrammen austauschbar sind (wie dies bei meinen der Fall ist, die mit ffmpeg funktionieren müssen), ist die Flexibilität hilfreich.

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

Dieses Beispiel zeigt, wie kurze und lange Parameter mit und ohne einen folgenden erforderlichen Wert verwendet getoptund evalund HEREDOCund shiftbehandelt werden. Auch die switch / case-Anweisung ist kurz und leicht zu befolgen.

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

Die wichtigsten Zeilen des obigen Skripts sind folgende:

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

Kurz, auf den Punkt, lesbar und handhabt fast alles (IMHO).

Hoffe das hilft jemandem.


1
Dies ist eine der besten Antworten.
Mr. Polywhirl

11

Ich gebe Ihnen die Funktion parse_params, die Parameter über die Befehlszeile analysiert.

  1. Es ist eine reine Bash-Lösung, keine zusätzlichen Dienstprogramme.
  2. Verschmutzt nicht den globalen Geltungsbereich.
  3. Sie erhalten mühelos einfach zu verwendende Variablen, auf denen Sie weitere Logik aufbauen können.
  4. Die Anzahl der Striche vor den Parametern spielt keine Rolle ( --allgleich -allgleich all=all)

Das folgende Skript ist eine Demonstration zum Kopieren und Einfügen. show_useInformationen zur Verwendung finden Sie unter Funktion parse_params.

Einschränkungen:

  1. Unterstützt keine durch Leerzeichen getrennten Parameter ( -d 1)
  2. Param-Namen verlieren also Bindestriche --any-paramund -anyparamsind gleichwertig
  3. eval $(parse_params "$@")muss innerhalb der Bash- Funktion verwendet werden ( funktioniert nicht im globalen Bereich)

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

Um die Demo zu verwenden, um Parameter zu analysieren, die in Ihr Bash-Skript kommen, tun Sie einfachshow_use "$@"
Oleksii Chekulaiev

Grundsätzlich habe ich herausgefunden, dass github.com/renatosilva/easyoptions dasselbe tut, aber etwas massiver ist als diese Funktion.
Oleksii Chekulaiev

10

EasyOptions erfordert keine Analyse:

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

Es dauerte eine Minute, bis mir klar wurde, dass die Kommentare oben in Ihrem Beispielskript analysiert werden, um eine Standard-Verwendungshilfezeichenfolge sowie Argumentspezifikationen bereitzustellen. Dies ist eine brillante Lösung und es tut mir leid, dass sie in 2 Jahren nur 6 Stimmen erhalten hat. Vielleicht ist diese Frage zu überfüllt, als dass die Leute sie bemerken könnten.
Metamorphic

In gewisser Hinsicht ist Ihre Lösung bei weitem die beste (abgesehen von der von @ OleksiiChekulaiev, die keine "Standard" -Optionssyntax unterstützt). Dies liegt daran, dass der Skriptschreiber für Ihre Lösung den Namen jeder Option nur einmal angeben muss . Die Tatsache, dass andere Lösungen eine dreifache Angabe erfordern - in der Verwendung, im Fallmuster und in der Einstellung der Variablen - hat mich immer wieder geärgert. Sogar getopt hat dieses Problem. Ihr Code ist jedoch auf meinem Computer langsam - 0,11 Sekunden für die Bash-Implementierung, 0,28 Sekunden für den Ruby. Versus 0.02s für explizites "while-case" -Parsing.
Metamorphic

Ich möchte eine schnellere Version, vielleicht in C geschrieben. Auch eine Version, die mit zsh kompatibel ist. Möglicherweise verdient dies eine separate Frage ("Gibt es eine Möglichkeit, Befehlszeilenargumente in Bash-ähnlichen Shells zu analysieren, die die Standardsyntax für lange Optionen akzeptieren und nicht erfordern, dass Optionsnamen mehrmals eingegeben werden?").
Metamorphic

10

getopts funktioniert hervorragend, wenn Sie # 1 installiert haben und # 2 beabsichtigen, es auf derselben Plattform auszuführen. OSX und Linux (zum Beispiel) verhalten sich in dieser Hinsicht unterschiedlich.

Hier ist eine (nicht getopts) Lösung, die gleich-, ungleich- und boolesche Flags unterstützt. Zum Beispiel könnten Sie Ihr Skript folgendermaßen ausführen:

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

So mache ich es in einer Funktion, um zu vermeiden, dass getopts gleichzeitig irgendwo höher im Stapel ausgeführt werden:

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

Als Erweiterung der Antwort von @ bruno-bronosky habe ich einen "Präprozessor" hinzugefügt, um einige gängige Formatierungen zu handhaben:

  • Erweitert --longopt=valin--longopt val
  • Erweitert -xyzin-x -y -z
  • Unterstützt --die Angabe des Endes von Flags
  • Zeigt einen Fehler für unerwartete Optionen an
  • Kompakter und einfach zu lesender Optionsschalter
#!/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

Es gibt verschiedene Möglichkeiten, cmdline-Argumente zu analysieren (z. B. GNU getopt (nicht portabel) gegen BSD (OSX) getopt gegen getopts) - alles problematisch. Diese Lösung ist

  • tragbar!
  • hat keine Abhängigkeiten, stützt sich nur auf integrierte Bash-Funktionen
  • ermöglicht sowohl kurze als auch lange Optionen
  • Behandelt Leerzeichen zwischen Option und Argument, kann aber auch ein =Trennzeichen verwenden
  • unterstützt verketteten kurzen Optionsstil -vxf
  • behandelt Option mit optionalen Argumenten (siehe Beispiel) und
  • erfordert kein Aufblähen des Codes im Vergleich zu Alternativen für denselben Funktionsumfang. Dh prägnant und daher leichter zu pflegen

Beispiele: Beliebig von

# 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

Ich möchte meine Version der Optionsanalyse anbieten, die Folgendes ermöglicht:

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

Berücksichtigt dies auch (könnte unerwünscht sein):

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

Sie müssen vor der Verwendung entscheiden, ob = für eine Option verwendet werden soll oder nicht. Dies dient dazu, den Code sauber zu halten (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
Was bedeutet "+ x" für $ {key + x}?
Luca Davanzo

1
Es ist ein Test, um festzustellen, ob 'Schlüssel' vorhanden ist oder nicht. Weiter unten entferne ich die Taste und dies unterbricht die innere while-Schleife.
Galmok

5

Lösung, die unbehandelte Argumente bewahrt. Demos enthalten.

Hier ist meine Lösung. Es ist SEHR flexibel und sollte im Gegensatz zu anderen keine externen Pakete erfordern und übrig gebliebene Argumente sauber behandeln.

Verwendung ist: ./myscript -flag flagvariable -otherflag flagvar2

Sie müssen lediglich die Zeile mit den gültigen Flags bearbeiten. Es wird ein Bindestrich vorangestellt und alle Argumente durchsucht. Es definiert dann das nächste Argument als Flaggenname, z

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

Der Hauptcode (Kurzversion, ausführlich mit Beispielen weiter unten, auch eine Version mit Fehler):

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

Die ausführliche Version mit integrierten Echo-Demos:

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

Letzteres ist ein Fehler, wenn ein ungültiges Argument durchlaufen wird.

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

Vorteile: Was es macht, geht es sehr gut. Es werden nicht verwendete Argumente beibehalten, die viele der anderen Lösungen hier nicht haben. Außerdem können Variablen aufgerufen werden, ohne im Skript von Hand definiert zu werden. Es ermöglicht auch die Vorbelegung von Variablen, wenn kein entsprechendes Argument angegeben wird. (Siehe ausführliches Beispiel).

Nachteile: Eine einzelne komplexe Argumentzeichenfolge kann nicht analysiert werden, z. B. würde -xcvf als einzelnes Argument verarbeitet. Sie könnten etwas leicht zusätzlichen Code in meinen schreiben, der diese Funktionalität hinzufügt.



3

Beachten Sie, dass getopt(1) ein kurzer Fehler von AT & T war.

getopt wurde 1984 erstellt, aber bereits 1986 begraben, weil es nicht wirklich verwendbar war.

Ein Beweis für die Tatsache , dass getoptsehr veraltet ist , ist , dass der getopt(1)Mann Seite noch erwähnt "$*"statt "$@", die mit dem auf den Bourne - Shell im Jahr 1986 zusammen hinzugefügt wurdegetopts(1) Shell builtin , um mit Argumenten mit Leerzeichen innerhalb zu befassen.

Übrigens: Wenn Sie lange Optionen in Shell-Skripten analysieren möchten, ist es möglicherweise von Interesse zu wissen, dass die getopt(3)Implementierung von libc (Solaris) und ksh93beide eine einheitliche Implementierung langer Optionen hinzugefügt haben, die lange Optionen als Aliase für kurze Optionen unterstützt. Dies bewirkt ksh93und die Bourne ShellImplementierung einer einheitlichen Schnittstelle für lange Optionen übergetopts .

Ein Beispiel für lange Optionen aus der Bourne Shell-Manpage:

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

zeigt, wie lange Optionsaliasnamen sowohl in Bourne Shell als auch in ksh93 verwendet werden können.

Siehe die Manpage einer aktuellen Bourne Shell:

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

und die Manpage für getopt (3) von OpenSolaris:

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

und zuletzt die Manpage getopt (1), um das veraltete $ * zu überprüfen:

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


3

Ich habe einen Bash-Helfer geschrieben, um ein schönes Bash-Tool zu schreiben

Projekthaus: https://gitlab.mbedsys.org/mbedsys/bashopts

Beispiel:

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

wird helfen:

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

genießen :)


Ich bekomme dies unter Mac OS X: `` `lib / bashopts.sh: Zeile 138: deklarieren: -A: ungültige Option deklarieren: Verwendung: deklarieren [-afFirtx] [-p] [Name [= Wert] ...] Fehler in lib / bashopts.sh: 138. 'deklariere -x -A bashopts_optprop_name' mit Status 2 beendet Aufrufbaum: 1: lib / controller.sh: 4 source (...) Beendet mit Status 1 `` `
Josh Wulf

Sie benötigen Bash Version 4, um dies zu verwenden. Auf dem Mac ist die Standardversion 3. Sie können Home Brew verwenden, um Bash 4 zu installieren.
Josh Wulf

3

Hier ist mein Ansatz - mit Regexp.

  • keine getopts
  • Es behandelt Block von kurzen Parametern -qwerty
  • Es behandelt kurze Parameter -q -w -e
  • Es behandelt lange Optionen --qwerty
  • Sie können das Attribut an die Option short oder long übergeben (wenn Sie einen Block mit kurzen Optionen verwenden, wird das Attribut an die letzte Option angehängt).
  • Sie können Leerzeichen verwenden oder =Attribute angeben, aber die Attribute stimmen überein, bis Bindestrich + Leerzeichen "Trennzeichen" angezeigt werden. In --q=qwe ty qwe tyist also ein Attribut
  • Es behandelt die Mischung aller oben genannten, -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributeist also gültig

Skript:

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

Wie dieser. Vielleicht fügen Sie einfach -e param hinzu, um mit der neuen Zeile zu widerhallen.
Mauron85

3

Angenommen , wir ein Shell - Skript namens erstellen test_args.shwie folgt

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

Nachdem wir den folgenden Befehl ausgeführt haben:

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

Die Ausgabe wäre:

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

5
Dies entspricht dem Ansatz von Noahs Antwort , hat jedoch weniger Sicherheitsüberprüfungen / Schutzmaßnahmen. Dies ermöglicht es uns, beliebige Argumente in die Umgebung des Skripts zu schreiben, und ich bin mir ziemlich sicher, dass Ihre Verwendung von eval hier die Befehlsinjektion ermöglicht.
Will Barnwell

2

Verwenden Sie Modul "Argumente" aus Bash-Modulen

Beispiel:

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

Positions- und Flaggenargumente mischen

--param = arg (gleich begrenzt)

Frei mischende Flags zwischen Positionsargumenten:

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

kann mit einem ziemlich präzisen Ansatz erreicht werden:

# 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 (durch Leerzeichen getrennt)

Es ist normalerweise klarer, nicht zu mischen --flag=valueund zu stylen --flag value.

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

Dies ist ein wenig schwierig zu lesen, aber es ist immer noch gültig

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

Quelle

# 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

Hier ist ein Getopts, der das Parsen mit minimalem Code erreicht und es Ihnen ermöglicht, zu definieren, was Sie in einem Fall mit eval mit Teilzeichenfolge extrahieren möchten.

Grundsätzlich 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}"
}

Deklariert die Variablen als Einheimische anstelle von Globalen als die meisten Antworten hier.

Genannt als:

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

Das $ {k: 3} ist im Grunde ein Teilstring, um den ersten ---aus dem Schlüssel zu entfernen .


1

Dies kann auch hilfreich sein, um zu wissen, dass Sie einen Wert festlegen können. Wenn jemand Eingaben bereitstellt, überschreiben Sie den Standardwert mit diesem Wert.

myscript.sh -f ./serverlist.txt oder einfach ./myscript.sh (und es werden Standardeinstellungen verwendet)

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

Eine andere Lösung ohne getopt [s], POSIX, alter Unix-Stil

Ähnlich wie bei der Lösung, die Bruno Bronosky hier gepostet hat, handelt es sich um eine Lösung ohne Verwendung vongetopt(s) .

Das Hauptunterscheidungsmerkmal meiner Lösung besteht darin, dass Optionen miteinander verkettet werden können, genau wie dies tar -xzf foo.tar.gzgleich ist tar -x -z -f foo.tar.gz. Und genau wie in tar,ps usw. der führenden Bindestrich ist optional für einen Block von Short - Optionen (aber dies kann leicht geändert werden). Es werden auch lange Optionen unterstützt (aber wenn ein Block mit einem beginnt, sind zwei führende Bindestriche erforderlich).

Code mit Beispieloptionen

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

Die Beispielverwendung finden Sie in den Beispielen weiter unten.

Position von Optionen mit Argumenten

Für was es dort wert ist, sind die Optionen mit Argumenten nicht die letzten (nur lange Optionen müssen sein). Während z. B. in tar(zumindest in einigen Implementierungen) die fOptionen zuletzt sein müssen, da der Dateiname folgt ( tar xzf bar.tar.gzfunktioniert, tar xfz bar.tar.gzaber nicht funktioniert), ist dies hier nicht der Fall (siehe die späteren Beispiele).

Mehrere Optionen mit Argumenten

Als weiteren Bonus werden die Optionsparameter in der Reihenfolge der Optionen von den Parametern mit den erforderlichen Optionen verbraucht. Schauen Sie sich hier die Ausgabe meines Skripts mit der Befehlszeile abc X Y Z(oder -abc X Y Z) an:

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

Auch lange Optionen verkettet

Sie können auch lange Optionen im Optionsblock haben, da diese zuletzt im Block vorkommen. Die folgenden Befehlszeilen sind also alle gleichwertig (einschließlich der Reihenfolge, in der die Optionen und ihre Argumente verarbeitet werden):

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

All dies führt zu:

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

Nicht in dieser Lösung

Optionale Argumente

Optionen mit optionalen Argumenten sollten mit ein wenig Arbeit möglich sein, z. B. indem Sie nach vorne schauen, ob es einen Block ohne Bindestrich gibt. Der Benutzer müsste dann vor jedem Block nach einem Block mit einem Parameter mit einem optionalen Parameter einen Bindestrich setzen. Möglicherweise ist dies zu kompliziert, um mit dem Benutzer zu kommunizieren, sodass in diesem Fall besser nur ein führender Bindestrich erforderlich ist.

Mit mehreren möglichen Parametern wird es noch komplizierter. Ich würde davon abraten, die Optionen so zu gestalten, dass sie klug sind, indem ich feststelle, ob ein Argument dafür geeignet ist oder nicht (z. B. wenn eine Option nur eine Zahl als optionales Argument verwendet), da dies in Zukunft möglicherweise nicht mehr funktioniert.

Ich persönlich bevorzuge zusätzliche Optionen anstelle optionaler Argumente.

Optionsargumente mit Gleichheitszeichen

Genau wie bei optionalen Argumenten bin ich kein Fan davon (Übrigens, gibt es einen Thread, in dem die Vor- und Nachteile verschiedener Parameterstile diskutiert werden?), Aber wenn Sie dies möchten, können Sie es wahrscheinlich selbst implementieren, wie unter http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop mit einer --long-with-arg=?*case-Anweisung und anschließendem Entfernen des Gleichheitszeichens (dies ist übrigens die Site, die besagt, dass die Verkettung von Parametern mit einigem Aufwand möglich ist, aber [es] als Übung für den Leser hinterlassen hat "was mich dazu brachte, sie beim Wort zu nehmen, aber ich fing von vorne an).

Weitere Hinweise

POSIX-konform, funktioniert auch auf alten Busybox - Setups ich zu tun hatte (mit zB cut, headund getoptsfehlt).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.