Dlaczego „sed q” działa inaczej podczas czytania z potoku?

25

Utworzyłem plik testowy o nazwie „test”, który zawiera następujące elementy:

xxx
yyy
zzz

Uruchomiłem polecenie:

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

i mam:

xxx
yyy
aaa
zzz

Potem pobiegłem:

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

i dostał:

xxx
yyy
aaa

Pytanie

sedczyta i drukuje, aż napotka linię z „y”, a następnie zatrzymuje się. W pierwszym przypadku, ale nie drugim, kot czyta i drukuje resztę.

Czy ktoś może wyjaśnić, jakie zjawisko kryje się za tą różnicą w zachowaniu?

Zauważyłem też, że działa to w ten sposób w Ubuntu 16.04 i Centos 6, ale w Centos 7 żadne polecenie nie wypisuje „zzz”.

Antti Kuusela
źródło
Domyślam się, że cat(w podpowłoce) może ponownie użyć deskryptora pliku w pierwszym przypadku, ponieważ stdin jest powiązany z prawdziwym plikiem. W drugim przypadku stdin pochodzi z potoku, a nie z prawdziwego pliku. Pamiętaj, że również (sed '/y/ q'; echo aaa; cat) < <(cat test)nie drukuje zzz.
Martin Nyolt,
1
Prostszy przykład: (head -n1; head -n1) < testicat test | (head -n1; head -n1)
Martin Nyolt

Odpowiedzi:

22

Kiedy plik wejściowy jest widoczny (jak czytanie ze zwykłego pliku) lub nie widoczny (jak czytanie z potoku), sed(i inne standardowe narzędzia) będą zachowywać się inaczej (przeczytaj INPUT FILESsekcję w tym linku ).

Cytat z dokumentu:

Kiedy standardowe narzędzie odczytuje widoczny plik wejściowy i kończy się bezbłędnie, zanim osiągnie koniec pliku, narzędzie musi upewnić się, że przesunięcie pliku w otwartym opisie pliku jest właściwie ustawione tuż za ostatnim bajtem przetworzonym przez narzędzie.

Więc w:

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

sedwykonał qpolecenie uit przed osiągnięciem EOF, więc pozostawił przesunięcie pliku na początku zzzlinii, więc catmoże kontynuować drukowanie pozostałych linii (GNU sed nie jest zgodny z POSIX w niektórych warunkach, patrz poniżej).

Kontynuując od dokumentu:

W przypadku plików, których nie można zobaczyć, stan przesunięcia pliku w otwartym opisie pliku dla tego pliku jest nieokreślony

W takim przypadku zachowanie nie jest określone. Większość standardowych narzędzi, w tym sed, zużywa dane wejściowe w jak największym stopniu. Odczytuje yyylinię i quit bez przywracania przesunięcia pliku, więc nic nie pozostanie cat.


GNU sednie jest zgodne ze standardem, zależy od implementacji stdio systemu i wersji glibc:

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

Tutaj wynik uzyskano z Mac OSX 10.11.6, maszyn wirtualnych Centos 7.2 - glibc 2.17, Ubuntu 14.04 - glibc 2.19, które są uruchomione na Openstack z backendem CEPH.

W tych systemach można użyć -uopcji, aby osiągnąć standardowe zachowanie:

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

a dla rury:

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

co prowadzi do strasznie nieefektywnej wydajności, ponieważ sedmusi czytać jeden bajt na raz. Częściowe wyjście z 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
źródło
1
W przypadku GNU sedzależy to od implementacji stdio systemu. W systemach GNU (z GNU libc) GNU sedbędzie zgodne, tak jak exit()będzie szukać plików zarządzanych przez stdio.
Stéphane Chazelas
@ StéphaneChazelas: Jak to sprawdzić? Z moim Centos 7.2, Ubuntu 14.04 VM, sednie jest zgodny, mój laptop manjaro ma, wszystkie mają tę samą sed wersję 4.2.2
cuonglm
@ StéphaneChazelas: Brzmi jak coś, co wydarzyło się pod maską. Na moich maszynach wirtualnych, strace -f sh -c '{ sed "/y/q"; echo aaa; cat; } <test'pokazują, że nie lseek()przeprowadzono, podczas gdy w moim manjaro lseek()nazwano wcześniej exit_group().
cuonglm
Przypuszczam, że to zależy od wersji GNU libc. Możesz przetestować za pomocą main() { char buf[999]; gets(buf); }'programu.
Stéphane Chazelas,
1
@ StéphaneChazelas: Potwierdzony. Obie moje maszyny wirtualne mają 2.17 i 2.19, podczas gdy jedna w moim manjaro to 2.23. Czy to jest błąd glibc? Czy masz jakieś informacje na temat zmiany między wersjami glibc
cuonglm