grep ne sort pas avant EOF s'il passe par cat

19

Étant donné cet exemple minimal

( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; )

il sort LINE 1et puis, après une seconde, sort LINE 2, comme prévu .


Si nous le canalisons grep LINE

( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; ) | grep LINE

le comportement est le même que dans le cas précédent, comme prévu .


Si, alternativement, nous cat

( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; ) | cat

le comportement est à nouveau le même, comme prévu .


Cependant , si nous redirigeons vers grep LINE, puis vers cat,

( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; ) | grep LINE | cat

il n'y a pas de sortie avant qu'une seconde ne passe et les deux lignes apparaissent immédiatement sur la sortie, ce à quoi je ne m'attendais pas .


Pourquoi cela se produit-il et comment puis-je faire en sorte que la dernière version se comporte de la même manière que les trois premières commandes?

lisyarus
la source
catconcatène les fichiers. Qu'essayez-vous de faire en jouant cat?
Douglas tenue le
15
@DouglasHeld Lorsqu'il est appelé sans arguments, catlit simplement stdinet sort dans stdout. Bien sûr, j'ai posé cette question avec beaucoup de choses complexes à la place de echoet cat, mais celles-ci se sont révélées non pertinentes, car le problème apparaît avec des exemples beaucoup plus simples.
lisyarus
3
@DouglasHeld: La tuyauterie vers cat est souvent utile pour forcer stdout à ne pas être un terminal. Par exemple, c'est un moyen facile d'obtenir de nombreuses commandes pour ne pas utiliser la sortie colorisée.
wchargin
Je jure que c'est une copie d' une autre question sur Stack Overflow!
iBug
@wchargin merci beaucoup, vous m'avez appris quelque chose de nouveau sur le posix que je n'ai jamais connu.
Douglas a eu lieu

Réponses:

38

Lorsque la grepsortie (au moins de GNU) n'est pas un terminal, elle met en mémoire tampon sa sortie, ce qui est à l'origine du comportement que vous voyez. Vous pouvez désactiver cela soit en utilisant grepl' --line-bufferedoption GNU :

( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; ) | grep --line-buffered LINE | cat

ou l' stdbufutilitaire:

( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; ) | stdbuf -oL grep LINE | cat

Désactiver la mise en mémoire tampon dans le tuyau a plus sur ce sujet.

Stephen Kitt
la source
26

Explication simplifiée

Comme de nombreux utilitaires, ce n'est pas quelque chose de particulier à un programme, grepsa sortie standard varie entre être mise en mémoire tampon de ligne et entièrement mise en mémoire tampon . Dans le premier cas, les tampons de la bibliothèque C produisent des données en mémoire jusqu'à ce que le tampon contenant ces données soit rempli ou qu'un caractère de saut de ligne y soit ajouté (ou que le programme se termine proprement), après quoi il appelle write()pour réellement écrire le contenu du tampon. Dans ce dernier cas, seul le tampon en mémoire saturé (ou le programme se terminant proprement) déclenche le write().

Explication plus détaillée

C'est l'explication bien connue, mais légèrement erronée. En fait, la sortie standard n'est pas mise en mémoire tampon de ligne mais mise en mémoire tampon intelligente dans la bibliothèque GNU C et la bibliothèque BSD C. La sortie standard est également vidée lorsque la lecture de l' entrée standard épuise son tampon en mémoire (d'entrée pré-lue) et la bibliothèque C doit appeler read()pour récupérer plus d'entrée et elle lit le début d'une nouvelle ligne. (Une des raisons pour cela est d'empêcher un blocage lorsqu'un autre programme se connecte aux deux extrémités d'un filtre et s'attend à pouvoir fonctionner ligne par ligne, en alternant entre l'écriture sur le filtre et la lecture de celui-ci; comme les "coprocesses" dans GNU awkpar exemple.)

Influence de la bibliothèque C

