Utilisation de getopts pour traiter les options de ligne de commande longues et courtes

410

Je souhaite que des formes longues et courtes d'options de ligne de commande soient invoquées à l'aide de mon script shell.

Je sais que cela getoptspeut être utilisé, mais comme en Perl, je n'ai pas pu faire de même avec shell.

Toutes les idées sur la façon dont cela peut être fait, afin que je puisse utiliser des options telles que:

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

Dans ce qui précède, les deux commandes signifient la même chose pour mon shell, mais en utilisant getopts, je n'ai pas pu les implémenter?

gagneet
la source
2
À mon humble avis, la réponse acceptée n'est pas la meilleure. Il ne montre pas comment utiliser getopts pour gérer les arguments "-" et "-", ce qui peut être fait, comme l'a démontré @Arvid Requate. J'insère une autre réponse en utilisant un concept similaire, mais traite également de l'erreur utilisateur "d'oublier" d'insérer des valeurs pour les arguments qui sont nécessaires. Point clé: les getopts peuvent fonctionner. L'utilisateur doit éviter d'utiliser "getopt" à la place si la portabilité multiplateforme est nécessaire. De plus, getopts fait partie du standard POSIX pour les shells, il est donc susceptible d'être portable.
pauljohn32

Réponses:

304

Trois implémentations peuvent être envisagées:

  • Bash intégré getopts. Cela ne prend pas en charge les noms d'options longs avec le préfixe à double tiret. Il ne prend en charge que les options à un seul caractère.

  • Implémentation BSD UNIX de la getoptcommande autonome (c'est ce que MacOS utilise). Cela ne prend pas non plus en charge les options longues.

  • Implémentation GNU autonome getopt. GNU getopt(3)(utilisé par la ligne de commande getopt(1)sous Linux) prend en charge l'analyse des options longues.


Certaines autres réponses montrent une solution pour utiliser la commande bash getoptspour imiter les options longues. Cette solution fait en fait une option courte dont le caractère est "-". Vous obtenez donc "-" comme drapeau. Ensuite, tout ce qui suit qui devient OPTARG, et vous testez l'OPTARG avec un imbriqué case.

C'est intelligent, mais il comporte des mises en garde:

  • getoptsne peut pas appliquer la spécification opt. Il ne peut pas renvoyer d'erreurs si l'utilisateur fournit une option non valide. Vous devez faire votre propre vérification des erreurs pendant que vous analysez OPTARG.
  • OPTARG est utilisé pour le nom de l'option longue, ce qui complique l'utilisation lorsque votre option longue elle-même a un argument. Vous finissez par devoir le coder vous-même comme cas supplémentaire.

Ainsi, bien qu'il soit possible d'écrire plus de code pour contourner le manque de prise en charge des options longues, cela représente beaucoup plus de travail et va partiellement à l'encontre du but d'utiliser un analyseur getopt pour simplifier votre code.

Bill Karwin
la source
18
Donc. Qu'est-ce que la solution portable multiplateforme?
troelskn
6
GNU Getopt semble être le seul choix. Sur Mac, installez GNU getopt à partir de macports. Sous Windows, j'installerais GNU getopt avec Cygwin.
Bill Karwin
2
Apparemment , les getopts de ksh peuvent gérer les options longues.
Tgr
1
@Bill +1, bien qu'il soit également assez simple de créer getopt à partir des sources ( software.frodo.looijaard.name/getopt ) sur Mac. Vous pouvez également vérifier la version de getopt installée sur votre système à partir de scripts avec "getopt -T; echo $?".
Chinasaur
8
@Bill Karwin: "La commande bash getopts ne prend pas en charge les noms d'options longs avec le préfixe à double tiret." Mais getopts peut être conçu pour prendre en charge les options longues: voir stackoverflow.com/a/7680682/915044 ci-dessous.
TomRoche
307

getoptet getoptssont des bêtes différentes, et les gens semblent avoir un peu de malentendu sur ce qu'ils font. getoptsest une commande intégrée permettant bashde traiter les options de ligne de commande dans une boucle et d'affecter tour à tour chaque option et valeur trouvées aux variables intégrées, afin que vous puissiez les traiter davantage. getopt, cependant, est un programme utilitaire externe, et il ne traite pas réellement vos options pour vous comme le font par exemple bash getopts, le Getoptmodule Perl ou Python optparse/ argparsemodules. Tout ce getoptqui est fait est de canoniser les options qui sont passées - c'est-à-dire les convertir en une forme plus standard, afin qu'il soit plus facile pour un script shell de les traiter. Par exemple, une application de getoptpourrait convertir les éléments suivants:

myscript -ab infile.txt -ooutfile.txt

en cela:

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

Vous devez effectuer vous-même le traitement. Vous ne devez pas utiliser getoptdu tout si vous faites diverses restrictions sur la façon dont vous pouvez spécifier les options:

  • ne mettez qu'une seule option par argument;
  • toutes les options passent avant tous les paramètres positionnels (c'est-à-dire les arguments sans option);
  • pour les options avec des valeurs (par exemple -oci-dessus), la valeur doit aller comme un argument séparé (après un espace).

Pourquoi utiliser getoptau lieu de getopts? La raison fondamentale est que seul GNU getoptvous permet de prendre en charge les options de ligne de commande portant un nom long. 1 (GNU getoptest la valeur par défaut sous Linux. Mac OS X et FreeBSD sont livrés avec une version basique et pas très utilegetopt , mais la version GNU peut être installée; voir ci-dessous.)

Par exemple, voici un exemple d'utilisation de GNU getopt, à partir d'un de mes scripts appelé javawrap:

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

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

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

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

Cela vous permet de spécifier des options similaires --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt"ou similaires. L'appel à a getoptpour effet de canoniser les options pour --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt"que vous puissiez les traiter plus facilement. La citation autour "$1"et "$2"est importante car elle garantit que les arguments avec des espaces sont traités correctement.

Si vous supprimez les 9 premières lignes (tout en haut de la eval setligne), le code fonctionnera toujours ! Cependant, votre code sera beaucoup plus pointilleux dans les types d'options qu'il accepte: En particulier, vous devrez spécifier toutes les options sous la forme "canonique" décrite ci-dessus. getoptCependant, avec l'utilisation de , vous pouvez regrouper des options à une seule lettre, utiliser des formes non ambiguës plus courtes d'options longues, utiliser le style --file foo.txtou --file=foo.txt, utiliser le style -m 4096ou -m4096, mélanger les options et les non-options dans n'importe quel ordre, etc. getoptgénère également un message d'erreur si des options non reconnues ou ambiguës sont trouvées.

REMARQUE : Il existe en fait deux versions totalement différentes de getopt, de base getoptet GNU getopt, avec des fonctionnalités différentes et des conventions d'appel différentes. 2 Basic getoptest assez cassé: non seulement il ne gère pas les options longues, mais il ne peut même pas gérer les espaces incorporés à l'intérieur des arguments ou des arguments vides, alors getoptsqu'il le fait correctement. Le code ci-dessus ne fonctionnera pas en base getopt. GNU getoptest installé par défaut sous Linux, mais sous Mac OS X et FreeBSD, il doit être installé séparément. Sur Mac OS X, installez MacPorts ( http://www.macports.org ), puis sudo port install getoptinstallez GNU getopt(généralement dans /opt/local/bin) et assurez-vous qu'il se /opt/local/bintrouve dans votre chemin de shell avant/usr/bin. Sur FreeBSD, installez misc/getopt.

Un guide rapide pour modifier l'exemple de code pour votre propre programme: Sur les premières lignes, tout est "passe-partout" qui doit rester le même, sauf la ligne qui appelle getopt. Vous devez modifier le nom du programme après -n, spécifier les options courtes après -oet les options longues après --long. Mettez deux points après les options qui prennent une valeur.

Enfin, si vous voyez du code qui vient de setremplacer eval set, il a été écrit pour BSD getopt. Vous devriez le changer pour utiliser le eval setstyle, qui fonctionne bien avec les deux versions de getopt, tandis que le clair setne fonctionne pas correctement avec GNU getopt.

1 En fait, getoptsin ksh93prend en charge les options nommées depuis longtemps, mais ce shell n'est pas utilisé aussi souvent que bash. Dans zsh, utilisez zparseoptspour obtenir cette fonctionnalité.

2 Techniquement, «GNU getopt» est un terme impropre; cette version a été écrite pour Linux plutôt que pour le projet GNU. Cependant, il suit toutes les conventions GNU, et le terme "GNU getopt" est couramment utilisé (par exemple sur FreeBSD).

Urban Vagabond
la source
3
Cela a été très utile, l'idée d'utiliser getopt pour vérifier les options puis de les traiter dans une boucle très simple a très bien fonctionné lorsque je voulais ajouter des options de style longues à un script bash. Merci.
ianmjones
2
getoptsous Linux n'est pas un utilitaire GNU et le traditionnel getoptne vient pas initialement de BSD mais d'AT & T Unix. ksh93 getopts(également d'AT & T) prend en charge les options longues de style GNU.
Stephane Chazelas
@StephaneChazelas - modifié pour refléter vos commentaires. Je préfère toujours le terme "GNU getopt" même si c'est un terme impropre, car cette version suit les conventions GNU et agit généralement comme un programme GNU (par exemple en utilisant POSIXLY_CORRECT), tandis que "Linux-Enhanced Getopt" suggère à tort que cette version n'existe que sur Linux.
Urban Vagabond
1
Il vient du paquet util-linux, donc c'est uniquement Linux car cet ensemble de logiciels est destiné à Linux uniquement (qui getoptpourrait facilement être porté sur d'autres Unices, mais de nombreux autres logiciels util-linuxsont spécifiques à Linux). Tous les programmes non GNU utilisant GNU getopt (3) le comprennent $POSIX_CORRECT. Par exemple, vous ne diriez pas que aplayc'est GNU uniquement pour ces raisons. Je soupçonne que lorsque FreeBSD mentionne GNU getopt, cela signifie l'API GNU getopt (3) C.
Stephane Chazelas
@StephaneChazelas - FreeBSD a un message d'erreur "Build dependency: Please install GNU getopt" qui fait référence sans ambiguïté à l' getoptutil, pas à getopt (3).
Urban Vagabond
202

La fonction intégrée getopts de Bash peut être utilisée pour analyser les options longues en plaçant un caractère tiret suivi de deux points dans l'optspec:

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

Après avoir copié dans le fichier exécutable nom = getopts_test.shdans le répertoire de travail courant , on peut produire une sortie comme

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

De toute évidence, getopts n'effectue ni OPTERRvérification ni analyse d'arguments d'option pour les options longues. Le fragment de script ci-dessus montre comment cela peut être fait manuellement. Le principe de base fonctionne également dans le shell Debian Almquist ("tiret"). Notez le cas particulier:

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

Notez que, comme le souligne GreyCat sur http://mywiki.wooledge.org/BashFAQ , cette astuce exploite un comportement non standard du shell qui autorise l'argument option (c'est-à-dire le nom de fichier dans "-f filename") à concaténer à l'option (comme dans "-nomfichier"). Le standard POSIX dit qu'il doit y avoir un espace entre eux, ce qui dans le cas de "- longoption" mettrait fin à l'analyse des options et transformerait toutes les options longues en arguments sans option.

Arvid Requate
la source
2
Une question: quelle est la sémantique de !in val="${!OPTIND}?
TomRoche
2
@TomRoche c'est Substitution indirecte: unix.stackexchange.com/a/41293/84316
ecbrodie
2
@ecbrodie: C'est parce que deux arguments ont effectivement été traités, par opposition à un seul. Le premier argument est le mot "loglevel" et le suivant est l'argument de cet argument. Pendant ce temps, getoptsincrémente automatiquement uniquement OPTINDavec 1, mais dans notre cas, nous en avons besoin pour incrémenter de 2, nous l'incrémentons donc de 1 manuellement, puis laissons l' getoptsincrémenter de 1 pour nous automatiquement.
Victor Zamanian
3
Puisque nous sommes dans l'équilibre bash ici: les noms de variables nues sont autorisés à l'intérieur des expressions arithmétiques, pas $nécessaire. OPTIND=$(( $OPTIND + 1 ))peut être juste OPTIND=$(( OPTIND + 1 )). Encore plus intéressant, vous pouvez même affecter et augmenter des variables à l'intérieur d'une expression arithmétique, il est donc possible de l'abréger davantage : $(( ++OPTIND )), ou même de (( ++OPTIND ))prendre en compte ce qui ++OPTINDsera toujours positif, de sorte qu'il ne déclenchera pas une exécution de shell avec l' -eoption. :-) gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html
clacke
3
Pourquoi ne --very-baddonne pas d' avertissement?
Tom Hale
148

La getoptscommande intégrée est toujours, AFAIK, limitée aux options à un seul caractère.

Il existe (ou existait auparavant) un programme externe getoptqui réorganiserait un ensemble d'options de sorte qu'il serait plus facile à analyser. Vous pouvez également adapter cette conception pour gérer de longues options. Exemple d'utilisation:

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

# Process remaining non-option arguments
...

Vous pouvez utiliser un schéma similaire avec une getoptlongcommande.

Notez que la faiblesse fondamentale du getoptprogramme externe est la difficulté de gérer les arguments contenant des espaces et de conserver ces espaces avec précision. C'est pourquoi la fonction intégrée getoptsest supérieure, bien que limitée par le fait qu'elle ne gère que les options à une seule lettre.

Jonathan Leffler
la source
11
getopt, à l'exception de la version GNU (qui a une convention d'appel différente), est fondamentalement cassé. Ne l'utilise pas. Veuillez utiliser ** getopts à la place bash-hackers.org/wiki/doku.php/howto/getopts_tutorial
hendry
9
@hendry - à partir de votre propre lien: "Notez que getopts n'est pas capable d'analyser les options longues de style GNU (--myoption) ou les options longues de style XF86 (-myoption)!"
Tom Auger
1
Jonathan - vous devriez réécrire l'exemple à utiliser eval setavec des guillemets (voir ma réponse ci-dessous) afin qu'il fonctionne également correctement avec GNU getopt (la valeur par défaut sous Linux) et gère correctement les espaces.
Urban Vagabond
@UrbanVagabond: Je ne sais pas pourquoi je devrais faire ça. La question est balisée Unix, pas Linux. Je montre délibérément le mécanisme traditionnel, et il y a des problèmes avec des blancs dans les arguments, etc. Vous pouvez démontrer la version moderne spécifique à Linux si vous le souhaitez, et votre réponse le fait. (Je note, passim, que votre utilisation de ${1+"$@"}est étrange et en contradiction avec ce qui est nécessaire dans les shells modernes et spécifiquement avec n'importe quel shell que vous trouveriez sur Linux. Voir Utilisation de $ 1: + "$ @"} dans / bin / sh pour un discussion de cette notation.)
Jonathan Leffler
Vous devriez le faire parce que eval setfait la bonne chose avec GNU et BSD getopt, alors que plain fait setseulement la bonne chose avec BSD getopt. Vous pouvez donc aussi utiliser eval setpour encourager les gens à prendre l'habitude de le faire. BTW merci, je ne savais pas que ce ${1+"$@"}n'était plus nécessaire. Je dois écrire des choses qui fonctionnent à la fois sur Mac OS X et Linux - entre les deux, elles forcent beaucoup de portabilité. Je viens de vérifier et "$@"ne le fait en effet la bonne chose sur tous sh, bash, kshet zshsous Mac OS X; sûrement sous Linux aussi.
Urban Vagabond
78

Voici un exemple qui utilise réellement getopt avec de longues options:

aflag=no
bflag=no
cargument=none

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

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag="yes" ;;
    -b|--blong) bflag="yes" ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done
pme
la source
1
Vous devriez réécrire l'exemple à utiliser eval setavec des guillemets (voir ma réponse ci-dessous) afin qu'il fonctionne également correctement avec GNU getopt (la valeur par défaut sous Linux) et gère correctement les espaces.
Urban Vagabond
2
Cela utilise getoptalors que la question concerne getopts.
Niklas Berglund
1
Les modèles sont (--- ils (-*et (*valides? En quoi sont-ils différents de --, -*et *?
Maëlan
1
@ Maëlan - La parenthèse ouverte principale est facultative, elle (--)est donc identique à celle --)d'une casestrophe. Il est étrange de voir l'indentation inégale et l'utilisation incohérente de cette parenthèse facultative, mais le code actuel de la réponse me semble valide.
Adam Katz
59

Les options longues peuvent être analysées par le getoptsmodule intégré standard en tant qu '«arguments» de -«l'option»

Il s'agit d'un shell POSIX portable et natif - aucun programme externe ni bashism n'est nécessaire.

Ce guide implémente les options longues comme arguments de l' -option, il --alphaest donc vu getoptscomme -avec argument alphaet --bravo=fooest vu comme -avec argument bravo=foo. L'argument vrai peut être récolté avec un simple remplacement: ${OPTARG#*=}.

Dans cet exemple, -bet -c(et leurs formes longues, --bravoet --charlie) ont des arguments obligatoires. Les arguments pour les options longues viennent après des signes égaux, par exemple --bravo=foo(les délimiteurs d'espace pour les options longues seraient difficiles à implémenter, voir ci-dessous).

Parce que cela utilise la fonction getoptsintégrée , cette solution prend en charge l'utilisation comme cmd --bravo=foo -ac FILE(qui a combiné des options -aet -cet entrelace des options longues avec des options standard) tandis que la plupart des autres réponses ici ont du mal ou échouent à le faire.

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

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

Lorsque l'option est un tiret ( -), c'est une option longue. getoptsaura analysé l'option réelle réelle $OPTARG, par exemple, à l' --bravo=fooorigine les ensembles OPT='-'et OPTARG='bravo=foo'. La ifstrophe définit $OPTle contenu de$OPTARG avant le premier signe égal ( bravodans notre exemple), puis le supprime depuis le début de $OPTARG(donnant =foocette étape, ou une chaîne vide s'il n'y en a pas =). Enfin, nous supprimons l'argument principal =. À ce stade, $OPTest soit une option courte (un caractère), soit une option longue (2+ caractères).

L' caseoption correspond alors aux options courtes ou longues. Pour les options courtes, getoptsse plaint automatiquement des options et des arguments manquants, nous devons donc les répliquer manuellement à l'aide duneeds_arg fonction, qui se ferme fatalement lorsqu'elle $OPTARGest vide. La ??*condition correspondra à toute option longue restante ( ?correspond à un seul caractère et *correspond à zéro ou plus, donc ??*correspond à 2+ caractères), ce qui nous permet d'émettre l'erreur "Option illégale" avant de quitter.

(Remarque sur les noms de variables en majuscules: en général, le conseil est de réserver des variables en majuscules à l'utilisation du système. Je garde $OPT que tout en majuscules pour le maintenir en ligne avec $OPTARG., Mais cela ne rompt cette convention , je pense qu'il convient car c'est quelque chose que le système aurait dû faire, et il devrait être sûr car il n'y a pas de normes (afaik) qui utilisent une telle variable.)


Pour vous plaindre d'arguments inattendus sur les options longues, imitez ce que nous avons fait pour les arguments obligatoires: utilisez une fonction d'assistance. Il suffit de retourner le test pour se plaindre d'un argument alors que l'on n'est pas attendu:

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

Une ancienne version de cette réponse avait tenté d'accepter des options longues avec des arguments délimités par des espaces, mais elle n'était pas fiable; getoptspourrait se terminer prématurément en supposant que l'argument dépassait sa portée et que l'incrémentation manuelle $OPTINDne fonctionne pas dans tous les shells.

Cela serait accompli en utilisant l'une de ces techniques:

puis a conclu avec quelque chose comme [ $# -gt $OPTIND ] && OPTIND=$((OPTIND+1))

Adam Katz
la source
Très belle solution autonome. Une question: letter-cne nécessitant aucun argument, ne serait-il pas suffisant de l'utiliser letter-c)?; le *semble redondant.
Philip Kearns
1
@Arne Les arguments positionnels sont de mauvais UX; ils sont difficiles à comprendre et les arguments facultatifs sont assez compliqués. getoptss'arrête au premier argument positionnel car il n'est pas conçu pour les traiter. Cela permet aux sous-commandes avec leurs propres arguments, par exemple git diff --color, donc j'interpréterais command --foo=moo bar --baz wazcomme ayant --foocomme argument commandet --baz wazcomme argument (avec option) la barsous-commande. Cela peut être fait avec le code ci-dessus. Je rejette --bravo -blahparce que --bravonécessite un argument et il n'est pas clair que ce -blahne soit pas une autre option.
Adam Katz
1
Je ne suis pas d'accord sur l'UX: les arguments positionnels sont utiles et faciles, tant que vous limitez leur nombre (au maximum 2 ou 1 plus N-du-même-type). Il devrait être possible de les intercaler avec des arguments de mots clés, car les utilisateurs peuvent ensuite créer une commande étape par étape (c'est-à-dire ls abc -la).
Arne Babenhauserheide
1
@AdamKatz: J'ai écrit un petit article avec ceci: draketo.de/english/free-software/shell-argument-parsing - inclut la lecture répétée des arguments restants pour intercepter les options de fin.
Arne Babenhauserheide
1
@ArneBabenhauserheide: J'ai mis à jour cette réponse pour prendre en charge les arguments séparés par des espaces. Parce qu'il nécessite evaldans le shell POSIX, il est répertorié sous le reste de la réponse.
Adam Katz
33

Jetez un œil à shFlags qui est une bibliothèque de shell portable (ce qui signifie: sh, bash, dash, ksh, zsh sous Linux, Solaris, etc.).

Il rend l'ajout de nouveaux indicateurs aussi simple que l'ajout d'une ligne à votre script, et il fournit une fonction d'utilisation générée automatiquement.

Voici un simple Hello, world!utilisant shFlag :

#!/bin/sh

# source shflags from current directory
. ./shflags

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

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

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

Pour les systèmes d'exploitation qui ont le getopt amélioré qui prend en charge les options longues (par exemple Linux), vous pouvez faire:

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

Pour le reste, vous devez utiliser l'option courte:

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

L'ajout d'un nouveau drapeau est aussi simple que l'ajout d'un nouveau DEFINE_ call.

k0pernikus
la source
2
C'est fantastique mais malheureusement mon getopt (OS X) ne supporte pas les espaces dans les arguments: / me demande s'il y a une alternative.
Alastair Stuart
@AlastairStuart - il existe en effet une alternative sur OS X. Utilisez MacPorts pour installer GNU getopt (il sera généralement installé dans / opt / local / bin / getopt).
Urban Vagabond
3
@UrbanVagabond - l'installation d'outils par défaut non système n'est malheureusement pas une exigence acceptable pour un outil suffisamment portable.
Alastair Stuart
@AlastairStuart - voir ma réponse pour une solution portable qui utilise le getopts intégré plutôt que GNU getopt. C'est la même chose que l'utilisation de getopts de base, mais avec une itération supplémentaire pour les options longues.
Adam Katz
31

Utilisation getoptsavec des options et des arguments courts / longs


Fonctionne avec toutes les combinaisons, par exemple:

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

Quelques déclarations pour cet exemple

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

A quoi ressemblerait la fonction Usage

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

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

getops avec des drapeaux longs / courts ainsi que des arguments longs

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

Production

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

Combiner ce qui précède dans un script cohérent

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

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

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

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

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

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

##################################################################    
#######  "getopts" with: short options  AND  long options  #######
#######            AND  short/long arguments               #######
while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
               --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done
Rapa Nui
la source
Cela ne fonctionne-t-il pas avec plusieurs arguments longs (-). Il semble que je n'ai lu que le premier.
Sinaesthetic
@Sinaesthetic - Oui, je jouais avec l' evalapproche des arguments espacés sur les options longues et je l'ai trouvée peu fiable avec certains shells (bien que je m'attende à ce que cela fonctionne avec bash, auquel cas vous n'avez pas à utiliser eval). Voir ma réponse pour savoir comment accepter des arguments d'option longs avec =et mes tentatives notées pour utiliser l'espace. Ma solution ne fait pas d'appels externes alors que celle-ci utilise cutplusieurs fois.
Adam Katz
24

Autrement...

# translate long options to short
for arg
do
    delim=""
    case "$arg" in
       --help) args="${args}-h ";;
       --verbose) args="${args}-v ";;
       --config) args="${args}-c ";;
       # pass through anything else
       *) [[ "${arg:0:1}" == "-" ]] || delim="\""
           args="${args}${delim}${arg}${delim} ";;
    esac
done
# reset the translated args
eval set -- $args
# now we can process with getopt
while getopts ":hvc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        c)  source $OPTARG ;;
        \?) usage ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage
        ;;
    esac
done
mtvee
la source
1
Cela n'a-t-il pas besoin d'un espace dans chaque $argsréaffectation? Cela pourrait même être fait sans bashismes, mais ce code perdra des espaces dans les options et les arguments (je ne pense pas que l' $delimastuce fonctionnera). Vous pouvez à la place exécuter set à l' intérieur de la forboucle si vous êtes assez prudent pour la vider uniquement sur la première itération. Voici une version plus sûre sans bashismes.
Adam Katz
18

J'ai en quelque sorte résolu de cette façon:

# A string with command options
options=$@

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

# Loop index
index=0

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

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

exit;

Suis-je stupide ou quelque chose? getoptet getoptssont tellement déroutants.

Benjamin
la source
1
Cela semble fonctionner pour moi, je ne sais pas quel est le problème avec cette méthode, mais cela semble simple, donc il doit y avoir une raison pour laquelle tout le monde ne l'utilise pas.
Billy Moon
1
@Billy Oui, c'est simple parce que je n'utilise aucun script pour gérer mes paramètres et etc. En gros, je convertis la chaîne d'arguments ($ @) en un tableau et je le boucle. Dans la boucle, la valeur actuelle sera la clé et la suivante sera la valeur. Aussi simple que cela.
1
@Theodore, je suis heureux que cela vous ait été utile! C'était aussi une douleur pour moi. Si vous êtes intéressé, vous pouvez en voir un exemple en action ici: raw.github.com/rafaelrinaldi/swf-to-html/master/swf-to-html.sh
2
Certainement la façon la plus simple que j'ai vue. Je l'ai changé un peu, par exemple en utilisant i = $ (($ i + 1)) au lieu de expr, mais le concept est étanche à l'air.
Thomas Dignan
6
Vous n'êtes pas stupide du tout, mais il se peut qu'il vous manque une fonctionnalité: getopt (s) peut reconnaître les options qui sont mélangées (ex: -ltrou -lt -raussi -l -t -r). Et il fournit également une gestion des erreurs et un moyen facile de décaler les paramètres traités une fois le traitement des options terminé.
Olivier Dulac
14

Dans le cas où vous ne voulez pas la getoptdépendance, vous pouvez le faire:

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

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

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

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

  # Done with options
    *)
      break
      ;;
  esac

  # for testing purposes:
  echo "$1"

  shift
done

Bien sûr, vous ne pouvez pas utiliser les options de style longues avec un seul tiret. Et si vous souhaitez ajouter des versions raccourcies (par exemple --verbos au lieu de --verbose), vous devez les ajouter manuellement.

Mais si vous cherchez à obtenir des getoptsfonctionnalités avec de longues options, c'est un moyen simple de le faire.

J'ai également mis cet extrait dans un sens .

jakesandlund
la source
Cela ne semble fonctionner qu'avec une seule option longue à la fois, mais cela a répondu à mon besoin. Je vous remercie!
kingjeffrey
Dans le cas particulier, --)il semble y avoir un shift ;manquant. Pour le moment, le --reste comme premier argument non optionnel.
dgw
Je pense que c'est en fait la meilleure réponse, bien que comme le souligne dgw, l' --option a besoin d'un shiftdedans. Je dis que c'est mieux parce que les alternatives sont des versions dépendantes de la plate-forme de getoptou getopts_longou vous devez forcer les options courtes à être utilisées uniquement au début de la commande (c'est-à-dire - vous utilisez getoptspuis traitez les options longues ensuite), alors que cela donne n'importe quel ordre et un contrôle complet.
Haravikk
Cette réponse me fait me demander pourquoi nous avons un fil de dizaines de réponses pour faire le travail qui ne peut être fait sans plus que cette solution absolument claire et simple , et s'il y a une raison pour le milliard de cas d'utilisation de getopt (s) autre que la preuve soi-même.
Florian Heigl
11

Le intégré getoptsne peut pas faire cela. Il existe un programme externe getopt (1) qui peut le faire, mais vous ne l'obtenez que sur Linux à partir du package util-linux . Il est livré avec un exemple de script getopt-parse.bash .

Il existe également une getopts_longfonction shell écrite.

Nietzche-jou
la source
3
Le a getoptété inclus dans FreeBSD version 1.0 en 1993, et fait partie de FreeBSD depuis lors. En tant que tel, il a été adopté à partir de FreeBSD 4.x pour être inclus dans le projet Darwin d'Apple. Depuis OS X 10.6.8, la page de manuel incluse par Apple reste un duplicata exact de la page de manuel FreeBSD. Alors oui, il est inclus dans OS X et les autres systèmes d'exploitation à côté de Linux. -1 sur cette réponse pour la désinformation.
ghoti
8
#!/bin/bash
while getopts "abc:d:" flag
do
  case $flag in
    a) echo "[getopts:$OPTIND]==> -$flag";;
    b) echo "[getopts:$OPTIND]==> -$flag";;
    c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
    d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
  esac
