Question Comment analyser les arguments de la ligne de commande dans Bash?


Dites, j'ai un script qui est appelé 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 l'analyser de telle sorte que dans chaque cas (ou une combinaison des deux) $v, $f, et $d seront tous mis à true et $outFile sera égal à /fizz/someOtherFile ?


1340
2017-10-10 16:57


origine


Réponses:


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

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

Bash Space-Separated (par exemple, --option argument) (sans getopt [s])

Usage ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

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

Bash est égal à séparé (par exemple, --option=argument) (sans getopt [s])

Usage ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

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

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

Utiliser getopt [s]

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

getopt (1) limitations (plus vieux, relativement récent getopt versions):

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

Plus récent getopt les versions n'ont pas ces limitations.

De plus, l'offre shell POSIX (et autres) getopts qui n'a pas ces limitations. Voici un simpliste getopts Exemple:

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

# End of file

Les avantages de getopts sont:

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

L'inconvénient de getopts c'est qu'il ne peut gérer que les options courtes (-h, ne pas --help) sans code supplémentaire.

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


1936
2018-01-07 20:01



Aucune réponse getopt amélioré. Et le réponse la mieux votée est trompeur: Il ignore -⁠vfd style options courtes (demandées par l'OP), options après les arguments positionnels (également demandées par l'OP) et ignorant les erreurs d'analyse. Au lieu:

  • Utiliser amélioré getopt de util-linux ou anciennement GNU glibc.1
  • Cela fonctionne avec getopt_long() la fonction C de GNU glibc.
  • A tout caractéristiques distinctives utiles (les autres n'en ont pas):
    • gère les espaces, en citant les caractères et même en binaire dans les arguments2
    • il peut gérer les options à la fin: script.sh -o outFile file1 file2 -v
    • permet =options longues: script.sh --outfile=fileOut --infile fileIn
  • Est déjà si vieux3 qu'aucun système GNU ne manque (c'est le cas de Linux).
  • Vous pouvez tester son existence avec: getopt --test → renvoie la valeur 4.
  • Autre getopt ou shell-builtin getopts sont d'utilisation 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

tout retour

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

! 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

# -use ! and PIPESTATUS to get exit code with errexit set
# -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 "bash-systems", y compris Cygwin; sur OS X essayer infuser installer gnu-getopt
2 le POSIX exec() conventions n'ont aucun moyen fiable de passer NULL binaire dans les arguments de ligne de commande; ces octets mettent prématurément fin à l'argument
3 première version sortie en 1997 ou avant (je l'ai seulement repérée en 1997)


328
2018-04-20 17:47



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#*=} rechercher "Suppression de sous-chaîne" dans ce guide. Il est fonctionnellement équivalent à `sed 's/[^=]*=//' <<< "$i"` qui appelle un sous-processus inutile ou `echo "$i" | sed 's/[^=]*=//'` qui appelle deux sous-processus inutiles.


103
2017-11-13 10:31



getopt()/getopts() est une bonne option. Volé à 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 -a,   -b, -c ou -d sera autorisé, mais -c est suivi d'un argument (le "c:" le dit).

Si nous appelons cela "g" et l'essayer:

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

Nous commençons avec deux arguments, et   "getopt" rompt les options et   met chacun dans son propre argument. Ça aussi   ajoutée "--".


101
2017-10-10 17:03



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

  • poignées -n arg et --name=arg
  • permet des arguments à la fin
  • montre des erreurs saines 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

59
2017-07-15 23:43



Manière plus succincte

script.sh

#!/bin/bash

while [[ "$#" > 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

39
2017-11-20 12:28



J'ai environ 4 ans de retard à cette question, mais je veux donner en retour. J'ai utilisé les réponses précédentes comme un point de départ pour ranger mon vieux paramètre ad hoc. J'ai ensuite refacturé le code suivant. Il gère à la fois les paramètres longs et courts, en utilisant des arguments séparés par des espaces ou séparés par des espaces, ainsi que plusieurs paramètres courts groupés ensemble. 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

36
2017-07-01 01:20



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 un 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, ainsi que 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'arg que vous pouvez utiliser dans bash

    • Signification -a ou -arg ou --arg ou -a-r-g ou autre
  • Bash pur. Pas besoin d'apprendre / 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 que je peux penser du haut de ma tête


21
2017-09-08 18:59



J'ai trouvé l'affaire d'écrire l'analyse portable dans des scripts si frustrant que j'ai écrit Argbash - un générateur de code FOSS capable de générer le code d'analyse des arguments pour votre script, plus quelques fonctionnalités intéressantes:

https://argbash.io


21
2017-07-10 22:40



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

12
2018-03-01 15:15