Comprensione di "IFS = read -r line"


61

Ovviamente capisco che si può aggiungere valore alla variabile del separatore di campo interno. Per esempio:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Capisco anche che read -r linesalverà i dati dalla stdinvariabile denominata line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Tuttavia, come può un comando assegnare un valore variabile? E prima memorizza i dati da stdina variabili linee quindi dà valore linea IFS?


Risposte:


104

Alcune persone hanno quell'idea errata che readè il comando di leggere una riga. Non è.

readlegge le parole da una riga (possibilmente con barra rovesciata), in cui le parole sono $IFSdelimitate e la barra rovesciata può essere utilizzata per sfuggire ai delimitatori (o per le linee continue).

La sintassi generica è:

read word1 word2... remaining_words

readlegge stdin un byte alla volta finché non trova un carattere di nuova riga escape (o fine input), spaccature che secondo regole complesse e memorizza il risultato di tale frazionamento in $word1, $word2... $remaining_words.

Ad esempio su un input come:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

e con il valore predefinito di $IFS, read a b cassegnerebbe:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Ora se viene passato solo un argomento, questo non diventa read line. È ancora read remaining_words. L'elaborazione della barra rovesciata viene ancora eseguita, i caratteri degli spazi bianchi IFS vengono comunque rimossi dall'inizio e dalla fine.

L' -ropzione rimuove l'elaborazione della barra rovesciata. Quindi lo stesso comando sopra con -rinvece avrebbe assegnato

  • $afoo
  • $bbar\
  • $cbaz bl\ah 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 (ovvero spazio e tabulazione (e newline, anche se qui non importa a meno che non si usi -d), che si verificano anche essere nel valore predefinito di $IFS) e gli altri. Il trattamento per queste due classi di personaggi è diverso.

Con IFS=:( :essendo non un IFS spazi di carattere), un ingresso come :foo::bar::sarebbe diviso in "", "foo", "", bare ""(e un extra ""con alcune implementazioni anche se questo non importa tranne read -a). Mentre se lo sostituiamo :con lo spazio, la divisione viene eseguita in solo fooe bar. Ciò significa che quelli iniziali e finali vengono ignorati e le loro sequenze vengono trattate come una sola. Ci sono regole aggiuntive quando si combinano caratteri bianchi e non bianchi $IFS. Alcune implementazioni possono aggiungere / rimuovere il trattamento speciale raddoppiando i caratteri in IFS ( IFS=::o IFS=' ').

Quindi, qui, se non vogliamo che vengano eliminati i caratteri di spazi bianchi senza escape iniziali e finali, dobbiamo rimuovere quei caratteri di spazi bianchi IFS da IFS.

Anche con caratteri IFS non di 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 wordsu un input come foo:) con shell POSIX (non zshné alcune pdkshversioni), quell'input è considerato come una sola fooparola perché in quelle shell, i caratteri $IFSsono considerati come terminatori , quindi wordconterranno foo, non foo:.

Quindi, il modo canonico di leggere una riga di input con l' readintegrato è:

IFS= read -r line

(nota che per la maggior parte delle readimplementazioni, che funziona solo per le righe di testo in quanto il carattere NUL non è supportato tranne in zsh).

L'uso della var=value cmdsintassi assicura che IFSsia impostato diversamente solo per la durata di quel cmdcomando.

Nota storica

Il readbuiltin fu introdotto dalla shell Bourne e doveva già leggere le parole , non le righe. Ci sono alcune differenze importanti con le moderne shell POSIX.

La shell Bourne readnon supportava -run'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 di simile sed 's/\\/&&/g'lì.

La shell Bourne non aveva quella nozione di due classi di personaggi (che di nuovo fu introdotta da ksh). Nella Bourne shell tutti i caratteri subiscono lo stesso trattamento IFS spazi caratteri fare a ksh, che è IFS=: read a b csu un ingresso come foo::barassegnerebbe bara $b, non la stringa vuota.

Nella shell Bourne, con:

var=value cmd

Se cmdè un built-in (come readè), varrimane impostato su valuedopo che cmdè terminato. Questo è particolarmente critico $IFSperché nella shell Bourne $IFSviene utilizzato per dividere tutto, non solo le espansioni. Inoltre, se si rimuove il carattere spazio dalla $IFSshell Bourne, "$@"non funziona più.

Nella shell Bourne, il reindirizzamento di un comando composto provoca l'esecuzione in una subshell (nelle prime versioni, anche cose come read var < fileo exec 3< file; read var <&3non funzionavano), quindi era raro nella shell Bourne utilizzare readper qualsiasi cosa tranne l'input dell'utente sul terminale (dove aveva senso la gestione della continuazione di linea)

