$ ls -l /tmp/test/my dir/
total 0
Je me demandais pourquoi les manières suivantes d'exécuter la commande ci-dessus échouent ou réussissent ?
$ abc='ls -l "/tmp/test/my dir"'
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
$ bash -c $abc
'my dir'
$ bash -c "$abc"
total 0
$ eval $abc
total 0
$ eval "$abc"
total 0
Réponse acceptée :
Cela a été discuté dans un certain nombre de questions sur unix.SE, je vais essayer de rassembler tous les problèmes que je peux trouver ici. Références à la fin.
Pourquoi ça échoue
La raison pour laquelle vous rencontrez ces problèmes est la séparation des mots et le fait que les guillemets développés à partir de variables n'agissent pas comme des guillemets, mais sont simplement des caractères ordinaires.
Les cas présentés dans la question :
L'affectation ici affecte la chaîne unique ls -l "/tmp/test/my dir"
à abc
:
$ abc='ls -l "/tmp/test/my dir"'
Ci-dessous, $abc
est divisé en espaces blancs, et ls
obtient les trois arguments -l
, "/tmp/test/my
et dir"
(avec une citation au début de la seconde et une autre à la fin de la troisième). L'option fonctionne, mais le chemin est mal traité :
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Ici, l'expansion est citée, elle est donc conservée comme un seul mot. Le shell essaie de trouver un programme appelé littéralement ls -l "/tmp/test/my dir"
, espaces et guillemets inclus.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
Et ici, $abc
est divisé, et seul le premier mot résultant est pris comme argument de -c
, donc Bash exécute simplement ls
dans le répertoire courant. Les autres mots sont des arguments de bash et sont utilisés pour remplir $0
, $1
, etc.
$ bash -c $abc
'my dir'
Avec bash -c "$abc"
, et eval "$abc"
, il y a une étape supplémentaire de traitement du shell, qui fait fonctionner les guillemets, mais provoque également le traitement à nouveau de toutes les extensions du shell , il y a donc un risque de courir accidentellement, par ex. une substitution de commande à partir de données fournies par l'utilisateur, à moins que vous ne fassiez très attention aux guillemets.
Meilleures façons de procéder
Les deux meilleures façons de stocker une commande sont a) d'utiliser une fonction à la place, b) d'utiliser une variable de tableau (ou les paramètres de position).
Utiliser une fonction :
Déclarez simplement une fonction avec la commande à l'intérieur et exécutez la fonction comme s'il s'agissait d'une commande. Les développements dans les commandes de la fonction ne sont traités que lorsque la commande s'exécute, pas lorsqu'elle est définie, et vous n'avez pas besoin de citer les commandes individuelles.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Utiliser un tableau :
Les tableaux permettent de créer des variables multi-mots où les mots individuels contiennent des espaces blancs. Ici, les mots individuels sont stockés en tant qu'éléments de tableau distincts, et le "${array[@]}"
expansion développe chaque élément en tant que mots shell distincts :
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
La syntaxe est légèrement horrible, mais les tableaux vous permettent également de construire la ligne de commande pièce par pièce. Par exemple :
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
ou gardez des parties de la ligne de commande constantes et utilisez le tableau pour n'en remplir qu'une partie, comme les options ou les noms de fichiers :
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
L'inconvénient des tableaux est qu'ils ne sont pas une fonctionnalité standard, donc des shells POSIX simples (comme dash
, le /bin/sh
par défaut dans Debian/Ubuntu) ne les prennent pas en charge (mais voir ci-dessous). Bash, ksh et zsh le font, cependant, il est donc probable que votre système dispose d'un shell qui prend en charge les tableaux.
Utiliser "[email protected]"
Dans les shells ne prenant pas en charge les tableaux nommés, on peut toujours utiliser les paramètres de position (le pseudo-tableau "[email protected]"
) pour contenir les arguments d'une commande.
Les éléments suivants doivent être des bits de script portables qui font l'équivalent des bits de code de la section précédente. Le tableau est remplacé par "[email protected]"
, la liste des paramètres positionnels. Paramètre "[email protected]"
se fait avec set
, et les guillemets autour de "[email protected]"
sont importants (c'est-à-dire que les éléments de la liste sont cités individuellement).
Tout d'abord, stockez simplement une commande avec des arguments dans "[email protected]"
et l'exécuter :
set -- ls -l "/tmp/test/my dir"
"[email protected]"
Définition conditionnelle de certaines parties des options de ligne de commande pour une commande :
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"
"[email protected]"
Utiliser uniquement "[email protected]"
pour les options et opérandes :
set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir
transmutate "[email protected]"
(Bien sûr, "[email protected]"
est généralement rempli avec les arguments du script lui-même, vous devrez donc les enregistrer quelque part avant de réutiliser "[email protected]"
.)
Utiliser eval
(faites attention ici !)
eval
prend une chaîne et l'exécute comme une commande, comme si elle était entrée sur la ligne de commande du shell. Cela inclut tous les traitements de devis et d'expansion, qui sont à la fois utiles et dangereux.
Dans le cas simple, cela permet de faire exactement ce que l'on veut :
cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"
Avec eval
, les guillemets sont traités, donc ls
ne voit finalement que les deux arguments -l
et /tmp/test/my dir
, comme on veut. eval
est également assez intelligent pour concaténer tous les arguments qu'il obtient, donc eval $cmd
pourrait également fonctionner dans certains cas, mais par ex. toutes les séries d'espaces blancs seraient changées en espaces simples. Il est toujours préférable de citer la variable ici car cela garantira qu'elle ne sera pas modifiée en eval
.
Cependant, il est dangereux d'inclure l'entrée de l'utilisateur dans la chaîne de commande pour eval
. Par exemple, cela semble fonctionner :
read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";
Mais si l'utilisateur donne une entrée contenant des guillemets simples, il peut sortir des guillemets et exécuter des commandes arbitraires ! Par exemple. avec l'entrée '$(whatever)'.txt
, votre script exécute avec plaisir la substitution de commande. Que cela aurait pu être rm -rf
(ou pire) à la place.
Le problème est que la valeur de $filename
a été intégré dans la ligne de commande que eval
court. Il a été développé avant eval
, qui a vu par ex. la commande ls -l ''$(whatever)'.txt'
. Vous auriez besoin de pré-traiter l'entrée pour être sûr.
Si nous le faisons dans l'autre sens, en gardant le nom du fichier dans la variable et en laissant le eval
commande développez-le, c'est à nouveau plus sûr :
read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";
Notez que les guillemets extérieurs sont désormais des guillemets simples, de sorte que les extensions à l'intérieur ne se produisent pas. Par conséquent, eval
voit la commande ls -l "$filename"
et développe le nom de fichier en toute sécurité lui-même.
Mais ce n'est pas très différent du simple stockage de la commande dans une fonction ou un tableau. Avec les fonctions ou les tableaux, il n'y a pas un tel problème puisque les mots sont gardés séparés pendant tout le temps, et il n'y a pas de guillemet ou autre traitement pour le contenu de filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
À peu près la seule raison d'utiliser eval
est celui où la partie variable implique des éléments de syntaxe shell qui ne peuvent pas être introduits via des variables (pipelines, redirections, etc.). Même dans ce cas, assurez-vous de ne pas intégrer l'entrée de l'utilisateur dans le eval
commande !
Références
- Séparation de mots dans BashGuide
- BashFAQ/050 ou "J'essaie de mettre une commande dans une variable, mais les cas complexes échouent toujours !"
- La question Pourquoi mon script shell s'étouffe-t-il avec des espaces ou d'autres caractères spéciaux ?, qui aborde un certain nombre de problèmes liés aux guillemets et aux espaces, y compris le stockage des commandes.