Pourquoi mon script shell s'étouffe-t-il sur des espaces ou d'autres caractères spéciaux?

285

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és helloet world.
  • 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?

Gilles
la source
16
shellcheckvous aider à améliorer la qualité de vos programmes.
aurelien
3
Outre les techniques de protection décrites dans les réponses, et bien que cela soit probablement évident pour la plupart des lecteurs, je pense qu’il serait utile de préciser que, lorsque les fichiers doivent être traités à l’aide d’outils de ligne de commande, il est recommandé d’éviter les caractères de fantaisie. noms en premier lieu, si possible.
bli
1
@bli Non, seuls les bogues mettent plus de temps à apparaître. Il cache des insectes aujourd'hui. Et maintenant, vous ne connaissez pas tous les noms de fichiers utilisés plus tard avec votre code.
Volker Siegel
Tout d’abord, si vos paramètres contiennent des espaces, vous devez les citer avant de les entrer (sur la ligne de commande). Cependant, vous pouvez saisir toute la ligne de commande et l'analyser vous-même. Deux espaces ne se tournent pas vers un seul espace; N'importe quelle quantité d'espace indique à votre script que c'est la variable suivante. Si vous faites quelque chose comme "echo $ 1 $ 2", c'est votre script qui met un espace entre les deux. Utilisez également "find (-exec)" pour parcourir les fichiers contenant des espaces plutôt qu'une boucle for; vous pouvez gérer les espaces plus facilement.
Patrick Taylor

Réponses:

352

Toujours utiliser des guillemets doubles autour de substitutions variables et substitutions de commandes: "$foo","$(foo)"

Si vous utilisez $foonon 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 readintégrées, utilisezwhile IFS= read -r line; do … spécialement
    Plain readtraite 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 … .
    xargstraite \"'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?

$foone 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 IFSpose 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 myfilescontient la chaîne de 5 caractères *.txtet c'est lorsque vous écrivez $myfilesque 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 someprefixest 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 setet 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 fen 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 evalinté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 codesoit la chaîne /path/to/executable --option --message="hello world" -- /path/to/file1. La commande evalinté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 evalest 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, readautorise les lignes de continuation - il s'agit d'une seule ligne logique d'entrée:

hello \
world

readdivise la ligne de saisie en champs délimités par des caractères $IFS(sans la -rbarre oblique inverse, ils échappent également à ceux-ci). Par exemple, si l'entrée est une ligne contenant trois mots, read first second thirddéfinissez firstle premier mot d'entrée, secondle deuxième mot et thirdle 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 IFSde 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 xargsest 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 -L1ou xargs -lest 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 -0cas é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_commanddoit ê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 shexplicitement.

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

J'ai une autre question

