web-dev-qa-db-fra.com

Comment parcourir des arguments dans un script Bash

J'ai une commande complexe que je voudrais faire un script Shell/bash de. Je peux l'écrire en termes de $1 facilement:

foo $1 args -o $1.ext

Je veux pouvoir transmettre plusieurs noms d'entrée au script. Quelle est la bonne façon de le faire?

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

813
Thelema

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

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

Cela va parcourir chaque argument et l’imprimer sur une ligne séparée. $ @ se comporte comme $ * sauf que, lorsqu'ils sont cités, les arguments sont correctement divisés s'il y a des espaces:

sh test.sh 1 2 '3 4'
1
2
3 4
1332
Robert Gamble

Réécriture d'un réponse maintenant supprimé ( 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 correct et $* (sans guillemets) est presque toujours faux. Ceci est dû au fait que "$@" fonctionne correctement lorsque les arguments contiennent des espaces et fonctionne de la même manière que $* quand ils ne le sont pas. Dans certaines circonstances, "$*" est également valide, mais "$@" fonctionne généralement (mais pas toujours) aux mêmes endroits. Non cité, $@ 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és, $* et $@ font la même chose. Ils traitent chaque "mot" (séquence de non-espaces) comme un argument séparé. Les formes citées sont assez différentes, cependant: "$*" traite la liste des arguments comme une seule chaîne séparée par des espaces, alors que "$@" traite les arguments presque exactement tels qu'ils étaient spécifiés sur la ligne de commande. "$@" ne développe rien du tout lorsqu'il n'y a pas d'argument de position; "$*" 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 aurez parfois besoin d'outils non standard pour vous aider. (Ou vous devez utiliser les 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 les processus Shell citent avant de développer les variables. Donc, pour que le shell prête attention aux guillemets incorporés dans $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 guillemets, guillemets doubles et 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 interpréteurs de commandes (tous) ne facilitent pas particulièrement l'utilisation de ce type de matériel, aussi (curieusement) de nombreux programmes Unix ne les traitent pas bien. Sous Unix, un nom de fichier (composant unique) peut contenir des caractères autres que slash et NUL '\0'. Cependant, les shells n'encouragent fortement aucun espace, aucune nouvelle ligne ni aucun onglet dans les noms de chemins. C'est aussi pourquoi les noms de fichiers Unix standard ne contiennent pas d'espaces, etc.

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

Voici un exemple de escape utilisé - avec le système de contrôle SCCS. C’est un script de couverture qui fait à la fois une delta (pensezcheck-in) et un get (pensezcheck-out). Divers arguments, en particulier -y (la raison pour laquelle vous avez apporté la modification) contiendraient des blancs et des nouvelles lignes. Notez que le script date de 1992. Il utilise donc des rétrogradations au lieu de la notation $(cmd ...) et n'utilise pas #!/bin/sh sur 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 autant d'échappement si minutieusement ces jours-ci - ce n'est pas nécessaire avec l'argument -e, par exemple - mais globalement, c'est l'un de mes scripts les plus simples qui utilise escape.)

Le programme escape affiche simplement ses arguments, un peu comme echo, mais il garantit que les arguments sont protégés en vue d'une utilisation avec eval (un niveau de eval; j'ai un programme qui exécutait 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é al qui liste ses arguments un par ligne (et il est encore plus ancien: version 1.1 du 1987-01-27T14: 35: 49). C'est très utile lors du débogage de scripts, car il peut être branché sur une ligne de commande pour voir quels arguments sont réellement transmis à la commande.

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

[Added: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 espaces d'origine entre * et */* sur la ligne de commande. Notez également que vous pouvez modifier les "arguments de ligne de commande" dans le shell à l'aide de:

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

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

Hum, c'est un peu longréponse- peut-êtreexégèseest le meilleur terme. Le code source de escape est disponible sur demande (courrier électronique au prénom nom point nom au gmail point com). Le code source de al est 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 script test.sh que Robert Gamble a montré et peut être écrit en tant que fonction Shell (mais les fonctions Shell n'existaient pas dans la version locale de Bourne Shell lorsque j'ai écrit al pour la première fois.).

Notez également que vous pouvez écrire al sous la forme d'un simple script Shell:

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

La condition est nécessaire pour ne produire aucune sortie quand aucun argument n'est passé. La commande printf générera une ligne vide avec uniquement l'argument de chaîne de formatage, mais le programme C ne produira rien.

223
Jonathan Leffler

Notez que la réponse de Robert est correcte et que cela fonctionne également dans sh. Vous pouvez (de manière portable) le simplifier encore davantage:

for i in "$@"

est équivalent à:

for i

C'est-à-dire que 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 nix Programming Environment par Kernighan et Pike.

Dans bash, help for documente ceci:

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

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

121
Alok Singhal

Pour les cas simples, vous pouvez également utiliser shift. Il traite la liste d'arguments comme une file d'attente. Chaque shift jette 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
54
nuoritoveri

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

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
14
baz
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
3
g24l