Comment analyser les arguments de ligne de commande dans Bash?

1922

Dis, j'ai un script qui s'appelle avec cette ligne:

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

ou celui-ci:

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

Quelle est la façon acceptée de telle sorte que cette analyse dans chaque cas (ou une combinaison des deux) $v, $fet $dseront tous mis à trueet $outFilesera égal /fizz/someOtherFile?

Lawrence Johnston
la source
1
Pour les utilisateurs de zsh, il existe une excellente fonction intégrée appelée zparseopts qui peut faire: zparseopts -D -E -M -- d=debug -debug=dEt avoir les deux -det --debugdans le $debugtableau echo $+debug[1]retournera 0 ou 1 si l'un d'eux est utilisé. Ref: zsh.org/mla/users/2011/msg00350.html
dezza
1
Très bon tutoriel: linuxcommand.org/lc3_wss0120.php . J'aime particulièrement l'exemple "Options de ligne de commande".
Gabriel Staples le

Réponses:

2677

Méthode n ° 1: utiliser bash sans getopt [s]

Deux façons courantes de passer des arguments de paire clé-valeur sont:

Séparé par l'espace Bash (par exemple, --option argument) (sans getopt [s])

Usage 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

sortie du copier-coller du bloc ci-dessus:

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 égal à séparé (par exemple, --option=argument) (sans getopt [s])

Usage 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

sortie du copier-coller du bloc ci-dessus:

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

Pour mieux comprendre, ${i#*=}recherchez «Suppression de sous-chaîne» dans ce guide . Il est fonctionnellement équivalent à celui `sed 's/[^=]*=//' <<< "$i"`qui appelle un sous-processus inutile ou `echo "$i" | sed 's/[^=]*=//'`qui appelle deux sous-processus inutiles.

Méthode n ° 2: utilisation de bash avec getopt [s]

à partir de: http://mywiki.wooledge.org/BashFAQ/035#getopts

Limitations de getopt (1) (anciennes getoptversions relativement récentes ):

  • ne peut pas gérer les arguments qui sont des chaînes vides
  • ne peut pas gérer les arguments avec des espaces blancs intégrés

Les getoptversions plus récentes n'ont pas ces limitations.

De plus, le shell POSIX (et autres) offre getoptsqui n'a pas ces limitations. J'ai inclus un simplistegetopts exemple .

Usage 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

sortie du copier-coller du bloc ci-dessus:

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

Les avantages de getoptssont:

  1. Il est plus portable et fonctionnera dans d'autres coquilles comme dash.
  2. Il peut gérer plusieurs options uniques comme -vf filenamedans la manière typique d'Unix, automatiquement.

L'inconvénient getoptsest qu'il ne peut gérer que des options courtes ( -h, pas --help) sans code supplémentaire.

Il y a un tutoriel getopts qui explique ce que signifient toutes les syntaxes et variables. En bash, il y en a aussi help getopts, qui pourrait être informatif.

Bruno Bronosky
la source
44
Est-ce vraiment vrai? Selon Wikipedia, il existe une nouvelle version améliorée de GNU getoptqui inclut toutes les fonctionnalités de getopts, puis certaines. man getoptsur les sorties Ubuntu 13.04 getopt - parse command options (enhanced)comme nom, donc je suppose que cette version améliorée est standard maintenant.
Livven
47
Que quelque chose soit d'une certaine manière sur votre système est une prémisse très faible pour fonder des hypothèses de "standardisation".
szablica
13
@Livven, ce getoptn'est pas un utilitaire GNU, il en fait partie util-linux.
Stephane Chazelas
4
Si vous utilisez -gt 0, supprimez votre shiftaprès le esac, augmentez tout shiftde 1 et ajoutez ce cas: *) break;;vous pouvez gérer des arguments non optionnels. Ex: pastebin.com/6DJ57HTc
Nicolas Lacombe
2
Vous ne faites pas d'écho –default. Dans le premier exemple, je remarque que si –defaultest le dernier argument, il n'est pas traité (considéré comme non-opt), sauf s'il while [[ $# -gt 1 ]]est défini comme while [[ $# -gt 0 ]]
kolydart
562

