Quand enrouler des guillemets autour d'une variable shell?

184

Quelqu'un pourrait-il me dire si je devrais ou non entourer des guillemets des variables dans un script shell?

Par exemple, ce qui suit est-il correct:

xdg-open $URL 
[ $? -eq 2 ]

ou

xdg-open "$URL"
[ "$?" -eq "2" ]

Et si oui, pourquoi?

Cristian
la source
Cette question reçoit beaucoup de doublons, dont beaucoup ne concernent pas les variables, j'ai donc rebaptisé «valeur» au lieu de «variable». J'espère que cela aidera plus de gens à trouver ce sujet.
tripleee
1
@codeforester Que se passe-t-il avec la modification annulée?
tripleee

Réponses:

131

Règle générale: citez-le s'il peut être vide ou contenir des espaces (ou tout autre espace en fait) ou des caractères spéciaux (jokers). Ne pas citer de chaînes avec des espaces conduit souvent le shell à séparer un seul argument en plusieurs.

$?n'a pas besoin de guillemets car il s'agit d'une valeur numérique. Qu'il s'agisse$URL besoin dépend de ce que vous permettez là - bas et si vous voulez encore un argument si elle est vide.

J'ai tendance à toujours citer des chaînes juste par habitude car c'est plus sûr de cette façon.

paxdiablo
la source
2
Notez que "espaces" signifie en réalité "n'importe quel espace".
William Pursell
4
@Cristian: Si vous n'êtes pas sûr de ce que peut contenir la variable, il est plus sûr de la citer. J'ai tendance à suivre le même principe que paxdiablo et à prendre l'habitude de tout citer (à moins qu'il y ait une raison spécifique de ne pas le faire).
Gordon Davisson
11
Si vous ne connaissez pas la valeur d'IFS, citez-la quoi qu'il arrive. Si IFS=0, alors echo $?peut être très surprenant.
Charles Duffy
3
Citation basée sur le contexte, pas sur ce que vous attendez des valeurs, sinon vos bogues seront pires. Par exemple, vous êtes sûr qu'aucun de vos chemins ne contient d' espaces, vous pensez donc pouvoir écrire cp $source1 $source2 $dest, mais si pour une raison inattendue destn'est pas défini, le troisième argument disparaît tout simplement, et il se recopiera silencieusement source1au source2lieu de vous donner un erreur appropriée pour la destination vide (comme ce serait le cas si vous aviez cité chaque argument).
Derek Veit
3
quote it if...a le processus de réflexion à l'envers - les citations ne sont pas quelque chose que vous ajoutez quand vous en avez besoin, c'est quelque chose que vous supprimez quand vous en avez besoin. Mettez toujours les chaînes et les scripts entre guillemets simples, à moins que vous n'ayez besoin d'utiliser des guillemets doubles (par exemple pour laisser une variable se développer) ou de ne pas utiliser de guillemets (par exemple pour faire une extension de nom de fichier).
Ed Morton
92

En bref, citez tout ce qui ne nécessite pas que le shell effectue le fractionnement des jetons et l'expansion des caractères génériques.

Les guillemets simples protègent le texte entre eux textuellement. C'est l'outil approprié lorsque vous devez vous assurer que la coque ne touche pas du tout la corde. En règle générale, il s'agit du mécanisme de cotation de choix lorsque vous n'avez pas besoin d'interpolation de variable.

$ echo 'Nothing \t in here $will change'
Nothing \t in here $will change

$ grep -F '@&$*!!' file /dev/null
file:I can't get this @&$*!! quoting right.

Les guillemets doubles conviennent lorsqu'une interpolation variable est requise. Avec des adaptations appropriées, c'est également une bonne solution de contournement lorsque vous avez besoin de guillemets simples dans la chaîne. (Il n'y a pas de moyen simple d'échapper un guillemet simple entre guillemets simples, car il n'y a pas de mécanisme d'échappement à l'intérieur des guillemets simples - s'il y en avait, ils ne citeraient pas complètement textuellement.)

$ echo "There is no place like '$HOME'"
There is no place like '/home/me'

Aucun guillemet n'est approprié lorsque vous avez spécifiquement besoin du shell pour effectuer le fractionnement de jetons et / ou l'expansion de caractères génériques.

Fractionnement de jetons;

 $ words="foo bar baz"
 $ for word in $words; do
 >   echo "$word"
 > done
 foo
 bar
 baz

Par contre:

 $ for word in "$words"; do echo "$word"; done
 foo bar baz