done

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

exit

.

#!/bin/bash
until [ -z "$1" ]; do
  case $1 in
    "--dlong")
      shift
      if [ "${1:1:0}" != "-" ]
      then
        echo "==> dlong $1"
        shift
      fi;;
    *) echo "==> other $1"; shift;;
  esac
done
exit
3ED
la source
2
Une explication serait bien. Le premier script accepte uniquement les options courtes tandis que le second script contient un bogue dans son analyse des arguments des options longues; sa variable doit être "${1:0:1}"pour l'argument # 1, sous-chaîne à l'index 0, longueur 1. Cela ne permet pas de mélanger les options courtes et longues.
Adam Katz
7

Dans ksh93, getoptsprend en charge les noms longs ...

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

Ou du moins les tutoriels que j'ai trouvés l'ont dit. Essayez-le et voyez.

Richard Lynch
la source
4
C'est le getopts intégré de ksh93. En dehors de cette syntaxe, il a également une syntaxe plus compliquée qui permet également des options longues sans équivalent court, et plus encore.
jilles
2
Une réponse raisonnable. L'OP n'a pas spécifié QUEL shell.
ghoti
6

Je n'écris que des scripts shell de temps en temps et je ne pratique plus, donc tout commentaire est apprécié.

En utilisant la stratégie proposée par @Arvid Requate, nous avons remarqué quelques erreurs utilisateur. Un utilisateur qui oublie d'inclure une valeur verra accidentellement le nom de l'option suivante traité comme une valeur:

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