Aucune réponse ne mentionne le getopt amélioré . Et la réponse la plus votée est trompeuse: elle ignore -⁠vfdles options courtes de style (demandées par l'OP) ou les options après les arguments de position (également demandées par l'OP); et il ignore les erreurs d'analyse. Au lieu:

  • Utilisation améliorée getoptde util-linux ou anciennement GNU glibc . 1
  • Il fonctionne avec getopt_long()la fonction C de GNU glibc.
  • Possède toutes les caractéristiques distinctives utiles (les autres n'en ont pas):
    • gère les espaces, cite les caractères et même binaire dans les arguments 2 (non amélioré getoptne peut pas le faire)
    • il peut gérer les options à la fin: script.sh -o outFile file1 file2 -v( getoptsne fait pas cela)
    • autorise =les options longues de style:script.sh --outfile=fileOut --infile fileIn (autoriser les deux est long si l'auto-analyse)
    • permet des options courtes combinées, par exemple -vfd(travail réel si auto-analyse)
    • permet de toucher des arguments d'option, par exemple -oOutfileou-vfdoOutfile
  • Est déjà si vieux 3 qu'aucun système GNU ne manque à cela (par exemple, n'importe quel Linux l'a).
  • Vous pouvez tester son existence avec: getopt --test→ valeur de retour 4.
  • Les autres getoptou intégrés au shell getoptssont d'une utilité limitée.

Les appels suivants

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

tous reviennent

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

avec ce qui suit 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 amélioré est disponible sur la plupart des «systèmes bash», y compris Cygwin; sur OS X, essayez brew install gnu-getopt ousudo port install getopt
2, lesexec()conventionsPOSIXn'ont aucun moyen fiable de passer NULL binaire dans les arguments de la ligne de commande; ces octets mettent fin prématurément à l'argument
3, première version publiée en 1997 ou avant (je ne l'ai suivi qu'en 1997)

Robert Siemer
la source
4
Merci pour cela. Vous venez de le confirmer dans le tableau des fonctionnalités sur en.wikipedia.org/wiki/Getopts , si vous avez besoin de prise en charge pour les options longues et que vous n'êtes pas sur Solaris, getoptc'est la voie à suivre.
johncip
4
Je crois que la seule mise en garde getoptest qu'il ne peut pas être utilisé de manière pratique dans les scripts wrapper où l'on peut avoir quelques options spécifiques au script wrapper, puis transmettre les options non-wrapper-script à l'exécutable encapsulé, intact. Disons que j'ai un grepwrapper appelé mygrepet que j'ai une option --foospécifique à mygrep, alors je ne peux pas faire mygrep --foo -A 2, et je -A 2passe automatiquement à grep; Je dois faire mygrep --foo -- -A 2. Voici mon implémentation au dessus de votre solution.
Kaushal Modi
2
@bobpaul Votre déclaration sur util-linux est également erronée et trompeuse: le paquet est marqué «essentiel» sur Ubuntu / Debian. En tant que tel, il est toujours installé. - De quelles distributions parlez-vous (où vous dites qu'il doit être installé exprès)?
Robert Siemer
3
Notez que cela ne fonctionne pas sur Mac au moins jusqu'à la version actuelle 10.14.3. Le getopt qui est expédié est BSD getopt de 1999 ...
jjj
2
@transang Négation booléenne de la valeur de retour. Et son effet secondaire: permettre à une commande d'échouer (sinon errexit annulerait le programme en cas d'erreur). - Les commentaires du script vous en disent plus. Sinon:man bash
Robert Siemer
145

Manière plus succincte

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"

Usage:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify
Inanc Gumus
la source
3
Voilà ce que je fais. Je dois le faire while [[ "$#" > 1 ]]si je veux soutenir la fin de la ligne avec un drapeau booléen ./script.sh --debug dev --uglify fast --verbose. Exemple: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
12
Hou la la! Simple et propre! Voici comment j'utilise ceci: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
2
C'est beaucoup plus agréable à coller dans chaque script plutôt que de traiter avec la source ou de demander aux gens où commence réellement votre fonctionnalité.
RealHandy
Attention: cela tolère les arguments dupliqués, le dernier argument prévaut. par exemple ./script.sh -d dev -d prodentraînerait deploy == 'prod'. Je l'ai quand même utilisé: P :): +1:
yair
J'utilise ceci (merci!) Mais notez qu'il autorise une valeur d'argument vide, par exemple ./script.sh -dne générerait pas d'erreur mais serait simplement défini $deploysur une chaîne vide.
EM0
137

de: digitalpeer.com avec des modifications mineures

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

Pour mieux comprendre, ${i#*=}recherchez «Suppression de sous-chaîne» dans ce guide . Il est fonctionnellement équivalent à celui `sed 's/[^=]*=//' <<< "$i"`qui appelle un sous-processus inutile ou `echo "$i" | sed 's/[^=]*=//'`qui appelle deux sous-processus inutiles.

guneysus
la source
4
Soigné! Bien que cela ne fonctionne pas pour les arguments séparés par des espaces à la mount -t tempfs .... On peut probablement résoudre ce problème via quelque chose comme while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;etc
Tobias Kienzler
3
Cela ne peut pas gérer -vfdles options courtes combinées de style.
Robert Siemer
105

getopt()/ getopts()est une bonne option. Volé d' ici :

L'utilisation simple de "getopt" est montrée dans ce 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

Ce que nous avons dit, c'est que tout -a, -b, -c ou -d sera autorisé, mais que -c est suivi d'un argument (le "c:" le dit).

Si nous appelons cela "g" et essayons:

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

Nous commençons avec deux arguments, et "getopt" sépare les options et place chacune dans son propre argument. Il a également ajouté "-".

Matt J
la source
4
L'utilisation $*est une utilisation cassée de getopt. (Il contient des arguments avec des espaces.) Voir ma réponse pour une utilisation correcte.
Robert Siemer
Pourquoi voudriez-vous rendre cela plus compliqué?
SDsolar
@Matt J, la première partie du script (pour i) serait capable de gérer des arguments avec des espaces si vous utilisez "$ i" au lieu de $ i. Les getopts ne semblent pas être capables de gérer des arguments avec des espaces. Quel serait l'avantage d'utiliser getopt sur la boucle for i?
thebunnyrules
99

Au risque d'ajouter un autre exemple à ignorer, voici mon schéma.

  • poignées -n arget--name=arg
  • autorise les arguments à la fin
  • affiche des erreurs sensées si quelque chose est mal orthographié
  • compatible, n'utilise pas de bashismes
  • lisible, ne nécessite pas de maintenir l'état dans une boucle

J'espère que c'est utile à quelqu'un.

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

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

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done
bronson
la source
4
Désolé pour le retard. Dans mon script, la fonction handle_argument reçoit tous les arguments sans option. Vous pouvez remplacer cette ligne par ce que vous voulez, peut-être *) die "unrecognized argument: $1"ou collecter les arguments dans une variable *) args+="$1"; shift 1;;.
bronson
Incroyable! J'ai testé quelques réponses, mais c'est la seule qui a fonctionné pour tous les cas, y compris de nombreux paramètres de position (avant et après les indicateurs)
Guilherme Garnier
2
joli code succinct, mais l'utilisation de -n et d'aucun autre argument provoque une boucle infinie en raison d'une erreur shift 2, émettant shiftdeux fois au lieu de shift 2. Suggéré la modification.
lauksas
42

J'ai environ 4 ans de retard sur cette question, mais je veux redonner. J'ai utilisé les réponses précédentes comme point de départ pour ranger mon ancienne analyse param param adhoc. J'ai ensuite refactorisé le code de modèle suivant. Il gère les paramètres longs et courts, en utilisant des arguments = ou séparés par des espaces, ainsi que plusieurs paramètres courts regroupés. Enfin, il réinsère tous les arguments non param dans les variables $ 1, $ 2 ... J'espère que c'est 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
Shane Day
la source
Ce code ne peut pas gérer les options avec des arguments comme celui - ci: -c1. Et l'utilisation de =pour séparer les options courtes de leurs arguments est inhabituelle ...
Robert Siemer
2
J'ai rencontré deux problèmes avec ce morceau de code utile: 1) le "shift" dans le cas de "-c = foo" finit par manger le paramètre suivant; et 2) «c» ne devrait pas être inclus dans le modèle «[cfr]» pour les options courtes combinables.
sfnd
36

J'ai trouvé le problème d'écrire une analyse portable dans des scripts si frustrant que j'ai écrit Argbash - un générateur de code FOSS qui peut générer le code d'analyse des arguments pour votre script et il a de belles fonctionnalités:

https://argbash.io

bubla
la source
Merci d'avoir écrit argbash, je viens de l'utiliser et j'ai trouvé que cela fonctionne bien. J'ai surtout opté pour argbash car c'est un générateur de code prenant en charge l'ancien bash 3.x trouvé sur OS X 10.11 El Capitan. Le seul inconvénient est que l'approche du générateur de code signifie beaucoup de code dans votre script principal, par rapport à l'appel d'un module.
RichVel
Vous pouvez réellement utiliser Argbash de manière à produire une bibliothèque d'analyse sur mesure juste pour vous que vous pouvez avoir incluse dans votre script ou vous pouvez l'avoir dans un fichier séparé et simplement la source. J'ai ajouté un exemple pour le démontrer et je l'ai également rendu plus explicite dans la documentation.
bubla
Bon à savoir. Cet exemple est intéressant mais pas encore très clair - vous pouvez peut-être changer le nom du script généré en 'parse_lib.sh' ou similaire et montrer où le script principal l'appelle (comme dans la section de script d'habillage qui est un cas d'utilisation plus complexe).
RichVel
Les problèmes ont été résolus dans la version récente d'argbash: la documentation a été améliorée, un script argbash-init à démarrage rapide a été introduit et vous pouvez même utiliser argbash en ligne sur argbash.io/generate
bubla
29

Ma réponse est largement basée sur la réponse de Bruno Bronosky , mais j'ai en quelque sorte écrasé ses deux implémentations de bash pur en une que j'utilise assez fréquemment.

# 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

Cela vous permet d'avoir à la fois des options / valeurs séparées par des espaces et des valeurs définies égales.

Vous pouvez donc exécuter votre script en utilisant:

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

aussi bien que:

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

et les deux devraient avoir le même résultat final.

AVANTAGES:

  • Permet à la fois -arg = valeur et -arg valeur

  • Fonctionne avec n'importe quel nom d'argument que vous pouvez utiliser dans bash

    • Signification -a ou -arg ou --arg ou -arg ou autre
  • Bash pur. Pas besoin d'apprendre / d'utiliser getopt ou getopts

LES INCONVÉNIENTS:

  • Impossible de combiner les arguments

    • Ce qui signifie pas -abc. Vous devez faire -a -b -c

Ce sont les seuls avantages / inconvénients auxquels je peux penser du haut de ma tête

Ponyboy47
la source
15

Je pense que celui-ci est assez simple à utiliser:

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

Exemple d'invocation:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile
Alek
la source
1
J'ai tout lu et celui-ci est mon préféré. Je n'aime pas utiliser -a=1comme style argc. Je préfère mettre d'abord les options principales -options et plus tard les options spéciales avec un seul espacement -o option. Im à la recherche de la manière la plus simple vs meilleure de lire les arguments.
m3nda
Cela fonctionne très bien, mais si vous passez un argument à une option non a: toutes les options suivantes seront prises comme arguments. Vous pouvez vérifier cette ligne ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFileavec votre propre script. L'option -d n'est pas définie comme d:
m3nda
15

Développant l'excellente réponse de @guneysus, voici un ajustement qui permet à l'utilisateur d'utiliser la syntaxe qu'il préfère, par exemple

command -x=myfilename.ext --another_switch 

contre

command -x myfilename.ext --another_switch

C'est-à-dire que les égaux peuvent être remplacés par des espaces.

Cette "interprétation floue" peut ne pas vous plaire, mais si vous créez des scripts interchangeables avec d'autres utilitaires (comme c'est le cas avec le mien, qui doit fonctionner avec ffmpeg), la flexibilité est 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
non synchronisé
la source
13