Alcuni Unices (come HP / UX, ce n'è anche uno in util-linux) hanno ancora un linecomando per leggere una riga di input (che era un comando UNIX standard fino alla specifica Single UNIX versione 2 ).

Questo è fondamentalmente lo stesso head -n 1tranne 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 leggere il suo output attraverso una pipe, quindi molto meno efficiente di quello di Ksh IFS= read -r line, ma ancora molto più intuitivo.


3
+1 Grazie per alcune utili informazioni sui diversi trattamenti su spazio / scheda vs "altri" in IFS a bash ... Sapevo che erano trattati in modo diverso, ma questa spiegazione semplifica molto tutto. (E l'intuizione tra bash (e altre shell posix) e le shdifferenze regolari è utile anche per scrivere script portatili!)
Olivier Dulac,

Almeno per bash-4.4.19, while read -r; do echo "'$REPLY'"; donefunziona come while IFS= read -r line; do echo "'$line'"; done.
x-yuri,

Questo: "... quell'idea errata che legge è il comando per leggere una riga ..." mi porta a pensare che se usare la readlettura di una riga è errato, ci deve essere qualcos'altro. Cosa potrebbe essere questa nozione non errata? O questa prima affermazione è tecnicamente corretta, ma in verità l'idea non errata è: "read è il comando per leggere le parole da una riga. Perché è così potente, puoi usarlo per leggere le righe da un file facendo: IFS= read -r line"
Mike S,

8

La teoria

Ci sono due concetti che sono in gioco qui:

  • IFSè il separatore del campo di input, il che significa che la stringa letta verrà suddivisa in base ai caratteri in IFS. Su una riga di comando, IFSnormalmente sono presenti tutti gli spazi bianchi, ecco perché la riga di comando si divide in spazi.
  • Fare qualcosa del genere VAR=value commandsignifica "modificare l'ambiente di comando in modo che VARabbia il valore value". Fondamentalmente, il comando commandvedrà VARcome avere il valore value, ma qualsiasi comando eseguito dopo vedrà comunque VARcome avere il valore precedente. In altre parole, quella variabile verrà modificata solo per quell'istruzione.

In questo caso

Quindi, quando lo fai IFS= read -r line, quello che stai facendo è impostare IFSuna stringa vuota (nessun carattere verrà usato per dividere, quindi non si verificherà alcuna divisione) in modo che readlegga l'intera riga e la veda come una parola che verrà assegnata alla linevariabile. Le modifiche IFSinfluiscono solo su tale affermazione, in modo che i seguenti comandi non siano interessati dalla modifica.

Come nota a margine

Mentre il comando è corretto e funziona come previsto, modificando IFSin questo caso non è potrebbe 1 non essere necessaria. Come scritto nella bashpagina man nella readsezione integrata:

Una riga viene letta dall'input standard [...] e la prima parola viene assegnata al primo nome, la seconda parola al secondo nome e così via, con le parole rimanenti e i loro separatori intermedi assegnati al cognome . Se il flusso di input legge un numero inferiore di parole rispetto ai nomi, ai nomi rimanenti vengono assegnati valori vuoti. I caratteri in IFSsono usati per dividere la linea in parole. [...]

Dato che hai solo la linevariabile, ogni parola le verrà assegnata comunque, quindi se non hai bisogno di nessuno dei precedenti e finali caratteri di spazi bianchi 1 potresti semplicemente scrivere read -r linee farci .

[1] Proprio come un esempio di come un valore unsetpredefinito $IFScauserà lo spazio bianco IFS inizialeread / finale , potresti provare:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Eseguilo e vedrai che i personaggi precedenti e finali non sopravviveranno se IFSnon sono disinseriti. Inoltre, potrebbero accadere alcune cose strane se $IFSdovesse essere modificato da qualche parte in precedenza nella sceneggiatura.


5

Si consiglia di leggere questa affermazione in due parti, la prima cancella il valore della variabile IFS, cioè è equivalente al più leggibile IFS="", la seconda è la lettura del linevariabile da stdin, read -r line.

Ciò che è specifico in questa sintassi è che l'affetto IFS è transcient e valido solo per il readcomando.

A meno che non mi manchi qualcosa, in quel caso particolare la cancellazione IFSnon ha alcun effetto, dato che qualunque cosa IFSsia impostata, l'intera riga verrà letta nella linevariabile. Ci sarebbe stato un cambiamento nel comportamento solo nel caso in cui più di una variabile fosse stata passata come parametro readall'istruzione.

Modificare:

L' -rè lì per consentire l'input termina con \non da lavorare appositamente, cioè la barra rovesciata da includere nella linevariabile e non come un carattere di continuazione per consentire input multi-linea.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

La cancellazione dell'IFS ha l'effetto collaterale di impedire la lettura per tagliare potenziali spazi iniziali e finali o caratteri di tabulazione, ad esempio:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Grazie a rici per aver indicato questa differenza.


Quello che ti manca è che se IFS non viene modificato, read -r linetaglierà gli spazi bianchi iniziali e finali prima di assegnare l'input alla linevariabile.
rici,

@rici Sospettavo qualcosa del genere, ma controllavo solo i caratteri IFS tra le parole, non quelli iniziali / finali. Grazie per aver sottolineato questo fatto!
jlliagre,

la cancellazione dell'IFS impedirà anche l'assegnazione di più variabili (effetto collaterale). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"mostrerà-aa bb--
Kyodev il
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.