GNU/Linux >> Tutoriels Linux >  >> Linux

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

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

Connexe :la commande "eval" dans bash ?

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 !

En relation :Ajouter un chemin d'accès à l'environnement à l'invite de commande de Visual Studio ?

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.

Linux
  1. Comment exécuter une commande en tant qu'administrateur système (racine) ?

  2. Comment exécuter une commande dans un conteneur Systemd en cours d'exécution ?

  3. Comment exécuter une commande sans propriétés racine ?

  4. Comment exécuter une commande sur un conteneur Docker en cours d'exécution

  5. Comment puis-je exécuter une commande après le démarrage ?

Comment exécuter une commande / un script Linux Shell en arrière-plan

Comment exécuter la commande Sudo sans mot de passe

Comment exécuter des commandes Linux en arrière-plan

Comment exécuter une commande pendant une durée spécifique sous Linux

Comment stocker une commande Linux en tant que variable dans un script shell

Comment exécuter une commande périodiquement sous Linux à l'aide de Watch