Cet exemple montre comment utiliser getoptet evalet HEREDOCet shiftpour gérer les paramètres courts et longs avec et sans une valeur requise qui suit. De plus, la déclaration switch / case est concise et facile à suivre.

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

Les lignes les plus significatives du script ci-dessus sont les suivantes:

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

Court, précis, lisible et gère à peu près tout (à mon humble avis).

J'espère que cela aide quelqu'un.

phyatt
la source
1
C'est l'une des meilleures réponses.
M. Polywhirl
11

Je vous donne la fonction parse_paramsqui analysera les paramètres à partir de la ligne de commande.

  1. Il s'agit d'une pure solution Bash, sans utilitaires supplémentaires.
  2. Ne pollue pas la portée mondiale.
  3. Vous renvoie sans effort des variables simples à utiliser, sur lesquelles vous pourriez construire une logique supplémentaire.
  4. Le nombre de tirets avant les paramètres n'a pas d'importance ( --allégal à -allégal all=all)

Le script ci-dessous est une démonstration de travail copier-coller. Voir la show_usefonction pour comprendre comment utiliser parse_params.

Limites:

  1. Ne prend pas en charge les paramètres séparés par des espaces ( -d 1)
  2. Les noms de paramètres perdront donc des tirets --any-paramet -anyparamsont équivalents
  3. eval $(parse_params "$@")doit être utilisé dans la fonction bash (cela ne fonctionnera pas dans la portée 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
Oleksii Chekulaiev
la source
Pour utiliser la démo pour analyser les paramètres qui entrent dans votre script bash, vous venez de le faireshow_use "$@"
Oleksii Chekulaiev
Fondamentalement, j'ai découvert que github.com/renatosilva/easyoptions fait de même de la même manière mais est un peu plus massif que cette fonction.
Oleksii Chekulaiev
10

EasyOptions ne nécessite aucune 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
Renato Silva
la source
Il m'a fallu une minute pour réaliser que les commentaires en haut de votre exemple de script sont analysés pour fournir une chaîne d'aide d'utilisation par défaut, ainsi que des spécifications d'argument. C'est une solution brillante et je suis désolé qu'elle n'ait obtenu que 6 votes en 2 ans. Peut-être que cette question est trop submergée pour que les gens la remarquent.
Metamorphic
Dans un sens, votre solution est de loin la meilleure (à part celle de @ OleksiiChekulaiev qui ne prend pas en charge la syntaxe des options "standard"). Ceci est parce que votre solution ne nécessite que l'auteur de script pour spécifier le nom de chaque option une fois . Le fait que d'autres solutions nécessitent qu'il soit spécifié 3 fois - dans l'utilisation, dans le modèle «cas» et dans le réglage de la variable - m'a toujours ennuyé. Même getopt a ce problème. Cependant, votre code est lent sur ma machine - 0,11s pour l'implémentation Bash, 0,28s pour le Ruby. Versus 0,02 s pour une analyse explicite "en temps réel".
Metamorphic
Je veux une version plus rapide, peut-être écrite en C. Aussi, une version compatible avec zsh. Peut-être que cela mérite une question distincte ("Existe-t-il un moyen d'analyser les arguments de ligne de commande dans des shells de type Bash qui accepte la syntaxe standard des options longues et n'exige pas que les noms des options soient tapés plus d'une fois?").
Metamorphic
10

getopts fonctionne très bien si # 1 vous l'avez installé et # 2 vous avez l'intention de l'exécuter sur la même plate-forme. OSX et Linux (par exemple) se comportent différemment à cet égard.

Voici une solution (non getopts) qui prend en charge les indicateurs égaux, non égaux et booléens. Par exemple, vous pouvez exécuter votre script de cette manière:

./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
vangorra
la source
8

Voici comment je fais dans une fonction pour éviter de casser des getopts exécutés en même temps quelque part plus haut dans la pile:

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

En développant la réponse de @ bruno-bronosky, j'ai ajouté un "préprocesseur" pour gérer une mise en forme courante:

  • Se développe --longopt=valen--longopt val
  • Se développe -xyzen-x -y -z
  • Prise --en charge pour indiquer la fin des drapeaux
  • Affiche une erreur pour les options inattendues
  • Commutateur d'options compact et facile à lire
#!/bin/bash

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

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

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

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

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

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

# Restore positional parameters
set -- "${POSITIONAL[@]}"
jchook
la source
6

Il existe plusieurs façons d'analyser les arguments cmdline (par exemple GNU getopt (non portable) vs BSD (OSX) getopt vs getopts) - tous problématiques. Cette solution est

  • portable!
  • n'a aucune dépendance, ne repose que sur les bash intégrés
  • permet des options courtes et longues
  • gère les espaces entre l'option et l'argument mais peut également utiliser le =séparateur
  • prend en charge le style d'option court concaténé -vxf
  • gère l'option avec des arguments facultatifs (voir l'exemple), et
  • ne nécessite pas de surcharge de code par rapport aux alternatives pour le même ensemble de fonctionnalités. C'est-à-dire succinct, et donc plus facile à entretenir

