Perché le opzioni in una variabile quotata falliscono, ma funzionano quando non quotate?


18

Ho letto che dovrei citare le variabili in bash, ad esempio "$ foo" invece di $ foo. Tuttavia, mentre scrivevo una sceneggiatura, ho trovato un caso in cui funziona senza virgolette ma non con loro:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Questo funziona:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Questo non lo fa (notare le doppie virgolette intorno a $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • Qual è la ragione di ciò?

  • La prima riga è la buona versione; o dovrei sospettare che ci sia un errore nascosto da qualche parte che causa questo comportamento?

  • In generale, dove trovo una buona documentazione per capire come funziona bash e le sue quotazioni? Durante la stesura di questo script sento di aver iniziato a lavorare su una base di tentativi ed errori invece di comprendere le regole.



3
Vai alla fonte per le regole: il manuale di bash . Presta molta attenzione alla sezione 3.5 "Espansioni della shell", in particolare la suddivisione delle parole e l'espansione del nome file: questi 2 fattori sono ciò che usi le virgolette per controllare.
Glenn Jackman,


4
Penso che aiuti a capire come funzionano gli argomenti della riga di comando a un livello basso. Quando viene eseguito un programma, riceve argomenti come un elenco di elenchi di caratteri (abbastanza vicino). Ogni elenco interno è ciò che chiamiamo un "argomento". La maggior parte dei programmi dipende dalla separazione logica tra args. Qui, vedi che wgetnon sa cosa --mirror --no-host-directoriessignifichi (come un argomento), ma lo gestisce quando è diviso in due argomenti. Pochissimi programmi trattano gli spazi e le virgolette specialmente quando sono all'interno del vettore argomento. Il problema è che bash, e altre shell, dovrebbero essere>
HTNW il

2
> usato dagli umani. Sarebbe fastidioso definire manualmente i confini tra argomenti, quindi le shell si dividono su spazi bianchi per trasformare una linea (un elenco di caratteri) in un vettore di argomento (un elenco di elenchi di caratteri). L'espansione variabile è una delle prime espansioni bash, quindi puoi immaginare che $asia esattamente equivalente a scrivere direttamente il suo contenuto. Ora il problema è evidente: si a="-a -b"; cmd "$a"espande cmd "-a -b", ma cmdprobabilmente non sa cosa significhi. cmd $asi espande a cmd -a -b, che probabilmente fa il lavoro.
HTNW,

Risposte:


28

Fondamentalmente, dovresti citare due volte le espansioni variabili per proteggerle dalla suddivisione delle parole (e dalla generazione del nome file). Tuttavia, nel tuo esempio,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

dividere le parole è esattamente quello che vuoi .

Con "$wget_options"(citato), wgetnon sa cosa fare del singolo argomento --mirror --no-host-directoriese si lamenta

wget: unknown option -- mirror --no-host-directories

Per wgetvedere le due opzioni --mirrore --no-host-directoriescome separate, deve avvenire la divisione delle parole.

Ci sono modi più solidi per farlo. Se stai usando basho qualsiasi altra shell che utilizza array come bashfare, vedi la risposta di Glenn Jackman . La risposta di Gilles descrive inoltre una soluzione alternativa per gusci più semplici come lo standard /bin/sh. Entrambi essenzialmente memorizzano ogni opzione come elemento separato in un array.

Domanda correlata con buone risposte: perché il mio script shell si soffoca su spazi bianchi o altri caratteri speciali?


Le espansioni di variabili con virgolette doppie sono una buona regola pratica. Fallo . Quindi fai attenzione ai pochissimi casi in cui non dovresti farlo. Questi si presenteranno tramite messaggi diagnostici, come il messaggio di errore sopra riportato.

Ci sono anche alcuni casi in cui non è necessario citare espansioni variabili. Ma è comunque più facile continuare a usare le virgolette doppie perché non fa molta differenza. Uno di questi casi è

variable=$other_variable

Un altro è

case $variable in
    ...) ... ;;
