Ovviamente capisco che si può aggiungere valore alla variabile del separatore di campo interno. Ad esempio:
$ IFS=blah
$ echo "$IFS"
blah
$
Capisco anche che read -r line
salverà i dati da stdin
alla variabile denominata line
:
$ read -r line <<< blah
$ echo "$line"
blah
$
Tuttavia, come può un comando assegnare un valore variabile? E prima memorizza i dati da stdin
alla variabile line
e poi dai il valore di line
a IFS
?
Risposta accettata:
Nelle shell POSIX, read
, senza alcuna opzione non legge una riga , legge parole da una riga (possibilmente barra rovesciata), dove le parole sono $IFS
delimitato e barra rovesciata possono essere utilizzati per evitare i delimitatori (o continuare le righe).
La sintassi generica è:
read word1 word2... remaining_words
read
legge lo stdin un byte alla volta¹ finché non trova un carattere di nuova riga senza escape (o fine input), lo divide in base a regole complesse e memorizza il risultato di tale divisione in $word1
, $word2
… $remaining_words
.
Ad esempio su un input come:
<tab> foo bar baz blah blah
whatever whatever
e con il valore predefinito di $IFS
, read a b c
assegnerebbe:
$a
⇐foo
$b
⇐bar baz
$c
⇐blah blahwhatever whatever
Ora, se viene passato solo un argomento, quello non diventa read line
. È ancora read remaining_words
. L'elaborazione della barra rovesciata è ancora eseguita, gli spazi bianchi IFS² vengono ancora rimossi dall'inizio e dalla fine.
Il -r
l'opzione rimuove l'elaborazione della barra rovesciata. Quindi lo stesso comando sopra con -r
assegnerebbe invece
$a
⇐foo
$b
⇐bar
$c
⇐baz blah blah
Ora, per la parte di divisione, è importante rendersi conto che ci sono due classi di caratteri per $IFS
:i caratteri degli spazi bianchi IFS² (inclusi spazio e tabulazione (e newline, anche se qui non importa se non usi -d), che si trovano anche nel valore predefinito di $IFS
) e gli altri. Il trattamento per queste due classi di caratteri è diverso.
Con IFS=:
(:
non essendo un carattere di spazio vuoto IFS), un input come :foo::bar::
verrebbe suddiviso in ""
, "foo"
, ""
, bar
e ""
(e un ulteriore ""
con alcune implementazioni, tuttavia, non importa tranne che per read -a
). Mentre se sostituiamo quel :
con lo spazio, la divisione viene eseguita solo in foo
e bar
. Cioè quelli iniziali e finali vengono ignorati e le loro sequenze vengono trattate come tali. Esistono regole aggiuntive quando gli spazi bianchi e i caratteri non vuoti vengono combinati in $IFS
. Alcune implementazioni possono aggiungere/rimuovere il trattamento speciale raddoppiando i caratteri in IFS (IFS=::
o IFS=' '
).
Quindi qui, se non vogliamo che gli spazi vuoti iniziali e finali senza caratteri di escape vengano rimossi, dobbiamo rimuovere quei caratteri di spazio vuoto IFS da IFS.
Anche con caratteri IFS diversi da spazi bianchi, se la riga di input contiene uno (e solo uno) di quei caratteri ed è l'ultimo carattere della riga (come IFS=: read -r word
su un input come foo:
) con shell POSIX (non zsh
né alcuni pdksh
versioni), quell'input è considerato come un foo
word perché in quelle shell, i caratteri $IFS
sono considerati terminatori , quindi word
conterrà foo
, non foo:
.
Quindi, il modo canonico di leggere una riga di input con read
integrato è:
IFS= read -r line
(nota che per la maggior parte dei read
implementazioni, che funziona solo per le righe di testo poiché il carattere NUL non è supportato tranne che in zsh
).
Usando var=value cmd
la sintassi assicura che IFS
è impostato in modo diverso solo per la durata di quel cmd
comando.
Nota sulla cronologia
Il read
builtin è stato introdotto dalla shell Bourne ed era già per leggere parole , non linee. Ci sono alcune differenze importanti con le moderne shell POSIX.
La read
della shell Bourne non supportava un -r
opzione (che è stata introdotta dalla shell Korn), quindi non c'è modo di disabilitare l'elaborazione della barra rovesciata se non la pre-elaborazione dell'input con qualcosa come sed 's/\/&&/g'
lì.
La shell Bourne non aveva quella nozione di due classi di caratteri (che di nuovo è stata introdotta da ksh). Nella shell Bourne tutti i caratteri subiscono lo stesso trattamento dei caratteri degli spazi bianchi IFS in ksh, ovvero IFS=: read a b c
su un input come foo::bar
assegnerebbe bar
a $b
, non la stringa vuota.
Nella shell Bourne, con:
var=value cmd
Se cmd
è un built-in (come read
è), var
rimane impostato su value
dopo cmd
ha finito. Questo è particolarmente critico con $IFS
perché nella shell Bourne, $IFS
serve per dividere tutto, non solo le espansioni. Inoltre, se rimuovi lo spazio da $IFS
nella shell Bourne, "[email protected]"
non funziona più.
Nella shell Bourne, il reindirizzamento di un comando composto ne provoca l'esecuzione in una sottoshell (nelle prime versioni, anche cose come read var < file
o exec 3< file; read var <&3
non funzionava), quindi era raro nella shell Bourne usare read
per qualsiasi cosa tranne l'input dell'utente sul terminale (dove aveva senso la gestione della continuazione della riga)
Alcuni Unice (come HP/UX, ce n'è uno anche in util-linux
) hanno ancora una line
comando per leggere una riga di input (che era un comando UNIX standard fino alla specifica UNIX singola versione 2).
È praticamente lo stesso di head -n 1
tranne per il fatto che legge un byte alla volta per assicurarsi che non legga più di una riga. Su quei sistemi, puoi fare:
line=`line`
Ovviamente, ciò significa generare un nuovo processo, eseguire un comando e leggerne l'output attraverso una pipe, quindi molto meno efficiente della IFS= read -r line
di ksh , ma ancora molto più intuitivo.