Exemples: tout

# flag
-f
--foo

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

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

#!/usr/bin/env bash

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

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

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option

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

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

EOF
}

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

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

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

shift $((OPTIND-1))

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

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"
tmoschou
la source
5

Je voudrais proposer ma version d'analyse d'options, qui permet les opérations suivantes:

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

Permet également cela (peut être indésirable):

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

Vous devez décider avant utilisation si = doit être utilisé sur une option ou non. C'est pour garder le code propre (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done
galmok
la source
1
quelle est la signification de "+ x" sur $ {key + x}?
Luca Davanzo
1
C'est un test pour voir si la «clé» est présente ou non. Plus bas, je désactive la clé, ce qui rompt la boucle intérieure.
galmok
5

Solution qui préserve les arguments non gérés. Démos incluses.

Voici ma solution. Il est TRÈS flexible et contrairement à d'autres, ne devrait pas nécessiter de packages externes et gère les arguments restants proprement.

L'utilisation est: ./myscript -flag flagvariable -otherflag flagvar2

Tout ce que vous avez à faire est de modifier la ligne des drapeaux valides. Il ajoute un tiret et recherche tous les arguments. Il définit ensuite l'argument suivant comme le nom du drapeau, par exemple

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

Le code principal (version courte, détaillée avec des exemples plus bas, également une version avec erreur):