entraînera la valeur de "loglevel" comme "--toc = TRUE". Cela peut être évité.

J'ai adapté quelques idées sur la vérification des erreurs utilisateur pour CLI à partir de la discussion http://mwiki.wooledge.org/BashFAQ/035 sur l'analyse manuelle. J'ai inséré la vérification des erreurs dans la gestion des arguments "-" et "-".

Ensuite, j'ai commencé à jouer avec la syntaxe, donc toutes les erreurs ici sont strictement de ma faute, pas les auteurs originaux.

Mon approche aide les utilisateurs qui préfèrent entrer longtemps avec ou sans signe égal. Autrement dit, il devrait avoir la même réponse à "--loglevel 9" que "--loglevel = 9". Dans la méthode - / space, il n'est pas possible de savoir avec certitude si l'utilisateur oublie un argument, il est donc nécessaire de deviner.

  1. si l'utilisateur a le format de signe long / égal (--opt =), un espace après = déclenche une erreur car aucun argument n'a été fourni.
  2. si l'utilisateur a des arguments long / space (--opt), ce script provoque un échec si aucun argument ne suit (fin de la commande) ou si l'argument commence par un tiret)

Au cas où vous commenceriez sur ce point, il existe une différence intéressante entre les formats "--opt = value" et "--opt value". Avec le signe égal, l'argument de ligne de commande est considéré comme "opt = valeur" et le travail à gérer qui est l'analyse de chaîne, à séparer au niveau "=". En revanche, avec "--opt value", le nom de l'argument est "opt" et nous avons le défi d'obtenir la prochaine valeur fournie dans la ligne de commande. C'est là que @Arvid Requate a utilisé $ {! OPTIND}, la référence indirecte. Je ne comprends toujours pas cela, enfin, et les commentaires dans BashFAQ semblent mettre en garde contre ce style ( http://mywiki.wooledge.org/BashFAQ/006 ). BTW, je ne pense pas que les commentaires de l'affiche précédente sur l'importance de OPTIND = $ (($ OPTIND + 1)) sont corrects. Je veux dire,

