Jak przechwycić zamówiony STDOUT / STDERR i dodać znacznik czasu / prefiksy?

25

Zbadałem prawie wszystkie dostępne podobne pytania , ale bezskutecznie.

Pozwól mi szczegółowo opisać problem:

Uruchamiam niektóre skrypty nienadzorowane, które mogą generować standardowe dane wyjściowe i standardowe linie błędów, chcę uchwycić je w dokładnej kolejności wyświetlanej przez emulator terminala, a następnie dodać do nich przedrostek „STDERR:” i „STDOUT:”.

Próbowałem na nich używać rur, a nawet podejścia opartego na epollach, ale bezskutecznie. Myślę, że rozwiązanie jest w użyciu, choć nie jestem w tym mistrzem. Zajrzałem również do kodu źródłowego VTE Gnome , ale nie było to zbyt produktywne.

Idealnie byłoby użyć Go zamiast Bash, aby to osiągnąć, ale nie byłem w stanie. Wydaje się, że rury automatycznie zabraniają utrzymywania prawidłowej kolejności linii z powodu buforowania.

Czy ktoś był w stanie zrobić coś podobnego? Czy to po prostu niemożliwe? Myślę, że jeśli emulator terminala może to zrobić, to nie jest - może przez utworzenie małego programu C obsługującego PTY (y) inaczej?

Idealnie byłoby użyć danych asynchronicznych, aby odczytać te 2 strumienie (STDOUT i STDERR), a następnie wydrukować je na nowo według moich potrzeb, ale kolejność wprowadzania jest kluczowa!

UWAGA: Zdaję sobie sprawę ze stopniowania, ale nie działa to dla mnie ze skryptami Bash i nie można go łatwo edytować w celu dodania prefiksu (ponieważ w zasadzie pakuje wiele wywołań systemowych).

Aktualizacja: dodano poniżej dwóch znaczników

(losowe opóźnienia poniżej drugiej sekundy można dodać w przykładowym skrypcie, który podałem, aby uzyskać spójny wynik)

Aktualizacja: rozwiązanie tego pytania rozwiązałoby również inne pytanie , jak wskazał @Gilles. Doszedłem jednak do wniosku, że nie jest możliwe zrobienie tego, o co prosi się tu i tam. Podczas korzystania z 2>&1obu strumieni są one poprawnie łączone na poziomie pty / pipe, ale aby używać strumieni oddzielnie i we właściwej kolejności, należy rzeczywiście zastosować podejście stopniowane, które ingeruje w zaczepianie systemu i może być postrzegane jako brudne na wiele sposobów.

Będę chętny zaktualizować to pytanie, jeśli ktoś będzie w stanie podważyć powyższe.

Deim0s
źródło
1
Czy nie tego chcesz? stackoverflow.com/questions/21564/…
slm
@slm prawdopodobnie nie, ponieważ OP musi przygotować różne ciągi znaków dla różnych strumieni.
peterph
Czy możesz wyjaśnić, dlaczego zamówienie jest tak ważne? Może może być jakiś inny sposób na rozwiązanie twojego problemu ...
Peter
@peterph jest to warunek wstępny, jeśli nie mogę uzyskać spójnego wyjścia, wolę wysłać go do / dev / null niż go przeczytać i się tym pogubić :) 2> & 1 zachowuje porządek na przykład, ale nie pozwala na tego rodzaju personalizacji, którą zadaję w tym pytaniu
Deim0s

Odpowiedzi:

12

Możesz użyć koprocesów. Proste opakowanie, które przekazuje oba dane wyjściowe polecenia do dwóch sedinstancji (jedna dla stderrdrugiej dla stdout), które wykonują tagowanie.

#!/bin/bash
exec 3>&1
coproc SEDo ( sed "s/^/STDOUT: /" >&3 )
exec 4>&2-
coproc SEDe ( sed "s/^/STDERR: /" >&4 )
eval $@ 2>&${SEDe[1]} 1>&${SEDo[1]}
eval exec "${SEDo[1]}>&-"
eval exec "${SEDe[1]}>&-"

Zwróć uwagę na kilka rzeczy:

  1. Jest to magiczne zaklęcie dla wielu ludzi (w tym dla mnie) - z jakiegoś powodu (patrz link poniżej).

  2. Nie ma gwarancji, że czasami nie zamieni kilku linii - wszystko zależy od harmonogramu koprocesów. W rzeczywistości jest prawie pewne, że w pewnym momencie tak się stanie. To powiedziawszy, jeśli zachowując porządek dokładnie taki sam, musisz przetwarzać dane zarówno z jednego, jak stderri stdintego samego procesu, w przeciwnym razie program planujący jądro może (i zrobi) bałagan.

    Jeśli dobrze rozumiem problem, oznacza to, że musisz poinstruować powłokę, aby przekierowała oba strumienie do jednego procesu (co można zrobić AFAIK). Kłopoty zaczynają się, gdy proces ten zaczyna od podjęcia decyzji, co należy podjąć w pierwszej kolejności - musiałby sondować oba źródła danych, a w pewnym momencie znalazłby się w stanie, w którym przetwarzałby jeden strumień, a dane docierałyby do obu strumieni przed zakończeniem. I właśnie tam się psuje. Oznacza to również, że owijanie wyjściowych systemów alarmowych stderredjest prawdopodobnie jedynym sposobem na osiągnięcie pożądanego rezultatu (i nawet wtedy możesz mieć problem, gdy coś stanie się wielowątkowe w systemie wieloprocesorowym).

