web-dev-qa-db-fra.com

Récupération de plusieurs arguments pour une seule option à l'aide de getopts dans Bash

J'ai besoin d'aide avec getopts.

J'ai créé un script Bash qui ressemble à ceci à l'exécution:

$ foo.sh -i env -d répertoire -s sous-répertoire fichier -f

Cela fonctionne correctement lorsque vous manipulez un argument de chaque drapeau. Mais lorsque j'appelle plusieurs arguments de chaque drapeau, je ne suis pas sûr de savoir comment extraire les informations de variable multiple des variables dans getopts.

while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=$OPTARG;;
        f ) files=$OPTARG;;

     esac
done

Après avoir saisi les options, je souhaite ensuite construire des structures de répertoires à partir des variables

foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3

Ensuite, la structure de répertoire serait

/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3

Des idées?

39
vegasbrianc

Vous pouvez utiliser plusieurs fois la même option et ajouter toutes les valeurs à un tableau .

Pour la question originale très spécifique ici, la solution mkdir -p de Ryan est évidemment la meilleure.

Cependant, pour la question plus générale de obtenir plusieurs valeurs de la même option avec getopts , la voici:

#!/bin/bash

while getopts "m:" opt; do
    case $opt in
        m) multi+=("$OPTARG");;
        #...
    esac
done
shift $((OPTIND -1))

echo "The first value of the array 'multi' is '$multi'"
echo "The whole list of values is '${multi[@]}'"

echo "Or:"

for val in "${multi[@]}"; do
    echo " - $val"
done

La sortie serait:

$ /tmp/t
The first value of the array 'multi' is ''
The whole list of values is ''
Or:

$ /tmp/t -m "one arg with spaces"
The first value of the array 'multi' is 'one arg with spaces'
The whole list of values is 'one arg with spaces'
Or:
 - one arg with spaces

$ /tmp/t -m one -m "second argument" -m three
The first value of the array 'multi' is 'one'
The whole list of values is 'one second argument three'
Or:
 - one
 - second argument
 - three
60
mivk

Je sais que cette question est ancienne, mais je voulais jeter cette réponse ici au cas où quelqu'un viendrait chercher une réponse.

Des shells comme BASH prennent déjà en charge la création de répertoires de manière récursive, un script n’est donc pas vraiment nécessaire. Par exemple, l'affiche originale voulait quelque chose comme:

$ foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3
/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3

Cela se fait facilement avec cette ligne de commande:

pong:~/tmp
[10] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/{file1,file2,file3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

Ou même un peu plus court:

pong:~/tmp
[12] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

Ou plus court, avec plus de conformité:

pong:~/tmp
[14] rmclean$ mkdir -pv test/directory/subdirectory{1,2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

Ou enfin, en utilisant des séquences:

pong:~/tmp
[16] rmclean$ mkdir -pv test/directory/subdirectory{1..2}/file{1..3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’
21
Ryan

les options de getopts ne peuvent prendre que zéro ou un argument. Vous voudrez peut-être modifier votre interface pour supprimer l'option -f, et simplement parcourir les arguments non-option restants

usage: foo.sh -i end -d dir -s subdir file [...]

Alors,

while getopts ":i:d:s:" opt; do
  case "$opt" in
    i) initial=$OPTARG ;;
    d) dir=$OPTARG ;;
    s) sub=$OPTARG ;;
  esac
done
shift $(( OPTIND - 1 ))

path="/$initial/$dir/$sub"
mkdir -p "$path"

for file in "$@"; do
  touch "$path/$file"
done
18
glenn jackman

J'ai corrigé le même problème que vous aviez comme ceci:

Au lieu de:

foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3

Faire ceci:

foo.sh -i test -d directory -s "subdirectory subdirectory2" -f "file1 file2 file3"

Avec le séparateur d'espace, vous pouvez simplement le parcourir avec une boucle de base .

while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=$OPTARG;;
        f ) files=$OPTARG;;

     esac
done

for subdir in $sub;do
   for file in $files;do
      echo $subdir/$file
   done   
done

Voici un exemple de sortie:

$ ./getopts.sh -s "testdir1 testdir2" -f "file1 file2 file3"
testdir1/file1
testdir1/file2
testdir1/file3
testdir2/file1
testdir2/file2
testdir2/file3
8
David Berkan

