bash getopts, options courtes uniquement, toutes nécessitent des valeurs, propre validation

8

J'essaie de construire un script shell qui accepte diverses options et getoptssemble être une bonne solution car il peut gérer l'ordre des variables des options et des arguments (je pense!).

Je n'utiliserai que des options courtes et chaque option courte nécessitera une valeur correspondante, par exemple: ./command.sh -a arga -g argg -b argbmais je voudrais autoriser la saisie des options dans un ordre non spécifique, comme c'est la façon dont la plupart des gens sont habitués à travailler avec des commandes shell .

L'autre point est que je voudrais faire ma propre vérification des valeurs des arguments d'option, idéalement dans les caseinstructions. La raison en est que mes tests :)dans ma casedéclaration ont donné des résultats incohérents (probablement par manque de compréhension de ma part).
Par exemple:

#!/bin/bash
OPTIND=1 # Reset if getopts used previously
if (($# == 0)); then
        echo "Usage"
        exit 2
fi
while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                :)
                       echo "Option -$OPTARG requires an argument" >&2
                       exit 2;;
        esac
done
shift $((OPTIND-1))
echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Échouait pour des événements comme celui-ci ... ./command.sh -d -h
Quand je veux qu'il marque -d comme nécessitant un argument, mais j'obtiens la valeur -d=-hdont je n'ai pas besoin.

J'ai donc pensé qu'il serait plus facile d'exécuter ma propre validation dans les déclarations de cas pour garantir que chaque option n'est définie et définie qu'une seule fois.

J'essaie de faire ce qui suit mais mes if [ ! "$MYSQL_HOST" ]; thenblocs ne sont pas déclenchés.

OPTIND=1 # Reset if getopts used previously

if (($# == 0)); then
        echo "Usage"
        exit 2
fi

while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        if [ ! "$MYSQL_HOST" ]; then
                                echo "host not set"
                                exit 2
                        fi
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        if [ ! "$MYSQL_USER" ]; then
                                echo "username not set"
                                exit 2
                        fi
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        if [ ! "$MYSQL_PASS" ]; then
                                echo "password not set"
                                exit 2
                        fi
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        if [ ! "$BACKUP_DIR" ]; then
                                echo "backup dir not set"
                                exit 2
                        fi
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                #:)
                #       echo "Option -$opt requires an argument" >&2
                #       exit 2;;
        esac
done
shift $((OPTIND-1))

echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Y a-t-il une raison pour laquelle je ne peux pas vérifier si un OPTARGa une longueur nulle de l'intérieur getopts ... while ... case?

Quelle est la meilleure façon d'exécuter ma propre validation d'argument getoptsdans un cas où je ne veux pas compter sur le :). Effectuer ma validation d'argument en dehors du while ... case ... esac?
Ensuite, je pourrais me retrouver avec des valeurs d'argument de -detc et ne pas attraper une option manquante.

batfastad
la source

Réponses:

4

Lorsque vous appelez votre deuxième script (je l'ai enregistré sous getoptit) avec:

getoptit -d -h

Cela imprimera:

MYSQL_HOST=''  MYSQL_USER=''  MYSQL_PASS=''  BACKUP_DIR='-h' Additionals: 

Donc BACKUP_DIR est défini, et vous testez avec if [ ! "$BACKUP_DIR" ]; thens'il ne l'est pas, il est donc normal que le code à l'intérieur ne soit pas déclenché.

Si vous souhaitez tester si chaque option est définie une fois, vous devez le faire avant d'effectuer l'assignation à partir de la valeur $ OPTARG. Et vous devriez probablement aussi vérifier que $ OPTARG commence par un '-'(pour l' -d -herreur) avant d'assigner:

...
            d)
                    if [ ! -z "$BACKUP_DIR" ]; then
                            echo "backup dir already set"
                            exit 2
                    fi
                    if [ z"${OPTARG:0:1}" == "z-" ]; then
                            echo "backup dir starts with option string"
                            exit 2
                    fi
                    BACKUP_DIR=$OPTARG
                    ;;
...
Anthon
la source
Excellente explication. C'est parfaitement logique une fois que j'ai pensé au fait qu'il y avait en fait une whileboucle qui parcourait les options. C'est la méthode que j'ai choisie car bien qu'elle soit verbeuse par rapport aux autres, c'est très clair ce qui se passe dans le script. Et la maintenabilité par d'autres est importante dans ce cas.
batfastad
3

Étant donné que les autres réponses ne répondent pas vraiment à votre question: il s'agit d'un comportement attendu et basé sur la façon dont POSIX est interprété:

Lorsque l'option nécessite un argument-option, l'utilitaire getopts le place dans la variable shell OPTARG. Si aucune option n'a été trouvée, ou si l'option trouvée n'a pas d'argument option, OPTARG doit être désactivé.)

C'est tout ce qui est énoncé, et pour faire court (pas si long): getoptsne sait pas comme par magie que l'argument parfaitement valide qu'il voit pour l'option dont vous avez besoin pour avoir un argument n'est en fait pas un argument d'option mais une autre option . Rien ne vous empêche d'avoir -hcomme argument valide l' -doption, ce qui signifie en pratique que getoptsne lancera une erreur que si votre option qui nécessite un argument vient en dernier sur la ligne de commande, par exemple:

test.sh:

#!/bin/sh

while getopts ":xy:" o; do
    case "${o}" in
        :) echo "${OPTARG} requires an argument"; exit 1;
    esac
done

Exemple:

$ ./test.sh -y -x
$

$ ./test.sh -x -y
y requires an argument

La raison pour laquelle votre deuxième approche ne fonctionne pas est que l'analyse syntaxique getoptsest toujours la même, donc une fois que vous êtes dans la boucle, le "dommage" est déjà fait.

Si vous voulez absolument interdire cela, alors, comme l'a souligné Benubird, vous devrez vérifier vous-même vos arguments d'option et lancer une erreur s'ils équivalent à une option valide.

Adrian Frühwirth
la source
2

Je continuerais à utiliser, getoptsmais ferais quelques vérifications supplémentaires après: (non testé)

errs=0
declare -A option=(
    [MYSQL_HOST]="-h"
    [MYSQL_USER]="-u"
    [MYSQL_PASS]="-p"
    [BACKUP_DIR]="-d" 
)
for var in "${!option[@]}"; do
    if [[ -z "${!var}" ]]; then
        echo "error: specify a value for $var with ${option[var]}"
        ((errs++))
    fi
done
((errs > 0)) && exit 1

nécessite bash version 4

glenn jackman
la source
1

Le getopt intégré non shell de Gnu fait moins de travail pour vous mais permet une plus grande flexibilité à mesure que vous déterminez ce qui se passe après qu'un indicateur est trouvé, y compris en désactivant vous-même les arguments.

J'ai trouvé un article comparant getopts et getopt qui sera probablement utile car le manuel est un peu difficile à comprendre (au moins avant le café).

msw
la source
1

Tout d'abord, vous devez utiliser if [ -z "$MYSQL_USER" ].

Deuxièmement, la tâche n'est pas nécessaire - if [ -z "$OPTARG" ]fonctionnera bien.

Troisièmement, je soupçonne que vous voulez vraiment if [ ${#OPTARG} = 0 ]. $ {# x} est une chose bash qui retourne la longueur de la chaîne $ x (voir plus ici ).

Quatrièmement, si vous effectuez votre propre validation, je recommanderais getopt au lieu de getopts, car cela offre beaucoup plus de flexibilité.

Enfin, pour répondre à votre première question, vous pouvez détecter quand un indicateur est passé en argument en plaçant une liste d'indicateurs en haut, comme ceci:

args=( -h -u -p -d )

Et puis avoir une vérification dans l'option où vous voulez vérifier si l'argument fourni est une option, quelque chose comme ceci:

d)
    BACKUP_DIR=$OPTARG
    if [ "$(echo ${args[@]/$OPTARG/})" = "$(echo ${args[@]})" ]
    then
        echo "Argument is an option! Error!"
    fi

Pas une réponse parfaite, mais ça marche! Votre problème est que "-h" est un argument parfaitement valide, et vous ne donnez aucun moyen au shell de savoir qu'il ne recherche que des paramètres qui ne sont pas également des indicateurs valides.

Benubird
la source