(La boucle ne s'exécute qu'une seule fois, sur la chaîne unique entre guillemets.)

 $ for word in '$words'; do echo "$word"; done
 $words

(La boucle ne s'exécute qu'une seule fois, sur la chaîne littérale entre guillemets simples.)

Extension de caractères génériques:

$ pattern='file*.txt'
$ ls $pattern
file1.txt      file_other.txt

Par contre:

$ ls "$pattern"
ls: cannot access file*.txt: No such file or directory

(Il n'y a pas de fichier nommé littéralement file*.txt.)

$ ls '$pattern'
ls: cannot access $pattern: No such file or directory

(Il n'y a pas de fichier nommé $pattern !)

En termes plus concrets, tout ce qui contient un nom de fichier doit généralement être entre guillemets (car les noms de fichier peuvent contenir des espaces et d'autres métacaractères du shell). Tout ce qui contient une URL doit généralement être cité (car de nombreuses URL contiennent des métacaractères shell comme ?et& ). Tout ce qui contient une expression régulière doit généralement être cité (idem idem). Tout ce qui contient des espaces blancs significatifs autres que des espaces simples entre des caractères autres que des espaces doit être entre guillemets (car sinon, le shell réduira effectivement les espaces blancs en espaces simples et coupera tout espace de début ou de fin).

Lorsque vous savez qu'une variable ne peut contenir qu'une valeur qui ne contient pas de métacaractères shell, les guillemets sont facultatifs. Ainsi, un sans guillemets $?est fondamentalement bien, car cette variable ne peut contenir qu'un seul nombre. cependant,"$?" est également correct, et recommandé pour la cohérence et l'exactitude générale (bien que ce soit ma recommandation personnelle, pas une politique largement reconnue).

Les valeurs qui ne sont pas des variables suivent fondamentalement les mêmes règles, bien que vous puissiez également échapper à tous les métacaractères au lieu de les citer. Pour un exemple courant, une URL avec un &dedans sera analysée par le shell en tant que commande d'arrière-plan, sauf si le métacaractère est échappé ou entre guillemets:

$ wget http://example.com/q&uack
[1] wget http://example.com/q
-bash: uack: command not found

(Bien sûr, cela se produit également si l'URL est dans une variable sans guillemets.) Pour une chaîne statique, les guillemets simples ont le plus de sens, bien que toute forme de citation ou d'échappement fonctionne ici.

wget 'http://example.com/q&uack'  # Single quotes preferred for a static string
wget "http://example.com/q&uack"  # Double quotes work here, too (no $ or ` in the value)
wget http://example.com/q\&uack   # Backslash escape
wget http://example.com/q'&'uack  # Only the metacharacter really needs quoting

Le dernier exemple suggère également un autre concept utile, que j'aime appeler "la citation à bascule". Si vous devez mélanger des guillemets simples et doubles, vous pouvez les utiliser côte à côte. Par exemple, les chaînes entre guillemets suivantes

'$HOME '
"isn't"
' where `<3'
"' is."

peuvent être collés ensemble dos à dos, formant une seule longue chaîne après la tokenisation et la suppression du devis.

$ echo '$HOME '"isn't"' where `<3'"' is."
$HOME isn't where `<3' is.

Ce n'est pas très lisible, mais c'est une technique courante et donc bon à savoir.

En passant, les scripts ne devraient généralement pas être utilisés lspour quoi que ce soit. Pour développer un caractère générique, utilisez-le simplement.

$ printf '%s\n' $pattern   # not ``ls -1 $pattern''
file1.txt
file_other.txt

$ for file in $pattern; do  # definitely, definitely not ``for file in $(ls $pattern)''
>  printf 'Found file: %s\n' "$file"
> done
Found file: file1.txt
Found file: file_other.txt

(La boucle est complètement superflue dans ce dernier exemple; printf fonctionne particulièrement bien avec plusieurs arguments stat. Mais la boucle sur une correspondance générique est un problème courant et souvent mal effectuée.)

Une variable contenant une liste de jetons à boucler ou un caractère générique à développer est moins fréquemment vue, donc nous abrégons parfois en "tout citer à moins que vous ne sachiez précisément ce que vous faites".

tripleee
la source
1
Ceci est une variante de (une partie de) une réponse que j'ai publiée à une question connexe . Je le colle ici parce que c'est assez succinct et bien défini pour devenir une question canonique pour ce problème particulier.
tripleee
4
Je noterai qu'il s'agit de l'élément n ° 0 et d'un thème récurrent sur la collection mywiki.wooledge.org/BashPitfalls d'erreurs Bash courantes. De nombreux éléments de cette liste concernent essentiellement ce problème.
tripleee
27

Voici une formule en trois points pour les citations en général:

Double citation

Dans les contextes où nous voulons supprimer le fractionnement et la globalisation des mots. Également dans les contextes où nous voulons que le littéral soit traité comme une chaîne et non comme une expression régulière.

Guillemets simples

Dans les chaînes littérales où nous voulons supprimer l'interpolation et le traitement spécial des barres obliques inverses. En d'autres termes, des situations où l'utilisation de guillemets doubles serait inappropriée.

Pas de devis

Dans les contextes où nous sommes absolument sûrs qu'il n'y a pas de problèmes de fractionnement ou de globalisation de mots ou nous voulons un fractionnement et une globalisation de mots .


Exemples

Double citation

  • chaînes littérales avec espace ( "StackOverflow rocks!", "Steve's Apple")
  • extensions de variables ( "$var", "${arr[@]}")
  • substitutions de commandes ( "$(ls)", "`ls`")
  • globs où le chemin du répertoire ou la partie du nom de fichier comprend des espaces ( "/my dir/"*)
  • pour protéger les guillemets simples ( "single'quote'delimited'string")
  • Expansion des paramètres Bash ( "${filename##*/}")

Guillemets simples

  • noms de commande et arguments contenant des espaces
  • chaînes littérales qui nécessitent une interpolation pour être supprimées ( 'Really costs $$!', 'just a backslash followed by a t: \t')
  • pour protéger les guillemets doubles ( 'The "crux"')
  • littéraux regex qui nécessitent une interpolation pour être supprimés
  • utiliser les guillemets shell pour les littéraux impliquant des caractères spéciaux ( $'\n\t')
  • utiliser les guillemets shell où nous devons protéger plusieurs guillemets simples et doubles ( $'{"table": "users", "where": "first_name"=\'Steve\'}')

Pas de devis

  • variables numériques autour standards ( $$, $?, $#etc.)
  • dans des contextes arithmétiques aiment ((count++)), "${arr[idx]}","${string:start:length}"
  • [[ ]]expression intérieure qui est exempte de problèmes de fractionnement de mots et de globbing (c'est une question de style et les opinions peuvent varier considérablement)
  • où nous voulons diviser les mots (for word in $words )
  • où nous voulons globbing ( for txtfile in *.txt; do ...)
  • où nous voulons ~être interprétés comme $HOME( ~/"some dir"mais pas "~/some dir")

Voir également:

codeforester
la source
3
Selon ces directives, on obtiendrait une liste des fichiers dans le répertoire racine en écrivant "ls" "/" L'expression «tous les contextes de chaîne» doit être qualifiée plus soigneusement.
William Pursell
5
Dans [[ ]], les guillemets importent du côté droit de =/ ==et =~: cela fait la différence entre interpréter une chaîne comme un motif / regex ou littéralement.
Benjamin W.
6
Un bon aperçu, mais les commentaires de @ BenjaminW. valent la peine d'être intégrés et les chaînes entre guillemets C ANSI ( $'...') devraient certainement avoir leur propre section.
mklement0
3
@ mklement0, en effet ils sont équivalents. Ces lignes directrices indiquent que vous devez toujours taper "ls" "/"au lieu du plus courant ls /, et je considère cela comme un défaut majeur dans les lignes directrices.
William Pursell du
4
Pour aucun devis, vous pouvez ajouter une affectation de variable ou case:)
PesaLe
4

J'utilise généralement cité comme "$var"pour plus de sécurité, sauf si je suis sûr qu'il $varne contient pas d'espace.

J'utilise $varcomme moyen simple de joindre des lignes:

lines="`cat multi-lines-text-file.txt`"
echo "$lines"                             ## multiple lines
echo $lines                               ## all spaces (including newlines) are zapped
Lien de Bach
la source
Le dernier commentaire est quelque peu trompeur; les nouvelles lignes sont effectivement remplacées par des espaces, pas simplement supprimées.
tripleee
-1

Pour utiliser les variables dans le script shell, utilisez "" les variables entre guillemets comme celle citée signifie que la variable peut contenir des espaces ou des caractères spéciaux qui n'affecteront pas l'exécution de votre script shell. Sinon, si vous êtes sûr de ne pas avoir d'espaces ou de caractères spéciaux dans votre nom de variable, vous pouvez les utiliser sans "".

Exemple:

echo "$ url name" - (peut être utilisé à tout moment)

echo "$ url name" - (Ne peut pas être utilisé dans de telles situations donc prenez des précautions avant de l'utiliser)

Vipul Sharma
la source