@Kusalananda ha già spiegato il problema di base e come risolverlo, e la voce FAQ di Bash collegata a @glenn jackmann fornisce anche molte informazioni utili. Ecco una spiegazione dettagliata di ciò che sta accadendo nel mio problema in base a queste risorse.
Useremo un piccolo script che stampa ciascuno dei suoi argomenti su una riga separata per illustrare le cose ( argtest.bash
):
#!/bin/bash
for var in "$@"
do
echo "$var"
done
Passando le opzioni "manualmente":
$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*
Come previsto, le parti -rnv
e --exclude='.*'
sono divise in due argomenti, poiché sono separate da spazi bianchi non quotati (questo si chiama suddivisione di parole ).
Si noti inoltre che le virgolette in giro .*
sono state rimosse: le virgolette singole indicano alla shell di passare il contenuto senza interpretazioni speciali , ma le virgolette stesse non vengono passate al comando .
Se ora memorizziamo le opzioni in una variabile come stringa (anziché utilizzare una matrice), le virgolette non vengono rimosse :
$ OPTS="--exclude='.*'"
$ ./argtest.bash $OPTS
--exclude='.*'
Ciò è dovuto a due motivi: le doppie virgolette utilizzate durante la definizione $OPTS
impediscono il trattamento speciale delle virgolette singole, quindi queste ultime fanno parte del valore:
$ echo $OPTS
--exclude='.*'
Quando ora utilizziamo $OPTS
come argomento un comando, le virgolette vengono elaborate prima dell'espansione del parametro , quindi le virgolette vengono visualizzate $OPTS
"troppo tardi".
Ciò significa che (nel mio problema originale) rsync
utilizza il modello exclude '.*'
(con virgolette!) Anziché il modello .*
- esclude i file il cui nome inizia con una virgoletta seguita da un punto e termina con una virgoletta singola. Ovviamente non è quello che era previsto.
Una soluzione alternativa sarebbe stata quella di omettere le doppie virgolette durante la definizione $OPTS
:
$ OPTS2=--exclude='.*'
$ ./argtest.bash $OPTS2
--exclude=.*
Tuttavia, è buona norma citare sempre le assegnazioni di variabili a causa delle sottili differenze nei casi più complessi.
Come ha notato @Kusalananda, anche la citazione non .*
avrebbe funzionato. Avevo aggiunto le virgolette per impedire l' espansione del pattern , ma non era strettamente necessario in questo caso speciale :
$ ./argtest.bash --exclude=.*
--exclude=.*
Si scopre che Bash fa eseguire l'espansione modello, ma il modello --exclude=.*
non corrisponde a qualsiasi file, in modo che il modello è passato al comando. Confrontare:
$ touch some_file
$ ./argtest.bash some_*
some_file
$ ./argtest.bash does_not_exit_*
does_not_exit_*
Tuttavia, non citare il modello è pericoloso, perché se (per qualsiasi motivo) fosse presente un file corrispondente, --exclude=.*
il modello viene espanso:
$ touch -- --exclude=.special-filenames-happen
$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen
Infine, vediamo perché l'utilizzo di un array previene il mio problema di quotazione (oltre agli altri vantaggi dell'utilizzo di array per memorizzare argomenti di comando).
Quando si definisce l'array, la suddivisione delle parole e la gestione delle virgolette avvengono come previsto:
$ ARRAY_OPTS=( -rnv --exclude='.*' )
$ echo length of the array: "${#ARRAY_OPTS[@]}"
length of the array: 2
$ echo first element: "${ARRAY_OPTS[0]}"
first element: -rnv
$ echo second element: "${ARRAY_OPTS[1]}"
second element: --exclude=.*
Quando passiamo le opzioni al comando, usiamo la sintassi "${ARRAY[@]}"
, che espande ogni elemento dell'array in una parola separata:
$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*