web-dev-qa-db-fra.com

Utilisation de variables Shell pour les options de commande

Dans un script Bash, j'essaie de stocker les options que j'utilise pour rsync dans une variable distincte. Cela fonctionne bien pour les 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, en passant --exclude='.*' à rsync "manuellement" fonctionne correctement (.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.

22
Florian Brucker

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 citation 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 de position pour cela:

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

rsync "$@" source/ target

Encore une fois, il est essentiel de citer deux fois l'extension 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 valeur de l'option --exclude Deviennent une 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 que $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 du shell nullglob ou failglob Shell ne sont pas définis.

42
Kusalananda

@Kusalananda a déjà expliqué le problème de base et comment le résoudre, et Bash FAQ entry lié par @glenn jackmann fournit également beaucoup d'informations utiles. Voici une explication détaillée de ce qui se passe dans mon problème sur la base 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 pièces -rnv et --exclude='.*' sont divisés en deux arguments, car ils sont séparés par des espaces blancs sans guillemets (c'est ce qu'on appelle division des mots ).

Notez également que les guillemets autour de .* ont été supprimés: les guillemets simples indiquent au Shell de transmettre 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 de $OPTS empêche le traitement spécial des guillemets simples, donc ces derniers font partie de la valeur:

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

Lorsque nous utilisons maintenant $OPTS comme argument d'une commande puis les guillemets sont traités avant l'expansion des paramètres , donc les guillemets dans $OPTS se produit "trop ​​tard".

Cela signifie que (dans mon problème d'origine) rsync utilise le motif d'exclusion '.*' (avec guillemets!) au lieu du motif .* - 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 de $OPTS:

$ OPTS2=--exclude='.*'

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

Cependant, c'est une bonne pratique de toujours citer les affectations de variables en raison de différences subtiles dans des cas plus complexes.

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

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

Il s'avère que Bash effectue une expansion de modèle, mais le modèle --exclude=.* ne correspond à aucun fichier, le modèle est donc 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 un fichier correspondant à --exclude=.* alors 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 de Word et la gestion des guillemets se produisent 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=.*
5
Florian Brucker

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 pour World

La référence de variable, $0, se développera jusqu'au nom du script actuel, par exemple my_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"
0
champion-runner