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

35
$ 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
Tim
la source

Réponses:

54

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 le fractionnement de 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, $abcest divisé et lsobtient les deux arguments "/tmp/test/myet dir"(avec les guillemets à l'avant du premier et à l'arrière du second):

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Ici, l'extension est citée, elle est donc conservée en 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 $abcest pris comme argument -c, donc Bash s'exécute simplement lsdans le répertoire courant. Les autres mots sont des arguments bash, et sont utilisés pour remplir $0, $1etc.

$ 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 entraîne également le nouveau traitement de toutes les extensions du shell , donc il y a un risque d'exécuter accidentellement une extension de commande à partir des données fournies par l'utilisateur, sauf si vous êtes très attention à citer.


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

En utilisant 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' "${array[@]}"expansion développe chaque élément en tant que mots shell séparés:

# 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, par défaut /bin/shdans 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 ait un shell qui supporte les tableaux.

En utilisant "$@"

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. Le réglage "$@"se fait avec set, et les guillemets autour "$@"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 les opérandes:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(Bien sûr, il "$@"contient généralement les arguments du script lui-même, vous devrez donc les enregistrer quelque part avant de les redéfinir "$@".)


Soyez prudent avec eval!

Comme evalintroduit un niveau supplémentaire de traitement des devis et d'expansion, vous devez être prudent avec la saisie de l'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 joyeusement la substitution de commande.

Une version avec des tableaux est à l'abri de 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[@]}"

Les références

ilkkachu
la source
2
vous pouvez contourner la citation eval en faisant cmd="ls -l $(printf "%q" "$filename")". pas joli, mais si l'utilisateur est prêt à utiliser un eval, cela aide. Il est également très utile pour envoyer la commande si des choses semblables, comme ssh foohost "ls -l $(printf "%q" "$filename")", ou dans l'esprit de cette question: ssh foohost "$cmd".
Patrick
Pas directement lié, mais avez-vous codé en dur le répertoire? Dans ce cas, vous voudrez peut-être regarder l'alias. Quelque chose comme: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny
4

La manière 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"
Hauke ​​Laging
la source
3

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

Wattana Gaming
la source
1
Oui, cela fonctionne tant qu'il n'y a pas besoin, par exemple, de noms de fichiers avec des espaces intégrés (ou ceux contenant des caractères globaux).
ilkkachu
0

Si cela ne fonctionne pas '', vous devez utiliser ``:

abc=`ls -l /tmp/test/my\ dir/`

Mettre à jour mieux:

abc=$(ls -l /tmp/test/my\ dir/)
balon
la source
Cela stocke le résultat de la commande dans une variable. L'OP souhaite (étrangement) enregistrer la commande elle-même dans une variable. Oh, et vous devriez vraiment commencer à utiliser $( command... )au lieu de backticks.
roaima
Merci beaucoup pour les clarifications et les conseils!
balon
0

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 ..
Marius Mitrofan
la source