Utilizzo delle variabili shell per le opzioni di comando


19

In uno script Bash, sto cercando di memorizzare le opzioni che sto usando rsyncin una variabile separata. Funziona bene con opzioni semplici (come --recursive), ma sto riscontrando problemi con --exclude='.*':

$ find source
source
source/.bar
source/foo

$ rsync -rnv --exclude='.*' source/ dest
sending incremental file list
foo

sent 57 bytes  received 19 bytes  152.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

$ RSYNC_OPTIONS="-rnv --exclude='.*'"

$ rsync $RSYNC_OPTIONS source/ dest
sending incremental file list
.bar
foo

sent 78 bytes  received 22 bytes  200.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

Come puoi vedere, il passaggio --exclude='.*'a rsync"manualmente" funziona bene ( .barnon viene copiato), non funziona quando le opzioni vengono prima memorizzate in una variabile.

Immagino che questo sia correlato alle virgolette o al carattere jolly (o entrambi), ma non sono stato in grado di capire cosa sia esattamente sbagliato.



1
Oppure vedi questa risposta sul nostro sito.
Scott,

Risposte:


38

In generale, è una cattiva idea ridurre un elenco di elementi separati in una singola stringa, indipendentemente dal fatto che si tratti di un elenco di opzioni della riga di comando o di un elenco di nomi di percorso.

Utilizzando invece un array:

rsync_options=( -rnv --exclude='.*' )

o

rsync_options=( -r -n -v --exclude='.*' )

e più tardi...

rsync "${rsync_options[@]}" source/ target

In questo modo, viene mantenuta la quotazione delle singole opzioni (purché si citino due volte l'espansione di ${rsync_options[@]}). Inoltre, consente di manipolare facilmente le singole voci dell'array, prima di chiamare, se fosse necessario rsync.

In qualsiasi shell POSIX, è possibile utilizzare l'elenco di parametri posizionali per questo:

set -- -rnv --exclude='.*'

rsync "$@" source/ target

Ancora una volta, la doppia citazione dell'espansione di $@è fondamentale qui.

Tangenzialmente correlati:


Il problema è che quando si inseriscono i due set di opzioni in una stringa, le virgolette singole --excludedel valore dell'opzione diventano parte di quel valore. Quindi,

RSYNC_OPTIONS='-rnv --exclude=.*'

avrebbe funzionato¹ ... ma è meglio (come in più sicuro) utilizzare un array o i parametri posizionali con voci quotate singolarmente. Ciò consentirebbe anche di utilizzare le cose con spazi al loro interno, se necessario, ed evitare che la shell esegua la generazione del nome file (globbing) sulle opzioni.


¹ a condizione che $IFSnon sia stato modificato e che non vi siano file il cui nome inizia --exclude=.nella directory corrente e che le opzioni della shell nullglobo failglobnon sono impostate.


L'uso di un array funziona bene, grazie per la risposta dettagliata!
Florian Brucker,

3

@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 -rnve --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 $OPTSimpediscono il trattamento speciale delle virgolette singole, quindi queste ultime fanno parte del valore:

$ echo $OPTS
--exclude='.*'

Quando ora utilizziamo $OPTScome 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) rsyncutilizza 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=.*

Questa roba mi ha confuso per molto tempo, quindi una spiegazione dettagliata come questa è utile.
Joe

0

Quando scriviamo funzioni e script di shell, in cui gli argomenti vengono passati per essere elaborati, gli argomenti verranno passati in variabili con nome numerico, ad esempio $ 1, $ 2, $ 3

Per esempio :

bash my_script.sh Hello 42 World

All'interno my_script.sh, i comandi verranno utilizzati $1per fare riferimento a Hello, $2to 42e $3forWorld

Il riferimento alla variabile,, $0si espanderà al nome dello script corrente, ad esmy_script.sh

Non riprodurre l'intero codice con i comandi come variabili.

Ricorda :

1 Evitare di usare i nomi delle variabili in maiuscolo negli script.

2 Non usare i backquotes, usa $ (...) invece, nidifica meglio.

if [ $# -ne 2 ]
then
    echo "Usage: $(basename $0) DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

directory=$1
backup_directory=$2
current_date=$(date +%Y-%m-%dT%H-%M-%S)
backup_file="${backup_directory}/${current_date}.backup"

tar cv "$directory" | openssl des3 -salt | split -b 1024m - "$backup_file"
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.