grepet les autres utilitaires le font - ou, plus strictement, les bibliothèques C qu'ils utilisent le font, car c'est une caractéristique définie de la programmation en langage C - en fonction de ce qu'ils détectent être leur sortie standard. Si (et seulement si) ce n'est pas un appareil interactif, ils choisissent la mise en mémoire tampon complète, sinon ils choisissent la mise en mémoire tampon intelligente. Un canal n'est pas considéré comme un périphérique interactif, car la définition d'un périphérique interactif, au moins dans le monde d'Unix et de Linux, est essentiellement l' isatty()appel renvoyant true pour le descripteur de fichier correspondant.

Solutions de contournement pour désactiver la mise en mémoire tampon complète

Certains utilitaires comme grepont des options idiosyncratiques telles que celles --line-bufferedqui modifient cette décision, qui, comme vous pouvez le voir, est mal nommée. Mais une fraction infiniment petite des programmes de filtrage que l'on pourrait utiliser a en fait une telle option.

Plus généralement, on peut utiliser des outils qui creusent dans les internes spécifiques de la bibliothèque C et changent sa prise de décision (qui ont des problèmes de sécurité si le programme à modifier est set-UID, et sont également spécifiques à des bibliothèques C particulières, et en fait sont spécifiques aux programmes écrits en ou superposés au langage C), ou à des outils tels que ptybandageceux -ci qui ne modifient pas les internes du programme mais simplement interposent un pseudo-terminal en sortie standard afin que la décision apparaisse comme "interactive", pour affecter cela.

Lectures complémentaires

JdeBP
la source
1
Si l'expression "ligne mise en mémoire tampon" est un terme impropre, ce n'est pas vraiment la faute de grep, mais des appels de bibliothèque sous-jacents, setbuf/setvbuf . Je ne connais pas de référence fiable en ligne pour la norme C, mais par exemple les pages de manuel Linux et FreeBSD ainsi que la description POSIX de l' setvbufappeler "ligne tamponnée". Même la constante symbolique en est _IOLBF.
ilkkachu
Eh bien maintenant, vous avez mieux appris. Cette stratégie de mise en mémoire tampon est décrite dans le doco de la bibliothèque GNU C, quoique brièvement. Laurent Bercot est plus franc sur le sujet. Je l'ai également mentionné.
JdeBP
Je ne pensais pas que "Votre attente est fausse" était un bon titre pour cette excellente explication de la mise en mémoire tampon de sortie. J'espère que cela ne vous dérange pas que je l'ai supprimé et ajouté quelques titres descriptifs pour chaque section de la réponse.
Anthony G - justice pour Monica
2
@ilkkachu Le standard C utilise en effet "line buffered". Par 7.21.3 Fichiers , paragraphe 3 : "Lorsqu'un flux est sans tampon, ... Quand un flux est entièrement tamponné, ... Quand un flux est mis en mémoire tampon de ligne, les caractères sont destinés à être transmis vers ou depuis l'environnement hôte en tant que bloquer lorsqu'un caractère de nouvelle ligne est rencontré. ... "En fait, la norme C utilise l'expression exacte" ligne mise en mémoire tampon "cinq fois. Ce n'est donc pas un terme impropre.
Andrew Henle
1
De plus, l'approche décrite ici comme «mise en mémoire tampon intelligente», si je comprends bien, semble être exactement ce que la norme C décrit comme «mise en mémoire tampon de ligne». Plus précisément, en plus de vider le tampon sur les sauts de ligne, "Lorsqu'un flux est mis en mémoire tampon de ligne, les caractères sont destinés à être transmis vers ou depuis l'environnement hôte sous forme de bloc lorsqu'une entrée est demandée sur un flux sans tampon, ou lorsque l'entrée est demandée sur un flux de ligne tamponné qui nécessite la transmission de caractères à partir de l'environnement hôte. " Ce n'est donc pas une bizarrerie GNU ou BSD, mais plutôt ce que le langage appelle.
John Bollinger
7

Utilisation

grep --line-buffered

pour que grep ne mette pas en mémoire tampon plus d'une ligne à la fois.

choroba
la source