Parcourir la balise de sur ce site, ou ou . (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 .

Gilles
la source
6
@ John1024 C'est une fonctionnalité GNU uniquement, je vais donc m'en tenir à «aucun outil standard».
Gilles
2
Vous avez également besoin de guillemets autour $(( ... ))(également $[...]dans certains coquillages) sauf dans zsh(même en émulation de sh) et mksh.
Stéphane Chazelas
3
Notez que ce xargs -0n'est pas POSIX. Sauf avec FreeBSD xargs, vous voulez généralement xargs -r0au lieu de xargs -0.
Stéphane Chazelas
2
@ John1024, non, ls --quoting-style=shell-alwaysn'est pas compatible avec xargs. Trytouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas
3
Une autre fonctionnalité intéressante (uniquement pour GNU) est xargs -d "\n"que vous pouvez par exemple lancer locate PATTERN1 |xargs -d "\n" grep PATTERN2une 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
Adam Katz
26

Alors que Gilles répond est excellent, je suis en désaccord avec son point principal

Utilisez toujours des guillemets autour des substitutions de variables et de commandes: "$ foo", "$ (foo)"

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

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

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.

Steven Penny
la source
19
Comme je le mentionne dans ma réponse, voir unix.stackexchange.com/questions/68694/… pour plus de détails. Remarquez la question «Pourquoi mon script shell s’étouffe-t-il?». Le problème le plus courant (des années d'expérience sur ce site et ailleurs) est l'absence de guillemets doubles. «Toujours utiliser des guillemets» est plus facile à retenir que «Toujours utiliser des guillemets, sauf dans les cas où ils ne sont pas nécessaires».
Gilles
14
Les règles sont difficiles à comprendre pour les débutants. Par exemple, foo=$barest OK, mais export foo=$barou env foo=$varne 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 .
Stéphane Chazelas
5
@StevenPenny Est-ce vraiment plus correct? Y a-t-il des cas raisonnables où des citations briseraient le script? Dans des situations où la moitié des cas guillemets doivent être utilisés, et dans d' autres citations de la moitié peuvent être utilisés en option - alors une recommandation « toujours utiliser des guillemets, juste au cas où » est celui qui devrait être pensé, car il est vrai, simple et moins risqué. Enseigner de telles listes d'exceptions aux débutants est bien connu pour être inefficace (sans contexte, ils ne s'en souviendront pas) et contre-productif, car ils vont confondre les citations nécessaires / inutiles, briser leurs scripts et les démotiver pour apprendre plus loin.
Peteris
6
Mon 0,02 $ serait que recommander de citer tout est un bon conseil. Citer à tort quelque chose qui n'en a pas besoin est inoffensif, c'est à tort de ne pas citer quelque chose qui en a besoin est nocif. Ainsi, pour la majorité des auteurs de scripts shell qui ne comprendront jamais les subtilités de la division exacte des mots, il est beaucoup plus prudent de citer tout ce que vous ne voulez essayer de citer lorsque cela est nécessaire.
godlygeek
5
@Peteris et godlygeek: "Y a-t-il des cas raisonnables où des citations briseraient le script?" Cela dépend de votre définition de «raisonnable». Si un script définit criteria="-type f", puis find . $criteriafonctionne , mais find . "$criteria"ne fonctionne pas.
G-Man
22

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 à:

  1. accepter l'entrée

  2. 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 $wordouecho $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

  3. é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 .

  4. puis pour exécuter la commande résultante

    • dans la plupart des cas, il s’agit de transmettre les résultats de son interprétation sous une forme ou une autre

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 execeux. La plupart des obus ne traitent pas bien l' NULoctet - voire pas du tout - et c'est parce qu'ils se dédoublent déjà. Le shell a exec beaucoup à faire et il doit le faire avec un NULtableau d’arguments délimité qu’il remet au noyau du système à la execfois. 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. zshnotamment ne gâche pas cela.

Et c’est là que l’ $IFSintervient. Il $IFSexiste 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. $IFSdivise l' expansion de coquille sur délimiteurs autres que NUL- ou, en d' autres termes , les substituts de coquille octets résultant d'une expansion qui correspondent à ceux de la valeur $IFSavec NULdans ses données internes-réseaux. En regardant cela, vous constaterez peut-être que chaque extension de shell à division de champ est un $IFStableau de données délimité.

Il est important de comprendre que $IFSne 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, $IFSne 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 quand IFS=est défini sur une valeur vide.

Sauf indication contraire, $IFSest en soi une $IFSextension 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 $IFSest 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.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Mais il s’agit simplement de $IFSsé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.

set -f

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 ....

set +f

... 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:

echo "*" *

... 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:

set -f; echo "*" *

... les résultats pour les deux arguments sont identiques - le développement *ne se développe pas dans ce cas.

Mikeserv
la source
En fait, je suis d’accord avec @ StéphaneChazelas pour dire que cela déroute (surtout) plus que d’aider ... mais j’ai trouvé cela utile, personnellement, j’ai donc voté. J'ai maintenant une meilleure idée (et quelques exemples) de la façon dont IFSfonctionne réellement. Ce que je ne reçois est pourquoi il serait jamais une bonne idée de mettre IFSautre chose que par défaut.
Wildcard
1
@Wildcard - c'est un délimiteur de champ. si vous avez une valeur dans une variable que vous souhaitez développer en plusieurs champs, vous la divisez en deux $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; doneimprime \nensuite usr\npuis bin\n. Le premier echoest 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 font awktout le temps, de toute façon. votre coquille le fait aussi
mikeserv
3

J'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 -0fonctionne à 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:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

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:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

et en conséquence:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

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.)

Russ
la source
3
Mais vous aurez toujours des problèmes avec les noms de fichiers contenant des caractères de nouvelle ligne ou glob. Voir aussi: Pourquoi la boucle sur la sortie de find est-elle une mauvaise pratique? . Si vous utilisez zsh, vous pouvez utiliser IFS=$'\0'et utiliser -print0( zshne faites pas de déplacement sur les extensions, donc les caractères de déplacement ne sont pas un problème ici).
Stéphane Chazelas
1
Cela fonctionne avec les noms de fichiers contenant des espaces, mais cela ne fonctionne pas avec les noms de fichiers potentiellement hostiles ou les noms de fichiers accidentels «insensés». Vous pouvez facilement résoudre le problème des noms de fichiers contenant des caractères génériques en ajoutant 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.
Gilles
Bien, mon avertissement est que cela ne fonctionnera pas avec les nouvelles lignes dans les noms de fichiers. Cependant, je crois que nous devons tracer la ligne un peu timide de la folie ;-)
Russ
Et je ne sais pas pourquoi cela a reçu un vote négatif. C'est une méthode parfaitement raisonnable pour effectuer une itération sur des noms de fichiers contenant des espaces. L'utilisation de -print0 nécessite xargs, et certaines choses sont difficiles à utiliser avec cette chaîne. Je suis désolé que quelqu'un ne soit pas d'accord avec ma réponse, mais ce n'est pas une raison pour la rejeter.
Russ
0

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!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
Mattias Wadman
la source