$ ls -l /tmp/test/my\ dir/
total 0
Je me demandais pourquoi les façons 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
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.
La raison pour laquelle vous rencontrez ces problèmes est séparation des mots et le fait que les guillemets développés à partir de variables n'agissent pas comme des guillemets, mais sont juste des caractères ordinaires.
Les cas présentés dans la question:
$ abc='ls -l "/tmp/test/my dir"'
Ici, $abc
Est divisé et ls
obtient les deux arguments "/tmp/test/my
Et dir"
(Avec les guillemets à l'avant du premier et à l'arrière du seconde):
$ $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é 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, seul le premier mot ou $abc
Est pris comme argument pour -c
, Donc Bash exécute simplement ls
dans le répertoire courant. Les autres mots sont des arguments pour 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 de traitement Shell supplémentaire, qui fait fonctionner les guillemets, mais entraîne également le traitement de toutes les extensions Shell, il y a donc un risque d'exécuter accidentellement une extension de commande à partir de données fournies par l'utilisateur, sauf si vous faites très attention aux devis.
Les deux meilleures façons de stocker une commande sont a) utiliser une fonction à la place, b) utiliser une variable de tableau (ou les paramètres positionnels).
Utilisation d'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 extensions dans les commandes de la fonction ne sont traitées 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
Utilisation d'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 l'expansion "${array[@]}"
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 garder des parties de la ligne de commande constantes et utiliser le tableau en remplir seulement une partie, des options ou des 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 les shells POSIX simples (comme dash
, la valeur par défaut /bin/sh
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 possède un shell qui prend en charge les tableaux.
Utilisation de "$@"
Dans les shells sans prise en charge des tableaux nommés, on peut toujours utiliser les paramètres de position (le pseudo-tableau "$@"
) 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 "$@"
, La liste des paramètres de position. La définition de "$@"
Se fait avec set
, et les guillemets autour de "$@"
Sont importants (ceux-ci entraînent la citation individuelle des éléments de la liste).
Tout d'abord, il suffit de stocker une commande avec des arguments dans "$@"
Et de l'exécuter:
set -- ls -l "/tmp/test/my dir"
"$@"
Définition conditionnelle de parties des options de ligne de commande pour une commande:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Utiliser uniquement "$@"
Pour les options et opérandes:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(Bien sûr, "$@"
Est généralement rempli avec les arguments du script lui-même, vous devrez donc les enregistrer quelque part avant de réaffecter "$@"
.)
eval
!Comme eval
introduit un niveau supplémentaire de traitement de devis et d'expansion, vous devez être prudent avec la saisie utilisateur. Par exemple, cela fonctionne tant que l'utilisateur ne tape pas de guillemets simples:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
Mais s'ils donnent l'entrée '$(uname)'.txt
, votre script exécute heureusement la substitution de commande.
Une version avec des tableaux est immunisée contre cela puisque les mots sont maintenus séparés pendant tout le temps, il n'y a pas de citation ou autre traitement pour le contenu de filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
La façon la plus sûre d'exécuter une commande (non triviale) est eval
. Ensuite, vous pouvez écrire la commande comme vous le feriez sur la ligne de commande et elle est exécutée exactement comme si vous veniez de la saisir. Mais vous devez tout citer.
Cas simple:
abc='ls -l "/tmp/test/my dir"'
eval "$abc"
cas pas si simple:
# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"
Le deuxième signe de citation rompt la commande.
Quand je cours:
abc="ls -l '/home/wattana/Desktop'"
$abc
Cela m'a donné une erreur.
Mais quand je cours
abc="ls -l /home/wattana/Desktop"
$abc
Il n'y a aucune erreur du tout
Il n'y a aucun moyen de résoudre ce problème pour le moment (pour moi) mais vous pouvez éviter l'erreur en n'ayant pas d'espace dans le nom du répertoire.
Cette réponse a dit que la commande eval peut être utilisée pour résoudre ce problème mais cela ne fonctionne pas pour moi :(
Une autre astuce simple pour exécuter n'importe quelle commande (triviale/non triviale) stockée dans la variable abc
est:
$ history -s $abc
et appuyez sur UpArrow
ou Ctrl-p
pour le mettre en ligne de commande. Contrairement à toute autre méthode de cette façon, vous pouvez la modifier avant l'exécution si nécessaire.
Cette commande ajoutera le contenu variable comme nouvelle entrée à l'historique Bash et vous pouvez le rappeler par UpArrow
.
Si cela ne fonctionne pas avec ''
, vous devez utiliser ``
:
abc=`ls -l /tmp/test/my\ dir/`
Mettre à jour mieux:
abc=$(ls -l /tmp/test/my\ dir/)
Que diriez-vous d'un one-liner python3?
bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", Shell=True)"
total 8
drwxr-xr-x 2 root root 4096 Mar 4 2019 .
drwxr-xr-x 1 root root 4096 Nov 27 16:42 ..