#!/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 version détaillée avec des démos d'écho intégrées:

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

Un dernier, celui-ci commet une erreur si un argument non valide est transmis.

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

Avantages: Ce qu'il fait, il gère très bien. Il conserve les arguments inutilisés, contrairement à la plupart des autres solutions. Il permet également d'appeler des variables sans être définies manuellement dans le script. Il permet également de préremplir les variables si aucun argument correspondant n'est donné. (Voir l'exemple détaillé).

Inconvénients: impossible d'analyser une seule chaîne d'argument complexe, par exemple -xcvf serait traité comme un seul argument. Vous pouvez écrire un peu plus facilement du code supplémentaire dans le mien qui ajoute cette fonctionnalité.


la source
3

Notez que getopt(1)c'était une courte erreur vivante d'AT & T.

getopt a été créé en 1984 mais déjà enterré en 1986 car il n'était pas vraiment utilisable.

Une preuve du fait qui getoptest très dépassé est que la getopt(1)page de manuel mentionne toujours"$*" au lieu de "$@", qui a été ajoutée au Bourne Shell en 1986 avec legetopts(1) shell intégré afin de traiter les arguments avec des espaces à l'intérieur.

BTW: si vous êtes intéressé par l'analyse des options longues dans les scripts shell, il peut être intéressant de savoir que l' getopt(3)implémentation de libc (Solaris) et les ksh93deux ont ajouté une implémentation uniforme des options longues qui prend en charge les options longues comme alias pour les options courtes. Cela provoque ksh93etBourne Shell implémentation d'une interface uniforme pour les options longues viagetopts .

