Perché il mio script shell si soffoca su spazi bianchi o altri caratteri speciali?


284

Oppure, una guida introduttiva alla solida gestione dei nomi dei file e altre stringhe che passano negli script della shell.

Ho scritto uno script di shell che funziona bene per la maggior parte del tempo. Ma soffoca su alcuni input (ad esempio su alcuni nomi di file).

Ho riscontrato un problema come il seguente:

  • Ho un nome file contenente uno spazio hello world, ed è stato trattato come due file separati helloe world.
  • Ho una linea di input con due spazi consecutivi e si sono ridotti a uno nell'input.
  • Gli spazi bianchi iniziali e finali scompaiono dalle righe di input.
  • A volte, quando l'input contiene uno dei caratteri \[*?, vengono sostituiti da del testo che è in realtà il nome dei file.
  • C'è un apostrofo '(o una doppia citazione ") nell'input e le cose sono diventate strane dopo quel punto.
  • C'è una barra rovesciata nell'input (o: sto usando Cygwin e alcuni dei miei nomi di file hanno \separatori in stile Windows ).

Cosa sta succedendo e come posso risolverlo?


16
shellcheckaiutarti a migliorare la qualità dei tuoi programmi.
aurelien,

3
Oltre alle tecniche protettive descritte nelle risposte, e sebbene sia probabilmente ovvio per la maggior parte dei lettori, penso che valga la pena di commentare che quando i file sono destinati ad essere elaborati utilizzando gli strumenti da riga di comando, è buona prassi evitare personaggi fantasiosi nel nomi in primo luogo, se possibile.
bli


1
@bli No, ciò richiede solo più tempo per la visualizzazione dei bug. Oggi nasconde bug. E ora, non conosci tutti i nomi di file utilizzati successivamente con il tuo codice.
Volker Siegel,

Prima di tutto, se i parametri contengono spazi, è necessario che siano citati andando nella riga di comando. Tuttavia, puoi prendere l'intera riga di comando e analizzarla da solo. Due spazi non si trasformano in uno spazio; qualsiasi quantità di spazio dice al tuo script che è la prossima variabile, quindi se fai qualcosa come "echo $ 1 $ 2" è il tuo script che mette uno spazio in mezzo. Utilizzare anche "find (-exec)" per scorrere i file con spazi anziché un ciclo for; puoi gestire gli spazi più facilmente.
Patrick Taylor,

Risposte:


352

Usare sempre le virgolette intorno sostituzioni variabili e sostituzioni di comando: "$foo","$(foo)"

Se si utilizza $foonon quotato, lo script verrà soffocato su input o parametri (o output di comando, con $(foo)) contenenti spazi bianchi o \[*?.

Lì, puoi smettere di leggere. Bene, ecco alcuni altri:

  • read- Per leggere l'input riga per riga con l' readintegrato, utilizzare lewhile IFS= read -r line; do … barre rovesciate e gli spazi bianchi in modo
    normale read.
  • xargs- Evitaxargs . Se devi usare xargs, fallo xargs -0. Invece di find … | xargs, preferiscifind … -exec … .
    xargstratta in particolare gli spazi bianchi e i personaggi \"'.

Questa risposta vale per le coperture / POSIX-style Bourne ( sh, ash, dash, bash, ksh, mksh, yash...). Gli utenti di Zsh dovrebbero saltarlo e leggere la fine di Quando è necessaria la doppia citazione? anziché. Se vuoi tutto il nocciolo, leggi lo standard o il manuale della tua shell.


Si noti che le spiegazioni seguenti contengono alcune approssimazioni (affermazioni che sono vere nella maggior parte delle condizioni ma che possono essere influenzate dal contesto circostante o dalla configurazione).

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

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

  • Innanzitutto, prendi il valore della variabile.
  • Suddivisione dei campi: considera quel valore come un elenco di campi separato da spazi bianchi 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 del nome file: tratta ogni campo come un glob, ovvero come un modello jolly, e sostituiscilo con l'elenco dei nomi di file che corrispondono a questo modello. Se il modello non corrisponde ad alcun file, viene lasciato non 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.

Si noti 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 è il più delle volte. Le virgolette doppie delimitano un contesto di stringa: l'intera stringa tra virgolette è una stringa singola, da non suddividere. (Eccezione: "$@"espandere l'elenco dei parametri posizionali, ad es. "$@"Equivale a "$1" "$2" "$3"se ci sono tre parametri posizionali. Vedi Qual è la differenza tra $ * e $ @? )

Lo stesso accade per comandare la sostituzione con $(foo)o con `foo`. Per contro, non usare `foo`: le sue regole di quotazione sono strane e non portatili, e supportano tutte le shell moderne $(foo)che sono assolutamente equivalenti tranne che per avere regole di quotazione intuitive.

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

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

A meno che tu non voglia che accada tutto questo rigmarole, ricorda solo di usare sempre virgolette doppie attorno alla sostituzione di variabili e comandi. Fai attenzione: tralasciare le virgolette può portare non solo a errori ma a buchi di sicurezza .

Come posso elaborare un elenco di nomi di file?

Se scrivi myfiles="file1 file2", con spazi per separare i file, questo non può funzionare con nomi di file contenenti spazi. I nomi dei file Unix possono contenere qualsiasi carattere diverso da /(che è sempre un separatore di directory) e byte null (che non è possibile utilizzare negli script di shell con la maggior parte delle shell).

Lo stesso problema con myfiles=*.txt; … process $myfiles. Quando lo fai, la variabile myfilescontiene la stringa di 5 caratteri *.txted è quando scrivi $myfilesche il carattere jolly è espanso. Questo esempio funzionerà effettivamente, fino a quando non cambi il tuo script myfiles="$someprefix*.txt"; … process $myfiles. Se someprefiximpostato su final report, questo non funzionerà.

Per elaborare un elenco di qualsiasi tipo (come i nomi dei file), inserirlo in un array. Ciò richiede mksh, ksh93, yash o bash (o zsh, che non ha tutti questi problemi di quotazione); una semplice shell POSIX (come ash o dash) non ha variabili di array.

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88 ha variabili di array con una diversa sintassi dell'assegnazione set -A myfiles "someprefix"*.txt(vedere la variabile di assegnazione in un diverso ambiente ksh se è necessaria la portabilità di ksh88 / bash). Le shell in stile Bourne / POSIX hanno un solo array, l'array di parametri posizionali con "$@"cui si imposta sete che è locale in una funzione:

set -- "$someprefix"*.txt
process -- "$@"

E i nomi dei file che iniziano con -?

In una nota correlata, tieni presente che i nomi dei file possono iniziare con un -(trattino / meno), che la maggior parte dei comandi interpreta come un'opzione. Se hai un nome file che inizia con una parte variabile, assicurati di passarlo --prima, come nel frammento sopra. Ciò indica al comando che ha raggiunto la fine delle opzioni, quindi qualsiasi cosa successiva è un nome file anche se inizia con -.

In alternativa, puoi assicurarti che i nomi dei tuoi file inizino con un carattere diverso da -. I nomi di file assoluti iniziano con /ed è possibile aggiungere ./all'inizio dei nomi relativi. Il frammento seguente trasforma il contenuto della variabile fin un modo "sicuro" di fare riferimento allo stesso file che è garantito non iniziare -.

case "$f" in -*) "f=./$f";; esac

