Les variables doivent-elles être citées lors de leur exécution?

18

La règle générale dans les scripts shell est que les variables doivent toujours être citées sauf s'il existe une raison impérieuse de ne pas le faire. Pour plus de détails que vous ne voudriez probablement en savoir, jetez un œil à ce grand Q&R: implications de sécurité d'oublier de citer une variable dans des shells bash / POSIX .

Considérez cependant une fonction comme la suivante:

run_this(){
    $@
}

Doit- $@on y citer ou non? J'ai joué un peu avec et je n'ai trouvé aucun cas où le manque de guillemets a causé un problème. D'un autre côté, l'utilisation des guillemets le casse lors du passage d'une commande contenant des espaces en tant que variable entre guillemets:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

L'exécution du script ci-dessus renvoie:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Je peux contourner cela si j'utilise à la run_that $commplace de run_that "$comm", mais comme la fonction run_this(sans guillemets) fonctionne avec les deux, cela semble être le pari le plus sûr.

Donc, dans le cas spécifique de l'utilisation $@dans une fonction dont le travail consiste à exécuter en $@tant que commande, doit $@être cité? Veuillez expliquer pourquoi il doit / ne doit pas être cité et donnez un exemple de données qui peuvent le casser.

terdon
la source
6
run_thatLe comportement est certainement ce à quoi je m'attendais (et s'il y a un espace sur le chemin de la commande?). Si vous vouliez l'autre comportement, sûrement vous le citez sur le site d' appel où vous savez quelles sont les données? Je m'attendrais à appeler cette fonction comme run_that ls -l, ce qui fonctionne de la même manière dans l'une ou l'autre version. Y a-t-il un cas qui vous a fait vous attendre différemment?
Michael Homer
@MichaelHomer Je suppose que ma modification ici a provoqué ceci: unix.stackexchange.com/a/250985/70524
muru
@MichaelHomer pour une raison quelconque (probablement parce que je n'ai pas encore pris ma deuxième tasse de café) Je n'avais pas considéré les espaces dans les arguments ou le chemin de la commande, mais seulement dans la commande elle-même (options). Comme c'est souvent le cas, cela semble très évident rétrospectivement.
terdon
Il y a une raison pour laquelle les shells supportent toujours des fonctions au lieu de simplement bourrer des commandes dans un tableau et de l'exécuter avec ${mycmd[@]}.
chepner du

Réponses:

20

Le problème réside dans la façon dont la commande est passée à la fonction:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"doit être utilisé dans le cas général où votre run_thisfonction est préfixée à une commande normalement écrite. run_thisconduit à citer l'enfer:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Je ne sais pas comment je dois passer un nom de fichier avec des espaces run_this.

muru
la source
1
C'est bien votre montage qui a provoqué cela. Pour une raison quelconque, je n'ai pas pensé à tester avec un nom de fichier avec des espaces. Je ne sais absolument pas pourquoi, mais voilà. Vous avez tout à fait raison bien sûr, je ne vois pas non plus de moyen de le faire correctement run_this.
terdon
Les citations @terdon sont devenues tellement une habitude que j'ai supposé que vous aviez laissé des $@citations accidentellement. J'aurais dû laisser un exemple. : D
muru
2
Non, c'est en effet tellement une habitude que je l'ai testé (à tort) et conclu que "hein, peut-être que celui-ci n'a pas besoin de guillemets". Une procédure connue sous le nom de brainfart.
terdon
1
Vous ne pouvez pas passer un nom de fichier avec des espaces à run_this. Il s'agit essentiellement du même problème que vous rencontrez avec le bourrage de commandes complexes dans des chaînes, comme indiqué dans la FAQ Bash 050 .
Etan Reisner
9

C'est soit:

interpret_this_shell_code() {
  eval "$1"
}

Ou:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

ou:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Mais:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Cela n'a pas beaucoup de sens.

Si vous voulez exécuter la ls -lcommande (pas la lscommande avec lset -lcomme arguments), vous feriez:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Mais si (plus probablement), c'est la lscommande avec lset -lcomme arguments, vous exécuteriez:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Maintenant, si c'est plus qu'une simple commande que vous voulez exécuter, si vous voulez faire des affectations de variables, des redirections, des tuyaux ..., seul interpret_this_shell_codefera:

interpret_this_shell_code 'ls -l 2> /dev/null'

bien sûr, vous pouvez toujours faire:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Stéphane Chazelas
la source
5

Le regarder du point de vue bash / ksh / zsh, $*et $@sont un cas particulier de l'expansion générale des tableaux. Les extensions de tableau ne sont pas comme les extensions de variable normales:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

Avec les extensions $*/ ${a[*]}vous obtenez le tableau joint avec la première valeur de IFS- qui est l'espace par défaut - en une chaîne géante. Si vous ne le citez pas, il est divisé comme une chaîne normale.

Avec les extensions $@/ ${a[@]}, le comportement dépend du fait que l' extension $@/ ${a[@]}soit citée ou non:

  1. s'il est cité ( "$@"ou "${a[@]}"), vous obtenez l'équivalent de "$1" "$2" "$3" #... ou"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. s'il n'est pas cité ( $@ou ${a[@]}) vous obtenez l'équivalent de $1 $2 $3 #... ou${a[1]} ${a[2]} ${a[3]} # ...

Pour les commandes d'encapsulation, vous voulez certainement les extensions @ citées (1.).


Plus de bonnes informations sur les tableaux bash (et de type bash): https://lukeshu.com/blog/bash-arrays.html

PSkocik
la source
1
Je viens de réaliser que je fais référence à un lien commençant par Luke, tout en portant un masque Vader. La force est forte avec ce poste.
PSkocik
4

Depuis que vous ne doublez pas les guillemets $@, vous avez laissé tous les problèmes de globalisation dans le lien que vous avez donné à votre fonction.

Comment pourriez-vous exécuter une commande nommée *? Vous ne pouvez pas le faire avec run_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

Et vous voyez, même lorsqu'une erreur s'est produite, run_thatvous a donné un message plus significatif.

La seule façon de développer $@des mots individuels est de les mettre entre guillemets. Si vous souhaitez l'exécuter en tant que commande, vous devez passer la commande et ses paramètres en tant que mots séparés. C'est ce que vous avez fait du côté de l'appelant, pas à l'intérieur de votre fonction.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

est un meilleur choix. Ou si votre tableau prend en charge les tableaux:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Même lorsque le shell ne prend pas du tout en charge le tableau, vous pouvez toujours jouer avec lui en utilisant"$@" .

cuonglm
la source
3

L'exécution de variables dans bashest une technique sujette aux pannes. Il est tout simplement impossible d'écrire une run_thisfonction qui gère correctement tous les cas de bord, comme:

  • pipelines (par exemple ls | grep filename)
  • redirections d'entrée / sortie (par exemple ls > /dev/null)
  • instructions shell comme if whileetc.

Si tout ce que vous voulez faire est d'éviter la répétition du code, il vaut mieux utiliser les fonctions. Par exemple, au lieu de:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Tu devrais écrire

command() {
    ls -l
}
...
command

Si les commandes ne sont disponibles qu'au moment de l'exécution, vous devez utiliser evalce qui est spécifiquement conçu pour gérer toutes les bizarreries qui feront run_thiséchouer:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Notez que cela evalest connu pour des problèmes de sécurité, mais si vous transmettez des variables de sources non fiables à run_this, vous serez également confronté à l'exécution de code arbitraire.

Dmitry Grigoryev
la source
1

Le choix t'appartient. Si vous ne citez $@aucune de ses valeurs, faites l' objet d'une expansion et d'une interprétation supplémentaires. Si vous le citez tous les arguments passés, la fonction est reproduite textuellement dans son expansion. Vous ne serez jamais en mesure de gérer de manière fiable les jetons de syntaxe du shell comme &>|et etc de toute façon sans analyser les arguments vous-même de toute façon - et vous avez donc le choix plus raisonnable de remettre votre fonction l'une des deux:

  1. Exactement les mots utilisés dans l'exécution d'une seule commande simple avec "$@".

...ou...

  1. Une autre version développée et interprétée de vos arguments qui ne sont ensuite appliqués que comme une simple commande avec $@.

Aucune des deux façons n'est fausse si elle est intentionnelle et si les effets de ce que vous choisissez sont bien compris. Les deux méthodes présentent des avantages l'une par rapport à l'autre, bien que les avantages de la seconde soient rarement susceptibles d'être particulièrement utiles. Encore...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... ce n'est pas inutile , mais rarement susceptible d'être d'une grande utilité . Et dans un bashshell, parce bashque par défaut ne colle pas une définition de variable à son environnement même lorsque ladite définition est ajoutée à la ligne de commande d'un builtin spécial ou à une fonction, la valeur globale de $IFSn'est pas affectée et sa déclaration est locale uniquement à l' run_this()appel.

De même:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... le globbing est également configurable. Les citations ont un but - elles ne sont pas pour rien. Sans eux, l'expansion du shell subit une interprétation supplémentaire - une interprétation configurable . Auparavant - avec de très vieux shells - qui $IFSétait globalement appliqué à toutes les entrées, et pas seulement aux extensions. En fait, lesdits obus se sont comportés de manière très similaire run_this()à ce qu'ils ont brisé tous les mots saisis sur la valeur de $IFS. Et donc, si ce que vous recherchez est ce comportement de shell très ancien, alors vous devriez l' utiliser run_this().

Je ne le cherche pas, et j'ai du mal à trouver un exemple utile pour le moment. Je préfère généralement que les commandes exécutées par mon shell soient celles que je tape dessus. Et donc, étant donné le choix, je le ferais presque toujours run_that(). Excepté...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

À peu près tout peut être cité. Les commandes s'exécuteront entre guillemets. Cela fonctionne car au moment où la commande est réellement exécutée, tous les mots d'entrée ont déjà subi la suppression des guillemets - qui est la dernière étape du processus d'interprétation des entrées du shell. Ainsi, la différence entre 'ls'et lsne peut avoir d'importance que lorsque le shell interprète - et c'est pourquoi la citation lsgarantit qu'aucun alias nommé lsn'est substitué à mon lsmot de commande cité . En dehors de cela, les seules choses affectées par les guillemets sont la délimitation des mots (qui explique comment et pourquoi la citation des variables / espaces blancs fonctionne) et l'interprétation des métacaractères et des mots réservés.

Donc:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Vous ne pourrez jamais le faire avec run_this()ou run_that().

Mais les noms de fonction, ou $PATHles commandes d , ou les commandes intégrées s'exécuteront très bien entre guillemets ou sans guillemets, et c'est exactement comment run_this()et run_that()fonctionner en premier lieu. Vous ne pourrez rien faire d'utile avec $<>|&(){}aucun d'entre eux. À court de eval, c'est.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

Mais sans cela, vous êtes contraint aux limites d'une simple commande en raison des guillemets que vous utilisez (même si vous ne le faites pas, car il $@agit comme une citation au début du processus lorsque la commande est analysée pour les métacaractères) . La même contrainte est vraie pour les affectations et les redirections de ligne de commande, qui sont limitées à la ligne de commande de la fonction. Mais ce n'est pas grave:

(run_that(){ "$@";}; echo hey | run_that cat)

hey

J'aurais pu y <rediriger l'entrée ou la >sortie aussi facilement que j'ai ouvert le tuyau.

Quoi qu'il en soit, de manière détournée, il n'y a pas de bonne ou de mauvaise voie ici - chaque voie a ses utilisations. C'est juste que vous devez l'écrire comme vous avez l'intention de l'utiliser, et vous devez savoir ce que vous voulez faire. Citations Omettre peuvent avoir un but - sinon il n'y aurait pas être citations du tout - mais si vous les omettez pour des raisons non pertinentes à votre but, vous êtes juste à écrire du mauvais code. Faites ce que vous voulez dire; J'essaye de toute façon.

mikeserv
la source