Comment analyser des arguments facultatifs dans un script bash si aucun ordre n'est donné?

13

Je ne sais pas comment inclure des arguments / indicateurs facultatifs lors de l'écriture d'un script bash pour le programme suivant:

Le programme nécessite deux arguments:

run_program --flag1 <value> --flag2 <value>

Cependant, il existe plusieurs indicateurs facultatifs:

run_program --flag1 <value> --flag2 <value> --optflag1 <value> --optflag2 <value> --optflag3 <value> --optflag4 <value> --optflag5 <value> 

Je voudrais exécuter le script bash de telle sorte qu'il prenne des arguments utilisateur. Si les utilisateurs ne saisissent que deux arguments dans l'ordre, ce serait:

#!/bin/sh

run_program --flag1 $1 --flag2 $2

Mais que se passe-t-il si l'un des arguments facultatifs est inclus? Je pense que ce serait

if [ --optflag1 "$3" ]; then
    run_program --flag1 $1 --flag2 $2 --optflag1 $3
fi

Mais que se passe-t-il si 4 $ sont donnés mais pas 3 $?

ShanZhengYang
la source
getoptsc'est ce que tu veux. Sans cela, vous pourriez utiliser une boucle avec une instruction switch pour détecter chaque indicateur, facultatif ou non.
orion
@orion Avec getopts, aurais-je besoin de spécifier chaque combinaison d'arguments? 3 & 4, 3 & 5, 3 & 4 & 5, etc.?
ShanZhengYang
Non, vous les définissez simplement si vous les obtenez, sinon il indique qu'il n'a pas été trouvé, donc en gros vous "obtenez" chaque option, dans n'importe quel ordre, et les spécifiez dans n'importe quel ordre, le cas échéant. Mais lisez simplement la page de manuel de bash, tout y est.
orion
@orion Je suis désolé, mais je ne comprends toujours pas très bien getopts. Disons que je force les utilisateurs à exécuter le script avec tous les arguments: run_program.sh VAL VAL FALSE FALSE FALSE FALSE FALSEqui exécute le programme en tant que program --flag1 VAL --flag2 VAL. Si vous exécutiez run_program.sh VAL VAL FALSE 10 FALSE FALSE FALSE, le programme s'exécuterait sous program --flag1 VAL --flag2 VAL --optflag2 10. Comment pouvez-vous obtenir un tel comportement getopts?
ShanZhengYang

Réponses:

25

Cet article présente deux manières différentes - shiftet getopts(et présente les avantages et les inconvénients des deux approches).

Avec shiftvotre apparence de script à $1, décide des mesures à prendre, puis exécute shift, en mouvement $2à $1, $3à $2, etc.

Par exemple:

while :; do
    case $1 in
        -a|--flag1) flag1="SET"            
        ;;
        -b|--flag2) flag2="SET"            
        ;;
        -c|--optflag1) optflag1="SET"            
        ;;
        -d|--optflag2) optflag2="SET"            
        ;;
        -e|--optflag3) optflag3="SET"            
        ;;
        *) break
    esac
    shift
done

Avec getoptsvous définissez les options (courtes) dans l' whileexpression:

while getopts abcde opt; do
    case $opt in
        a) flag1="SET"
        ;;
        b) flag2="SET"
        ;;
        c) optflag1="SET"
        ;;
        d) optflag2="SET"
        ;;
        e) optflag3="SET"
        ;;
    esac
done

Évidemment, ce ne sont que des extraits de code, et j'ai laissé de côté la validation - en vérifiant que les arguments obligatoires flag1 et flag2 sont définis, etc.

Quelle approche vous utilisez est dans une certaine mesure une question de goût - comment portable vous voulez que votre script soit, si vous pouvez vivre avec des options courtes (POSIX) seulement ou si vous voulez des options longues (GNU), etc.

John N
la source
Je fais d'habitude while (( $# ))au lieu de while :;et quitte souvent avec une erreur dans le *cas
Jasen
cela répond au titre mais pas à la question.
Jasen
@Jasen J'ai regardé ça hier soir et je n'ai pas pu voir pourquoi pour la vie de moi. Ce matin, c'est beaucoup plus clair (et j'ai également vu la réponse d'Orion maintenant). Je supprimerai cette réponse plus tard dans la journée (je voulais d'abord reconnaître votre commentaire, et cela semblait la façon la plus simple de le faire).
John N
pas de problème, c'est toujours une bonne réponse, juste la question l'a esquivée.
Jasen
1
NB: Dans le cas de set -o nounsetla première solution, l'erreur se produira si aucun paramètre n'est donné. Corrigé:case ${1:-} in
Raphael
3

utilisez un tableau.

#!/bin/bash

args=( --flag1 "$1" --flag2 "$2" )
[  "x$3" = xFALSE ] ||  args+=( --optflag1 "$3" )
[  "x$4" = xFALSE ] ||  args+=( --optflag2 "$4" )
[  "x$5" = xFALSE ] ||  args+=( --optflag3 "$5" )
[  "x$6" = xFALSE ] ||  args+=( --optflag4 "$6" )
[  "x$7" = xFALSE ] ||  args+=( --optflag5 "$7" )