In una nota finale su questo argomento, attenzione che alcuni comandi interpretano -come input standard o output standard, anche dopo --. Se hai bisogno di fare riferimento a un vero file chiamato -, o se stai chiamando un tale programma e non vuoi che legga da stdin o scriva su stdout, assicurati di riscrivere -come sopra. Vedi Qual è la differenza tra "du -sh *" e "du -sh ./*"? per ulteriori discussioni.

Come posso memorizzare un comando in una variabile?

"Comando" può significare tre cose: un nome di comando (il nome come eseguibile, con o senza percorso completo, o il nome di una funzione, builtin o alias), un nome di comando con argomenti o un pezzo di codice shell. Esistono pertanto diversi modi per memorizzarli in una variabile.

Se si dispone di un nome di comando, è sufficiente memorizzarlo e utilizzare la variabile con virgolette doppie come al solito.

command_path="$1"

"$command_path" --option --message="hello world"

Se hai un comando con argomenti, il problema è lo stesso di un elenco di nomi di file sopra: questo è un elenco di stringhe, non una stringa. Non puoi semplicemente inserire gli argomenti in una singola stringa con spazi in mezzo, perché se lo fai non puoi distinguere gli spazi che fanno parte degli argomenti e gli spazi che separano gli argomenti. Se la shell ha array, puoi usarli.

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

Cosa succede se si utilizza una shell senza array? Puoi comunque utilizzare i parametri posizionali, se non ti dispiace modificarli.

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

