Pourquoi dois-je citer une variable pour if, mais pas pour echo?

26

J'ai lu que vous avez besoin de guillemets doubles pour développer des variables, par exemple

if [ -n "$test" ]; then echo '$test ok'; else echo '$test null'; fi

fonctionnera comme prévu, tandis que

if [ -n $test ]; then echo '$test ok'; else echo '$test null'; fi

dira toujours $test okmême si $testest nul.

mais alors pourquoi n'avons-nous pas besoin de devis echo $test?

CharlesB
la source
2
Si vous ne citez pas une variable à utiliser comme argument echo, les espaces supplémentaires et les nouvelles lignes seront supprimés.
jordanm

Réponses:

36

Vous avez toujours besoin de guillemets autour des variables dans tous les contextes de liste , c'est-à-dire partout où la variable peut être étendue à plusieurs valeurs, sauf si vous voulez les 3 effets secondaires de laisser une variable sans guillemets.

les contextes de liste incluent des arguments à des commandes simples comme [ou echo, les for i in <here>affectations aux tableaux ... Il existe d'autres contextes où les variables doivent également être citées. Le mieux est de toujours citer des variables, sauf si vous avez une très bonne raison de ne pas le faire.

Considérez l'absence de guillemets (dans les contextes de liste) comme l' opérateur split + glob .

Comme si echo $testc'était echo glob(split("$test")).

Le comportement du shell est déroutant pour la plupart des gens car dans la plupart des autres langues, vous mettez des guillemets autour de chaînes fixes, comme puts("foo")et non autour de variables (comme puts(var)) tandis que dans le shell, c'est l'inverse: tout est une chaîne dans le shell, donc mettre des guillemets autour de tout serait lourd, vous echo test, vous n'en avez pas besoin "echo" "test". Dans le shell, les guillemets sont utilisés pour autre chose: empêcher une signification particulière de certains caractères et / ou affecter le comportement de certaines extensions.

Dans [ -n $test ]ou echo $test, le shell se divisera $test(sur les blancs par défaut), puis effectuera la génération du nom de fichier (développez tous les *modèles, '?' ... dans la liste des fichiers correspondants), puis passera cette liste d'arguments aux commandes [ou echo.

Encore une fois, pensez-y comme "[" "-n" glob(split("$test")) "]". Si $testest vide ou ne contient que des blancs (spc, tab, nl), alors l'opérateur split + glob renverra une liste vide, donc le [ -n $test ]sera "[" "-n" "]", qui est un test pour vérifier si "-n" est la chaîne vide ou non. Mais imaginez ce qui se serait passé si $test"*" ou "= foo" ...

Dans [ -n "$test" ], [est passé les quatre arguments "[", "-n", ""et "]"(sans les guillemets), ce qui est ce que nous voulons.

Que ce soit echoou [ne fait aucune différence, c'est juste que cela echogénère la même chose, qu'il ait passé un argument vide ou aucun argument du tout.

Voir aussi cette réponse à une question similaire pour plus de détails sur la [commande et la [[...]]construction.

Stéphane Chazelas
la source
7

La réponse de @ h3rrmiller est bonne pour expliquer pourquoi vous avez besoin des guillemets pour le if(ou plutôt, [/ test), mais je suppose en fait que votre question est incorrecte.

Essayez les commandes suivantes, et vous verrez ce que je veux dire.

export testvar="123    456"
echo $testvar
echo "$testvar"

Sans les guillemets, la substitution de variable entraîne l'extension de la deuxième commande:

echo 123    456

et les multiples espaces sont réduits en un seul:

echo 123 456

Avec les guillemets, les espaces sont préservés.

Cela se produit parce que lorsque vous citez un paramètre (si ce paramètre est passé à echo, testou une autre commande), la valeur de ce paramètre est envoyé comme une valeur à la commande. Si vous ne le citez pas, le shell fait sa magie normale de recherche d'espaces pour déterminer où chaque paramètre commence et se termine.

Ceci peut également être illustré par le programme C (très très simple) suivant. Essayez ce qui suit sur la ligne de commande (vous voudrez peut-être le faire dans un répertoire vide afin de ne pas risquer d'écraser quelque chose).

cat <<EOF >paramtest.c
#include <stdio.h>
int main(int argc, char **argv) {
  int nparams = argc-1; /* because 1 parameter means only the executable's name */
  printf("%d parameters received\n", nparams);
  return nparams;
}
EOF
cc -o paramtest paramtest.c

et alors...

./paramtest 123 456
./paramtest "123 456"
./paramtest 123   456
./paramtest "123   456"

Après l'exécution paramtest, $?contiendra le nombre de paramètres qui lui ont été transmis (et ce nombre sera imprimé).

un CVn
la source
2

Il s'agit de la façon dont le shell interprète la ligne avant l'exécution d'un programme.

Si la ligne est lue echo I am $USER, le shell la développe echo I am blrflet echon'a aucune idée si l'origine du texte est un développement littéral ou variable. De même, si une ligne se lit echo I am $UNDEFINED, le shell se développera $UNDEFINEDen rien et les arguments d'écho le seront I am, et c'est la fin. Puisque echofonctionne très bien sans arguments, echo $UNDEFINEDest complètement valide.

Votre problème avec ifn'est pas vraiment avec if, car ifexécute simplement le programme et les arguments qui le suivent et exécute la thenpartie si le programme se termine 0(ou la elsepartie s'il y en a un et que le programme quitte non 0):

if /bin/true ; then echo True dat. ; fi
if fgrep -q blrfl /etc/passwd ; then echo Blrfl has an account. ; fi

Lorsque vous utilisez if [ ... ]pour faire une comparaison, vous n'utilisez pas de primitives intégrées au shell. Vous demandez en fait au shell d'exécuter un programme appelé [qui est un très léger sur-ensemble test(1)qui nécessite que son dernier argument soit ]. Les deux programmes se terminent 0si la condition de test s'est vérifiée et 1si ce n'est pas le cas.

La raison pour laquelle certains tests échouent lorsqu'une variable n'est pas définie est parce testque ne voit pas que vous utilisez une variable. Ergo, se [ $UNDEFINED -eq 2 ]casse car au moment où le shell en a fini, tous les testarguments sont -eq 2 ], ce qui n'est pas un test valide. Si vous le faisiez avec quelque chose de défini, tel que [ $DEFINED -ne 0 ], cela fonctionnerait parce que le shell le développerait en un test valide (par exemple, 0 -ne 0).

Il y a une différence sémantique entre foo $UNDEFINED bar, qui s'étend à deux arguments ( fooet bar) parce qu'elle a $UNDEFINEDété à la hauteur de son nom. Comparez cela avec foo "$UNDEFINED" bar, qui se développe en trois arguments ( foo, une chaîne vide et `bar). Les citations forcent le shell à les interpréter comme un argument, qu'il y ait quelque chose entre eux ou non.

Blrfl
la source
0

Sans les guillemets, il $testpeut s'agir de plusieurs mots, il doit donc être cité pour ne pas casser la syntaxe, car chaque commutateur à l'intérieur de la [commande attend un argument, ce que font les guillemets ( $testtransforme ce qui se développe en un seul argument)

La raison pour laquelle vous n'avez pas besoin de guillemets pour développer une variable echoest parce qu'elle n'attend pas un argument. Il imprimera simplement ce que vous lui direz. Ainsi, même s'il $tests'étend à 100 mots, l'écho l'imprimera toujours.

Jetez un œil aux pièges de Bash

h3rrmiller
la source
oui mais pourquoi n'en avons-nous pas besoin echo?
CharlesB
@CharlesB Vous avez besoin des guillemets pour echo. Qu'est-ce qui vous fait penser le contraire?
Gilles 'SO- arrête d'être méchant'
Je n'en ai pas besoin, je le peux echo $testet ça marche (ça donne la valeur de $ test)
CharlesB
1
@CharlesB Il ne produit la valeur de $ test que si elle ne contient aucun espace multiple. Essayez le programme dans ma réponse pour une illustration de la raison.
un CVn du
0

Les paramètres vides sont supprimés s'ils ne sont pas cités:

start cmd:> strace -e trace=execve echo foo $bar baz
execve("/usr/bin/echo", ["echo", "foo", "baz"], [/* 100 vars */]) = 0

start cmd:> strace -e trace=execve echo foo "$bar" baz
execve("/usr/bin/echo", ["echo", "foo", "", "baz"], [/* 100 vars */]) = 0

La commande appelée ne voit pas qu'il y avait un paramètre vide sur la ligne de commande du shell. Il semble que [soit défini pour renvoyer 0 pour -n sans rien suivre. Pourquoi toujours.

La citation fait également une différence pour l'écho, dans plusieurs cas:

var='*'
echo $var
echo "$var"

var="foo        bar"
echo $var
echo "$var"
Hauke ​​Laging
la source
2
Ce n'est pas echo, c'est la coquille. Vous verriez le même comportement avec ls. Essayez un touch '*'peu de temps si vous vous sentez aventureux. :)
un CVn
C'est juste un libellé car il n'y a aucune différence avec le cas «si [...]». [n'est pas une commande shell spéciale. C'est différent de [[(en bash) où la citation n'est pas nécessaire.
Hauke ​​Laging