program_name "${args[@]}"

cela gérera correctement les arguments avec des espaces.

[modifier] J'utilisais la syntaxe à peu près équivalente args=( "${args[@]}" --optflag1 "$3" )mais G-Man a suggéré une meilleure façon.

Jasen
la source
1
Vous pouvez rationaliser un peu cela en disant args+=( --optflag1 "$3" ). Vous voudrez peut-être voir ma réponse à notre travail de référence, Conséquences pour la sécurité de ne pas citer une variable dans les shells bash / POSIX , où je discute de cette technique (de construction d'une ligne de commande dans un tableau en ajoutant conditionnellement des arguments facultatifs).
G-Man dit `` Réintègre Monica ''
@ G-Man Merci, je n'avais pas vu ça dans la documentation, comprenant [@]que j'avais un outil suffisant pour résoudre mon problème.
Jasen
1

Dans un script shell, les arguments sont "$ 1", "$ 2", "$ 3", etc. Le nombre d'arguments est $ #.
Si votre script ne reconnaît pas les options, vous pouvez exclure la détection des options et traiter tous les arguments comme des opérandes.
Pour reconnaître les options, utilisez le getopts intégré

Michael Durrant
la source
0

Si vos options de saisie sont positionnelles (vous savez à quels endroits elles se trouvent), et non spécifiées avec des drapeaux, alors ce que vous voulez est simplement de construire la ligne de commande. Préparez simplement les arguments de commande pour chacun d'eux:

FLAG1="--flag1 $1"
FLAG2="--flag2 $2"
OPTFLAG1=""
OPTFLAG2=""
OPTFLAG3=""
if [ xFALSE != x"$3" ]; then
   OPTFLAG1="--optflag1 $3"
fi
if [ xFALSE != x"$4" ]; then
   OPTFLAG2="--optflag2 $4"
fi
#the same for other flags

#now just run the program
runprogram $FLAG1 $FLAG2 $OPTFLAG1 $OPTFLAG2 $OPTFLAG3

Si les paramètres ne sont pas spécifiés, les chaînes correspondantes sont vides et se développent en rien. Notez que dans la dernière ligne, il n'y a pas de guillemets. C'est parce que vous voulez que le shell divise les paramètres en mots (pour donner --flag1et $1comme arguments séparés à votre programme). Bien sûr, cela ira mal si vos paramètres d'origine contiennent des espaces. Si vous êtes celui qui exécute cela, vous pouvez le laisser, mais s'il s'agit d'un script général, il peut avoir un comportement inattendu si l'utilisateur entre quelque chose avec des espaces. Pour gérer cela, vous devrez rendre le code un peu plus laid.

Le xpréfixe du []test est là au cas où $3ou $4est vide. Dans ce cas, bash se développerait [ FALSE != $3 ]dans [ FALSE != ]ce qui est une erreur de syntaxe, donc un autre caractère arbitraire est là pour se prémunir contre cela. C'est un moyen très courant, vous le verrez dans de nombreux scripts.

Je les ai définis OPTFLAG1et les autres ""au début juste pour être sûr (au cas où ils auraient été définis sur quelque chose auparavant), mais s'ils n'étaient pas réellement déclarés dans l'environnement, alors vous n'avez pas strictement besoin de le faire.

Quelques remarques supplémentaires:

  • Vous pouvez en fait simplement recevoir les paramètres de la même manière que runprogram: avec des drapeaux. Voilà de quoi John Nparle. C'est là que getoptsdevient utile.
  • L'utilisation de FALSE à cette fin est un peu standard et assez longue. Habituellement, on utiliserait un seul caractère (éventuellement -) pour signaler une valeur vide, ou simplement passer une chaîne vide, comme "", si ce n'est pas autrement une valeur valide du paramètre. Une chaîne vide raccourcit également le test, il suffit de l'utiliser if [ -n "$3" ].
  • S'il n'y a qu'un seul indicateur facultatif, ou s'ils sont dépendants, vous ne pouvez donc jamais avoir OPTFLAG2, mais pas OPTFLAG1, alors vous pouvez simplement ignorer ceux que vous ne souhaitez pas définir. Si vous utilisez ""des paramètres vides, comme suggéré ci-dessus, vous pouvez de toute façon ignorer tous les vides de fin.

Cette méthode est à peu près la façon dont les options sont passées aux compilateurs dans Makefiles.

Et encore une fois - si les entrées peuvent contenir des espaces, cela devient moche.

orion
la source
Non, vous ne voulez pas que le shell divise les paramètres en mots. Vous devez toujours citer toutes les références aux variables shell, sauf si vous avez une bonne raison de ne pas le faire, et vous êtes sûr de savoir ce que vous faites. Voir Conséquences pour la sécurité de ne pas citer une variable dans les shells bash / POSIX , au cas où vous ne la connaissez pas, et faites défiler jusqu'à ma réponse , où j'aborde précisément ce problème (avec la même technique que Jasen utilise ).
G-Man dit `` Réintègre Monica ''