Cosa succede se è necessario memorizzare un comando shell complesso, ad esempio con reindirizzamenti, pipe, ecc.? O se non vuoi modificare i parametri posizionali? Quindi è possibile creare una stringa contenente il comando e utilizzare il comando evalincorporato.

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

Nota le virgolette nidificate nella definizione di code: le virgolette singole '…'delimitano una stringa letterale, in modo che il valore della variabile codesia la stringa /path/to/executable --option --message="hello world" -- /path/to/file1. Il evalbuiltin dice alla shell di analizzare la stringa passata come argomento come se fosse apparsa nello script, quindi a quel punto vengono analizzate le virgolette e la pipe, ecc.

L'uso evalè complicato. Pensa attentamente a cosa viene analizzato quando. In particolare, non puoi semplicemente inserire un nome di file nel codice: devi citarlo, proprio come faresti se fosse in un file di codice sorgente. Non esiste un modo diretto per farlo. Qualcosa di simile code="$code $filename"si rompe se il nome del file contiene un carattere speciale guscio (spazi, $, ;, |, <, >, etc.). code="$code \"$filename\""si rompe ancora "$\`. Si code="$code '$filename'"interrompe anche se il nome del file contiene a '. Esistono due soluzioni.

  • Aggiungi uno strato di virgolette attorno al nome del file. Il modo più semplice per farlo è quello di aggiungere virgolette singole e sostituirle con '\''.

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
  • Mantieni l'espansione della variabile all'interno del codice, in modo che venga cercata quando viene valutato il codice, non quando viene creato il frammento di codice. Questo è più semplice ma funziona solo se la variabile è ancora in giro con lo stesso valore al momento dell'esecuzione del codice, non ad esempio se il codice è incorporato in un ciclo.

    code="$code \"\$filename\""

Infine, hai davvero bisogno di una variabile contenente codice? Il modo più naturale di assegnare un nome a un blocco di codice è definire una funzione.

Che succede read?

Senza -r, readconsente le linee di continuazione - questa è una singola linea logica di input:

hello \
world

readdivide la riga di input in campi delimitati da caratteri $IFS(senza -r, anche la barra rovesciata sfugge a quelli). Ad esempio, se l'input è una riga contenente tre parole, quindi read first second thirdimposta firstsulla prima parola di input, secondsulla seconda parola e thirdsulla terza parola. Se ci sono più parole, l'ultima variabile contiene tutto ciò che rimane dopo aver impostato le precedenti. Gli spazi bianchi iniziali e finali vengono tagliati.

L'impostazione IFSsu una stringa vuota evita qualsiasi taglio. Vedi Perché è `while IFS = read` usato così spesso, invece di` IFS =; mentre leggi..`? per una spiegazione più lunga.

Cosa c'è che non va xargs?

Il formato di input di xargsè stringhe separate da spazi bianchi che possono essere opzionalmente a virgoletta singola o doppia. Nessuno strumento standard genera questo formato.

L'input per xargs -L1o xargs -lè quasi un elenco di linee, ma non del tutto - se c'è uno spazio alla fine di una linea, la riga seguente è una linea di continuazione.

Puoi usare xargs -0dove applicabile (e dove disponibile: GNU (Linux, Cygwin), BusyBox, BSD, OSX, ma non è in POSIX). È sicuro, perché i byte null non possono apparire nella maggior parte dei dati, in particolare nei nomi dei file. Per produrre un elenco di nomi file separati da null, utilizzare find … -print0(oppure è possibile utilizzare find … -exec …come spiegato di seguito).

Come posso elaborare i file trovati da find?

find  -exec some_command a_parameter another_parameter {} +

some_commanddeve essere un comando esterno, non può essere una funzione di shell o un alias. Se è necessario richiamare una shell per elaborare i file, chiamare in modo shesplicito.

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

Ho qualche altra domanda

Sfoglia il tag di su questo sito o o . (Fai clic su "Ulteriori informazioni ..." per visualizzare alcuni suggerimenti generali e un elenco selezionato manualmente di domande comuni.) Se hai cercato e non riesci a trovare una risposta, chiedi pure .


6
@ John1024 È solo una funzione GNU, quindi continuerò con "nessuno strumento standard".
Gilles,

2
Hai anche bisogno di virgolette $(( ... ))(anche $[...]in alcune shell) tranne in zsh(anche in emulazione sh) e mksh.
Stéphane Chazelas,

