Comment parcourir les arguments dans un script Bash

901

J'ai une commande complexe dont j'aimerais faire un script shell / bash. Je peux l'écrire en termes de $1facilement:

foo $1 args -o $1.ext

Je veux pouvoir passer plusieurs noms d'entrée au script. Quelle est la bonne façon de procéder?

Et, bien sûr, je veux gérer les noms de fichiers avec des espaces.

Thelema
la source
67
Pour info, c'est une bonne idée de toujours mettre des paramètres entre guillemets. Vous ne savez jamais quand un paramètre peut contenir un espace intégré.
David R Tribble

Réponses:

1483

Utilisez "$@"pour représenter tous les arguments:

for var in "$@"
do
    echo "$var"
done

Cela itérera sur chaque argument et l'imprimera sur une ligne distincte. $ @ se comporte comme $ * sauf que lorsqu'ils sont cités, les arguments sont décomposés correctement s'il y a des espaces:

sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble
la source
38
Soit dit en passant, l'un des autres mérites de "$@"est qu'il ne se développe à rien lorsqu'il n'y a pas de paramètres de position alors qu'il se "$*"développe à la chaîne vide - et oui, il y a une différence entre aucun argument et un argument vide. Voir ${1:+"$@"} in / bin / sh` .
Jonathan Leffler
9
Notez que cette notation doit également être utilisée dans les fonctions shell pour accéder à tous les arguments de la fonction.
Jonathan Leffler
En supprimant les guillemets doubles: $varj'ai pu exécuter les arguments.
eonist
comment commencer à partir du deuxième argument? Je veux dire, j'ai besoin de cela mais commencez toujours par le deuxième argument, c'est-à-dire, sautez $ 1.
m4l490n
4
@ m4l490n: shiftjette $1et décale tous les éléments suivants.
MSalters
239

Réécriture d'une réponse maintenant supprimée par VonC .

La réponse succincte de Robert Gamble traite directement de la question. Celui-ci amplifie certains problèmes avec les noms de fichiers contenant des espaces.

Voir aussi: $ {1: + "$ @"} dans / bin / sh

Thèse de base: "$@" est correcte et $*(non citée) est presque toujours fausse. En effet, "$@"fonctionne correctement lorsque les arguments contiennent des espaces et fonctionne de la même manière que $*lorsqu'ils n'en contiennent pas. Dans certaines circonstances, "$*"est OK aussi, mais "$@"généralement (mais pas toujours) fonctionne aux mêmes endroits. Sans guillemets, $@et $*sont équivalents (et presque toujours faux).

Alors, quelle est la différence entre $*, $@, "$*"et "$@"? Ils sont tous liés à «tous les arguments du shell», mais ils font des choses différentes. Lorsque non cité, $*et $@faites la même chose. Ils traitent chaque «mot» (séquence non blanche) comme un argument distinct. Les formes entre guillemets sont cependant assez différentes: "$*"traite la liste d'arguments comme une chaîne unique séparée par des espaces, tandis que "$@"traite les arguments presque exactement comme ils étaient lorsqu'ils étaient spécifiés sur la ligne de commande. "$@"se développe à rien du tout lorsqu'il n'y a pas d'arguments positionnels; "$*"se développe en une chaîne vide - et oui, il y a une différence, même s'il peut être difficile de la percevoir. Voir plus d'informations ci-dessous, après l'introduction de la commande (non standard) al.

Thèse secondaire: si vous devez traiter des arguments avec des espaces, puis les transmettre à d'autres commandes, vous avez parfois besoin d'outils non standard pour vous aider. (Ou vous devez utiliser des tableaux, avec précaution: "${array[@]}"se comporte de manière analogue à "$@".)

Exemple:

    $ mkdir "my dir" anotherdir
    $ ls
    anotherdir      my dir
    $ cp /dev/null "my dir/my file"
    $ cp /dev/null "anotherdir/myfile"
    $ ls -Fltr
    total 0
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir/
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir/
    $ ls -Fltr *
    my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ ls -Fltr "./my dir" "./anotherdir"
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ var='"./my dir" "./anotherdir"' && echo $var
    "./my dir" "./anotherdir"
    $ ls -Fltr $var
    ls: "./anotherdir": No such file or directory
    ls: "./my: No such file or directory
    ls: dir": No such file or directory
    $

Pourquoi ça ne marche pas? Cela ne fonctionne pas car le shell traite les guillemets avant d'étendre les variables. Donc, pour que le shell prête attention aux citations intégrées $var, vous devez utiliser eval:

    $ eval ls -Fltr $var
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ 

Cela devient vraiment délicat lorsque vous avez des noms de fichiers tels que " He said, "Don't do this!"" (avec des guillemets, des guillemets doubles et des espaces).

    $ cp /dev/null "He said, \"Don't do this!\""
    $ ls
    He said, "Don't do this!"       anotherdir                      my dir
    $ ls -l
    total 0
    -rw-r--r--   1 jleffler  staff    0 Nov  1 15:54 He said, "Don't do this!"
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir
    $ 

