J'ai une commande complexe dont j'aimerais faire un script shell/bash. Je peux l'écrire facilement en termes de $1
:
foo $1 args -o $1.ext
Je veux pouvoir passer 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.
Utilisez "$@"
pour représenter tous les arguments :
for var in "$@"
do
echo "$var"
done
Ceci va itérer sur chaque argument et l'imprimer sur une ligne séparée. $@ se comporte comme $* sauf que lorsqu'il est cité, les arguments sont séparés correctement s'ils contiennent des espaces :
sh test.sh 1 2 '3 4'
1
2
3 4
Réécriture d'une [réponse] (https://stackoverflow.com/a/255939/) maintenant supprimée par [VonC] (https://stackoverflow.com/users/6309/vonc).
La réponse succincte de Robert Gamble traite directement de la question.
Celle-ci développe certains problèmes liés aux noms de fichiers contenant des espaces.
Voir aussi : [${1:+"$@"} dans /bin/sh][1]
Thèse de base: "$@"
est correct, et $*
(non cité) est presque toujours faux.
En effet, "$@"
fonctionne bien lorsque les arguments contiennent des espaces, et
fonctionne de la même manière que $*
quand ils n'en contiennent pas.
Dans certaines circonstances, "$*"
est également correct, mais "$@"
fonctionne généralement (mais pas toujours) aux mêmes endroits.
toujours) fonctionne aux mêmes endroits.
Sans citation, $@
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. Lorsqu'ils ne sont pas cités, $*
et $@
font la même chose. Ils traitent chaque 'mot' ; (séquence de caractères non blancs) comme un argument séparé. Les formes citées sont cependant très différentes : "$*"
traite la liste des arguments comme une seule chaîne de caractères séparée par des espaces, alors que "$@"
traite les arguments presque exactement comme ils étaient spécifiés sur la ligne de commande.
"$@"
se développe en rien du tout quand il n'y a pas d'arguments positionnels ; "$*"
se développe en une chaîne vide &mdash ; et oui, il y a une différence, bien qu'il puisse être difficile de la percevoir.
Voir plus d'informations ci-dessous, après l'introduction de la commande (non standard) al
.
Méthode secondaire: si vous avez besoin de traiter des arguments avec des espaces et ensuite de les transmettre à d'autres commandes, il est préférable d'utiliser la commande al'. les transmettre à d'autres commandes, alors vous avez parfois besoin d'outils non standard pour vous aider. (Ou vous devriez 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 cela ne fonctionne-t-il pas ?
Cela ne fonctionne pas parce que le shell traite les guillemets avant d'étendre les variables.
les variables.
Donc, pour que l'interpréteur de commandes fasse 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 des guillemets, des doubles 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 shells (tous) ne facilitent pas particulièrement la gestion de ce genre de choses.
ce genre de choses, et donc (assez curieusement) de nombreux programmes Unix ne font pas un bon travail de
de les gérer.
Sous Unix, un nom de fichier (composant unique) peut contenir tous les caractères sauf
slash et NUL '\0'
.
Cependant, les shells encouragent fortement l'absence d'espace, de saut de ligne ou de tabulation
nulle part dans un nom de chemin.
C'est également la raison pour laquelle les noms de fichiers Unix standard ne contiennent pas d'espaces, etc.
Lorsque l'on traite des noms de fichiers qui peuvent contenir des espaces et d'autres
et d'autres caractères gênants, il faut être extrêmement prudent, et j'ai trouvé
j'ai découvert il y a longtemps que j'avais besoin d'un programme qui n'est pas standard sous Unix.
Je l'appelle escape
(la version 1.1 date de 1989-08-23T16:01:45Z).
Voici un exemple d'utilisation de escape
- avec le système de contrôle SCCS.
C'est un script de couverture qui fait à la fois un delta
(pensez check-in) et un get
(pensez check-in).
get
(pensez check-out).
Plusieurs arguments, en particulier -y
(la raison pour laquelle vous avez fait le changement)
contiennent des blancs et des retours à la ligne.
Notez que le script date de 1992, et qu'il utilise donc des back-ticks au lieu de
$(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 l'échappement de manière aussi approfondie de nos jours - il n'est pas nécessaire avec l'option -e'). pas nécessaire avec l'argument
-e, par exemple - mais dans l'ensemble, c'est l'un de mes scripts les plus simples utilisant
escape). Le programme
escapeaffiche simplement ses arguments, un peu comme le fait
echo mais il s'assure que les arguments sont protégés pour être utilisés avec le programme
eval(un niveau de
eval; j'ai un programme qui fait de l'exécution distante de à distance, et qui avait besoin d'é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 datée du 1987-01-27T14:35:49).
Il est surtout utile pour déboguer des scripts, car on peut le brancher sur une ligne de commande pour voir quels arguments sont réellement utilisés.
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 diverses 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
$
Remarquez que rien ne préserve les blancs originaux entre les *
et */*
sur la ligne de commande. Notez également que vous pouvez modifier les 'arguments de la ligne de commande' ; dans le shell en utilisant :
set -- -new -opt and "arg with space"
Ceci définit 4 options, '-new
' ;, '-opt
' ;, 'and
' ;, et 'arg with space
' ;.
]
Hmm, c’est une longue réponse - peut-être que exégèse est le meilleur terme.
Le code source de escape
est disponible sur demande (courriel à firstname dot
nom de famille à 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 pourrait être écrit comme une fonction shell (mais les fonctions shell n'existaient pas dans la version locale du Bourne shell quand j'ai écrit al
pour la première fois).
Notez aussi 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'on ne lui passe aucun argument. La commande printf
produira une ligne blanche avec seulement l'argument de la chaîne de format, mais le programme C ne produit rien.
[1] : https://stackoverflow.com/questions/154625/1-in-binsh
Notez que la réponse de Robert est correcte, et qu'elle fonctionne également avec sh
. Vous pouvez (de manière portative) la 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 lu ceci pour la première fois dans Unix Programming Environment de Kernighan et Pike.
Dans bash
, help for
documente cela :
for NAME [in WORDS ... ;] do COMMANDS ; done
Si
'in WORDS ...;'
n'est pas présent, alors'in "$@"'
est supposé.