3
Si noti che xargs -0non è POSIX. Tranne che con FreeBSD xargs, in genere si desidera xargs -r0invece di xargs -0.
Stéphane Chazelas,

2
@ John1024, no, ls --quoting-style=shell-alwaysnon è compatibile con xargs. Provatouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas,

3
Un'altra caratteristica interessante (solo GNU) è xargs -d "\n"quella di poter eseguire ad es. locate PATTERN1 |xargs -d "\n" grep PATTERN2Per cercare nomi di file corrispondenti a PATTERN1 con contenuti corrispondenti a PATTERN2 . Senza GNU, puoi farlo ad es. Comelocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz

26

Mentre la risposta di Gilles è eccellente, prendo in esame il suo punto principale

Usa sempre virgolette doppie attorno a sostituzioni variabili e sostituzioni di comandi: "$ foo", "$ (foo)"

Quando inizi con una shell simile a Bash che fa la divisione delle parole, sì, certo, il consiglio sicuro è sempre usare le virgolette. Tuttavia, la divisione delle parole non viene sempre eseguita

§ Suddivisione delle parole

Questi comandi possono essere eseguiti senza errori

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

Non sto incoraggiando gli utenti ad adottare questo comportamento, ma se qualcuno capisce fermamente quando si verifica la divisione delle parole, dovrebbero essere in grado di decidere autonomamente quando utilizzare le virgolette.


19
Come menziono nella mia risposta, consultare unix.stackexchange.com/questions/68694/… per i dettagli. Notare la domanda: "Perché la mia shell script soffoca?". Il problema più comune (da anni di esperienza in questo sito e altrove) è la mancanza di virgolette doppie. "Usa sempre virgolette doppie" è più facile da ricordare rispetto a "usa sempre virgolette doppie, ad eccezione di questi casi in cui non sono necessarie".
Gilles,

14
Le regole sono difficili da capire per i principianti. Per esempio, foo=$barè OK, ma export foo=$baro env foo=$varnon sono (almeno in alcune conchiglie). Un consiglio per i principianti: cita sempre le tue variabili se non sai cosa stai facendo e hai una buona ragione per non farlo .
Stéphane Chazelas,

5
@StevenPenny È davvero più corretto? Ci sono casi ragionevoli in cui le virgolette rompono la sceneggiatura? In situazioni in cui in metà casi si devono usare le virgolette e in altre metà le virgolette possono essere usate opzionalmente - allora una raccomandazione "usa sempre virgolette, nel caso in cui" sia quella che dovrebbe essere pensata, poiché è vera, semplice e meno rischiosa. Insegnare tali elenchi di eccezioni ai principianti è ben noto per essere inefficace (privo di contesto, non li ricorderanno) e controproducente, poiché confonderanno citazioni necessarie / non necessarie, rompendo i loro copioni e demotivandole per imparare ulteriormente.
Peteris,

6
Il mio $ 0,02 sarebbe che raccomandare di citare tutto è un buon consiglio. Citare erroneamente qualcosa che non ne ha bisogno è innocuo, erroneamente non citare qualcosa che ne ha bisogno è dannoso. Quindi, per la maggior parte degli autori di script di shell che non capiranno mai la complessità di quando si verifica esattamente la divisione delle parole, citare tutto è molto più sicuro che cercare di citare solo dove necessario.
godlygeek,

5
@Peteris e godlygeek: "Ci sono casi ragionevoli in cui le virgolette potrebbero spezzare la sceneggiatura?" Dipende dalla tua definizione di "ragionevole". Se imposta uno script criteria="-type f", allora find . $criteriafunziona ma find . "$criteria"non lo fa.
G-Man,

22

Per quanto ne so, ci sono solo due casi in cui è necessario racchiudere tra virgolette le espansioni, e quei casi riguardano i due parametri speciali della shell "$@"e "$*"- che sono specificati per espandersi in modo diverso se racchiusi tra virgolette doppie. In tutti gli altri casi (escludendo, forse, implementazioni di array specifici della shell), il comportamento di un'espansione è una cosa configurabile - ci sono opzioni per questo.

Questo non vuol dire, ovviamente, che le doppie virgolette dovrebbero essere evitate - al contrario, è probabilmente il metodo più conveniente e robusto per delimitare un'espansione che la shell ha da offrire. Tuttavia, penso che, poiché sono già state esposte sapientemente alternative, questo è un posto eccellente per discutere di ciò che accade quando la shell espande un valore.

