Je viens d'attribuer une variable, mais echo $ variable montre autre chose

104

Voici une série de cas où echo $varpeut afficher une valeur différente de celle qui vient d'être attribuée. Cela se produit indépendamment du fait que la valeur assignée ait été "guillemets doubles", "guillemets simples" ou non.

Comment faire pour que le shell définisse correctement ma variable?

Astérisques

La sortie attendue est /* Foobar is free software */, mais à la place, j'obtiens une liste de noms de fichiers:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Crochets

La valeur attendue est [a-z], mais parfois je reçois une seule lettre à la place!

$ var=[a-z]
$ echo $var
c

Sauts de ligne (nouvelles lignes)

La valeur attendue est une liste de lignes séparées, mais à la place toutes les valeurs sont sur une seule ligne!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Espaces multiples

Je m'attendais à un en-tête de tableau soigneusement aligné, mais à la place, plusieurs espaces disparaissent ou sont réduits en un seul!

$ var="       title     |    count"
$ echo $var
title | count

Onglets

Je m'attendais à deux valeurs séparées par des tabulations, mais à la place, j'obtiens deux valeurs séparées par des espaces!

$ var=$'key\tvalue'
$ echo $var
key value
cet autre gars
la source
2
Merci d'avoir fait ça. Je rencontre souvent les sauts de ligne. Tout var=$(cat file)va bien, mais echo "$var"c'est nécessaire.
snd
3
BTW, c'est aussi BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy

Réponses:

139

Dans tous les cas ci-dessus, la variable est correctement définie, mais pas correctement lue! La bonne façon est d' utiliser des guillemets doubles lors du référencement :

echo "$var"

Cela donne la valeur attendue dans tous les exemples donnés. Citez toujours des références variables!


Pourquoi?

Lorsqu'une variable n'est pas entre guillemets , elle:

  1. Faites l'objet d' un fractionnement de champ où la valeur est divisée en plusieurs mots sur un espace blanc (par défaut):

    Avant: /* Foobar is free software */

    Après: /*, Foobar, is, free, software,*/

  2. Chacun de ces mots subira une extension de chemin , où les modèles sont développés dans des fichiers correspondants:

    Avant: /*

    Après: /bin, /boot, /dev, /etc, /home, ...

  3. Enfin, tous les arguments sont passés à echo, qui les écrit séparés par des espaces simples , donnant

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    au lieu de la valeur de la variable.

Lorsque la variable est citée, elle:

  1. Être substitué à sa valeur.
  2. Il n'y a pas d'étape 2.

C'est pourquoi vous devez toujours citer toutes les références de variables , sauf si vous avez spécifiquement besoin de séparer les mots et de développer les chemins. Des outils comme shellcheck sont là pour vous aider et vous avertiront des guillemets manquants dans tous les cas ci-dessus.

cet autre gars
la source
ça ne marche pas toujours. Je peux donner un exemple: paste.ubuntu.com/p/8RjR6CS668
recolic
1
Oui, $(..)supprime les sauts de ligne de fin. Vous pouvez utiliser var=$(cat file; printf x); var="${var%x}"pour contourner ce problème.
cet autre gars
17

Vous voudrez peut-être savoir pourquoi cela se produit. Avec la bonne explication de cet autre gars , trouvez une référence de Pourquoi mon script shell s'étouffe-t-il avec des espaces ou d'autres caractères spéciaux? écrit par Gilles sous Unix et Linux :

Pourquoi ai-je besoin d'écrire "$foo"? Que se passe-t-il sans les guillemets?

$foone veut pas dire «prendre la valeur de la variable foo». Cela signifie quelque chose de beaucoup plus complexe:

  • Tout d'abord, prenez la valeur de la variable.
  • Fractionnement des champs: traitez cette valeur comme une liste de champs séparés par des espaces et créez la liste résultante. Par exemple, si la variable contient foo * bar ​le résultat de cette étape est la liste 3 éléments foo, *, bar.
  • Génération de nom de fichier: traitez chaque champ comme un glob, c'est-à-dire comme un modèle générique, et remplacez-le par la liste des noms de fichiers correspondant à ce modèle. Si le modèle ne correspond à aucun fichier, il n'est pas modifié. Dans notre exemple, cela se traduit par la liste contenant foo, suivie de la liste des fichiers dans le répertoire courant, et enfin bar. Si le répertoire courant est vide, le résultat est foo, *, bar.

Notez que le résultat est une liste de chaînes. Il existe deux contextes dans la syntaxe du shell: le contexte de liste et le contexte de chaîne. Le fractionnement de champ et la génération de nom de fichier se produisent uniquement dans un contexte de liste, mais c'est la plupart du temps. Les guillemets doubles délimitent un contexte de chaîne: toute la chaîne entre guillemets est une seule chaîne, à ne pas scinder. (Exception: "$@"développer la liste des paramètres de position, par exemple "$@"équivaut à "$1" "$2" "$3"s'il y a trois paramètres de position. Voir Quelle est la différence entre $ * et $ @? )

Il en va de même pour la substitution de commandes avec $(foo)ou avec `foo`. Sur une note latérale, ne pas utiliser `foo`: ses règles de citation sont étranges et non portables, et tous les shells modernes prennent en charge $(foo)ce qui est absolument équivalent, sauf pour avoir des règles de citation intuitives.

La sortie de la substitution arithmétique subit également les mêmes extensions, mais ce n'est normalement pas un problème car elle ne contient que des caractères non extensibles (en supposant IFSqu'elle ne contient pas de chiffres ou -).

Voir Quand les guillemets doubles sont-ils nécessaires? pour plus de détails sur les cas où vous pouvez omettre les citations.

À moins que vous ne vouliez que tout ce rigmarole se produise, n'oubliez pas de toujours utiliser des guillemets doubles autour des substitutions de variables et de commandes. Faites attention: le fait d'omettre les guillemets peut entraîner non seulement des erreurs, mais également des failles de sécurité .

fedorqui 'Alors arrêtez de nuire'
la source
7

En plus d'autres problèmes causés par l'échec de la citation, -net -epeuvent être utilisés echocomme arguments. (Seul le premier est légal selon la spécification POSIX pour echo, mais plusieurs implémentations courantes violent la spécification et consomment -eégalement).

Pour éviter cela, utilisez printfplutôt que echolorsque les détails comptent.

Donc:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

Cependant, une citation correcte ne vous sauvera pas toujours lorsque vous utilisez echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... alors que cela vous fera économiser avec printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n
Charles Duffy
la source
Ouais, nous avons besoin d'une bonne dédup pour ça! Je suis d'accord que cela correspond au titre de la question, mais je ne pense pas qu'elle obtiendra la visibilité qu'elle mérite ici. Que diriez-vous d'une nouvelle question à la "Pourquoi mon -e/ -n/ backslash n'apparaît pas?" Nous pouvons ajouter des liens à partir d'ici, le cas échéant.
cet autre gars
Vouliez-vous dire consommer -naussi ?
PesaLe
1
@PesaThe, non, je voulais dire -e. La norme pour echone spécifie pas la sortie lorsque son premier argument est -n, rendant toute sortie possible légale dans ce cas; il n'y a pas de telle disposition pour -e.
Charles Duffy
Oh ... je ne sais pas lire. Blâmons mon anglais pour cela. Merci pour l'explication.
PesaLe
6

guillemet double de l'utilisateur pour obtenir la valeur exacte. comme ça:

echo "${var}"

et il lira votre valeur correctement.

disparuzhou
la source
Works .. Merci
Tshilidzi Mudau
2

echo $varla sortie dépend fortement de la valeur de la IFSvariable. Par défaut, il contient des caractères d'espace, de tabulation et de nouvelle ligne:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Cela signifie que lorsque le shell effectue le fractionnement de champ (ou le fractionnement de mots), il utilise tous ces caractères comme séparateurs de mots. C'est ce qui se passe lors du référencement d'une variable sans guillemets doubles pour la faire écho ($var ) et ainsi la sortie attendue est modifiée.

Une façon d'éviter le fractionnement de mot (en plus d'utiliser des guillemets doubles) est de définir la valeur IFSnull. Voir http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Si la valeur de IFS est nulle, aucune division de champ ne doit être effectuée.

Définir sur null signifie définir une valeur vide:

IFS=

Tester:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 
ks1322
la source
2
Vous devriez également set -fempêcher le globbing
cet autre gars
@thatotherguy, est-ce vraiment nécessaire pour votre premier exemple avec extension de chemin? Avec IFSla valeur null, echo $varsera étendu à echo '/* Foobar is free software */'et l'expansion de chemin n'est pas effectuée à l'intérieur de chaînes entre guillemets simples.
ks1322
1
Oui. Si vous mkdir "/this thing called Foobar is free software etc/"voyez qu'il s'agrandit encore. C'est évidemment plus pratique pour l' [a-z]exemple.
cet autre gars
Je vois, cela a du sens par [a-z]exemple.
ks1322
2