Un exemple d'options longues extraites de la page de manuel Bourne Shell:

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

montre combien de temps les alias d'options peuvent être utilisés à la fois dans Bourne Shell et dans ksh93.

Voir la page de manuel d'un Bourne Shell récent:

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

et la page de manuel pour getopt (3) d'OpenSolaris:

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

et enfin, la page de manuel getopt (1) pour vérifier le $ * obsolète:

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

schily
la source
3

J'ai écrit un assistant bash pour écrire un bel outil bash

accueil du projet: https://gitlab.mbedsys.org/mbedsys/bashopts

exemple:

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

donnera de l'aide:

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

prendre plaisir :)

Emeric Verschuur
la source
J'obtiens ceci sur Mac OS X: `` `lib / bashopts.sh: ligne 138: declare: -A: option invalide declare: usage: declare [-afFirtx] [-p] [nom [= valeur] ...] Erreur dans lib / bashopts.sh: 138. 'declare -x -A bashopts_optprop_name' est sorti avec le statut 2 Arborescence des appels: 1: lib / controller.sh: 4 source (...) Sortie avec le statut 1 ''
Josh Wulf
Vous avez besoin de la version 4 de Bash pour l'utiliser. Sur Mac, la version par défaut est 3. Vous pouvez utiliser home brew pour installer bash 4.
Josh Wulf
3

