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
?
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:
$a
⇐ foo
$b
⇐ bar baz
$c
⇐ blah 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
$a
⇐ foo
$b
⇐ bar\
$c
⇐ baz 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
.
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é
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.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.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.
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.
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.
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.