Dans la dernière version de ce script, l'indicateur -v signifie l'impression VERBOSE.

Enregistrez-le dans un fichier appelé "cli-5.sh", rendez-le exécutable, et l'un d'eux fonctionnera ou échouera de la manière souhaitée

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

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

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

Voici un exemple de sortie de la vérification des erreurs sur l'utilisateur intpu

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

Vous devriez envisager d'activer -v, car il imprime les composants internes d'OPTIND et d'OPTARG

#/usr/bin/env bash

## Paul Johnson
## 20171016
##

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

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


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

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

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


VERBOSE=0
loglevel=0
toc="TRUE"

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

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

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

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



echo "
After Parsing values
"
echo "loglevel  $loglevel" 
echo "toc  $toc"
pauljohn32
la source
OPTIND=$(( $OPTIND + 1 )): il est nécessaire chaque fois que vous 'engloutissez' le paramètre OPTIND (par exemple: lorsque l'un est utilisé --toc value : la valeur est dans le numéro de paramètre $ OPTIND. Une fois que vous l'avez récupéré pour la valeur de toc, vous devez dire à getopts que le prochain paramètre à analyser n'est pas une valeur, mais celui qui le suit (d'où le :. OPTIND=$(( $OPTIND + 1 )) et votre script (ainsi que le script shift $(( $OPTIND -1 ))auquel vous vous référez) sont manquants, après le done: (comme getopts est sorti après avoir analysé les paramètres 1 en OPTIND-1, vous devez les déplacer afin $@est maintenant tout paramètre "non-options" restant
Olivier Dulac
oh, lorsque vous vous déplacez vous-même, vous "déplacez" les paramètres sous getopts, donc OPTIND pointe toujours la bonne chose ... mais je trouve cela très déroutant. Je crois (ne peut pas tester votre script pour le moment) que vous avez toujours besoin du décalage $ (($ OPTIND - 1)) après la boucle getopts while, de sorte que $ 1 ne pointe pas vers le $ 1 d'origine (une option) mais au premier des arguments restants (ceux qui viennent après toutes les options et leurs valeurs). ex: myrm -foo -bar = baz thisarg thenthisone thenanother
Olivier Dulac
5

Inventer une autre version de la roue ...

Cette fonction est (espérons-le) un remplacement de shell bourne simple compatible POSIX pour getopt GNU. Il prend en charge les options courtes / longues qui peuvent accepter des arguments obligatoires / facultatifs / aucun argument, et la façon dont les options sont spécifiées est presque identique à GNU getopt, donc la conversion est triviale.

Bien sûr, c'est encore un gros morceau de code à insérer dans un script, mais c'est environ la moitié des lignes de la fonction shell getopt_long bien connue, et cela pourrait être préférable dans les cas où vous souhaitez simplement remplacer les utilisations existantes de getopt GNU.

Ceci est un code assez nouveau, donc YMMV (et certainement, faites-moi savoir si ce n'est pas réellement compatible POSIX pour une raison quelconque - la portabilité était l'intention dès le départ, mais je n'ai pas d'environnement de test POSIX utile).

Le code et l'exemple d'utilisation suivent:

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

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

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

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

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

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

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

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

Exemple d'utilisation:

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

La réponse acceptée fait un très bon travail en soulignant toutes les lacunes de bash intégré getopts. La réponse se termine par:

Ainsi, bien qu'il soit possible d'écrire plus de code pour contourner le manque de prise en charge des options longues, cela représente beaucoup plus de travail et va partiellement à l'encontre du but d'utiliser un analyseur getopt pour simplifier votre code.

Et même si je suis d'accord en principe avec cette affirmation, je pense que le nombre de fois où nous avons tous implémenté cette fonctionnalité dans divers scripts justifie de mettre un peu d'effort dans la création d'une solution "standardisée" et bien testée.

En tant que tel, j'ai "mis à niveau" bash intégré en l' getoptsimplémentant getopts_longdans pure bash, sans dépendances externes. L'utilisation de la fonction est 100% compatible avec la fonction intégrée getopts.

En incluant getopts_long(qui est hébergé sur GitHub ) dans un script, la réponse à la question d'origine peut être implémentée aussi simplement que:

source "${PATH_TO}/getopts_long.bash"

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

shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
UrsaDK
la source
3

Je n'ai pas encore assez de représentants pour commenter ou voter sa solution, mais la réponse de Sme a extrêmement bien fonctionné pour moi. Le seul problème que j'ai rencontré est que les arguments finissent par être entre guillemets simples (j'ai donc une bande à retirer).

J'ai également ajouté quelques exemples d'utilisations et du texte d'aide. Je vais inclure ma version légèrement étendue ici:

#!/bin/bash

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

    Accepts the following forms:\n\n

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

aflag=false
bflag=false
cargument=""

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

set -- $options

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

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

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

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

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

done
kghastie
la source
3

Ici vous pouvez trouver quelques approches différentes pour l'analyse d'options complexes dans bash: http://mywiki.wooledge.org/ComplexOptionParsing

J'ai créé le suivant, et je pense que c'est un bon, car c'est un code minimal et les options longues et courtes fonctionnent. Une option longue peut également avoir plusieurs arguments avec cette approche.

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

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

# End of file
user3573558
la source
2

Je travaille sur ce sujet depuis assez longtemps ... et j'ai créé ma propre bibliothèque dont vous aurez besoin de vous approvisionner dans votre script principal. Voir libopt4shell et cd2mpc pour un exemple. J'espère que cela aide !

liealgebra
la source
2

Une solution améliorée:

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

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

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

shift $((OPTIND-1))
[[ "$1" == "--" ]] && shift
jbar
la source
2

Il est peut-être plus simple d'utiliser ksh, juste pour la partie getopts, si vous avez besoin de longues options de ligne de commande, car cela peut être plus facile.

# Working Getopts Long => KSH

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

while getopts "$USAGE" optchar ; do
    case $optchar in
    s)  echo "Displaying Configuration" ;;
        c)  echo "Creating Database $OPTARG" ;;
    l)  echo "Creating Listener LISTENER_$OPTARG" ;;
    g)  echo "Generating Scripts for Database $OPTARG" ;;
    r)  echo "Removing Database $OPTARG" ;;
    x)  echo "Removing Listener LISTENER_$OPTARG" ;;
    t)  echo "Creating Database Template" ;;
    h)  echo "Help" ;;
    esac
