Ho appena assegnato una variabile, ma echo $ variable mostra qualcos'altro


104

Ecco una serie di casi in cui echo $varpuò mostrare un valore diverso da quello appena assegnato. Ciò accade indipendentemente dal fatto che il valore assegnato fosse "double quoted", "single quoted" o non quotato.

Come faccio a fare in modo che la shell imposti correttamente la mia variabile?

Asterischi

L'output previsto è /* Foobar is free software */, ma invece ottengo un elenco di nomi di file:

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

Parentesi quadre

Il valore previsto è [a-z], ma a volte ricevo invece una sola lettera!

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

Avanzamenti di riga (nuove righe)

Il valore atteso è un elenco di righe separate, ma invece tutti i valori sono su una riga!

$ cat file
foo
bar
baz

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

Spazi multipli

Mi aspettavo un'intestazione di tabella accuratamente allineata, ma invece più spazi scompaiono o vengono compressi in uno solo!

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

Tab

Mi aspettavo due valori separati da tabulazione, ma invece ottengo due valori separati da spazi!

$ var=$'key\tvalue'
$ echo $var
key value

2
Grazie per averlo fatto. Incontro spesso il line feed. Quindi var=$(cat file)va bene, ma echo "$var"è necessario.
Snd

3
BTW, questo è anche BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy


Risposte:


139

In tutti i casi precedenti, la variabile è impostata correttamente, ma non letta correttamente! Il modo giusto è usare le virgolette doppie quando si fa riferimento :

echo "$var"

Questo fornisce il valore atteso in tutti gli esempi forniti. Cita sempre i riferimenti variabili!


Perché?

Quando è una variabile non quotate , lo farà:

  1. Sottoponiti alla suddivisione del campo in cui il valore viene suddiviso in più parole su spazi (per impostazione predefinita):

    Prima: /* Foobar is free software */

    Dopo: /*, Foobar, is, free, software,*/

  2. Ciascuna di queste parole subirà l' espansione del nome del percorso , dove i modelli vengono espansi in file corrispondenti:

    Prima: /*

    Dopo: /bin, /boot, /dev, /etc, /home, ...

  3. Infine, tutti gli argomenti vengono passati a echo, che li scrive separati da singoli spazi , dando

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

    invece del valore della variabile.

Quando la variabile è quotata, essa:

  1. Sostituisci il suo valore.
  2. Non c'è passaggio 2.

Questo è il motivo per cui dovresti sempre citare tutti i riferimenti alle variabili , a meno che tu non richieda specificamente la suddivisione delle parole e l'espansione del percorso. Strumenti come lo shellcheck sono lì per aiutarti e avvisano della mancanza di virgolette in tutti i casi sopra.


non funziona sempre. Posso fare un esempio: paste.ubuntu.com/p/8RjR6CS668
recolic

1
Sì, $(..)rimuove i linefeed finali. Puoi usarlo var=$(cat file; printf x); var="${var%x}"per aggirare il problema.
quell'altro ragazzo il

17

Potresti voler sapere perché sta accadendo. Insieme alla grande spiegazione di quell'altro ragazzo , trova un riferimento a Perché il mio script di shell si strozza con spazi bianchi o altri caratteri speciali? scritto da Gilles in Unix e Linux :

Perché devo scrivere "$foo"? Cosa succede senza le virgolette?

$foonon significa "prendere il valore della variabile foo". Significa qualcosa di molto più complesso:

  • Per prima cosa, prendi il valore della variabile.
  • Suddivisione dei campi: tratta quel valore come un elenco di campi separati da spazi e crea l'elenco risultante. Ad esempio, se la variabile contiene foo * bar ​il risultato di questa fase è la lista 3 elementi foo, *, bar.
  • Generazione di nomi di file: tratta ogni campo come un glob, cioè come un pattern di caratteri jolly, e sostituiscilo con l'elenco dei nomi di file che corrispondono a questo modello. Se il pattern non corrisponde ad alcun file, non viene modificato. Nel nostro esempio, questo risulta nella lista contenente foo, seguita dalla lista dei file nella directory corrente e infine bar. Se la directory corrente è vuota, il risultato è foo, *, bar.

Nota che il risultato è un elenco di stringhe. Esistono due contesti nella sintassi della shell: contesto elenco e contesto stringa. La suddivisione dei campi e la generazione del nome file avvengono solo nel contesto dell'elenco, ma è la maggior parte delle volte. Le virgolette doppie delimitano un contesto di stringa: l'intera stringa tra virgolette è una singola stringa, non deve essere divisa. (Eccezione: "$@"per espandere l'elenco dei parametri posizionali, ad es. "$@"È equivalente a "$1" "$2" "$3"se ci sono tre parametri posizionali. Vedere Qual è la differenza tra $ * e $ @? )

Lo stesso accade per la sostituzione del comando con $(foo)o con `foo`. Una nota a margine, non usare `foo`: le sue regole di quotazione sono strane e non portabili, e tutte le shell moderne supportano $(foo)che è assolutamente equivalente tranne che per avere regole di quotazione intuitive.

Anche l'output della sostituzione aritmetica subisce le stesse espansioni, ma normalmente non è un problema in quanto contiene solo caratteri non espandibili (supponendo IFSche non contenga cifre o -).

Vedere Quando è necessaria la doppia citazione? per maggiori dettagli sui casi in cui puoi tralasciare le virgolette.

A meno che tu non intenda far accadere tutta questa trafila, ricorda di usare sempre le virgolette doppie intorno alle sostituzioni di variabili e comandi. Attenzione: tralasciare le virgolette può portare non solo a errori ma a falle nella sicurezza .


7

Oltre ad altri problemi causati dalla mancata citazione, -ne -epossono essere utilizzati echocome argomenti. (Solo il primo è legale per le specifiche POSIX echo, ma diverse implementazioni comuni violano le specifiche e consumano -eanche).

Per evitare ciò, usa printfinvece di echoquando i dettagli contano.

Quindi:

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

Tuttavia, la citazione corretta non ti salverà sempre quando usi echo:

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

... mentre ti salverà con printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n

Sì, abbiamo bisogno di una buona detrazione per questo! Sono d'accordo che questo si adatti al titolo della domanda, ma non credo che avrà la visibilità che merita qui. Che ne dici di una nuova domanda à la "Perché la mia barra rovesciata -e/ -n/ non viene visualizzata?" Possiamo aggiungere collegamenti da qui come appropriato.
quell'altro ragazzo il

Cercavi consumano -npure ?
Pesa Il

1
@PesaThe, no, intendevo -e. Lo standard per echonon specifica l'output quando il suo primo argomento è -n, rendendo legale qualsiasi / tutti i possibili output in quel caso; non esiste tale disposizione -e.
Charles Duffy,

Oh ... non so leggere. Diamo la colpa al mio inglese per questo. Grazie per la spiegazione.
Pesa Il

6

virgolette doppie utente per ottenere il valore esatto. come questo:

echo "${var}"

e leggerà correttamente il tuo valore.


Funziona .. Grazie
Tshilidzi Mudau

2

echo $varl'output dipende molto dal valore della IFSvariabile. Per impostazione predefinita, contiene spazio, tabulazione e caratteri di nuova riga:

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

Ciò significa che quando la shell esegue la suddivisione dei campi (o la divisione delle parole) utilizza tutti questi caratteri come separatori di parole. Questo è ciò che accade quando si fa riferimento a una variabile senza virgolette per echo it ( $var) e quindi l'output atteso viene alterato.

Un modo per impedire la suddivisione delle parole (oltre a utilizzare le virgolette doppie) è impostare IFSsu null. Vedi http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Se il valore di IFS è nullo, non deve essere eseguita la suddivisione dei campi.

L'impostazione su null significa l'impostazione su un valore vuoto:

IFS=

Test:

[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 ~]$ 

2
Dovresti anche set -fimpedire il globbing
quell'altro ragazzo il

@thatotherguy, è davvero necessario per il tuo primo esempio con espansione del percorso? Con IFSimpostato su null, echo $varverrà espanso a echo '/* Foobar is free software */'e l'espansione del percorso non viene eseguita all'interno di singole stringhe tra virgolette.
ks1322

