Usare sempre le virgolette intorno sostituzioni variabili e sostituzioni di comando: "$foo"
,"$(foo)"
Se si utilizza $foo
non 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' read
integrato, 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 …
.
xargs
tratta 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?
$foo
non 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 IFS
che 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 myfiles
contiene la stringa di 5 caratteri *.txt
ed è quando scrivi $myfiles
che il carattere jolly è espanso. Questo esempio funzionerà effettivamente, fino a quando non cambi il tuo script 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 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 set
e 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 f
in 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 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.
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
, 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 $IFS
(senza -r
, anche la barra rovesciata sfugge a quelli). Ad esempio, se l'input è una riga contenente tre parole, quindi read first second third
imposta first
sulla prima parola di input, second
sulla seconda parola e third
sulla 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 IFS
su 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 -L1
o 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 -0
dove 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_command
deve 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 sh
esplicito.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
Ho qualche altra domanda
Sfoglia il tag di quotazione 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 pure .
shellcheck
aiutarti a migliorare la qualità dei tuoi programmi.