Les shells (tous) ne facilitent pas particulièrement la gestion de ces choses, donc (assez drôle) de nombreux programmes Unix ne font pas un bon travail pour les gérer. Sous Unix, un nom de fichier (composant unique) peut contenir n'importe quel caractère sauf la barre oblique et NUL '\0'. Cependant, les shells n'encouragent pas fortement les espaces, les sauts de ligne ou les tabulations n'importe où dans les noms de chemin. C'est aussi pourquoi les noms de fichiers Unix standard ne contiennent pas d'espaces, etc.

Lorsque vous traitez des noms de fichiers qui peuvent contenir des espaces et d'autres caractères gênants, vous devez être extrêmement prudent, et j'ai découvert il y a longtemps que j'avais besoin d'un programme qui n'est pas standard sur Unix. Je l'appelle escape(la version 1.1 était datée du 1989-08-23T16: 01: 45Z).

Voici un exemple d' escapeutilisation - avec le système de contrôle SCCS. C'est un script de couverture qui fait à la fois un delta(pensez à l' enregistrement ) et un get(pensez à un check-out ). Divers arguments, en particulier -y(la raison pour laquelle vous avez effectué la modification), contiendraient des espaces et des retours à la ligne. Notez que le script date de 1992, donc il utilise des back-ticks au lieu de la $(cmd ...)notation et ne l'utilise pas #!/bin/shsur la première ligne.

:   "@(#)$Id: delget.sh,v 1.8 1992/12/29 10:46:21 jl Exp $"
#
#   Delta and get files
#   Uses escape to allow for all weird combinations of quotes in arguments

case `basename $0 .sh` in
deledit)    eflag="-e";;
esac

sflag="-s"
for arg in "$@"
do
    case "$arg" in
    -r*)    gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    -e)     gargs="$gargs `escape \"$arg\"`"
            sflag=""
            eflag=""
            ;;
    -*)     dargs="$dargs `escape \"$arg\"`"
            ;;
    *)      gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    esac
done

eval delta "$dargs" && eval get $eflag $sflag "$gargs"

(Je n'utiliserais probablement pas aussi complètement escape de nos jours - ce n'est pas nécessaire avec l' -eargument, par exemple - mais dans l'ensemble, c'est l'un de mes scripts les plus simples à utiliser escape.)