esac

2
Prima di utilizzare quell'operatore split + glob, potrebbe essere necessario assicurarsi che $IFScontenga il valore corretto. Qui è necessario dividere lo spazio e il testo non contiene alcuna scheda o nuova riga, quindi il valore predefinito $IFSsarebbe, ma se quel codice dovesse essere utilizzato in una funzione che potrebbe essere chiamata in un contesto in cui $IFSavrebbe potuto essere modificato , vorresti impostare $IFSprima (e possibilmente ripristinarlo in seguito o utilizzare un ambito locale per esso se il resto del codice assume un non modificato $IFS)
Stéphane Chazelas,

32

Il modo più affidabile per codificare l'uso di un array:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"

Questa è la risposta esatta. Riferimento
l0b0,

2
È una buona risposta, quindi l'ho votato, ma Kusalanda mi ha aiutato di più a capire perché il mio codice era sbagliato e posso accettarne solo uno.
z32a7ul,

Stavo incontrando un mondo di problemi fino a quando qualcuno nella lista di rsync non mi ha mostrato questo costrutto. È particolarmente utile se alcuni degli elementi potrebbero essere stringhe vuote. Questo fa sparire le stringhe vuote. Ad alcuni comandi piace cpe rsyncfarà cose inaspettate se il tuo comando si espande in qualcosa di simile rsync '' rest of parameters. Questo è ottimo per costruire un comando pezzo per pezzo in modo condizionale e poi semplicemente eseguirlo una volta in un posto.
Joe,

17

Stai provando a memorizzare un elenco di stringhe in una variabile stringa. Non va bene. Non importa come accedi alla variabile, qualcosa è rotto.

wget_options='--mirror --no-host-directories'imposta la variabile wget_optionssu una stringa che contiene uno spazio. A questo punto, non c'è modo di sapere se lo spazio dovrebbe far parte di un'opzione o un separatore tra le opzioni.

Quando si accede alla variabile con una sostituzione tra virgolette wget "$wget_options", il valore della variabile viene utilizzato come stringa. Ciò significa che viene passato come singolo parametro a wget, quindi è una singola opzione. Questo si interrompe nel tuo caso perché intendevi che significasse più opzioni.

Quando si utilizza una sostituzione non quotata wget $wget_options, il valore della variabile stringa subisce un processo di espansione soprannominato "split + glob":

  1. Prendi il valore della variabile e dividilo in parti delimitate da spazi bianchi (supponendo che tu non abbia modificato la $IFSvariabile). Ciò si traduce in un elenco intermedio di stringhe.
  2. Per ogni elemento dell'elenco intermedio, se si tratta di un modello jolly che corrisponde a uno o più file, sostituire tale elemento con l'elenco dei file corrispondenti.

Questo accade nel tuo esempio, perché il processo di divisione trasforma lo spazio in un separatore, ma non funziona in generale poiché un'opzione potrebbe contenere spazi e caratteri jolly.

In ksh, bash, yash e zsh, puoi usare una variabile array. Un array nella terminologia della shell è un elenco di stringhe, quindi non vi è alcuna perdita di informazioni. Per creare una variabile array, metti le parentesi attorno agli elementi dell'array quando assegni il valore alla variabile. Per accedere a tutti gli elementi dell'array, utilizzare - questa è una generalizzazione di , che costituisce un elenco dagli elementi dell'array. Nota che qui hai bisogno anche delle doppie virgolette, altrimenti ogni elemento subisce split + glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

In plain sh, non ci sono variabili di array. Se non ti dispiace perdere gli argomenti posizionali, puoi usarli per memorizzare un elenco di stringhe.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Per ulteriori informazioni, vedi Perché il mio script shell si soffoca su spazi bianchi o altri caratteri speciali?


Per sh pianura una subshell conserverebbe gli argomenti posizionali: (set -- ...; exec wget "$@" ...).
John Kugelman supporta Monica 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.