Il guscio, nel suo cuore e l'anima (per coloro che hanno tale) , è un comando interprete - si tratta di un parser, come un grande, interattivo, sed. Se la tua istruzione shell sta soffocando su spazi bianchi o simili, è molto probabile perché non hai compreso appieno il processo di interpretazione della shell, specialmente come e perché traduce un'istruzione di input in un comando utilizzabile. Il compito della shell è di:

  1. accetta input

  2. interpretare e dividerlo correttamente in parole di input tokenizzate

    • le parole di input sono gli elementi della sintassi della shell come $wordoecho $words 3 4* 5

    • le parole sono sempre divise su spazi bianchi - questa è solo sintassi - ma solo i caratteri letterali di spazi bianchi sono serviti sulla shell nel suo file di input

  3. espandere quelli se necessario in più campi

    • i campi derivano da espansioni di parole : costituiscono il comando eseguibile finale

    • eccetto "$@", $IFS campo-splitting , ed espansione di percorso un ingresso parola deve sempre restituire un singolo campo .

  4. e quindi per eseguire il comando risultante

    • nella maggior parte dei casi ciò comporta la trasmissione dei risultati della sua interpretazione in una forma o nell'altra

Le persone spesso dicono che la shell è una colla e, se questo è vero, allora ciò che sta attaccando sono elenchi di argomenti - o campi - a un processo o ad un altro quando si tratta di execessi. La maggior parte delle shell non gestisce NULbene il byte - se non del tutto - e questo perché si stanno già dividendo su di esso. La shell deve fare exec molto e deve farlo con un NULarray delimitato di argomenti che passa al kernel di sistema alla execvolta. Se dovessi mescolare il delimitatore della shell con i suoi dati delimitati, probabilmente la shell lo rovinerebbe. Le sue strutture di dati interne - come la maggior parte dei programmi - si basano su quel delimitatore. zsh, in particolare, non rovina tutto.

Ed è qui che $IFSentra in gioco. $IFSÈ un parametro di shell sempre presente - e allo stesso modo impostabile - che definisce come la shell dovrebbe dividere le espansioni della shell da parola a campo - in particolare su quali valori dovrebbero delimitare quei campi . $IFSsuddivide le espansioni della shell su delimitatori diversi da NUL- o, in altre parole, la shell sostituisce i byte risultanti da un'espansione che corrisponde a quelli nel valore di $IFScon NULnei suoi array di dati interni. Quando lo guardi in quel modo potresti iniziare a vedere che ogni espansione della shell suddivisa in campi è un $IFSarray di dati delimitato.

È importante capire che delimita$IFS solo espansioni che non sono già delimitate altrimenti, cosa che puoi fare con virgolette doppie. Quando citate un'espansione, la delimitate alla testa e almeno alla coda del suo valore. In questi casi non si applica in quanto non ci sono campi da separare. In effetti, un'espansione tra virgolette mostra un comportamento di divisione del campo identico a un'espansione non quotata quando è impostato su un valore vuoto."$IFSIFS=

A meno che non sia citato, $IFSè esso stesso $IFSun'espansione delimitata della shell. Il valore predefinito è un valore specificato di <space><tab><newline>- tutti e tre i quali presentano proprietà speciali se contenuti all'interno $IFS. Considerando che qualsiasi altro valore per $IFSè specificato per valutare un singolo campo per occorrenza di espansione , gli $IFS spazi bianchi - uno qualsiasi di questi tre - sono specificati per elidire in un singolo campo per sequenza di espansione e le sequenze iniziali / finali vengono eluse interamente. Questo è probabilmente il più facile da capire tramite l'esempio.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Ma questo è solo $IFS- solo la divisione delle parole o lo spazio bianco come richiesto, quindi che dire dei personaggi speciali ?