Le escapeprogramme sort simplement ses arguments, un peu comme le echo fait, mais il garantit que les arguments sont protégés pour une utilisation avec eval(un niveau de eval; J'ai un programme qui a fait une exécution à distance du shell, et qui devait échapper à la sortie de escape).

    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape "$var"
    '"./my dir" "./anotherdir"'
    $ escape x y z
    x y z
    $ 

J'ai un autre programme appelé alqui répertorie ses arguments un par ligne (et il est encore plus ancien: version 1.1 datée du 1987-01-27T14: 35: 49). Il est très utile lors du débogage de scripts, car il peut être connecté à une ligne de commande pour voir quels arguments sont réellement passés à la commande.

    $ echo "$var"
    "./my dir" "./anotherdir"
    $ al $var
    "./my
    dir"
    "./anotherdir"
    $ al "$var"
    "./my dir" "./anotherdir"
    $

[ Ajouté: Et maintenant pour montrer la différence entre les différentes "$@"notations, voici un autre exemple:

$ cat xx.sh
set -x
al $@
al $*
al "$*"
al "$@"
$ sh xx.sh     *      */*
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al 'He said, "Don'\''t do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file'
He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file
+ al 'He said, "Don'\''t do this!"' anotherdir 'my dir' xx.sh anotherdir/myfile 'my dir/my file'
He said, "Don't do this!"
anotherdir
my dir
xx.sh
anotherdir/myfile
my dir/my file
$

Notez que rien ne conserve les blancs d'origine entre le *et */*sur la ligne de commande. Notez également que vous pouvez modifier les «arguments de ligne de commande» dans le shell en utilisant:

set -- -new -opt and "arg with space"

Cela définit 4 options, ' -new', ' -opt', ' and' et ' arg with space'.
]

Hmm, c'est une réponse assez longue - peut-être que l' exégèse est le meilleur terme. Code source escapedisponible sur demande (e-mail au prénom dot nom de famille sur gmail dot com). Le code source de alest incroyablement simple:

#include <stdio.h>
int main(int argc, char **argv)
{
    while (*++argv != 0)
        puts(*argv);
    return(0);
}

C'est tout. Il est équivalent au test.shscript que Robert Gamble a montré, et pourrait être écrit comme une fonction shell (mais les fonctions shell n'existaient pas dans la version locale du Bourne shell lorsque j'ai écrit pour la première fois al).

Notez également que vous pouvez écrire alcomme un simple script shell:

[ $# != 0 ] && printf "%s\n" "$@"

Le conditionnel est nécessaire pour qu'il ne produise aucune sortie lorsqu'il ne reçoit aucun argument. La printfcommande produira une ligne vierge avec uniquement l'argument de chaîne de format, mais le programme C ne produit rien.

Jonathan Leffler
la source
132

Notez que la réponse de Robert est correcte et qu'elle fonctionne shégalement. Vous pouvez (de manière portative) le simplifier encore plus:

for i in "$@"

est équivalent à:

for i

C'est à dire, vous n'avez besoin de rien!

Test ( $est l'invite de commande):

$ set a b "spaces here" d
$ for i; do echo "$i"; done
a
b
spaces here
d
$ for i in "$@"; do echo "$i"; done
a
b
spaces here
d

J'ai d'abord lu à ce sujet dans l' environnement de programmation Unix de Kernighan et Pike.

Dans bash, help fordocumente ceci:

for NAME [in WORDS ... ;] do COMMANDS; done

Si 'in WORDS ...;'n'est pas présent, alors 'in "$@"'est supposé.

Alok Singhal
la source
4
Je ne suis pas d'accord. Il faut savoir ce que signifie le cryptique "$@", et une fois que vous savez ce qu'il for isignifie, il n'est pas moins lisible que for i in "$@".
Alok Singhal
10
Je n'ai pas affirmé que for ic'était mieux car cela permet d'économiser les frappes. J'ai comparé la lisibilité de for iet for i in "$@".
Alok Singhal
c'est ce que je cherchais - comment appelle-t-on cette hypothèse de $ @ dans les boucles où vous n'avez pas à la référencer explicitement? Y a-t-il un inconvénient à ne pas utiliser $ @ reference?
qodeninja
58

Pour les cas simples, vous pouvez également utiliser shift. Il traite la liste d'arguments comme une file d'attente. Chacun shiftjette le premier argument et l'index de chacun des arguments restants est décrémenté.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
la source
Je pense que cette réponse est meilleure parce que si vous le faites shiftdans une boucle for, cet élément fait toujours partie du tableau, alors qu'avec cela shiftfonctionne comme prévu.
DavidG
2
Notez que vous devez utiliser echo "$1"pour conserver l'espacement dans la valeur de la chaîne d'argument. En outre, (un problème mineur), cela est destructeur: vous ne pouvez pas réutiliser les arguments si vous les avez tous déplacés. Souvent, ce n'est pas un problème - vous ne traitez la liste d'arguments qu'une seule fois de toute façon.
Jonathan Leffler
1
Convenez que le décalage est meilleur, car alors vous avez un moyen facile de saisir deux arguments dans un cas de commutateur pour traiter les arguments d'indicateur comme "-o fichier de sortie".
Baxissimo
D'un autre côté, si vous commencez à analyser des arguments de drapeau, vous voudrez peut-être envisager un langage de script plus puissant que bash :)
nuoritoveri
4
Cette solution est meilleure si vous avez besoin d'une paire valeur-paramètre, par exemple --file myfile.txt Donc, $ 1 est le paramètre, $ 2 est la valeur et vous appelez shift deux fois lorsque vous devez ignorer un autre argument
Daniele Licitra
16

Vous pouvez également y accéder en tant qu'éléments de tableau, par exemple si vous ne voulez pas les parcourir tous

argc=$#
argv=("$@")

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
baz
la source
5
Je crois que la ligne argv = ($ @) devrait être argv = ("$ @"), d'autres arguments judicieux avec des espaces ne sont pas traités correctement
kdubs
Je confirme que la syntaxe correcte est argv = ("$ @"). sauf si vous n'obtiendrez pas les dernières valeurs si vous avez des paramètres entre guillemets. Par exemple, si vous utilisez quelque chose comme ./my-script.sh param1 "param2 is a quoted param": - en utilisant argv = ($ @), vous obtiendrez [param1, param2], - en utilisant argv = ("$ @"), vous obtiendrez [param1, param2 est un
paramètre
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
la source
1
Comme indiqué ci-dessous, [ $# != 0 ]c'est mieux. Dans ce qui précède, #$devrait être $#comme danswhile [[ $# > 0 ]] ...
hute37
1

En amplifiant la réponse de baz, si vous devez énumérer la liste d'arguments avec un index (comme pour rechercher un mot spécifique), vous pouvez le faire sans copier la liste ni la muter.

Supposons que vous souhaitiez fractionner une liste d'arguments à un double tiret ("-") et passer les arguments avant les tirets à une commande et les arguments après les tirets à une autre:

 toolwrapper() {
   for i in $(seq 1 $#); do
     [[ "${!i}" == "--" ]] && break
   done || return $? # returns error status if we don't "break"

   echo "dashes at $i"
   echo "Before dashes: ${@:1:i-1}"
   echo "After dashes: ${@:i+1:$#}"
 }

Les résultats devraient ressembler à ceci:

 $ toolwrapper args for first tool -- and these are for the second
 dashes at 5
 Before dashes: args for first tool
 After dashes: and these are for the second
Rich Kadel
la source
1

getopt Utilisez la commande dans vos scripts pour formater les options ou paramètres de ligne de commande.

#!/bin/bash
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
JimmyLandStudios
la source
Délicat, je ferais des recherches approfondies sur cela. Exemple: fichier: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Manual: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios