Pourquoi les options d'une variable entre guillemets échouent, mais fonctionnent lorsqu'elles ne sont pas entre guillemets?

18

J'ai lu que je devrais citer des variables en bash, par exemple "$ foo" au lieu de $ foo. Cependant, lors de l'écriture d'un script, j'ai trouvé un cas où cela fonctionne sans guillemets mais pas avec eux:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Celui-ci fonctionne:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Ce n'est pas le cas (notez les guillemets doubles autour de $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • Quelle est la raison pour ça?

  • La première ligne est-elle la bonne version; ou devrais-je soupçonner qu'il y a une erreur cachée quelque part qui provoque ce comportement?

  • En général, où puis-je trouver une bonne documentation pour comprendre comment bash et ses citations fonctionnent? Lors de l'écriture de ce script, j'ai l'impression d'avoir commencé à travailler sur une base d'essais et d'erreurs au lieu de comprendre les règles.

z32a7ul
la source
3
On répond à votre question ici: mywiki.wooledge.org/BashFAQ/050
glenn jackman
3
Allez à la source des règles: le manuel bash . Portez une attention particulière à la section 3.5 «Expansions du shell», en particulier la séparation des mots et l'expansion des noms de fichiers - ces 2 facteurs sont ce que vous utilisez pour contrôler les guillemets.
glenn jackman
3
Également discuté sous Conséquences sur la sécurité de l'oubli de citer une variable dans les shells bash / POSIX
Scott
4
Je pense que cela aide à comprendre comment les arguments de ligne de commande fonctionnent à un bas niveau. Lorsqu'un programme est exécuté, il reçoit des arguments sous forme de liste de listes de caractères (suffisamment proches). Chaque liste intérieure est ce que nous appelons un «argument». La plupart des programmes dépendent de la séparation logique entre les arguments. Ici, vous voyez qui wgetne sait pas ce que --mirror --no-host-directoriessignifie (comme un argument), mais il le gère lorsqu'il est divisé en deux arguments. Très peu de programmes traitent les espaces et les guillemets spécialement une fois qu'ils sont à l'intérieur du vecteur argument. Le problème est que bash, et d'autres obus, sont censés être>
HTNW
2
> utilisé par l'homme. Il serait ennuyeux de définir manuellement les limites entre les arguments, de sorte que les coquilles se séparent sur des espaces pour transformer une ligne (une liste de caractères) en un vecteur d'argument (une liste de listes de caractères). L'expansion variable est l'une des premières extensions bash, vous pouvez donc imaginer que cela $aéquivaut exactement à écrire directement son contenu. Maintenant, le problème est évident: a="-a -b"; cmd "$a"s'étend à cmd "-a -b", mais cmdne sait probablement pas ce que cela signifie. cmd $ase développe pour cmd -a -b, ce qui a probablement fait le travail.
HTNW

Réponses:

28

Fondamentalement, vous devez doubler les extensions de variable pour les protéger contre le fractionnement de mots (et la génération de noms de fichiers). Cependant, dans votre exemple,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

le fractionnement de mots est exactement ce que vous voulez .

Avec "$wget_options"(cité), wgetne sait pas quoi faire avec le seul argument --mirror --no-host-directorieset se plaint

wget: unknown option -- mirror --no-host-directories

Pour wgetvoir les deux options --mirroret --no-host-directoriesséparément, la séparation des mots doit se produire.

Il existe des moyens plus robustes de procéder. Si vous utilisez bashou tout autre shell qui utilise des tableaux comme bashdo, consultez la réponse de glenn jackman . La réponse de Gilles décrit en outre une solution alternative pour les coques plus simples telles que la norme /bin/sh. Les deux stockent essentiellement chaque option en tant qu'élément séparé dans un tableau.

Question connexe avec de bonnes réponses: pourquoi mon script shell s'étouffe-t-il avec les espaces ou d'autres caractères spéciaux?


Les extensions de guillemets doubles sont une bonne règle de base. Fais ça . Soyez alors conscient des très rares cas où vous ne devriez pas faire cela. Ceux-ci se présenteront à vous par le biais de messages de diagnostic, tels que le message d'erreur ci-dessus.