done
efstathiou_e
la source
+1 - Notez que cela est limité à ksh93 - du projet open source AST (AT&T Research).
Henk Langeveld
2

Je voulais quelque chose sans dépendances externes, avec un support bash strict (-u), et j'en avais besoin pour fonctionner même sur les anciennes versions bash. Cela gère différents types de paramètres:

  • bools courts (-h)
  • options courtes (-i "image.jpg")
  • longs bools (--help)
  • est égal à options (--file = "filename.ext")
  • options d'espace (--file "filename.ext")
  • bools concaténés (-hvm)

Insérez simplement ce qui suit en haut de votre script:

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

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

  local param_pair
  local key
  local value
  local shift_count

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

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

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

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

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

Et utilisez-le comme ceci:

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

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

# Get the params from arguments provided
_get_params $*
Heath Dutton
la source
1

Afin de rester compatible entre les plates-formes et d'éviter la dépendance à des exécutables externes, j'ai porté du code à partir d'un autre langage.

Je le trouve très simple d'utilisation, voici un exemple:

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

ArgParser::parse "$@"

ArgParser::isset help && ArgParser::showArgs

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

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

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

Le BASH requis est un peu plus long qu'il ne pourrait l'être, mais je voulais éviter de me fier aux tableaux associatifs de BASH 4. Vous pouvez également le télécharger directement depuis http://nt4.com/bash/argparser.inc.sh