Jeśli chodzi o koprocesy, koniecznie przeczytaj doskonałą odpowiedź Stéphane'a w Jak używać polecenia coproc w Bash? dla dogłębnego wglądu.

Peter
źródło
Dzięki @peterph za twoją odpowiedź, jednak szukam konkretnie sposobów na zachowanie porządku. Uwaga: Myślę, że twój tłumacz powinien być bash z powodu zastępowania procesu, którego używasz (otrzymuję ./test1.sh: 3: ./test1.sh: Syntax error: "(" unexpectedprzez skopiowanie / wklejenie twojego skryptu)
Deim0s
Bardzo prawdopodobne więc, wpadłem go bashz /bin/sh(nie wiem dlaczego musiałem go tam).
peterph
Trochę zaktualizowałem pytanie dotyczące tego, gdzie może dojść do pomieszania strumienia.
peterph
1
eval $@jest dość wadliwy. Użyj, "$@"jeśli chcesz uruchamiać argumenty jako dokładną linię poleceń - dodanie warstwy evalinterpretacji wprowadza kilka trudnych do przewidzenia (i potencjalnie złośliwych, jeśli przekazujesz nazwy plików lub inne treści, których nie kontrolujesz jako argumenty) i nie cytując nawet samego cytatu (dzieli nazwy ze spacjami na wiele słów, rozszerza globusy, nawet jeśli wcześniej były cytowane jako dosłowne itp.).
Charles Duffy,
1
Ponadto w nowoczesnej wersji bash-coprocesses nie musisz eval zamykać deskryptorów plików o nazwie zmiennej. exec {SEDo[1]}>&-będzie działać tak, jak jest (tak, brak $wcześniejszego {było celowe).
Charles Duffy,
5

Metoda nr 1. Używanie deskryptorów plików i awk

Co powiesz na coś takiego przy użyciu rozwiązań z tego pytania i odpowiedzi SO: Czy istnieje narzędzie uniksowe do dodawania znaczników czasu do wierszy tekstu? a to SO pytania i odpowiedzi zatytułowane: potok STDOUT i STDERR do dwóch różnych procesów w skrypcie powłoki? .

Podejście

Krok 1, tworzymy 2 funkcje w Bash, które wykonają komunikat znacznika czasu po wywołaniu:

$ msgOut () {  awk '{ print strftime("STDOUT: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }
$ msgErr () {  awk '{ print strftime("STDERR: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }

Krok 2 użyjesz powyższych funkcji, aby uzyskać pożądane wiadomości:

$ { { { ...command/script... } 2>&3; } 2>&3 | msgErr; } 3>&1 1>&2 | msgOut

Przykład

Tutaj wymyśliłem przykład, który napisze ado STDOUT, śpi przez 10 sekund, a następnie zapisuje dane wyjściowe do STDERR. Kiedy umieścimy tę sekwencję poleceń w naszym konstrukcie powyżej, otrzymamy komunikat zgodnie z podanym przez ciebie opisem.

$ { { echo a; sleep 10; echo >&2 b; } 2>&3 | \
    msgErr; } 3>&1 1>&2 | msgOut
STDERR: 2014-09-26 09:22:12 a
STDOUT: 2014-09-26 09:22:22 b

Metoda nr 2. Korzystanie z danych wyjściowych adnotacji

Jest to narzędzie zwane annotate-outputczęścią devscriptspakietu, które zrobi to, co chcesz. Jedynym ograniczeniem jest to, że musi uruchamiać skrypty dla Ciebie.

Przykład

Jeśli umieścimy powyższą przykładową sekwencję poleceń w skrypcie o nazwie mycmds.bashtak:

$ cat mycmds.bash 
#!/bin/bash

echo a
sleep 10
echo >&2 b

Następnie możemy uruchomić go w następujący sposób:

$ annotate-output ./mycmds.bash 
09:48:00 I: Started ./mycmds.bash
09:48:00 O: a
09:48:10 E: b
09:48:10 I: Finished with exitcode 0

Format wyjściowy można kontrolować dla części znacznika czasu, ale nie dalej. Ale jest to wynik podobny do tego, czego szukasz, więc może pasować do rachunku.

slm
źródło
1
niestety nie rozwiązuje to również problemu zamiany niektórych linii.
peterph
dokładnie. Myślę, że odpowiedź na to moje pytanie jest „niemożliwa”. Zdarzenie z stderredtobą nie może łatwo ustalić granic linii (próba taka byłaby hackerska). Chciałem zobaczyć, czy ktoś może mi pomóc z tym problemem, ale najwyraźniej każdy chce zrezygnować z jednego ograniczenia ( porządku ), które jest podstawą pytania
Deim0s
Krok 2 metody 1 wymaga kolejnego {z przodu, aby działał poprawnie.
Austin Hanson,