Il y a aussi quelques cas où vous n'avez pas besoin de citer des extensions de variables. Mais il est plus facile de continuer à utiliser des guillemets, car cela ne fait pas beaucoup de différence. Un tel cas est

variable=$other_variable

Un autre est

case $variable in
    ...) ... ;;
esac
Kusalananda
la source
2
Avant d'utiliser cet opérateur split + glob, il peut être nécessaire de s'assurer qu'il $IFScontient la bonne valeur. Ici, vous devez diviser l'espace et le texte ne contient aucun onglet ni nouvelle ligne, donc la valeur par défaut de $IFSferait l'affaire, mais si ce code doit être utilisé dans une fonction qui peut être appelée dans un contexte où il $IFSaurait pu être modifié , vous voudriez le définir à l' $IFSavance (et éventuellement le restaurer par la suite ou utiliser une portée locale pour celui-ci si le reste du code suppose un non modifié $IFS)
Stéphane Chazelas
32

La façon la plus robuste de coder est d'utiliser un tableau:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
glenn jackman
la source
C'est la bonne réponse. Référence
l0b0
2
C'est une bonne réponse, j'ai donc voté en faveur mais Kusalanda m'a aidé à mieux comprendre pourquoi mon code était incorrect et je ne peux en accepter qu'un.
z32a7ul
Je me heurtais à un monde de problèmes jusqu'à ce que quelqu'un sur la liste rsync me montre cette construction. Il est particulièrement utile que certains éléments soient des chaînes vides. Cela fait disparaître les chaînes vides. Certaines commandes aiment cpet rsyncferont des choses inattendues si votre commande se développe en quelque chose comme rsync '' rest of parameters. C'est idéal pour créer une commande pièce par pièce conditionnellement, puis l'exécuter une seule fois au même endroit.
Joe
17

Vous essayez de stocker une liste de chaînes dans une variable de chaîne. Ça ne va pas. Peu importe comment vous accédez à la variable, quelque chose est cassé.

wget_options='--mirror --no-host-directories'définit la variable wget_optionssur une chaîne qui contient un espace. À ce stade, il n'y a aucun moyen de savoir si l'espace est censé faire partie d'une option ou un séparateur entre les options.

Lorsque vous accédez à la variable avec une substitution entre guillemets wget "$wget_options", la valeur de la variable est utilisée comme chaîne. Cela signifie qu'il est transmis en tant que paramètre unique à wget, c'est donc une seule option. Cela casse dans votre cas, car vous vouliez que cela signifie plusieurs options.

Lorsque vous utilisez une substitution sans guillemets wget $wget_options, la valeur de la variable chaîne subit un processus d'expansion surnommé «split + glob»:

  1. Prenez la valeur de la variable et divisez-la en parties séparées par des espaces (en supposant que vous n'avez pas modifié la $IFSvariable). Il en résulte une liste intermédiaire de chaînes.
  2. Pour chaque élément de la liste intermédiaire, s'il s'agit d'un modèle générique qui correspond à un ou plusieurs fichiers, remplacez cet élément par la liste des fichiers correspondants.

Cela se produit dans votre exemple, car le processus de fractionnement transforme l'espace en séparateur, mais ne fonctionne pas en général, car une option peut contenir des espaces et des caractères génériques.

Dans ksh, bash, yash et zsh, vous pouvez utiliser une variable de tableau. Un tableau en terminologie shell est une liste de chaînes, il n'y a donc pas de perte d'informations. Pour créer une variable de tableau, placez des parenthèses autour des éléments du tableau lors de l'attribution de la valeur à la variable. Pour accéder à tous les éléments du tableau, utilisez - ceci est une généralisation de , qui forme une liste à partir des éléments du tableau. Notez que vous avez également besoin des guillemets doubles, sinon chaque élément subit split + glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

En clair, il n'y a pas de variables de tableau. Si cela ne vous dérange pas de perdre les arguments positionnels, vous pouvez les utiliser pour stocker une liste de chaînes.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Pour plus d'informations, voir Pourquoi mon script shell s'étouffe-t-il sur les espaces ou d'autres caractères spéciaux?

Gilles 'SO- arrête d'être méchant'
la source
Pour sh plaine un sous - shell préserverait les arguments de position: (set -- ...; exec wget "$@" ...).
John Kugelman soutient Monica