#!/usr/bin/env bash

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

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

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

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

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

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

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


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

function decho
{
    :
}

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

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

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

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

function ArgParser::parse
{

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

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

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

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

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

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

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

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

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

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

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

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

        description="${EXPLODED[4]}"

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

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

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

    ArgParser::parse "$@"

    ArgParser::isset help && ArgParser::showArgs

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

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

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

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

}

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

Si toutes vos options longues ont des premiers caractères uniques et correspondants comme options courtes, par exemple

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

Est le même que

./slamm -c 23 -p test -q

Vous pouvez utiliser ceci avant getopts pour réécrire $ args:

# change long options to short options

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

# reset the incoming args
eval set -- $args

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

Merci pour mtvee pour l'inspiration ;-)

pic commun
la source
Je ne comprends pas la signification d'eval ici
user.friendly
1

si c'est simplement ainsi que vous voulez appeler le script

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

alors vous pouvez suivre ce moyen le plus simple pour y parvenir à l'aide de getopt et --longoptions

essayez ceci, j'espère que c'est utile

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



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


echo $opts

eval set --$opts

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

getopts "pourrait être utilisé" pour analyser les options longues tant que vous ne vous attendez pas à ce qu'elles aient des arguments ...

Voici comment:

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

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

Si vous essayez d'utiliser OPTIND pour obtenir un paramètre pour l'option longue, getopts le traitera comme le premier paramètre de position non optionnel et cessera d'analyser tous les autres paramètres. Dans un tel cas, vous feriez mieux de le traiter manuellement avec une simple déclaration de cas.