Voici mon approche - en utilisant regexp.

  • aucun getopts
  • il gère un bloc de paramètres courts -qwerty
  • il gère des paramètres courts -q -w -e
  • il gère les options longues --qwerty
  • vous pouvez passer l'attribut à une option courte ou longue (si vous utilisez un bloc d'options courtes, l'attribut est attaché à la dernière option)
  • vous pouvez utiliser des espaces ou =pour fournir des attributs, mais l'attribut correspond jusqu'à rencontrer un trait d'union + espace "délimiteur", donc il --q=qwe ty qwe tyy a un attribut
  • il gère le mélange de tous ci-dessus -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributeest donc valide

scénario:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

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

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

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

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done
a_z
la source
Comme celui-ci. Peut-être ajoutez simplement -e param à l'écho avec une nouvelle ligne.
mauron85
3

Supposons que nous créons un script shell nommé test_args.shcomme suit

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

Après avoir exécuté la commande suivante:

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

La sortie serait:

year=2017 month=12 day=22 flag=true
John
la source
5
Cela prend la même approche que la réponse de Noah , mais a moins de contrôles de sécurité / sauvegardes. Cela nous permet d'écrire des arguments arbitraires dans l'environnement du script et je suis sûr que votre utilisation d'eval ici peut permettre l'injection de commandes.
Will Barnwell
2

Utiliser les "arguments" du module à partir des modules bash

Exemple:

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

NAME="world"

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

info "Hello, $NAME!"
Volodymyr M. Lisivka
la source
2

Mélange d'arguments positionnels et basés sur des drapeaux

--param = arg (égal à délimité)

Mélanger librement les drapeaux entre les arguments positionnels:

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

peut être accompli avec une approche assez concise:

# 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 (espace délimité)

Il est généralement plus clair de ne pas mélanger --flag=valueet --flag valuestyles.

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

C'est un peu risqué à lire, mais c'est toujours valable

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

La source

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

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

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

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

# positional remain
node_name=$1
ip_address=$2
Mark Fox
la source
2

Voici un getopts qui réalise l'analyse syntaxique avec un code minimal et vous permet de définir ce que vous souhaitez extraire dans un cas en utilisant eval avec une sous-chaîne.

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

Déclare les variables comme locales au lieu de globales comme la plupart des réponses ici.

