Toujours utiliser des guillemets doubles autour de substitutions variables et substitutions de commandes: "$foo"
,"$(foo)"
Si vous utilisez $foo
non cité, votre script s'étouffera en entrée ou en paramètres (ou sortie de commande, avec $(foo)
) contenant des espaces ou \[*?
.
Là, tu peux arrêter de lire. Bon, d'accord, en voici quelques autres:
read
- Pour lire les entrées ligne par ligne avec les fonctions read
intégrées, utilisezwhile IFS= read -r line; do …
spécialement
Plain read
traite les barres obliques inverses et les espaces.
xargs
- évitexargs
. Si vous devez utiliser xargs
, faites-le xargs -0
. Au lieu de find … | xargs
, préférezfind … -exec …
.
xargs
traite \"'
spécialement les espaces et les caractères .
Cette réponse s'applique aux coquilles Bourne / style (POSIX sh
, ash
, dash
, bash
, ksh
, mksh
, yash
...). Les utilisateurs de Zsh doivent l'ignorer et lire la fin de Quand la double cotation est-elle nécessaire? au lieu. Si vous voulez tout savoir, lisez la norme ou le manuel de votre shell.
Notez que les explications ci-dessous contiennent quelques approximations (des déclarations qui sont vraies dans la plupart des conditions mais qui peuvent être affectées par le contexte ou la configuration environnante).
Pourquoi ai-je besoin d'écrire "$foo"
? Que se passe-t-il sans les citations?
$foo
ne signifie pas “prendre la valeur de la variable foo
”. Cela signifie quelque chose de beaucoup plus complexe:
- Tout d'abord, prenons la valeur de la variable.
- Fractionnement des champs: traitez cette valeur comme une liste de champs séparés par des espaces et construisez la liste résultante. Par exemple, si la variable contient
foo * bar
le résultat de cette étape est la liste 3 éléments foo
, *
, bar
.
- Génération de noms de fichiers: traitez chaque champ comme un glob, c'est-à-dire comme un modèle générique, et remplacez-le par la liste des noms de fichiers correspondant à ce modèle. Si le modèle ne correspond à aucun fichier, il n'est pas modifié. Dans notre exemple, cela donne la liste contenant
foo
, suivi de la liste des fichiers du répertoire en cours, et enfin bar
. Si le répertoire courant est vide, le résultat est foo
, *
, bar
.
Notez que le résultat est une liste de chaînes. Il existe deux contextes dans la syntaxe du shell: contexte de liste et contexte de chaîne. La division de champ et la génération de nom de fichier ne se produisent que dans un contexte de liste, mais c'est la plupart du temps. Les guillemets doubles délimitent un contexte de chaîne: la chaîne entière entre guillemets est une chaîne unique, à ne pas scinder. (Exception: "$@"
pour étendre à la liste des paramètres de position, par exemple , "$@"
équivaut à "$1" "$2" "$3"
s'il y a trois paramètres de position Voir. Quelle est la différence entre $ * et $ @? )
Il en va de même pour la substitution de commande avec $(foo)
ou avec `foo`
. Sur une note de côté, n'utilisez pas `foo`
: ses règles de citations sont étranges et non-portables, et tous les shells modernes supportent $(foo)
ce qui est absolument équivalent sauf pour avoir des règles de citations intuitives.
La sortie de la substitution arithmétique subit également les mêmes extensions, mais cela ne IFS
pose normalement pas de problème, car elle ne contient que des caractères non extensibles (en supposant qu'elle ne contient pas de chiffres ou -
).
Voir Quand la double cotation est-elle nécessaire? pour plus de détails sur les cas où vous pouvez omettre les citations.
À moins que vous ne vouliez dire que tout cela soit rigoureux, souvenez-vous de toujours utiliser des guillemets doubles autour des substitutions de variables et de commandes. Faites attention: omettre les guillemets peut entraîner non seulement des erreurs, mais également des failles de sécurité .
Comment traiter une liste de noms de fichiers?
Si vous écrivez myfiles="file1 file2"
avec des espaces pour séparer les fichiers, cela ne peut pas fonctionner avec des noms de fichiers contenant des espaces. Les noms de fichier Unix peuvent contenir n'importe quel caractère autre que /
(ce qui est toujours un séparateur de répertoire) et des octets nuls (que vous ne pouvez pas utiliser dans des scripts de shell avec la plupart des shells).
Même problème avec myfiles=*.txt; … process $myfiles
. Lorsque vous faites cela, la variable myfiles
contient la chaîne de 5 caractères *.txt
et c'est lorsque vous écrivez $myfiles
que le caractère générique est développé. Cet exemple fait travailler, jusqu'à ce que vous changez votre script pour être myfiles="$someprefix*.txt"; … process $myfiles
. Si someprefix
est défini sur final report
, cela ne fonctionnera pas.
Pour traiter une liste de tout type (tels que des noms de fichiers), placez-la dans un tableau. Cela nécessite mksh, ksh93, yash ou bash (ou zsh, qui n'a pas tous ces problèmes de citations); un shell POSIX simple (comme ash ou dash) n'a pas de variables de tableau.
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88 a des variables de tableau avec une syntaxe d'attribution différente set -A myfiles "someprefix"*.txt
(voir la variable d'affectation sous un environnement ksh différent si vous avez besoin de la portabilité de ksh88 / bash). Les shells Bourne / style POSIX ont un seul tableau, le tableau de paramètres de position "$@"
que vous définissez avec set
et qui est local à une fonction:
set -- "$someprefix"*.txt
process -- "$@"
Qu'en est-il des noms de fichiers qui commencent par -
?
Sur une note connexe, n'oubliez pas que les noms de fichier peuvent commencer par un -
(tiret / moins), ce que la plupart des commandes interprètent comme désignant une option. Si vous avez un nom de fichier qui commence par une partie variable, assurez-vous de passer --
avant, comme dans l'extrait de code ci-dessus. Cela indique à la commande qu'elle a atteint la fin des options, ainsi tout ce qui suit est un nom de fichier même s'il commence par -
.
Sinon, vous pouvez vous assurer que vos noms de fichiers commencent par un caractère autre que -
. Les noms de fichiers absolus commencent par /
, et vous pouvez ajouter ./
au début des noms relatifs. L'extrait de code suivant transforme le contenu de la variable f
en un moyen «sûr» de se référer au même fichier qu'il est garanti de ne pas commencer -
.
case "$f" in -*) "f=./$f";; esac
Sur une note finale à ce sujet, méfiez - vous que certaines commandes interprètent -
comme entrée standard sens ou la sortie standard, même après --
. Si vous devez vous référer à un fichier nommé -
, ou si vous appelez un tel programme et que vous ne voulez pas qu'il soit lu à partir de stdin ou écrit sur stdout, veillez à réécrire -
comme ci-dessus. Voir Quelle est la différence entre "du -sh *" et "du -sh ./*"? pour plus de discussion.
Comment stocker une commande dans une variable?
"Commande" peut signifier trois choses: un nom de commande (le nom en tant qu'exécutable, avec ou sans chemin d'accès complet, ou le nom d'une fonction, intégrée ou alias), un nom de commande avec des arguments ou un morceau de code shell. Il existe donc différentes manières de les stocker dans une variable.
Si vous avez un nom de commande, stockez-le et utilisez la variable avec des guillemets habituels.
command_path="$1"
…
"$command_path" --option --message="hello world"
Si vous avez une commande avec des arguments, le problème est le même que pour une liste de noms de fichiers ci-dessus: il s'agit d'une liste de chaînes, pas d'une chaîne. Vous ne pouvez pas simplement insérer les arguments dans une seule chaîne avec des espaces entre eux, car vous ne pouvez pas faire la différence entre les espaces qui font partie des arguments et les espaces qui séparent les arguments. Si votre shell a des tableaux, vous pouvez les utiliser.
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
Que faire si vous utilisez un shell sans tableaux? Vous pouvez toujours utiliser les paramètres de position, si cela ne vous dérange pas de les modifier.
set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
Et si vous avez besoin de stocker une commande shell complexe, par exemple avec des redirections, des pipes, etc.? Ou si vous ne voulez pas modifier les paramètres de position? Ensuite, vous pouvez construire une chaîne contenant la commande et utiliser la commande eval
intégrée.
code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
Notez les guillemets imbriqués dans la définition de code
: les guillemets simples '…'
délimitent un littéral de chaîne, de sorte que la valeur de la variable code
soit la chaîne /path/to/executable --option --message="hello world" -- /path/to/file1
. La commande eval
intégrée demande au shell d’analyser la chaîne passée en tant qu’argument comme si elle apparaissait dans le script. Ainsi, à ce stade, les guillemets et le canal sont analysés, etc.
L'utilisation eval
est délicate. Réfléchissez bien à ce qui sera analysé quand. En particulier, vous ne pouvez pas simplement insérer un nom de fichier dans le code: vous devez le citer, comme vous le feriez s'il se trouvait dans un fichier de code source. Il n'y a pas de moyen direct de le faire. Quelque chose comme des code="$code $filename"
pauses si le nom de fichier contient une coquille caractère spécial (espaces, $
, ;
, |
, <
, >
, etc.). code="$code \"$filename\""
brise encore "$\`
. Même les code="$code '$filename'"
pauses si le nom du fichier contient un '
. Il y a deux solutions.
Ajoutez une couche de guillemets autour du nom du fichier. Le moyen le plus simple consiste à ajouter des guillemets simples autour de celui-ci et à remplacer les guillemets simples par '\''
.
quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
code="$code '${quoted_filename%.}'"
Conservez le développement variable dans le code afin qu'il soit recherché lors de l'évaluation du code, pas lors de la création du fragment de code. C'est plus simple mais cela ne fonctionne que si la variable est toujours là avec la même valeur au moment de l'exécution du code, pas par exemple si le code est construit dans une boucle.
code="$code \"\$filename\""
Enfin, avez-vous vraiment besoin d’une variable contenant du code? Le moyen le plus naturel de donner un nom à un bloc de code est de définir une fonction.
Quoi de neuf avec read
?
Sans -r
, read
autorise les lignes de continuation - il s'agit d'une seule ligne logique d'entrée:
hello \
world
read
divise la ligne de saisie en champs délimités par des caractères $IFS
(sans la -r
barre oblique inverse, ils échappent également à ceux-ci). Par exemple, si l'entrée est une ligne contenant trois mots, read first second third
définissez first
le premier mot d'entrée, second
le deuxième mot et third
le troisième mot. S'il y a plus de mots, la dernière variable contient tout ce qui reste après avoir défini les précédentes. Les espaces de début et de fin sont supprimés.
La définition IFS
de la chaîne vide évite toute réduction. Voir Pourquoi «tant que IFS = read» est utilisé souvent, au lieu de `IFS =; pendant la lecture..`? pour une explication plus longue.
Quel est le problème avec xargs
?
Le format d’entrée xargs
est constitué de chaînes séparées par des espaces qui peuvent éventuellement être simples ou doubles. Aucun outil standard ne génère ce format.
L'entrée dans xargs -L1
ou xargs -l
est presque une liste de lignes, mais pas tout à fait - s'il y a un espace à la fin d'une ligne, la ligne suivante est une ligne de continuation.
Vous pouvez utiliser le xargs -0
cas échéant (et si disponible: GNU (Linux, Cygwin), BusyBox, BSD, OSX, mais ce n'est pas dans POSIX). C'est sûr, car les octets nuls ne peuvent pas apparaître dans la plupart des données, en particulier dans les noms de fichiers. Pour produire une liste de noms de fichiers séparés par un caractère null, utilisez find … -print0
(ou vous pouvez utiliser find … -exec …
comme expliqué ci-dessous).
Comment traiter les fichiers trouvés par find
?
find … -exec some_command a_parameter another_parameter {} +
some_command
doit être une commande externe, ce ne peut pas être une fonction shell ou un alias. Si vous avez besoin d'appeler un shell pour traiter les fichiers, appelez sh
explicitement.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
J'ai une autre question
Parcourir la balise de citation sur ce site, ou shell ou script-shell . (Cliquez sur «En savoir plus…» pour voir quelques astuces générales et une liste de questions courantes, sélectionnées à la main.) Si vous avez cherché et que vous ne trouvez pas de réponse, demandez plus loin .
shellcheck
vous aider à améliorer la qualité de vos programmes.