1
Sì. Se mkdir "/this thing called Foobar is free software etc/"vedrai che si espande ancora. È ovviamente più pratico per l' [a-z]esempio.
quell'altro ragazzo il

Capisco, questo ha senso per [a-z]esempio.
ks1322

2

La risposta di ks1322 mi ha aiutato a identificare il problema durante l'utilizzo docker-compose exec:

Se ometti il -Tflag, docker-compose execaggiungi un carattere speciale che interrompe l'output, vediamo binvece di 1b:

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

Con -Tflag, docker-compose execfunziona come previsto:

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

-2

Oltre a mettere la variabile tra virgolette, si potrebbe anche tradurre l'output della variabile utilizzando tre convertendo gli spazi in nuove righe.

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

Sebbene questo sia un po 'più complicato, aggiunge più diversità con l'output poiché è possibile sostituire qualsiasi carattere come separatore tra le variabili dell'array.


2
Ma questo sostituisce tutti gli spazi con le nuove righe. La citazione preserva le nuove righe e gli spazi esistenti.
user000001

È vero, sì. Suppongo che dipenda da cosa c'è nella variabile. In realtà uso tril contrario per creare array da file di testo.
Alek

3
Creare un problema non citando correttamente la variabile e quindi aggirandola con un processo aggiuntivo non corretto non è una buona programmazione.
tripleee

@ Alek, ... ehm, cosa? Non è trnecessario creare correttamente / correttamente un array da un file di testo: puoi specificare qualsiasi separatore desideri impostando IFS. Ad esempio: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')funziona fino a bash 3.2 (la versione più vecchia in circolazione) e imposta correttamente lo stato di uscita su false se catfallisci. E se volessi, ad esempio, tabulazioni invece di nuove righe, dovresti semplicemente sostituire il $'\n'con $'\t'.
Charles Duffy

1
@ Alek, ... se stai facendo qualcosa di simile arrayname=( $( cat file | tr '\n' ' ' ) ), allora è rotto su più livelli: sta inglobando i tuoi risultati (quindi a *si trasforma in un elenco di file nella directory corrente) e funzionerebbe altrettanto bene senza iltr ( o il cat, per quella materia; si potrebbe semplicemente usare arrayname=$( $(<file) )e sarebbe rotto nello stesso modo, ma in modo meno inefficiente).
Charles Duffy
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.