Appelé comme:

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

$ {K: 3} est essentiellement une sous-chaîne pour supprimer la première ---de la clé.

mmm
la source
1

Cela peut également être utile de savoir, vous pouvez définir une valeur et si quelqu'un fournit une entrée, remplacez la valeur par défaut par cette valeur.

myscript.sh -f ./serverlist.txt ou simplement ./myscript.sh (et cela prend les valeurs par défaut)

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

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

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

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


    echo "SERVER LIST   = ${SERVER_FILE_LIST}"
Mike Q
la source
1

Une autre solution sans getopt [s], POSIX, ancien style Unix

Semblable à la solution que Bruno Bronosky a postée, celle-ci est sans utilisation getopt(s).

La principale caractéristique de différenciation de ma solution est qu'elle permet d'avoir des options concaténées ensemble comme tar -xzf foo.tar.gzest égal à tar -x -z -f foo.tar.gz. Et comme dans tar,ps etc., le trait d'union est facultatif pour un bloc d'options courtes (mais cela peut être changé facilement). Les options longues sont également prises en charge (mais lorsqu'un bloc commence par un puis deux tirets de tête sont requis).

Code avec des exemples d'options

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

Pour l'exemple d'utilisation, veuillez consulter les exemples ci-dessous.

Position des options avec arguments

Pour ce que cela vaut, les options avec arguments ne sont pas les dernières (seules les options longues doivent l'être). Ainsi, par exemple dans tar(au moins dans certaines implémentations), les foptions doivent être les dernières car le nom de fichier suit ( tar xzf bar.tar.gzfonctionne mais tar xfz bar.tar.gzne fonctionne pas), ce n'est pas le cas ici (voir les exemples ultérieurs).

Plusieurs options avec arguments

Comme autre bonus, les paramètres d'options sont consommés dans l'ordre des options par les paramètres avec les options requises. Regardez simplement la sortie de mon script ici avec la ligne de commande abc X Y Z(ou -abc X Y Z):

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

Options longues concaténées également

Vous pouvez également avoir des options longues dans le bloc d'options étant donné qu'elles se produisent en dernier dans le bloc. Les lignes de commande suivantes sont donc toutes équivalentes (y compris l'ordre dans lequel les options et ses arguments sont traités):

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

Tous ces éléments conduisent à:

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

Pas dans cette solution

Arguments facultatifs

Les options avec des arguments optionnels devraient être possibles avec un peu de travail, par exemple en regardant en avant s'il y a un bloc sans trait d'union; l'utilisateur aurait alors besoin de mettre un trait d'union devant chaque bloc suivant un bloc avec un paramètre ayant un paramètre optionnel. Peut-être que cela est trop compliqué pour communiquer à l'utilisateur, il vaut donc mieux avoir simplement un trait d'union dans ce cas.

Les choses deviennent encore plus compliquées avec plusieurs paramètres possibles. Je déconseille de faire des options en essayant d'être intelligent en déterminant si un argument pourrait être pour lui ou non (par exemple avec une option prend juste un nombre comme argument optionnel) car cela pourrait casser à l'avenir.

Personnellement, je préfère des options supplémentaires au lieu d'arguments facultatifs.

Arguments d'option introduits avec un signe égal

Tout comme avec les arguments facultatifs, je ne suis pas fan de cela (BTW, y a-t-il un fil pour discuter des avantages / inconvénients des différents styles de paramètres?) Mais si vous le souhaitez, vous pouvez probablement l'implémenter vous-même comme sur http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop avec une --long-with-arg=?*déclaration de cas, puis en supprimant le signe égal (c'est BTW le site qui dit que la concaténation de paramètres est possible avec un certain effort mais "l'a laissé" comme exercice pour le lecteur) "ce qui m'a fait les croire sur parole mais je suis parti de zéro).

Autres notes

Compatible POSIX, fonctionne même sur les anciennes configurations Busybox auxquelles j'ai dû faire face (par exemple cut, headet getoptsmanquantes).

phk
la source