Utilisation de variables shell pour les options de commande

19

Dans un script Bash, j'essaie de stocker les options que j'utilise rsyncdans une variable distincte. Cela fonctionne bien pour des options simples (comme --recursive), mais je rencontre des problèmes avec --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)

Comme vous pouvez le voir, le passage --exclude='.*'à rsync"manuellement" fonctionne bien ( .barn'est pas copié), cela ne fonctionne pas lorsque les options sont d'abord stockées dans une variable.

Je suppose que cela est lié aux citations ou au caractère générique (ou aux deux), mais je n'ai pas été en mesure de comprendre ce qui ne va pas exactement.

Florian Brucker
la source

Réponses:

38

En général, c'est une mauvaise idée de rétrograder une liste d'éléments séparés en une seule chaîne, qu'il s'agisse d'une liste d'options de ligne de commande ou d'une liste de chemins.

En utilisant un tableau à la place:

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

ou

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

et ensuite...

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

De cette façon, la cotation des options individuelles est maintenue (tant que vous citez deux fois l'expansion de ${rsync_options[@]}). Il vous permet également de manipuler facilement les entrées individuelles du tableau, si vous en avez besoin, avant d'appeler rsync.

Dans n'importe quel shell POSIX, on peut utiliser la liste des paramètres positionnels pour cela:

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

rsync "$@" source/ target

Encore une fois, il $@est essentiel de citer deux fois l'expansion de .

Liées tangentiellement:


Le problème est que lorsque vous placez les deux ensembles d'options dans une chaîne, les guillemets simples de la --excludevaleur de l' option font partie de cette valeur. Par conséquent,

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

aurait fonctionné¹ ... mais il est préférable (comme dans plus sûr) d'utiliser un tableau ou les paramètres de position avec des entrées entre guillemets individuels. Cela vous permettrait également d'utiliser des éléments contenant des espaces, si vous en avez besoin, et évite que le shell effectue la génération de nom de fichier (globbing) sur les options.


¹ à condition qu'il $IFSne soit pas modifié et qu'il n'y ait pas de fichier dont le nom commence par --exclude=.dans le répertoire courant, et que les options shell nullglobou failglobne soient pas définies.

Kusalananda
la source
L'utilisation d'un tableau fonctionne bien, merci pour votre réponse détaillée!
Florian Brucker
3

@Kusalananda a déjà expliqué le problème de base et comment le résoudre, et la FAQ Bash liée à @glenn jackmann fournit également de nombreuses informations utiles. Voici une explication détaillée de ce qui se passe dans mon problème en fonction de ces ressources.

Nous allons utiliser un petit script qui imprime chacun de ses arguments sur une ligne distincte pour illustrer les choses ( argtest.bash):

#!/bin/bash

for var in "$@"
do
    echo "$var"
done

Passer les options "manuellement":

$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*

Comme prévu, les parties -rnvet --exclude='.*'sont divisées en deux arguments, car elles sont séparées par des espaces blancs sans guillemets (c'est ce qu'on appelle la division des mots ).

Notez également que les guillemets autour .*ont été supprimés: les guillemets simples indiquent au shell de passer leur contenu sans interprétation spéciale , mais les guillemets eux-mêmes ne sont pas passés à la commande .

Si nous stockons maintenant les options dans une variable sous forme de chaîne (par opposition à l'utilisation d'un tableau), les guillemets ne sont pas supprimés :

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

$ ./argtest.bash $OPTS
--exclude='.*'

Cela est dû à deux raisons: les guillemets doubles utilisés lors de la définition $OPTSempêchent un traitement spécial des guillemets simples, ces derniers font donc partie de la valeur:

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

Lorsque nous utilisons maintenant $OPTScomme argument d'une commande, les guillemets sont traités avant l'expansion des paramètres , de sorte que les guillemets $OPTSapparaissent "trop ​​tard".

Cela signifie que (dans mon problème d'origine) rsyncutilise le modèle d'exclusion '.*'(avec guillemets!) Au lieu du modèle .*- il exclut les fichiers dont le nom commence par un guillemet simple suivi d'un point et se termine par un guillemet simple. De toute évidence, ce n'est pas ce qui était prévu.

Une solution de contournement aurait été d'omettre les guillemets doubles lors de la définition $OPTS:

$ OPTS2=--exclude='.*'

$ ./argtest.bash $OPTS2
--exclude=.*

Cependant, il est recommandé de toujours citer les affectations de variables en raison de différences subtiles dans des cas plus complexes.

Comme l'a noté @Kusalananda, ne pas citer .*aurait également fonctionné. J'avais ajouté les guillemets pour empêcher l' expansion du modèle , mais ce n'était pas strictement nécessaire dans ce cas spécial :

$ ./argtest.bash --exclude=.*
--exclude=.*

Il se avère que Bash fait effectuer l' expansion du modèle, mais le modèle --exclude=.*ne correspond pas à un fichier, de sorte que le modèle est transmis à la commande. Comparer:

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*

Cependant, ne pas citer le modèle est dangereux, car si (pour une raison quelconque) il y avait une correspondance de fichier, --exclude=.*le modèle est développé:

$ touch -- --exclude=.special-filenames-happen

$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen

Enfin, voyons pourquoi l'utilisation d'un tableau empêche mon problème de citation (en plus des autres avantages de l'utilisation de tableaux pour stocker des arguments de commande).

Lors de la définition du tableau, le fractionnement des mots et la gestion des guillemets se déroulent comme prévu:

$ 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=.*

Lors du passage des options à la commande, nous utilisons la syntaxe "${ARRAY[@]}", qui développe chaque élément du tableau dans un mot distinct:

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*
Florian Brucker
la source
Ce truc m'a longtemps dérouté, donc une explication détaillée comme celle-ci est utile.
Joe
0

Lorsque nous écrivons des fonctions et des scripts shell, dans lesquels les arguments sont passés pour être traités, les arguments sont passés dans des variables nommées numériquement, par exemple $ 1, $ 2, $ 3

Par exemple :

bash my_script.sh Hello 42 World

À l'intérieur my_script.sh, les commandes utiliseront $1pour faire référence à Bonjour, $2à 42et $3pourWorld

La référence de variable,, $0se développera jusqu'au nom du script actuel, par exemplemy_script.sh

Ne jouez pas le code entier avec des commandes comme variables.

Gardez à l'esprit :

1 Évitez d'utiliser des noms de variables en majuscules dans les scripts.

2 N'utilisez pas de guillemets, utilisez plutôt $ (...), il s'emboîtera mieux.

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"
champion-coureur
la source