risposta-alla-domanda-sullo-sviluppo-web-bd.com

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

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

Ho scritto uno script 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 hello e 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 \[*?, sono 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 lo stile di Windows \ separatori).

Cosa sta succedendo e come posso risolverlo?

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

Se si usa $foo Non quotato, lo script si strozzerà sull'input o sui parametri (o sull'output del comando, con $(foo)) contenente spazi bianchi o \[*?.

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

  • read - Per leggere l'input riga per riga con il built-in read, usa while IFS= read -r line; do …
    Plain read tratta in modo speciale barre rovesciate e spazi bianchi.
  • xargs - Evita xargs. Se devi usare xargs, crea xargs -0. Invece di find … | xargs, preferisci find … -exec ….
    xargs tratta in particolare gli spazi bianchi e i caratteri \"'.

Questa risposta si applica alle shell in stile Bourne/POSIX (sh, ash, dash, bash, ksh, mksh, yash…). Gli utenti di Zsh dovrebbero saltarlo e leggere la fine di Quando è invece necessaria la doppia virgoletta? . Se vuoi il tutto nitido, leggi lo standard o il manuale di 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?

$foo Non significa "accetta 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 questo passaggio è l'elenco di 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 porta all'elenco contenente foo, seguito dall'elenco 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 di 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: "[email protected]" Per espandere l'elenco dei parametri posizionali, ad esempio "[email protected]" È equivalente a "$1" "$2" "$3" Se ci sono tre parametri posizionali. Vedi Qual è la differenza tra $ * e $ @? )

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

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

Vedere 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 falle 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 Shell con la maggior parte delle shell).

Stesso problema con myfiles=*.txt; … process $myfiles. Quando lo fai, la variabile myfiles contiene la stringa di 5 caratteri *.txt, Ed è quando scrivi $myfiles Che il carattere jolly viene espanso. Questo esempio funzionerà effettivamente, fino a quando non cambi il tuo script in myfiles="$someprefix*.txt"; … process $myfiles. Se someprefix è impostato 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 array con diversa sintassi dell'assegnazione set -A myfiles "someprefix"*.txt (Vedi variabile di assegnazione in un diverso ambiente ksh se hai bisogno della portabilità di ksh88/bash). Le shell in stile Bourne/POSIX hanno un solo array, l'array di parametri posizionali "[email protected]" Che si imposta con set e che è locale in una funzione:

set -- "$someprefix"*.txt
process -- "[email protected]"

Che dire dei nomi di 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 denotazione di un'opzione. Alcuni comandi (come sh, set o sort) accettano anche opzioni che iniziano con +. Se hai un nome file che inizia con una parte variabile, assicurati di passare -- Prima di esso, 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 - O +.

In alternativa, puoi assicurarti che i nomi dei tuoi file inizino con un carattere diverso da -. I nomi di file assoluti iniziano con / E puoi aggiungere ./ All'inizio dei nomi relativi. Lo snippet seguente trasforma il contenuto della variabile f in un modo "sicuro" di riferirsi allo stesso file che è garantito non iniziare con -+.

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

In un'ultima nota su questo argomento, attenzione che alcuni comandi interpretano - Come input standard o output standard, anche dopo --. Se devi fare riferimento a un file effettivo chiamato -, O se stai chiamando un programma del genere 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 dire la differenza tra gli spazi che fanno parte degli argomenti e gli spazi che separano gli argomenti. Se Shell ha array, è possibile utilizzarli.

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 -- "[email protected]" "$file1" "$file2"
"[email protected]"

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

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 code sia la stringa /path/to/executable --option --message="hello world" -- /path/to/file1. Il eval builtin 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.

Usare eval è complicato. Pensa attentamente a cosa viene analizzato quando. In particolare, non puoi semplicemente inserire il nome di un file nel codice: devi citarlo, proprio come faresti se fosse in un file di codice sorgente. Non esiste un modo diretto per farlo. Qualcosa come code="$code $filename" Si interrompe se il nome del file contiene caratteri speciali Shell (spazi, $, ;, |, <, >, Ecc.). code="$code \"$filename\"" Si interrompe ancora su "$\`. Anche code="$code '$filename'" Si interrompe se il nome del file contiene un '. Esistono due soluzioni.

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

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
    
  • Mantenere 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 es. se il codice è incorporato in un ciclo.

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

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

Che succede con read?

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

hello \
world

read divide la riga di input in campi delimitati da caratteri in $IFS (senza -r, anche la barra rovesciata sfugge a quelle). Ad esempio, se l'input è una riga contenente tre parole, read first second third Imposta first sulla prima parola di input, second sulla seconda parola e third alla 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 di IFS sulla stringa vuota evita qualsiasi taglio. Vedi Perché `while IFS = read` viene usato così spesso, invece di` IFS =; while read..`? per una spiegazione più lunga.

Cosa c'è che non va in xargs?

Il formato di input di xargs è costituito da stringhe separate da spazi bianchi che possono essere opzionalmente a virgolette singole o doppie. Nessuno strumento standard genera questo formato.

L'input di xargs -L1 O xargs -l È quasi un elenco di righe, ma non del tutto - se c'è uno spazio alla fine di una riga, la riga seguente è una riga di continuazione.

Puoi usare xargs -0 Dove applicabile (e dove disponibile: GNU (Linux, Cygwin), BusyBox, BSD, OSX, ma non è in POSIX). 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_command Deve essere un comando esterno, non può essere una funzione Shell o un alias. Se è necessario richiamare una Shell per elaborare i file, chiamare esplicitamente sh.

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

Ho qualche altra domanda

Sfoglia il tag citazione su questo sito o Shell o Shell-script . (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 via .

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 esegue la suddivisione in Word, sì, certo, il consiglio sicuro è sempre usare le virgolette. Tuttavia, la suddivisione 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.

26
Steven Penny

Per quanto ne so, ci sono solo due casi in cui è necessario citare due volte le espansioni, e quei casi riguardano i due parametri Shell speciali "[email protected]" 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 di 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 Shell ha da offrire. Tuttavia, penso che, poiché sono già state esposte sapientemente delle alternative, questo è un luogo eccellente per discutere di ciò che accade quando Shell espande un valore.

Shell, nel suo cuore e nella sua anima (per quelli che ne hanno), è un interprete di comandi - è un parser, come un grande, interattivo, sed. Se la tua istruzione Shell è soffocamento su spazio bianco o simile, è molto probabile perché non hai compreso appieno il processo di interpretazione della Shell, in particolare come e perché traduce un'istruzione di input in un comando utilizzabile. Il compito della Shell è di:

  1. accetta input

  2. interpretare e diviso correttamente in input tokenizzato parole

    • input parole sono gli elementi della sintassi di Shell come $Word o echo $words 3 4* 5

    • parole sono sempre divisi nello spazio bianco - questa è solo sintassi - ma solo i caratteri letterali dello spazio bianco sono stati offerti alla Shell nel suo file di input

  3. espandere quelli se necessario in più campi

    • campi risultato da Word espansioni: formano il comando eseguibile finale

    • eccetto "[email protected]", $IFSdivisione del campo e espansione del percorso un input Word deve sempre essere valutato in 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 Shell è un colla, e, se questo è vero, allora cosa è attaccare sono elenchi di argomenti - oppure campi - in un processo o in un altro quando execs. La maggior parte delle shell non gestisce bene il byte NUL - se non del tutto - e questo perché si stanno già dividendo su di esso. Shell deve execmolto e deve farlo con un array delimitato da NUL che passa al kernel di sistema a exec tempo. 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 $IFS entra. $IFS è un parametro Shell sempre presente - e allo stesso modo impostabile - che definisce il modo in cui Shell deve dividere le espansioni Shell da Word a field - in particolare su quali valori questi campi dovrebbe delimitare. $IFS suddivide le espansioni di Shell su delimitatori diversi da NUL - in altre parole, Shell sostituisce i byte risultanti da un'espansione che corrisponde a quelli nel valore di $IFS con NUL nei suoi array di dati interni. Quando lo guardi in quel modo potresti iniziare a vedere che ogni divisione del campo L'espansione della shell è un $IFS- array di dati delimitato.

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

Salvo virgolette, $IFS è esso stesso un $IFS espansione delimitata della shell. L'impostazione predefinita è un valore specificato di <space><tab><newline> - tutti e tre i quali presentano proprietà speciali se contenuti in $IFS. Considerando che qualsiasi altro valore per $IFS è specificato per valutare un singolo campo per espansione occorrenza, $IFSspazio bianco - uno qualsiasi di questi tre - è specificato per passare a un singolo campo per espansione sequenza 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 suddivisione in parole o spazi bianchi come richiesto, quindi quale dei caratteri speciali?

Shell, per impostazione predefinita, espande anche alcuni token non quotati (come ?*[ come indicato altrove qui) in più campi quando si presentano in un elenco. Questo si chiama espansione del percorso o globbing. È uno strumento incredibilmente utile e, come si verifica dopo divisione del campo nell'ordine di analisi della Shell, non è influenzato da $ IFS - campi generato da un'espansione del nome percorso è delimitato sulla testa/coda dei nomi dei file stessi, indipendentemente dal fatto che il loro contenuto contenga caratteri attualmente in $IFS. Questo comportamento è attivato per impostazione predefinita, ma altrimenti è facilmente configurabile.

set -f

Questo indica a Shell non a glob. L'espansione del nome percorso non si verificherà almeno fino a quando tale impostazione non viene in qualche modo annullata, ad esempio se l'attuale Shell viene sostituita con un altro nuovo processo Shell o ....

set +f

... viene rilasciato alla Shell. Virgolette doppie - come fanno anche per $IFSdivisione del campo - rende 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 a (e corrisponde a quasi tutti). Tuttavia se lo fai:

set -f; echo "*" *

... i risultati per entrambi gli argomenti sono identici: * non si espande in quel caso.

22
mikeserv

Ho avuto un grande progetto video con spazi nei nomi dei file e spazi nei nomi delle directory. Mentre find -type f -print0 | xargs -0 funziona per diversi scopi e su diverse shell, trovo che l'uso di un IFS personalizzato (input field separator) 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 con echo "$V" usando virgolette doppie per generare le nuove righe. (Credito a questa discussione per il $'\n' spiegazione.)

3
Russ

Il metodo di utilizzo di find directory -print0 | xargs -0 dovrebbe gestire tutte le offerte speciali. Tuttavia, richiede un PID per file/directory, che può essere montato su un problema di prestazioni.

Vorrei descrivere un altro metodo di gestione dei file robusta (e performante) che ho incontrato di recente, che è adatto se find output dovrebbe essere post-elaborati come dati CSV separati da tabulazioni, ad es di AWK. In tale elaborazione, in realtà solo le schede e le nuove righe nei nomi dei file sono dirompenti:

La directory viene scansionata tramite find directory -printf '%P\t///\n'. Se il percorso non contiene tab o newline, questo porta a un record con due campi CSV: il percorso stesso e il campo contenente ///.

Se una scheda è contenuta nel percorso, ci saranno tre campi: percorso frammento1, percorso frammento2 e il campo contenente ///.

Se è contenuta una nuova riga, ci saranno due record: il primo record conterrà il frammento di percorso1 e il secondo record conterrà il frammento di percorso2 e il campo contenente ///.

Ora il fatto chiave è che /// non può verificarsi naturalmente nei percorsi. Inoltre, è una sorta di fuga o terminazione impermeabile.

È anche possibile scrivere un programma (AWK) che scansiona l'output find e, fino a quando trova ///, riunisce i frammenti sapendo che un nuovo campo è una scheda nel percorso e un nuovo record è una nuova riga nel percorso.

Le schede possono essere salvate in sicurezza come ///t e le newline possono essere salvate in sicurezza come ///n, ancora una volta, sapendo che /// non può verificarsi naturalmente nei percorsi dei file. Conversione ///t e ///n torna alle schede e alla fine possono verificarsi nuove righe, quando dall'elaborazione viene generato un output.

Sì, sembra complicato, ma l'indizio è che sono necessari solo due PID: l'istanza find e awk che esegue l'algoritmo descritto. Ed è veloce.

L'idea non è mia, l'ho trovata implementata in questo nuovo script bash (2019) per la sincronizzazione delle directory: Zaloha.sh . Lì hanno un documento che descrive l'algoritmo, in realtà.

Non sono stato in grado di interrompere/soffocare quel programma con caratteri speciali nei nomi dei file. Ha anche elaborato correttamente le directory chiamate newline e tab da solo ...

0
user400462

Considerando tutte le implicazioni sulla sicurezza menzionate sopra e assumendo la fiducia e il controllo sulle variabili che si stanno espandendo, è possibile avere più percorsi con spazi bianchi usando 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
0
Mattias Wadman