È un approccio sano per "eseguire il backup" della variabile $ IFS?


19

Sono sempre davvero riluttante a scherzare $IFSperché sta bloccando un globale.

Ma spesso rende il caricamento delle stringhe in un array bash piacevole e conciso, e per gli script bash è difficile trovare concisione.

Quindi immagino che potrebbe essere meglio di niente se provo a "salvare" il contenuto iniziale $IFSin un'altra variabile e poi ripristinarlo immediatamente dopo che ho finito di usare $IFSqualcosa.

È pratico? O è essenzialmente inutile e dovrei semplicemente IFStornare indietro a ciò che deve essere per i suoi usi successivi?


Perché non sarebbe pratico?
Bratchley,

Perché l'IFS disordinato farebbe bene il lavoro.
llua,

1
Per coloro che affermano che IFS non funzionante funzionerà bene, tenere presente che è situazionale: stackoverflow.com/questions/39545837/… . Nella mia esperienza, è meglio impostare manualmente IFS sul valore predefinito per il tuo interprete di shell, vale a dire $' \t\n'se stai usando bash. unset $IFSsemplicemente non sempre lo ripristina a quello che ti aspetteresti di essere predefinito.
Darrel Holt,

Risposte:


9

È possibile salvare e assegnare a IFS secondo necessità. Non c'è niente di sbagliato nel farlo. Non è raro salvare il suo valore per il ripristino in seguito a una modifica temporanea e rapida, come nell'esempio di assegnazione dell'array.

Come menziona @llua nel suo commento alla tua domanda, il semplice disinserimento di IFS ripristinerà il comportamento predefinito, equivalente all'assegnazione di uno spazio-tab-newline.

Vale la pena considerare come può essere più problematico non impostare / disinserire IFS in modo esplicito piuttosto che farlo.

Dall'edizione POSIX 2013, 2.5.3 Variabili shell :

Le implementazioni possono ignorare il valore di IFS nell'ambiente o l'assenza di IFS dall'ambiente, nel momento in cui viene invocata la shell, nel qual caso la shell deve impostare IFS su <spazio> <tab> <nuova> quando viene invocata .