Cela fonctionnera "toujours":

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

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


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

Bien que ce ne soit pas aussi flexible que getopts et vous devez faire une grande partie du code de vérification des erreurs vous-même dans les instances de cas ...

Mais c'est une option.

estani
la source
Mais les options longues attendent souvent des arguments. Et vous pourriez faire plus avec le - pour le faire fonctionner même s'il s'agit d'un hack. En fin de compte, on pourrait faire valoir que s'il ne le prend pas en charge de manière native, toutes les manières de le mettre en œuvre sont en quelque sorte un hack, mais vous pouvez néanmoins étendre le - aussi. Et oui, shift est très utile mais bien sûr, s'il attend un argument, il pourrait en résulter que l'argument suivant (s'il n'est spécifié par l'utilisateur) fait partie de l'argument attendu.
Pryftan
oui, il s'agit d'un poc pour les noms d'arguments longs sans arguments, pour différencier les deux, vous avez besoin d'une configuration, comme getops. Et en ce qui concerne le décalage, vous pouvez toujours "le remettre" avec l'ensemble. Dans tous les cas, il doit être configurable si un paramètre est attendu ou non. Vous pouvez même utiliser de la magie pour cela, mais vous obligerez ensuite les utilisateurs à l'utiliser - pour signaler que la magie s'arrête et que les paramètres de position commencent, ce qui est pire à mon humble avis.
estani
C'est suffisant. C'est plus que raisonnable. Je ne me souviens même pas de ce que je faisais et j'avais totalement oublié cette question elle-même. Je ne me rappelle que vaguement comment je l'ai trouvé même. À votre santé. Oh et ayez un +1 pour l'idée. Vous avez fait l'effort et vous avez également clarifié ce que vous vouliez dire. Je respecte les gens qui font l'effort de donner des idées aux autres, etc.
Pryftan
0

Intégré getopts analyse uniquement les options courtes (sauf dans ksh93), mais vous pouvez toujours ajouter quelques lignes de script pour que getopts gère les options longues.

Voici une partie du code trouvé sur http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts

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

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

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

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

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

Voici un test:

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

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

Sinon, dans le récent Korn Shell ksh93, il getoptspeut naturellement analyser les options longues et même afficher une page de manuel. (Voir http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options )

Michel VONGVILAY uxora.com
la source
0

Th intégré dans OS X (BSD) getopt ne supporte pas les options longues, mais la version GNU fait: brew install gnu-getopt. Puis, quelque chose de similaire à: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt.

wprl
la source
0

EasyOptions gère les options courtes et longues:

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

source easyoptions || exit

if test -n "${verbose}"; then
    echo "log file: ${logfile}"
    echo "arguments: ${arguments[@]}"
fi
Renato Silva
la source