Comprendre «IFS = read -r line»

61

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 aussi que read -r lineles données de la stdinvariable seront sauvegardées line:

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

Cependant, comment une commande peut affecter une valeur de variable? Et stocke-t-il d’abord les données de stdinvariable à variable line, puis attribue la valeur de lineà IFS?

Martin
la source

Réponses:

104

Certaines personnes ont cette notion erronée qui readest la commande de lire une ligne. Ce n'est pas.

readlit les mots d'une ligne (éventuellement d'une barre oblique inversée), où les mots sont $IFSdélimités et une barre oblique inverse peut être utilisée pour échapper aux délimiteurs (ou aux lignes continues).

La syntaxe générique est la suivante:

read word1 word2... remaining_words

readlit stdin un octet à la fois jusqu'à ce qu'il trouve un caractère de saut de ligne sans échappement (ou de fin d'entrée), qui se divise selon des règles complexes et stocke le résultat de cette division en $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 cattribuerait:

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

Maintenant, si on ne passe qu'un seul argument, cela ne devient pas read line. C'est encore read remaining_words. Le traitement de la barre oblique inverse est toujours effectué, les caractères IFS sont toujours supprimés au début et à la fin.

L' -roption supprime le traitement des barres obliques inverses. Donc, cette même commande ci-dessus avec -rau lieu d'attribuer

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

Maintenant, pour la partie scission, il est important de comprendre qu’il existe deux classes de caractères $IFS: les caractères IFS blancs (à savoir, espace et tabulation (et newline, bien que cela ne soit pas important sauf si vous utilisez -d), ce qui se produit également. être dans la valeur par défaut de $IFS) et les autres. Le traitement de ces deux classes de caractères est différent.

Avec IFS=:( :étant pas un caractère IFS des espaces), une entrée comme :foo::bar::serait divisé en "", "foo", "", baret ""(et un supplément ""avec certaines implémentations si cela ne importe pas , sauf pour read -a). Tandis que si nous remplaçons cela :par de l’espace, la division se fait uniquement en fooet bar. C’est-à-dire que les principales et les dernières sont ignorées et que leurs séquences sont traitées comme une seule. Il existe des règles supplémentaires lorsque des espaces et des caractères non-blancs sont combinés $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’espace blanc qui restent et qui ne soient pas échappés soient supprimés, nous devons supprimer ces caractères d’espace blanc IFS de IFS.

Même avec les caractères IFS-non-blancs, si la ligne d’entrée contient un (et un seul) de ces caractères et que c’est le dernier caractère de la ligne (comme IFS=: read -r wordsur une entrée similaire foo:) avec des shells POSIX ( zshni certaines pdkshversions), cette entrée est considéré comme l' un foomot parce que dans ces coquilles, les caractères $IFSsont considérés comme terminateurs , donc wordcontiendra foo, non foo:.

Ainsi, la manière canonique de lire une ligne d’entrée avec l’ readintégré est:

IFS= read -r line

(notez que pour la plupart des readimplémentations, 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 var=value cmdsyntaxe permet de s'assurer IFSque le paramétrage est différent pour la durée de cette cmdcommande.

Note d'histoire

Le readconstruit a été introduit par le shell Bourne et devait déjà lire des mots , pas des lignes. Il existe quelques différences importantes avec les coques POSIX modernes.

Le shell Bourne readne prenant pas en charge une -roption (introduite par le shell Korn), il n’ya donc aucun moyen de désactiver le traitement des barres obliques inverses autrement que de prétraiter l’entrée avec quelque chose de similaire sed 's/\\/&&/g'.

Le shell Bourne 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 personnages subissent le même traitement que IFS Espaces blancs font ksh, qui est IFS=: read a b csur une entrée comme foo::barattribuerait barà $b, pas la chaîne vide.

Dans le shell Bourne, avec:

var=value cmd

Si cmdest intégré (comme l' readest), varreste défini valueaprès la cmdfin. C’est particulièrement critique $IFScar dans le shell Bourne, tout $IFSest utilisé, pas seulement les extensions. De même, si vous supprimez le caractère d'espacement $IFSdans le shell Bourne, cela "$@"ne fonctionnera plus.

Dans le shell Bourne, la redirection d’une commande composée l’exécute dans un sous-shell (dans les versions les plus anciennes, même si cela ne fonctionnait pas read var < fileou exec 3< file; read var <&3ne fonctionnait pas). Il était donc rare que le shell Bourne soit utilisé readpour autre chose que la saisie de l’utilisateur sur le terminal. (où le traitement de continuation de ligne était logique)

Certains Unices (comme HP / UX, il y en a aussi un util-linux) ont toujours une linecommande pour lire une ligne d’entrée (une commande UNIX standard jusqu’à la spécification Single UNIX version 2 ).

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

line=`line`

Bien sûr, cela signifie créer un nouveau processus, exécuter une commande et lire sa sortie via un tube, ce qui est beaucoup moins efficace que celui de ksh IFS= read -r line, mais toujours beaucoup plus intuitif.

Stéphane Chazelas
la source
3
+1 Merci pour quelques informations utiles sur les différents traitements sur espace / tab par rapport à "autres" dans IFS à Bash ... Je savais qu'ils étaient traités différemment, mais cette explication simplifie beaucoup le tout. (Et la compréhension entre bash (et d'autres coques posix) et les shdifférences régulières est également utile pour écrire des scripts portables!)
Olivier Dulac
Au moins pour bash-4.4.19, while read -r; do echo "'$REPLY'"; donefonctionne comme while IFS= read -r line; do echo "'$line'"; done.
x-yuri
Cela: "... cette notion erronée que read est la commande pour lire une ligne ..." me fait penser que si utiliser readpour lire une ligne est erroné, il doit y avoir autre chose. Quelle pourrait être cette notion non erronée? Ou bien cette première affirmation est-elle techniquement correcte, mais en réalité, la notion non erronée est: "read est la commande permettant de lire les mots d'une ligne. En raison de sa puissance, vous pouvez l'utiliser pour lire les lignes d'un fichier en effectuant: IFS= read -r line"
Mike S
8

La théorie

Deux concepts sont en jeu ici:

  • IFSest le séparateur de champ de saisie, ce qui signifie que la chaîne lue sera fractionnée en fonction des caractères contenus dans IFS. Sur une ligne de commande, il IFSy a normalement des caractères d'espacement, c'est pourquoi la ligne de commande se divise en espaces.
  • Faire quelque chose comme cela VAR=value commandsignifie "modifier l'environnement de commande pour qu'il VARait la valeur value". Fondamentalement, la commande commandverra VARcomme ayant la valeur value, mais toute commande exécutée après sera toujours considérée VARcomme ayant sa valeur précédente. En d'autres termes, cette variable ne sera modifiée que pour cette instruction.

Dans ce cas

Ainsi, lorsque vous le faites IFS= read -r line, vous définissez IFSune chaîne vide (aucun caractère ne sera utilisé pour la scission, donc aucune scission ne se produira), de sorte que readla ligne entière soit lue et affichée comme un mot attribué à la linevariable. Les modifications IFSne concernent que cette instruction, de sorte que les commandes suivantes ne seront pas affectées par la modification.

Comme note de côté

Bien que la commande est correcte et fonctionnera comme prévu, la mise IFSdans ce cas n'est pas peut - 1 ne pas être nécessaire. Comme indiqué dans la bashpage de readmanuel de la section Builtin:

Une ligne est lue à partir de l'entrée standard [...] et le premier mot est attribué au premier nom, le second mot au deuxième nom, etc., avec les mots restants et leurs 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 se voient attribuer des valeurs vides. Les caractères IFSsont utilisés pour diviser la ligne en mots. [...]

Puisque vous avez seulement la linevariable tous les mots seront affectés à toute façon, donc si vous n'avez pas besoin des précédentes et de suivi des caractères blancs 1 vous pouvez simplement écrire read -r lineet faire avec elle.

[1] À titre d'exemple de la manière dont une unsetvaleur ou une $IFSvaleur par défaut permettra readde considérer les espaces IFS en tête / en fin , 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 suivants ne survivront pas si ce IFSn'est pas défini. De plus, des choses étranges pourraient se produire si $IFSon devait modifier quelque part plus tôt dans le script.

utilisateur43791
la source
5

Vous devriez lire cette déclaration en deux parties, la première efface la valeur de la variable IFS, ce qui équivaut à plus lisible IFS="", le second est en train de lire la linevariable d'stdin, read -r line.

Ce qui est spécifique dans cette syntaxe, c’est que l’affectation IFS est transcendante et n’est valable que pour la readcommande.

À moins que quelque chose ne me manque, dans ce cas particulier, l'effacement IFSn'a aucun effet, même si ce qui IFSest défini est configuré pour lire toute la ligne dans la linevariable. Un changement de comportement n'aurait eu lieu que dans le cas où plus d'une variable avait été passée en paramètre à l' readinstruction.

Modifier:

Il -rexiste un moyen de permettre à l’entrée se terminant par \ne pas être traitée spécialement, c’est-à-dire que la barre oblique inversée soit incluse dans la linevariable et non 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\]

L'effacement de l'IFS a pour effet secondaire d'empêcher lecture de supprimer les caractères d'espacement ou de tabulation, comme 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 pour avoir souligné cette différence.

jlliagre
la source
Ce qui vous manque, c’est que si l’IFS n’est pas modifié, read -r lineles espaces seront précédés et suivis avant d’affecter l’entrée à la linevariable.
rici
@rici Je soupçonnais quelque chose comme ça, mais je vérifiais seulement les caractères IFS entre les mots, pas ceux qui précédaient / qui suivaient. Merci d'avoir signalé ce fait!
jlliagre
L’effacement de l’IFS empêchera également l’attribution de plusieurs variables (effets secondaires). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"montrera-aa bb--
Kyodev