La réponse de ks1322 m'a aidé à identifier le problème lors de l'utilisation docker-compose exec:

Si vous omettez l' -Tindicateur, docker-compose execajoutez un caractère spécial qui rompt la sortie, nous voyons à la bplace de 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

Avec -Tindicateur, docker-compose execfonctionne comme prévu:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b
AL
la source
-2

En plus de mettre la variable entre guillemets, on pourrait également traduire la sortie de la variable en utilisant tret en convertissant les espaces en nouvelles lignes.

$ echo $var | tr " " "\n"
foo
bar
baz

Bien que ce soit un peu plus compliqué, cela ajoute plus de diversité à la sortie car vous pouvez remplacer n'importe quel caractère comme séparateur entre les variables du tableau.

Alek
la source
2
Mais cela remplace tous les espaces par des retours à la ligne. La citation préserve les nouvelles lignes et les espaces existants.
user000001
C'est vrai, oui. Je suppose que cela dépend de ce qui se trouve dans la variable. J'utilise en fait trl'inverse pour créer des tableaux à partir de fichiers texte.
Alek
3
Créer un problème en ne citant pas correctement la variable, puis en le contournant avec un processus supplémentaire accablé n'est pas une bonne programmation.
tripleee
@Alek, ... euh, quoi? Il n'est pas trnécessaire de créer correctement / correctement un tableau à partir d'un fichier texte - vous pouvez spécifier le séparateur de votre choix en définissant IFS. Par exemple: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')fonctionne depuis bash 3.2 (la version la plus ancienne largement diffusée), et définit correctement l'état de sortie sur false si votre catéchec. Et si vous vouliez, disons, des tabulations à la place des nouvelles lignes, vous remplaceriez simplement le $'\n'par $'\t'.
Charles Duffy
1
@Alek, ... si vous faites quelque chose comme arrayname=( $( cat file | tr '\n' ' ' ) ), alors c'est cassé sur plusieurs couches: cela regroupe vos résultats (donc un *se transforme en une liste de fichiers dans le répertoire actuel), et cela fonctionnerait aussi bien sans letr ( ou le cat, d'ailleurs; on pourrait simplement utiliser arrayname=$( $(<file) )et il serait cassé de la même manière, mais de manière moins inefficace).
Charles Duffy