@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 -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 :
$ OPTS="--exclude='.*'"
$ ./argtest.bash $OPTS
--exclude='.*'
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:
$ echo $OPTS
--exclude='.*'
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
:
$ 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=.*