Una shell invocata conforme a POSIX può o meno ereditare IFS dal suo ambiente. Da ciò segue:

  • Uno script portatile non può ereditare in modo affidabile IFS attraverso l'ambiente.
  • Uno script che intende utilizzare solo il comportamento di suddivisione predefinito (o l'unione, nel caso di "$*"), ma che può essere eseguito in una shell che inizializza IFS dall'ambiente, deve impostare / disinserire esplicitamente IFS per difendersi dalle intrusioni ambientali.

NB È importante capire che per questa discussione la parola "invocato" ha un significato particolare. Una shell viene invocata solo quando viene esplicitamente chiamata usando il suo nome (incluso uno #!/path/to/shellshebang). Una subshell - come potrebbe essere creata da $(...)o cmd1 || cmd2 &- non è una shell invocata e il suo IFS (insieme alla maggior parte del suo ambiente di esecuzione) è identico a quello del suo genitore. Una shell invocata imposta il valore su $pid, mentre le subshell lo ereditano.


Questa non è semplicemente una inquietudine pedante; c'è un'effettiva divergenza in questo settore. Ecco un breve script che verifica lo scenario usando diverse shell. Esporta un IFS modificato (impostato su :) in una shell invocata che quindi stampa il suo IFS predefinito.

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFS non è generalmente contrassegnato per l'esportazione, ma, se lo fosse, nota come bash, ksh93 e mksh ignorano l'ambiente IFS=:, mentre dash e busybox lo onorano.

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

Alcune informazioni sulla versione:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

Anche se bash, ksh93 e mksh non inizializzano l'IFS dall'ambiente, riesportano l'IFS modificato.

Se per qualsiasi motivo è necessario trasferire IFS in modo portabile attraverso l'ambiente, non è possibile farlo utilizzando IFS stesso; dovrai assegnare il valore a una variabile diversa e contrassegnare quella variabile per l'esportazione. I bambini dovranno quindi assegnare esplicitamente quel valore al loro IFS.


Vedo, quindi se posso parafrasare, è probabilmente più portabile specificare esplicitamente il IFSvalore nella maggior parte delle situazioni in cui deve essere usato, e quindi spesso non è terribilmente produttivo nemmeno tentare di "preservare" il suo valore originale.
Steven Lu,

1
Il problema principale è che se lo script utilizza IFS, dovrebbe impostare / annullare l'impostazione IFS in modo esplicito per garantire che il suo valore sia quello desiderato. In genere, il comportamento dello script dipende da IFS se vi sono espansioni di parametri non readquotate , sostituzioni di comandi non quotate , espansioni aritmetiche non quotate , s o riferimenti tra virgolette doppie $*. Tale elenco è appena al di sopra della mia testa, quindi potrebbe non essere completo (soprattutto se si considerano le estensioni POSIX delle shell moderne).
A piedi nudi IO

10

In generale, è buona norma ripristinare le condizioni predefinite.

Tuttavia, in questo caso, non così tanto.

Perché?:

Inoltre, la memorizzazione del valore IFS ha un problema.
Se l'IFS originale non è stato impostato, il codice IFS="$OldIFS"imposterà IFS su "", non disinserirlo.

Per mantenere effettivamente il valore di IFS (anche se non impostato), utilizzare questo:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.

IFS non può davvero essere disinserito. Se lo si disinserisce, la shell lo riporta al valore predefinito. Quindi non è necessario verificarlo quando lo si salva.
filbranden,

Attenzione che in bash, unset IFSnon riesce a disinserire IFS se fosse stato dichiarato locale in un contesto padre (contesto della funzione) e non nel contesto corrente.
Stéphane Chazelas,

5

Hai ragione a essere titubante nel bloccare un globale. Non temere, è possibile scrivere un codice di lavoro pulito senza mai modificare l'attuale globale IFS, o fare una danza di salvataggio / ripristino ingombrante e soggetta a errori.

Puoi:

  • imposta IFS per una singola chiamata:

    IFS=value command_or_function

    o

  • imposta IFS all'interno di una subshell:

    (IFS=value; statement)
    $(IFS=value; statement)

Esempi

  • Per ottenere una stringa delimitata da virgole da un array:

    str="$(IFS=, ; echo "${array[*]-}")"

    Nota: -serve solo a proteggere un array vuoto set -ufornendo un valore predefinito quando non impostato (tale valore è la stringa vuota in questo caso) .

    La IFSmodifica è applicabile solo all'interno della subshell generata dalla $() sostituzione del comando . Questo perché le subshell hanno copie delle variabili della shell che invoca e possono quindi leggere i loro valori, ma qualsiasi modifica effettuata dalla subshell influisce solo sulla copia della subshell e non sulla variabile del genitore.

    Potresti anche pensare: perché non saltare la subshell e fare solo questo:

    IFS=, str="${array[*]-}"  # Don't do this!

    Qui non c'è invocazione di comando e questa riga viene invece interpretata come due assegnazioni di variabili successive indipendenti, come se fosse:

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    Infine, spieghiamo perché questa variante non funzionerà:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    Il echocomando verrà infatti chiamato con la sua IFSvariabile impostata su ,, ma echonon gli interessa o non usa IFS. La magia dell'espansione "${array[*]}"in una stringa viene fatta dalla (sotto) shell stessa prima echoancora di essere invocata.

  • Per leggere in un intero file (che non contiene NULLbyte) in una singola variabile denominata VAR:

    IFS= read -r -d '' VAR < "${filepath}"

    Nota: IFS=è lo stesso di IFS=""e IFS='', tutto ciò imposta IFS sulla stringa vuota, che è molto diverso da unset IFS: se IFSnon impostato, il comportamento di tutte le funzionalità bash che usano internamente IFSè esattamente lo stesso di se IFSavesse il valore predefinito di $' \t\n'.

    L'impostazione IFSsu una stringa vuota garantisce che gli spazi bianchi iniziali e finali vengano conservati.

    Il -d ''o -d ""dice a read di interrompere solo la sua attuale chiamata su un NULLbyte, invece della solita newline.

  • Per dividere $PATHlungo i suoi :delimitatori:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    Questo esempio è puramente illustrativo. Nel caso generale in cui si sta suddividendo lungo un delimitatore, potrebbe essere possibile che i singoli campi contengano (una versione con escape di quel delimitatore). Pensa di provare a leggere una riga di un .csvfile le cui colonne possono contenere delle virgole (scappate o citate in qualche modo). Lo snippet di cui sopra non funzionerà come previsto per tali casi.

    Detto questo, è improbabile che tu incontri tali :percorsi di contenimento all'interno $PATH. Mentre i percorsi UNIX / Linux possono contenere un :, sembra che bash non sia in grado di gestire tali percorsi se provi ad aggiungerli ai tuoi $PATHfile eseguibili e li memorizzi, poiché non esiste un codice per analizzare i due punti di escape / quotati : codice sorgente di bash 4.4 .

    Infine, nota che lo snippet aggiunge una nuova riga finale all'ultimo elemento dell'array risultante (come indicato da @ StéphaneChazelas nei commenti ora eliminati) e che se l'input è la stringa vuota, l'output sarà un singolo elemento array, in cui l'elemento sarà costituito da una newline ( $'\n').

Motivazione

L' old_IFS="${IFS}"; command; IFS="${old_IFS}"approccio di base che tocca il globale IFSfunzionerà come previsto per il più semplice degli script. Tuttavia, non appena aggiungi complessità, può facilmente rompersi e causare problemi sottili:

  • Se commandè una funzione bash che modifica anche il globale IFS(direttamente o, nascosto alla vista, all'interno di un'altra funzione che chiama), e mentre lo fa per errore usa la stessa old_IFSvariabile globale per fare il salvataggio / ripristino, si ottiene un bug.
  • Come sottolineato in questo commento di @Gilles , se lo stato originale di IFSera non impostato, l'ingenuo salvataggio e ripristino non funzionerebbe e provocherebbe addirittura veri e propri fallimenti se l' opzione di shell comunemente (erroneamente utilizzata set -u(aka set -o nounset) è in vigore.
  • È possibile che alcuni codici shell vengano eseguiti in modo asincrono rispetto al flusso di esecuzione principale, ad esempio con i gestori di segnali (vedere help trap). Se quel codice modifica anche il globale IFSo presuppone che abbia un valore particolare, puoi ottenere bug sottili.

Potresti escogitare una sequenza di salvataggio / ripristino più robusta (come quella proposta in questa altra risposta per evitare alcuni o tutti questi problemi. Tuttavia, dovresti ripetere quel pezzo di codice della piastra della caldaia rumoroso ovunque tu abbia temporaneamente bisogno di un'abitudine IFS. riduce la leggibilità e la manutenibilità del codice.

Ulteriori considerazioni per script simili a librerie

IFSè particolarmente preoccupante per gli autori di librerie di funzioni di shell che devono assicurarsi che il loro codice funzioni in modo robusto indipendentemente dallo stato globale ( IFS, opzioni di shell, ...) imposto dai loro invocatori, e anche senza disturbare affatto quello stato (gli invocatori potrebbero fare affidamento per rimanere sempre statico).

Quando si scrive il codice della libreria, non si può fare affidamento sul fatto di IFSavere un valore particolare (nemmeno quello predefinito) o addirittura di essere impostati affatto. Invece, devi impostare esplicitamente IFSqualsiasi frammento il cui comportamento dipende IFS.

Se IFSè esplicitamente impostato sul valore necessario (anche se quello è quello predefinito) in ogni riga di codice in cui il valore è importante utilizzando uno dei due meccanismi descritti in questa risposta è appropriato per localizzare l'effetto, allora il codice è entrambi indipendente dallo stato globale ed evita di ostacolarlo del tutto. Questo approccio ha l'ulteriore vantaggio di renderlo molto esplicito a una persona che legge la sceneggiatura che IFSconta proprio per questo comando / espansione al minimo costo testuale (rispetto anche al salvataggio / ripristino più elementare).

Quale codice è interessato IFScomunque?

Fortunatamente, non ci sono molti scenari in cui IFSconta (supponendo che tu citi sempre le tue espansioni ):

  • "$*"ed "${array[*]}"espansioni
  • invocazioni del readtargeting integrato per più variabili ( read VAR1 VAR2 VAR3) o una variabile di matrice ( read -a ARRAY_VAR_NAME)
  • invocazioni del readtargeting di una singola variabile quando si tratta di caratteri di spazi bianchi iniziali o finali che appaiono in IFS.
  • suddivisione delle parole (come per le espansioni non quotate, che potresti voler evitare come la peste )
  • alcuni altri scenari meno comuni (Vedi: IFS @ Greg's Wiki )

Non posso dire di aver capito la divisione di $ PATH lungo i suoi: delimitatori supponendo che nessuno dei componenti contenga una frase : stessi . Come potrebbero contenere i componenti :quando :è il delimitatore?
Stéphane Chazelas,

@ StéphaneChazelas Bene, :è un carattere valido da usare in un nome file sulla maggior parte dei filesystem UNIX / Linux, quindi è del tutto possibile avere una directory con un nome contenente :. Forse alcune conchiglie hanno una disposizione a fuggire :in PATH utilizzando qualcosa di simile \:, e quindi si dovrebbe vedere le colonne che appaiono che non sono delimitatori effettivi (sembra bash non consente tale fuga. La funzione di basso livello utilizzato quando l'iterazione oltre $PATHa ricerche per :a una stringa C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 ).
sls

Ho rivisto la risposta per spero di rendere più chiaro l' $PATHesempio di scissione :.
sls

1
Benvenuti in SO! Grazie per una risposta così approfondita :)
Steven Lu

1

È pratico? O è essenzialmente inutile e dovrei semplicemente riportare IFS direttamente su ciò che deve essere per i suoi usi successivi?

Perché rischiare un'impostazione di refuso su IFS $' \t\n'quando tutto ciò che devi fare è

OIFS=$IFS
do_your_thing
IFS=$OIFS

In alternativa, è possibile chiamare una subshell se non sono necessarie variabili impostate / modificate in:

( IFS=:; do_your_thing; )

Questo è pericoloso perché non funziona se IFSinizialmente non è stato impostato.
Gilles 'SO- smetti di essere malvagio' 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.