Ou encore, un guide d'introduction à la gestion robuste de nom de fichier et à d'autres chaînes de transmission de scripts shell.
J'ai écrit un script shell qui fonctionne bien la plupart du temps. Mais cela étouffe certaines entrées (par exemple, certains noms de fichiers).
J'ai rencontré un problème tel que le suivant:
- J'ai un nom de fichier contenant un espace
hello world
, et il a été traité comme deux fichiers séparéshello
etworld
. - J'ai une ligne d'entrée avec deux espaces consécutifs et ils se réduisent à un dans l'entrée.
- Les espaces de début et de fin disparaissent des lignes en entrée.
- Parfois, lorsque l'entrée contient l'un des caractères
\[*?
, ceux-ci sont remplacés par du texte correspondant au nom de fichiers. - Il y a une apostrophe
'
(ou une double citation"
) dans l'entrée et les choses sont devenues bizarres après ce point. - Il y a une barre oblique inverse dans l'entrée (ou: J'utilise Cygwin et certains de mes noms de fichiers ont des
\
séparateurs de style Windows ).
Qu'est-ce qui se passe et comment puis-je résoudre ce problème?
bash
shell
shell-script
quoting
whitespace
Gilles
la source
la source
shellcheck
vous aider à améliorer la qualité de vos programmes.Réponses:
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 fonctionsread
intégrées, utilisezwhile IFS= read -r line; do …
spécialementPlain
read
traite les barres obliques inverses et les espaces.xargs
- évitexargs
. Si vous devez utiliserxargs
, faites-lexargs -0
. Au lieu defind … | 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 variablefoo
”. Cela signifie quelque chose de beaucoup plus complexe:foo * bar
le résultat de cette étape est la liste 3 élémentsfoo
,*
,bar
.foo
, suivi de la liste des fichiers du répertoire en cours, et enfinbar
. Si le répertoire courant est vide, le résultat estfoo
,*
,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 variablemyfiles
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 êtremyfiles="$someprefix*.txt"; … process $myfiles
. Sisomeprefix
est défini surfinal 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.
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 avecset
et qui est local à une fonction: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 variablef
en un moyen «sûr» de se référer au même fichier qu'il est garanti de ne pas commencer-
.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.
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.
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.
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.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 variablecode
soit la chaîne/path/to/executable --option --message="hello world" -- /path/to/file1
. La commandeeval
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 descode="$code $filename"
pauses si le nom de fichier contient une coquille caractère spécial (espaces,$
,;
,|
,<
,>
, etc.).code="$code \"$filename\""
brise encore"$\`
. Même lescode="$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
'\''
.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.
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: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éfinissezfirst
le premier mot d'entrée,second
le deuxième mot etthird
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
ouxargs -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, utilisezfind … -print0
(ou vous pouvez utiliserfind … -exec …
comme expliqué ci-dessous).Comment traiter les fichiers trouvés par
find
?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, appelezsh
explicitement.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 .
la source
$(( ... ))
(également$[...]
dans certains coquillages) sauf danszsh
(même en émulation de sh) etmksh
.xargs -0
n'est pas POSIX. Sauf avec FreeBSDxargs
, vous voulez généralementxargs -r0
au lieu dexargs -0
.ls --quoting-style=shell-always
n'est pas compatible avecxargs
. Trytouch $'a\nb'; ls --quoting-style=shell-always | xargs
xargs -d "\n"
que vous pouvez par exemple lancerlocate PATTERN1 |xargs -d "\n" grep PATTERN2
une recherche pour les noms de fichiers correspondant à PATTERN1 avec un contenu correspondant à PATTERN2 . Sans GNU, vous pouvez le faire par exemple, par exemplelocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Alors que Gilles répond est excellent, je suis en désaccord avec son point principal
Lorsque vous débutez avec un shell de type Bash qui effectue le fractionnement des mots, il est bien sûr conseillé de toujours utiliser des guillemets. Cependant, le fractionnement des mots n'est pas toujours effectué
§ Fractionnement des mots
Ces commandes peuvent être exécutées sans erreur
Je n’encourage pas les utilisateurs à adopter ce comportement, mais si une personne comprend bien que des mots se séparent se déchire, elle devrait pouvoir décider elle-même du moment où elle utilisera des guillemets.
la source
foo=$bar
est OK, maisexport foo=$bar
ouenv foo=$var
ne sont pas (au moins dans certaines coquilles). Un conseil pour débutant: citez toujours vos variables sauf si vous savez ce que vous faites et avez une bonne raison de ne pas le faire .criteria="-type f"
, puisfind . $criteria
fonctionne , maisfind . "$criteria"
ne fonctionne pas.Pour autant que je sache, il n’existe que deux cas dans lesquels il est nécessaire de faire des doubles guillemets, et ces cas impliquent les deux paramètres spéciaux du shell
"$@"
et"$*"
- qui sont spécifiés pour se développer différemment lorsqu’ils sont placés entre guillemets. Dans tous les autres cas (à l'exception peut-être des implémentations de tableaux spécifiques à un shell), le comportement d'une expansion est configurable - il existe des options pour cela.Bien entendu, cela ne signifie pas qu'il faille éviter les doubles guillemets. Au contraire, il s'agit probablement de la méthode la plus pratique et la plus robuste pour délimiter une extension proposée par la coque. Mais, je pense, comme des alternatives ont déjà été exposées de manière experte, c’est un excellent endroit pour discuter de ce qui se passe lorsque la coque augmente une valeur.
La coquille, dans son cœur et l' âme (pour ceux qui ont un tel) , est une commande-interprète - il est un analyseur, comme un grand, interactif,
sed
. Si votre instruction shell est étouffée par des espaces ou similaires, il est fort probable que vous n'ayez pas bien compris le processus d'interprétation du shell - en particulier, comment et pourquoi il traduit une instruction d'entrée en une commande pouvant donner lieu à une action. Le travail du shell consiste à:accepter l'entrée
interpréter et scinder correctement en mots d' entrée marqués
les mots d' entrée sont les éléments de la syntaxe du shell tels que
$word
ouecho $words 3 4* 5
les mots sont toujours divisés sur les espaces blancs - c'est juste la syntaxe - mais seuls les caractères d'espaces blancs littéraux servis au shell dans son fichier d'entrée
étendre ceux-ci si nécessaire dans plusieurs domaines
les champs résultent des extensions de mots - ils constituent la commande exécutable finale
À l’exception
"$@"
,$IFS
de la division de champ et de l’ extension du nom de chemin, un mot d’ entrée doit toujours correspondre à un seul champ .puis pour exécuter la commande résultante
Les gens disent souvent que le shell est un ciment , et si cela est vrai, alors il s’en tient à des listes d’arguments - ou de champs - à un processus ou à un autre quand c’est
exec
eux. La plupart des obus ne traitent pas bien l'NUL
octet - voire pas du tout - et c'est parce qu'ils se dédoublent déjà. Le shell aexec
beaucoup à faire et il doit le faire avec unNUL
tableau d’arguments délimité qu’il remet au noyau du système à laexec
fois. Si vous mêliez le délimiteur du shell à ses données délimitées, alors le shell le bousillerait probablement. Ses structures de données internes - comme la plupart des programmes - reposent sur ce délimiteur.zsh
notamment ne gâche pas cela.Et c’est là que l’
$IFS
intervient. Il$IFS
existe un paramètre de shell toujours présent et paramétrable qui définit la manière dont le shell doit scinder les extensions du shell d’un mot à un autre , en particulier les valeurs que ces champs doivent délimiter.$IFS
divise l' expansion de coquille sur délimiteurs autres queNUL
- ou, en d' autres termes , les substituts de coquille octets résultant d'une expansion qui correspondent à ceux de la valeur$IFS
avecNUL
dans ses données internes-réseaux. En regardant cela, vous constaterez peut-être que chaque extension de shell à division de champ est un$IFS
tableau de données délimité.Il est important de comprendre que
$IFS
ne délimite extensions qui ne sont pas déjà par ailleurs - qui délimitaient que vous pouvez faire avec des"
guillemets doubles. Lorsque vous citez une extension, vous la délimitez en tête et au moins à la queue de sa valeur. Dans ces cas,$IFS
ne s'applique pas car il n'y a pas de champs à séparer. En fait, un développement entre guillemets double présente un comportement de division de champ identique à un développement sans guillemets quandIFS=
est défini sur une valeur vide.Sauf indication contraire,
$IFS
est en soi une$IFS
extension de shell délimitée. La valeur par défaut est égale à<space><tab><newline>
- les trois présentent des propriétés spéciales lorsqu'elles sont contenues dans$IFS
. Alors que toute autre valeur de$IFS
est spécifiée pour être évaluée à un seul champ par occurrence d' expansion , les$IFS
espaces blancs - l'une quelconque de ces trois - sont spécifiés pour élier à un seul champ par séquence d' expansion et les séquences de début / fin sont entièrement supprimées. Ceci est probablement plus facile à comprendre via exemple.Mais il s’agit simplement de
$IFS
séparer les mots ou les espaces comme demandé, qu’en est-il des caractères spéciaux ?Le shell - par défaut - étendra également certains jetons non cités (tels que
?*[
ceux notés ailleurs ici) en plusieurs champs lorsqu'ils apparaissent dans une liste. C'est ce qu'on appelle l' expansion du nom de chemin , ou globbing . Il est un outil incroyablement utile, et, comme cela se produit après champ de fractionnement dans Parse ordre de l'enveloppe ne soit pas affecté par IFS $ - champs générés par un développement des chemins sont délimités sur la tête / queue des noms de fichiers eux - mêmes indépendamment du fait que leur contenu contient les caractères actuellement présents dans$IFS
. Ce comportement est activé par défaut - mais il est très facilement configuré autrement.Cela demande au shell de ne pas glober . L’extension du nom de chemin ne se produira pas au moins jusqu’à ce que ce paramètre soit annulé - par exemple, si le shell actuel est remplacé par un autre nouveau processus de shell ou ....
... est délivré à la coquille. Les guillemets doubles - comme ils le font également pour la
$IFS
division de champs - rendent ce paramètre global inutile par extension. Alors:... si le développement du nom de chemin est actuellement activé, les résultats par argument seront probablement très différents - le premier ne se développant que jusqu'à sa valeur littérale (le caractère avec un astérisque unique, c'est-à-dire pas du tout) et le second à la même chose si le répertoire de travail actuel ne contient aucun nom de fichier susceptible de correspondre (et presque tous les noms ) . Cependant si vous le faites:
... les résultats pour les deux arguments sont identiques - le développement
*
ne se développe pas dans ce cas.la source
IFS
fonctionne réellement. Ce que je ne reçois est pourquoi il serait jamais une bonne idée de mettreIFS
autre chose que par défaut.$IFS
.cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; done
imprime\n
ensuiteusr\n
puisbin\n
. Le premierecho
est vide car il/
s'agit d'un champ nul. Path_components peut avoir des lignes, des espaces ou autre chose - cela n'a pas d'importance, car les composants ont été divisés/
et non la valeur par défaut. les gens le fontawk
tout le temps, de toute façon. votre coquille le fait aussiJ'ai eu un grand projet vidéo avec des espaces dans les noms de fichiers et des espaces dans les noms de répertoires. Bien que cela
find -type f -print0 | xargs -0
fonctionne à plusieurs fins et à travers différents shells, je trouve que l’utilisation d’un IFS (séparateur de champs d’entrée) personnalisé vous donne plus de flexibilité si vous utilisez bash. L'extrait ci-dessous utilise bash et définit IFS sur une nouvelle ligne; à condition qu'il n'y ait pas de nouvelles lignes dans vos noms de fichiers:Notez l'utilisation de parenthèses pour isoler la redéfinition d'IFS. J'ai lu d'autres articles sur la façon de récupérer IFS, mais c'est simplement plus facile.
De plus, définir IFS sur nouvelle ligne vous permet de définir des variables de shell à l'avance et de les imprimer facilement. Par exemple, je peux développer une variable V de manière incrémentielle en utilisant des nouvelles lignes comme séparateurs:
et en conséquence:
Maintenant, je peux "lister" le réglage de V en
echo "$V"
utilisant des guillemets doubles pour afficher les nouvelles lignes. (Merci à ce fil pour l'$'\n'
explication.)la source
zsh
, vous pouvez utiliserIFS=$'\0'
et utiliser-print0
(zsh
ne faites pas de déplacement sur les extensions, donc les caractères de déplacement ne sont pas un problème ici).set -f
. D'autre part, votre approche échoue fondamentalement avec les noms de fichiers contenant des nouvelles lignes. Lorsque vous traitez avec des données autres que des noms de fichiers, les éléments vides échouent également.En tenant compte de toutes les implications en matière de sécurité mentionnées ci-dessus et en supposant que vous avez confiance et que vous contrôlez les variables que vous développez, il est possible d'avoir plusieurs chemins avec des espaces
eval
. Mais fais attention!la source