Il existe en fait un moyen de récupérer plusieurs arguments en utilisant getopts, mais cela nécessite un piratage manuel avec la variable getopts 'OPTIND.

Voir le script suivant (reproduit ci-dessous): https://Gist.github.com/achalddave/290f7fcad89a0d7c3719 . Il y a probablement un moyen plus facile, mais c'est le moyen le plus rapide que j'ai pu trouver.

#!/bin/sh

usage() {
cat << EOF
$0 -a <a1> <a2> <a3> [-b] <b1> [-c]
    -a      First flag; takes in 3 arguments
    -b      Second flag; takes in 1 argument
    -c      Third flag; takes in no arguments
EOF
}

is_flag() {
    # Check if $1 is a flag; e.g. "-b"
    [[ "$1" =~ -.* ]] && return 0 || return 1
}

# Note:
# For a, we fool getopts into thinking a doesn't take in an argument
# For b, we can just use getopts normal behavior to take in an argument
while getopts "ab:c" opt ; do
    case "${opt}" in
        a)
            # This is the tricky part.

            # $OPTIND has the index of the _next_ parameter; so "\${$((OPTIND))}"
            # will give us, e.g., ${2}. Use eval to get the value in ${2}.
            # The {} are needed in general for the possible case of multiple digits.

            eval "a1=\${$((OPTIND))}"
            eval "a2=\${$((OPTIND+1))}"
            eval "a3=\${$((OPTIND+2))}"

            # Note: We need to check that we're still in bounds, and that
            # a1,a2,a3 aren't flags. e.g.
            #   ./getopts-multiple.sh -a 1 2 -b
            # should error, and not set a3 to be -b.
            if [ $((OPTIND+2)) -gt $# ] || is_flag "$a1" || is_flag "$a2" || is_flag "$a3"
            then
                usage
                echo
                echo "-a requires 3 arguments!"
                exit
            fi

            echo "-a has arguments $a1, $a2, $a3"

            # "shift" getopts' index
            OPTIND=$((OPTIND+3))
            ;;
        b)
            # Can get the argument from getopts directly
            echo "-b has argument $OPTARG"
            ;;
        c)
            # No arguments, life goes on
            echo "-c"
            ;;
    esac
done
4
Achal Dave

Si vous souhaitez spécifier un nombre quelconque de valeurs pour une option, vous pouvez utiliser une simple boucle pour les rechercher et les insérer dans un tableau. Par exemple, modifions l'exemple de l'OP pour autoriser un nombre quelconque de paramètres -s:

unset -v sub
while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=("$OPTARG")
            until [[ $(eval "echo \${$OPTIND}") =~ ^-.* ]] || [ -z $(eval "echo \${$OPTIND}") ]; do
                sub+=($(eval "echo \${$OPTIND}"))
                OPTIND=$((OPTIND + 1))
            done
            ;;
        f ) files=$OPTARG;;
     esac
done

Ceci prend le premier argument ($ OPTARG) et le place dans le tableau $ sub. Ensuite, il continuera à chercher parmi les paramètres restants jusqu'à ce qu'il rencontre un autre paramètre en pointillé OR. Il n'y a plus d'arguments à évaluer. S'il trouve plus de paramètres qui ne sont pas des paramètres en pointillés, il les ajoute au tableau $ sub et amplifie la variable $ OPTIND.

Ainsi, dans l'exemple du PO, on pourrait exécuter:

foo.sh -i test -d directory -s subdirectory1 subdirectory2 -f file1

Si nous avons ajouté ces lignes au script pour démontrer:

echo ${sub[@]}
echo ${sub[1]}
echo $files

La sortie serait:

subdirectory1 subdirectory2
subdirectory2
file1
3
KoZm0kNoT

