Pourquoi «sed q» fonctionne-t-il différemment lors de la lecture d'un tuyau?

25

J'ai créé un fichier de test nommé 'test' qui contient les éléments suivants:

xxx
yyy
zzz

J'ai exécuté la commande:

(sed '/y/ q'; echo aaa; cat) < test

et j'ai eu:

xxx
yyy
aaa
zzz

Puis j'ai couru:

cat test | (sed '/y/ q'; echo aaa; cat)

et a obtenu:

xxx
yyy
aaa

Question

sedlit et imprime jusqu'à ce qu'il rencontre une ligne avec «y», puis s'arrête. Dans le premier cas, mais pas dans le second, cat lit et imprime le reste.

Quelqu'un peut-il expliquer quel phénomène se cache derrière cette différence de comportement?

J'ai également remarqué que cela fonctionne de cette façon dans Ubuntu 16.04 et Centos 6, mais dans Centos 7, aucune des commandes n'imprime «zzz».

Antti Kuusela
la source
Je suppose que cat(dans le sous-shell) peut réutiliser le descripteur de fichier dans le premier cas, car stdin est lié à un vrai fichier. Dans le second cas, stdin provient d'un tube et non d'un vrai fichier. Notez que (sed '/y/ q'; echo aaa; cat) < <(cat test)ne s'imprime pas non plus zzz.
Martin Nyolt
1
Un exemple plus simple: (head -n1; head -n1) < testetcat test | (head -n1; head -n1)
Martin Nyolt

Réponses:

22

Lorsque le fichier d'entrée est recherché (comme la lecture d'un fichier normal) ou non recherché (comme la lecture d'un tuyau), sed(et d'autres utilitaires standard) se comporteront différemment ( INPUT FILESsection Lire de ce lien ).

Citation du doc:

Lorsqu'un utilitaire standard lit un fichier d'entrée recherché et se termine sans erreur avant d'atteindre la fin du fichier, l'utilitaire doit s'assurer que l'offset de fichier dans la description du fichier ouvert est correctement positionné juste après le dernier octet traité par l'utilitaire.

Donc dans:

(sed '/y/ q'; echo aaa; cat) < test

sedeffectué la qcommande uit avant d'atteindre EOF, donc il a laissé le fichier décalé au début de la zzzligne, donc catpeut continuer à imprimer les lignes restantes (GNU sed n'est pas compatible POSIX dans certaines conditions, voir ci-dessous).

Et en continuant du doc:

Pour les fichiers qui ne peuvent pas être recherchés, l'état de l'offset de fichier dans la description du fichier ouvert pour ce fichier n'est pas spécifié

Dans ce cas, le comportement n'est pas spécifié. La plupart des outils standard, notamment sed, consommeront l'entrée autant que possible. Il lit la yyyligne, et quit sans restaurer l'offset du fichier, donc rien n'est laissé pour cat.


GNU sedn'est pas conforme à la norme, dépend de l'implémentation stdio du système et de la version glibc:

$ (gsed '/y/ q'; echo aaa; cat) < test
xxx
yyy
aaa

Ici, le résultat a été obtenu à partir de Mac OSX 10.11.6, machines virtuelles Centos 7.2 - glibc 2.17, Ubuntu 14.04 - glibc 2.19, qui sont exécutées sur Openstack avec backend CEPH.

Sur ces systèmes, vous pouvez utiliser l' -uoption pour obtenir le comportement standard:

(gsed -u '/y/ q'; echo aaa; cat) </tmp/test

et pour tuyau:

$ cat test | (gsed -u '/y/ q'; echo aaa; cat)
xxx
yyy
aaa
zzz

ce qui conduit à des performances terriblement inefficaces, car seddoit lire un octet à la fois. Une sortie partielle de strace:

$ strace -fe read sh -c '{ sed -u "/y/q"; echo aaa; cat; } <test'
...
[pid  5248] read(3, "", 4096)           = 0
[pid  5248] read(0, "x", 1)             = 1
[pid  5248] read(0, "x", 1)             = 1
[pid  5248] read(0, "x", 1)             = 1
[pid  5248] read(0, "\n", 1)            = 1
xxx
[pid  5248] read(0, "y", 1)             = 1
[pid  5248] read(0, "y", 1)             = 1
[pid  5248] read(0, "y", 1)             = 1
[pid  5248] read(0, "\n", 1)            = 1
yyy
...
cuonglm
la source
1
Pour GNU sed, cela dépend de l'implémentation stdio du système. Sur les systèmes GNU (avec la libc GNU), GNUsed sera conforme, tout comme exit()les fichiers gérés par stdio.
Stéphane Chazelas
@ StéphaneChazelas: Comment le vérifier? Avec mon Centos 7.2, Ubuntu 14.04 VM,sed n'est pas conforme, mon portable manjaro en a, tous ont la même sed version 4.2.2
cuonglm
@ StéphaneChazelas: On dirait que quelque chose s'est passé sous le capot. Sur mes machines virtuelles, strace -f sh -c '{ sed "/y/q"; echo aaa; cat; } <test'montrez qu'aucune n'a lseek()été effectuée, alors que dans mon manjaro, une a lseek()été appelée avantexit_group() .
cuonglm
Je suppose que cela dépend de la version de la libc GNU. Vous pouvez tester avec un main() { char buf[999]; gets(buf); }'programme.
Stéphane Chazelas
1
@ StéphaneChazelas: Confirmé. Mes deux machines virtuelles ont 2.17 et 2.19, tandis que celle de mon manjaro est 2.23. S'agit-il d'un bug de la glibc? Avez-vous des informations sur le changement entre les versions de la glibc
cuonglm