web-dev-qa-db-fra.com

Comprendre "IFS = read -r line"

Je comprends évidemment que l'on peut ajouter de la valeur à la variable de séparateur de champ interne. Par exemple:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Je comprends également que read -r line enregistrera les données de stdin dans une variable nommée line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Cependant, comment une commande peut-elle attribuer une valeur variable? Et stocke-t-il d'abord les données de stdin dans la variable line et donne ensuite la valeur de line à IFS?

72
Martin

Dans les shells POSIX, read, sans aucune option ne lit pas une ligne , il lit mots à partir d'une ligne (éventuellement barre oblique inverse), où les mots sont délimités par $IFS et la barre oblique inverse peut être utilisée pour échapper aux délimiteurs (ou continuer les lignes).

La syntaxe générique est:

read Word1 Word2... remaining_words

read lit stdin un octet à la fois¹ jusqu'à ce qu'il trouve un caractère de nouvelle ligne non échappé (ou fin de saisie), le divise selon des règles complexes et stocke le résultat de ce fractionnement dans $Word1, $Word2 ... $remaining_words.

Par exemple sur une entrée comme:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

et avec la valeur par défaut de $IFS, read a b c attribuerait:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Maintenant, si passé un seul argument, cela ne devient pas read line. C'est toujours read remaining_words. Le traitement de la barre oblique inverse est toujours effectué, les espaces blancs IFS sont toujours supprimés du début et de la fin.

L'option -r Supprime le traitement de barre oblique inverse. Donc, la même commande ci-dessus avec -r Assignerait à la place

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Maintenant, pour la partie de fractionnement, il est important de réaliser qu'il existe deux classes de caractères pour $IFS: Les caractères d'espacement IFS (à savoir l'espace et la tabulation (et la nouvelle ligne, bien qu'ici, cela n'a pas d'importance sauf si vous utilisez - d), qui se trouve également être dans la valeur par défaut de $IFS) et les autres. Le traitement de ces deux classes de personnages est différent.