La question initiale traitait de getopts, mais il existe une autre solution offrant des fonctionnalités plus souples sans getopts (c'est peut-être un peu plus détaillé, mais fournit une interface de ligne de commande beaucoup plus flexible). Voici un exemple:

while [[ $# > 0 ]]
do
    key="$1"
    case $key in
        -f|--foo)
            nextArg="$2"
            while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
                case $nextArg in
                    bar)
                        echo "--foo bar found!"
                    ;;
                    baz)
                        echo "--foo baz found!"
                    ;;
                    *)
                        echo "$key $nextArg found!"
                    ;;
                esac
                if ! [[ "$2" =~ -.* ]]; then
                    shift
                    nextArg="$2"
                else
                    shift
                    break
                fi
            done
        ;;
        -b|--bar)
            nextArg="$2"
            while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
                case $nextArg in
                    foo)
                        echo "--bar foo found!"
                    ;;
                    baz)
                        echo "--bar baz found!"
                    ;;
                    *)
                        echo "$key $nextArg found!"
                    ;;
                esac
                if ! [[ "$2" =~ -.* ]]; then
                    shift
                    nextArg="$2"
                else
                    shift
                    break
                fi
            done
        ;;
        -z|--baz)
            nextArg="$2"
            while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do

                echo "Doing some random task with $key $nextArg"

                if ! [[ "$2" =~ -.* ]]; then
                    shift
                    nextArg="$2"
                else
                    shift
                    break
                fi
            done
        ;;
        *)
            echo "Unknown flag $key"
        ;;
    esac
    shift
done

Dans cet exemple, nous parcourons toutes les options de la ligne de commande en recherchant les paramètres qui correspondent à nos indicateurs de ligne de commande acceptés (tels que -f ou --foo). Une fois que nous avons trouvé un indicateur, nous parcourons chaque paramètre jusqu'à ce que nous manquions de paramètres ou que nous rencontrions un autre indicateur. Cela nous ramène dans notre boucle externe qui ne traite que les drapeaux. 

Avec cette configuration, les commandes suivantes sont équivalentes:

script -f foo bar baz
script -f foo -f bar -f baz
script --foo foo -f bar baz
script --foo foo bar -f baz

Vous pouvez également analyser des ensembles de paramètres incroyablement désorganisés tels que:

script -f baz derp --baz herp -z derp -b foo --foo bar -q llama --bar fight

Pour obtenir le résultat:

--foo baz found!
-f derp found!
Doing some random task with --baz herp
Doing some random task with -z derp
--bar foo found!
--foo bar found!
Unknown flag -q
Unknown flag llama
--bar fight found!
2
Brendon Dugan
#!/bin/bash
myname=$(basename "$0")

# help function
help () { cat <<EOP
   $myname: -c cluster [...] -a action [...] -i instance [...]
EOP
}

# parse sub options
get_opts () {
  rs='' && rc=0 # return string and return code
  while [[ $# -gt 0 ]]; do
    shift
    [[ "$1" =~ -.* ]] && break ||  rs="$rs $1" && rc=$((rc + 1))
  done
  echo "$rs"
}

#parse entire command-line
while [[ $# -gt 0 ]]; do
    case $1 in
        "-a") ACTS="$(get_opts $@)"
           ;;
        "-i") INSTS=$(get_opts $@)
           ;;
        "-c") CLUSTERS=$(get_opts $@)
           ;;
        "-h") help
           ;;
        ?) echo "sorry, I dont do $1"
           exit
           ;;
    esac
    shift
done
0
Neil Verkland

Comme vous ne montrez pas comment vous espérez construire votre liste

/test/directory/subdirectory/file1
. . .
test/directory/subdirectory2/file3

il est un peu difficile de savoir comment procéder, mais vous devez fondamentalement continuer à ajouter de nouvelles valeurs à la variable appropriée, c.-à-d.

 case $opt in
    d ) dirList="${dirList} $OPTARG" ;;
 esac

Notez que lors du premier passage, le répertoire sera vide et vous obtiendrez un espace devant le début de votre valeur finale pour ${dirList}. (Si vous avez vraiment besoin de code qui n'inclut pas d'espaces supplémentaires, recto ou verso, il y a une commande que je peux vous montrer, mais ce sera difficile à comprendre et il ne semble pas que vous en ayez besoin ici, mais faites le moi savoir)

Vous pouvez ensuite envelopper les variables de votre liste dans les boucles for pour émettre toutes les valeurs, c.-à-d.

for dir in ${dirList} do
   for f in ${fileList} ; do
      echo $dir/$f
   done
done

Enfin, il est considéré comme une bonne pratique de «piéger» toute entrée inconnue dans votre déclaration de cas, c.-à-d.

 case $opt in
    i ) initial=$OPTARG;;
    d ) dir=$OPTARG;;
    s ) sub=$OPTARG;;
    f ) files=$OPTARG;;
    * ) 
       printf "unknown flag supplied "${OPTARG}\nUsageMessageGoesHere\n"
       exit 1
    ;;

 esac 

J'espère que ça aide.

0
shellter