La shell, per impostazione predefinita, espande anche alcuni token non quotati (come ?*[quelli notati altrove qui) in più campi quando si verificano in un elenco. Questo si chiama espansione del nome di percorso o globbing . È uno strumento incredibilmente utile e, poiché si verifica dopo la divisione dei campi nell'ordine di analisi della shell, non è influenzato da $ IFS - i campi generati da un'espansione del nome percorso sono delimitati sulla testa / coda dei nomi dei file stessi, indipendentemente dal fatto che il loro contenuto contiene caratteri attualmente presenti $IFS. Questo comportamento è attivato per impostazione predefinita, ma altrimenti è facilmente configurabile.

set -f

Che istruisce la shell non a glob . L'espansione del nome percorso non si verificherà almeno fino a quando l'impostazione non viene in qualche modo annullata, ad esempio se la shell corrente viene sostituita con un altro nuovo processo della shell o ....

set +f

... viene rilasciato alla shell. Le doppie virgolette - come fanno anche per la $IFS suddivisione dei campi - rendono superflua questa impostazione globale per espansione. Così:

echo "*" *

... se l'espansione del percorso è attualmente abilitata produrrà probabilmente risultati molto diversi per argomento - poiché il primo si espanderà solo al suo valore letterale (il singolo carattere asterisco, vale a dire per niente) e il secondo solo allo stesso se la directory di lavoro corrente non contiene nomi di file che potrebbero corrispondere (e corrisponde a quasi tutti) . Tuttavia se lo fai:

set -f; echo "*" *

... i risultati per entrambi gli argomenti sono identici - *in questo caso non si espande.


In realtà sono d'accordo con @ StéphaneChazelas che (principalmente) confonde le cose più che aiutare ... ma l'ho trovato utile, personalmente, quindi ho votato. Ora ho un'idea migliore (e alcuni esempi) di come IFSfunziona davvero. Quello che non capisco è perché sarebbe mai una buona idea impostare IFSqualcosa di diverso dal default.
Wildcard il

1
@Wildcard - è un delimitatore di campo. se hai un valore in una variabile che vuoi espandere in più campi su cui lo dividi $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; donestampe \nallora usr\npoi bin\n. Il primo echoè vuoto perché /è un campo null. I path_components possono avere newline o spazi o altro - non importa perché i componenti sono stati divisi /e non il valore predefinito. la gente lo fa awksempre, comunque. lo fa anche la tua conchiglia
mikeserv

3

Ho avuto un grande progetto video con spazi nei nomi dei file e spazi nei nomi delle directory. Mentre find -type f -print0 | xargs -0funziona per diversi scopi e su diverse shell, trovo che l'uso di un IFS personalizzato (separatore di campi di input) ti dia maggiore flessibilità se stai usando bash. Lo snippet di seguito utilizza bash e imposta IFS su una nuova riga; purché non ci siano nuove righe nei nomi dei file:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

Nota l'uso di parentesi per isolare la ridefinizione dell'IFS. Ho letto altri post su come recuperare IFS, ma questo è solo più semplice.

Inoltre, impostando IFS su newline è possibile impostare anticipatamente le variabili shell e stamparle facilmente. Ad esempio, posso far crescere una variabile V in modo incrementale usando le nuove righe come separatori:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

e di conseguenza:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

Ora posso "elencare" l'impostazione di V echo "$V"usando le doppie virgolette per generare le nuove righe. (Ringraziamo questa discussione per la $'\n'spiegazione.)


3
Ma poi avrai ancora problemi con nomi di file contenenti caratteri newline o glob. Vedi anche: Perché il looping sull'output di find è una cattiva pratica? . Se lo si utilizza zsh, è possibile utilizzare IFS=$'\0'e utilizzare -print0( zshnon ostacola le espansioni, quindi i personaggi glob non sono un problema lì).
Stéphane Chazelas,

1
Funziona con nomi di file contenenti spazi, ma non funziona con nomi di file potenzialmente ostili o nomi di file "senza senso" accidentali. È possibile risolvere facilmente il problema dei nomi di file contenenti caratteri jolly aggiungendo set -f. D'altra parte, il tuo approccio fondamentalmente fallisce con i nomi di file contenenti newline. Quando si tratta di dati diversi dai nomi dei file, non riesce anche con elementi vuoti.
Gilles

Bene, il mio avvertimento è che non funzionerà con le nuove righe nei nomi dei file. Tuttavia, credo che dobbiamo tracciare la linea appena timidi di follia ;-)
Russ

E non sono sicuro del motivo per cui questo ha ricevuto un downvote. Questo è un metodo perfettamente ragionevole per scorrere su nomi di file con spazi. L'uso di -print0 richiede xargs e ci sono cose che sono difficili da usare con quella catena. Mi dispiace che qualcuno non sia d'accordo con la mia risposta, ma non c'è motivo di ridimensionarla.
Russ

0

Considerando tutte le implicazioni di sicurezza menzionate sopra e assumendo la fiducia e il controllo sulle variabili che si stanno espandendo, è possibile avere più percorsi con gli spazi bianchi eval. Ma fa attenzione!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
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.