Dans un script Bash, j'essaie de stocker les options que j'utilise rsync
dans 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 ( .bar
n'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.
Réponses:
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:
ou
et ensuite...
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'appelerrsync
.Dans n'importe quel shell POSIX, on peut utiliser la liste des paramètres positionnels pour cela:
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
--exclude
valeur de l' option font partie de cette valeur. Par conséquent,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
$IFS
ne 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 shellnullglob
oufailglob
ne soient pas définies.la source
@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
):Passer les options "manuellement":
Comme prévu, les parties
-rnv
et--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 :
Cela est dû à deux raisons: les guillemets doubles utilisés lors de la définition
$OPTS
empêchent un traitement spécial des guillemets simples, ces derniers font donc partie de la valeur:Lorsque nous utilisons maintenant
$OPTS
comme argument d'une commande, les guillemets sont traités avant l'expansion des paramètres , de sorte que les guillemets$OPTS
apparaissent "trop tard".Cela signifie que (dans mon problème d'origine)
rsync
utilise 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
: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 :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: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é: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:
Lors du passage des options à la commande, nous utilisons la syntaxe
"${ARRAY[@]}"
, qui développe chaque élément du tableau dans un mot distinct:la source
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$1
pour faire référence à Bonjour,$2
à42
et$3
pourWorld
La référence de variable,,
$0
se 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.
la source