Avec IFS=: (: N'étant pas un espace blanc IFS), une entrée comme :foo::bar:: Serait divisée en "", "foo", "", bar et "" (Et un supplément "" Avec quelques implémentations mais cela n'a pas d'importance, sauf pour read -a). Alors que si nous remplaçons : Par de l'espace, le fractionnement se fait uniquement en foo et bar. C'est le premier et le dernier sont ignorés, et leurs séquences sont traitées comme une seule. Il existe des règles supplémentaires lorsque les espaces et les espaces non blancs sont combinés dans $IFS. Certaines implémentations peuvent ajouter/supprimer le traitement spécial en doublant les caractères dans IFS (IFS=:: Ou IFS=' ').

Donc, ici, si nous ne voulons pas que les caractères d'espacement non échappés de début et de fin soient supprimés, nous devons supprimer ces caractères d'espace blanc IFS d'IFS.

Même avec des caractères non blancs IFS, si la ligne d'entrée contient un (et un seul) de ces caractères et qu'il s'agit du dernier caractère de la ligne (comme IFS=: read -r Word Sur une entrée comme foo:) avec des shells POSIX (pas zsh ni certaines versions pdksh), cette entrée est considérée comme un mot foo car dans ces shells, les caractères $IFS sont considérés comme terminateurs , donc Word contiendra foo, pas foo:.

Ainsi, la manière canonique de lire une ligne d'entrée avec le code interne read est:

IFS= read -r line

(notez que pour la plupart des implémentations de read, cela ne fonctionne que pour les lignes de texte car le caractère NUL n'est pas pris en charge sauf dans zsh).

L'utilisation de la syntaxe var=value cmd Garantit que IFS n'est défini différemment que pour la durée de cette commande cmd.

Note historique

La commande intégrée read a été introduite par le Bourne Shell et devait déjà lire les mots , pas les lignes. Il existe quelques différences importantes avec les shells POSIX modernes.

Le read de Bourne Shell ne prend pas en charge une option -r (Qui a été introduite par le Korn Shell), il n'y a donc aucun moyen de désactiver le traitement anti-slash autre que le prétraitement de l'entrée avec quelque chose comme sed 's/\\/&&/g' Là-bas.

Le Bourne Shell n'avait pas cette notion de deux classes de caractères (qui a de nouveau été introduite par ksh). Dans le Bourne Shell, tous les caractères subissent le même traitement que les caractères blancs IFS dans ksh, c'est-à-dire que IFS=: read a b c Sur une entrée comme foo::bar Assignerait bar à $b, pas la chaîne vide.

Dans le Bourne Shell, avec:

var=value cmd

Si cmd est intégré (comme read), var reste défini sur value après la fin de cmd. C'est particulièrement critique avec $IFS Car dans le Bourne Shell, $IFS Est utilisé pour tout fractionner, pas seulement les extensions. De plus, si vous supprimez le caractère espace de $IFS Dans le Bourne Shell, "$@" Ne fonctionne plus.

Dans Bourne Shell, la redirection d'une commande composée la fait fonctionner dans un sous-shell (dans les premières versions, même des choses comme read var < file Ou exec 3< file; read var <&3 Ne fonctionnaient pas), donc c'était rare dans le Bourne Shell à utiliser read pour autre chose que l'entrée utilisateur sur le terminal (où la gestion de la continuation de la ligne était logique)

Certains Unices (comme HP/UX, il y en a aussi un dans util-linux) Ont toujours une commande line pour lire une ligne d'entrée (qui était une commande UNIX standard jusqu'à la spécification UNIX unique version 2 ).

C'est fondamentalement la même chose que head -n 1 Sauf qu'il lit un octet à la fois pour s'assurer qu'il ne lit pas plus d'une ligne. Sur ces systèmes, vous pouvez faire:

line=`line`

Bien sûr, cela signifie générer un nouveau processus, exécuter une commande et lire sa sortie via un canal, donc beaucoup moins efficace que IFS= read -r line De ksh, mais toujours beaucoup plus intuitif.


¹ bien que sur les entrées recherchables, certaines implémentations peuvent revenir à la lecture par blocs et rechercher ensuite en tant qu'optimisation. ksh93 va encore plus loin et se souvient de ce qui a été lu et l'utilise pour la prochaine invocation de read, bien que qui est actuellement cassé

114
Stéphane Chazelas

La théorie

Il y a deux concepts en jeu ici:

  • IFS est le séparateur de champ d'entrée, ce qui signifie que la chaîne lue sera divisée en fonction des caractères dans IFS. Sur une ligne de commande, IFS est normalement tout caractère d'espacement, c'est pourquoi la ligne de commande se divise en espaces.
  • Faire quelque chose comme VAR=value command signifie "modifier l'environnement de commande pour que VAR ait la valeur value". Fondamentalement, la commande command verra VAR comme ayant la valeur value, mais toute commande exécutée après cela verra toujours VAR comme ayant sa valeur précédente. En d'autres termes, cette variable sera modifiée uniquement pour cette instruction.

Dans ce cas

Donc, lorsque vous faites IFS= read -r line, vous définissez IFS sur une chaîne vide (aucun caractère ne sera utilisé pour le fractionnement, donc aucun fractionnement ne se produira) afin que read lise la ligne entière et la voit comme un seul mot qui sera affecté à la variable line. Les modifications apportées à IFS n'affectent que cette instruction, de sorte que les commandes suivantes ne seront pas affectées par la modification.

En remarque

Bien que la commande soit correcte et fonctionne comme prévu, définissez IFS dans ce cas n'est pas  pourrait1 pas nécessaire. Comme écrit dans la page de manuel bash dans la section intégrée read:

Une ligne est lue à partir de l'entrée standard [...] et le premier mot est affecté au premier nom, le deuxième mot au deuxième nom, etc. avec les mots restants et leur séparateurs intermédiaires affectés au nom de famille . S'il y a moins de mots lus dans le flux d'entrée que de noms, les noms restants reçoivent des valeurs vides. Les caractères dans IFS sont utilisés pour diviser la ligne en mots. [...]

Puisque vous ne disposez que de la variable line, tous les mots lui seront de toute façon assignés, donc si vous n'avez besoin d'aucun des espaces blancs précédents et finaux 1 vous pourriez simplement écrire read -r line et en finir.

[1] Juste comme un exemple de la façon dont un unset ou par défaut $IFS la valeur entraînera read à considérer le début/la fin IFS espace blanc , vous pouvez essayer:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Exécutez-le et vous verrez que les caractères précédents et finaux ne survivront pas si IFS n'est pas désactivé. De plus, des choses étranges pourraient se produire si $IFS devait être modifié quelque part plus tôt dans le script.

9
user43791

Vous devriez lire cette instruction en deux parties, la première efface la valeur de la variable IFS, c'est-à-dire qu'elle est équivalente à la plus lisible IFS="", le second lit la variable line de stdin, read -r line.

Ce qui est spécifique dans cette syntaxe, c'est que l'affectation IFS est transitoire et valable uniquement pour la commande read.

Sauf si je manque quelque chose, dans ce cas particulier, la suppression de IFS n'a aucun effet, mais comme quel que soit IFS, la ligne entière sera lue dans la variable line. Il y aurait eu un changement de comportement uniquement si plus d'une variable avait été passée en paramètre à l'instruction read.

Éditer:

Le -r est là pour autoriser l'entrée se terminant par \ ne doit pas être traité spécialement, c'est-à-dire que la barre oblique inverse doit être incluse dans la variable line et non pas comme un caractère de continuation pour permettre la saisie sur plusieurs lignes.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

La suppression de IFS a pour effet secondaire d'empêcher la lecture de couper les espaces ou les tabulations potentiels en début et en fin, par exemple:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Merci à rici d'avoir souligné cette différence.

6
jlliagre

C'est un bonne réponse du point de vue de la grammaire Shell:

Une commande simple est une séquence d'affectations de variables facultatives suivie de mots séparés par des blancs et de redirections , et terminé par un opérateur de contrôle. Le premier mot spécifie la commande à exécuter et est passé comme argument zéro. Les mots restants sont passés comme arguments à la commande invoquée.

de référence bash 3.7.4 :

L'environnement de toute commande ou fonction simple peut être temporairement augmenté par en le préfixant avec des affectations de paramètres , comme décrit dans Paramètres du shell. Ces instructions d'affectation n'affectent que l'environnement vu par cette commande .

IFS="" n'est visible que par la commande, qui est read ici. En d'autres termes, IFS ne change pas la valeur de IFS sauf pour cette ligne.

0
Izana