Question Comment parcourir des arguments dans un script Bash


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

foo $1 args -o $1.ext

Je veux être capable de 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 avec des espaces en eux.


665
2017-11-01 18:14


origine


Réponses:


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

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

Cela va parcourir chaque argument et l'imprimer sur une ligne distincte. $ @ se comporte comme $ * sauf que lorsque les guillemets sont séparés, les arguments sont correctement séparés s'il y a des espaces:

sh test.sh 1 2 '3 4'
1
2
3 4

1116
2017-11-01 18:25



Réécriture d'un maintenant supprimé répondre par VonC.

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

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

Thèse de base  "$@" est correct, et $* (non cité) est presque toujours faux. Ceci est dû au fait "$@" fonctionne bien lorsque les arguments contiennent des espaces, et fonctionne de la même manière que $* quand ils ne le font pas. Dans certaines circonstances, "$*" est OK aussi, mais "$@" habituellement (mais pas toujours) travaille 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 de la coquille», mais ils font des choses différentes. Quand non cité, $* et $@ faire la même chose. Ils traitent chaque «mot» (séquence de non-espace) comme un argument séparé. Les formes citées sont assez différentes, cependant: "$*" traite la liste d'arguments comme une seule chaîne séparée par des espaces, "$@" traite les arguments presque exactement tels qu'ils étaient lorsqu'ils étaient spécifiés sur la ligne de commande. "$@" se développe à rien quand il n'y a pas d'arguments positionnels; "$*" se développe en une chaîne vide - et oui, il y a une différence, bien que cela puisse être difficile à percevoir. Voir plus d'informations ci-dessous, après l'introduction de la commande (non standard) al.

Thèse secondaire: si vous devez traiter les arguments avec des espaces, puis passez-les à d'autres commandes, alors vous avez parfois besoin de non-standard des outils pour aider. (Ou vous devriez utiliser des tableaux, avec soin: "${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 l'interpréteur de commandes traite les citations avant qu'elles ne se développent variables Ainsi, pour obtenir le shell de prêter attention aux citations intégrées dans $var, tu dois 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 difficile quand vous avez des noms de fichiers tels que "He said, "Don't do this!""(avec des guillemets et des guillemets 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 coquilles (tous) ne le rendent pas particulièrement facile à manipuler trucs, donc (curieusement) de nombreux programmes Unix ne font pas un bon travail de les manipuler. Sous Unix, un nom de fichier (composant unique) peut contenir n'importe quel caractère sauf slash et NUL '\0'. Cependant, les coques n'encouragent pas les espaces ou les sauts de ligne ou les onglets n'importe où dans les noms de chemins. 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 trouvé 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 de escape en cours d'utilisation - avec le système de contrôle SCCS. C'est un script de couverture qui fait à la fois delta (pense enregistrement) et un get (pense check-out). Divers arguments, en particulier -y (la raison pour laquelle vous avez fait le changement) contiendrait des espaces vides et des nouvelles lignes. Notez que le script date de 1992, donc il utilise des back-ticks au lieu de $(cmd ...) notation 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 si bien échapper ces jours-ci - il est pas nécessaire avec le -e argument, par exemple - mais dans l'ensemble, c'est un de mes scripts plus simples en utilisant escape.)

le escape programme sort simplement ses arguments, plutôt comme echo fait, mais il assure que les arguments sont protégés pour une utilisation avec eval (un niveau de eval; J'ai un programme qui a fait shell à distance l'exécution, et que nécessaire pour é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 énumère ses arguments un par ligne (et c'est encore plus ancien: version 1.1 datée du 1987-01-27T14: 35: 49). Il est plus utile lors du débogage de scripts, car il peut être branché dans un 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ée: Et maintenant, pour montrer la différence entre les différents "$@" 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 préserve 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 assez long répondre - peut-être exégèse est le meilleur terme. Code source pour escape disponible sur demande (email au prénom nom de famille à gmail point com). Le code source pour al est incroyablement simple:

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

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

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

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

Le conditionnel est nécessaire pour qu'il ne produise aucune sortie lorsqu'il n'est pas passé d'arguments. le printf La commande produira une ligne vide avec seulement l'argument de la chaîne de format, mais le programme C ne produit rien.


203
2017-11-01 23:04



Notez que la réponse de Robert est correcte, et cela fonctionne dans sh ainsi que. Vous pouvez (facilement) le simplifier encore plus:

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 Environnement de programmation Unix par 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 assumé.


103
2017-12-31 22:25



Pour les cas simples, vous pouvez également utiliser shift. Il traite la liste des arguments comme une file d'attente, chaque shift jette le premier argument, le nombre de chaque argument restant est décrémenté.

#this prints all arguments
while test $# -gt 0
do
    echo $1
    shift
done

50
2017-10-29 08:36



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

argc=$#
argv=($@)

for (( j=0; j<argc; j++ )); do
    echo ${argv[j]}
done

6
2017-10-30 00:58



aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"

3
2018-01-14 14:20