web-dev-qa-db-fra.com

Comment exécuter une commande stockée dans une variable?

$ 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
53
Tim

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 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.


De meilleures façons de le faire

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 "$@".)


Soyez prudent avec 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[@]}"

Références

72
ilkkachu

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"
8
Hauke Laging

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 :(

3
Wattana Gaming

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.

0
bloody

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/)
0
balon

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 ..
0
Marius Mitrofan