Jak uzyskać STDOUT i STDERR, aby przejść do terminala i pliku dziennika?

105

Mam skrypt, który będzie uruchamiany interaktywnie przez użytkowników nietechnicznych. Skrypt zapisuje aktualizacje statusu do STDOUT, aby użytkownik miał pewność, że skrypt działa poprawnie.

Chcę, aby zarówno STDOUT, jak i STDERR zostały przekierowane do terminala (aby użytkownik mógł zobaczyć, że skrypt działa, a także sprawdzić, czy wystąpił problem). Chcę też, aby oba strumienie były przekierowywane do pliku dziennika.

Widziałem wiele rozwiązań w sieci. Niektóre nie działają, a inne są strasznie skomplikowane. Opracowałem wykonalne rozwiązanie (które podam jako odpowiedź), ale jest niezdarne.

Idealnym rozwiązaniem byłaby pojedyncza linia kodu, którą można by umieścić na początku dowolnego skryptu, który wysyła oba strumienie zarówno do terminala, jak i do pliku dziennika.

EDYCJA: Przekierowywanie STDERR do STDOUT i przesyłanie wyniku do trójnika, ale zależy to od użytkowników pamiętających o przekierowaniu i potokowaniu wyjścia. Chcę, aby logowanie było niezawodne i automatyczne (dlatego chciałbym mieć możliwość osadzenia rozwiązania w samym skrypcie).

JPLemme
źródło
Dla innych czytelników: podobne pytanie: stackoverflow.com/questions/692000/ ...
pevik
1
Jestem zirytowany, że wszyscy (łącznie ze mną!) Z wyjątkiem @JasonSydes zostali wykolejeni i odpowiedzieli na inne pytanie. Jak już wspomniałem, odpowiedź Jasona jest niewiarygodna. Bardzo chciałbym zobaczyć naprawdę rzetelną odpowiedź na zadane przez Ciebie pytanie (i podkreślone w EDYCIE).
Don Hatch
Och, czekaj, cofam to. Zaakceptowana odpowiedź @PaulTromblin nie odpowiada. Nie przeczytałem wystarczająco daleko.
Don Hatch

Odpowiedzi:

169

Użyj „tee”, aby przekierować do pliku i ekranu. W zależności od używanej powłoki, najpierw musisz przekierować stderr na stdout za pomocą

./a.out 2>&1 | tee output

lub

./a.out |& tee output

W csh jest wbudowane polecenie o nazwie „script”, które przechwytuje wszystko, co przechodzi na ekran do pliku. Zaczynasz od wpisania „script”, następnie robisz cokolwiek chcesz przechwycić, po czym wciskasz Ctrl-D, aby zamknąć plik skryptu. Nie znam odpowiednika dla sh / bash / ksh.

Ponadto, ponieważ wskazałeś, że są to twoje własne skrypty sh, które możesz modyfikować, możesz wykonać przekierowanie wewnętrznie, otaczając cały skrypt nawiasami klamrowymi lub nawiasami, na przykład

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file
Paul Tomblin
źródło
4
Nie wiedziałem, że w skryptach powłoki można nawiasywać polecenia. Ciekawy.
Jamie
1
Doceniam też skrót do Bracket! Z jakiegoś powodu 2>&1 | tee -a filenamenie zapisywałem stderr do pliku z mojego skryptu, ale działało dobrze, gdy skopiowałem polecenie i wkleiłem je do terminala! Jednak sztuczka ze wspornikiem działa dobrze.
Ed Brannin
8
Zauważ, że rozróżnienie między stdout i stderr zostanie utracone, ponieważ tee wypisze wszystko na stdout.
Flimm,
2
FYI: Polecenie „script” jest dostępne w większości dystrybucji (jest częścią pakietu util-linux)
SamWN
2
@Flimm, czy istnieje sposób (w inny sposób), aby zachować rozróżnienie między stdout a stderr?
Gabriel,
20

Zbliża się pół dekady później ...

Uważam, że jest to „idealne rozwiązanie”, do którego dążył PO.

Oto jedna linijka, którą możesz dodać na początku swojego skryptu Bash:

exec > >(tee -a $HOME/logfile) 2>&1

Oto mały skrypt demonstrujący jego użycie:

#!/usr/bin/env bash

exec > >(tee -a $HOME/logfile) 2>&1

# Test redirection of STDOUT
echo test_stdout

# Test redirection of STDERR
ls test_stderr___this_file_does_not_exist

(Uwaga: to działa tylko z Basha to będzie. Nie praca z / bin / sh).

Zaczerpnięte stąd ; oryginał nie przechwycił, z tego co wiem, STDERR w logu. Naprawiono notatką z tego miejsca .

Jason Sydes
źródło
3
Zauważ, że rozróżnienie między stdout i stderr zostanie utracone, ponieważ tee wypisze wszystko na stdout.
Flimm,
@Flimm stderr może zostać przekierowany do innego procesu tee, który ponownie może zostać przekierowany na stderr.
jarno
@Flimm, napisałem sugestię jarno tutaj: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService
1
To rozwiązanie, podobnie jak większość innych proponowanych do tej pory rozwiązań, jest podatne na wyścigi. Oznacza to, że gdy bieżący skrypt zakończy pracę i powróci, albo do zachęty użytkownika, albo do jakiegoś skryptu wywołującego wyższego poziomu, koszulka, która działa w tle, nadal będzie działać i może wyemitować kilka ostatnich linii na ekran i do plik dziennika późno (to znaczy do ekranu po monicie i do pliku dziennika po spodziewanym zakończeniu pliku dziennika).
Don Hatch
1
Jest to jednak jedyna jak dotąd zaproponowana odpowiedź, która faktycznie odpowiada na to pytanie!
Don Hatch
9

Wzór

the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )

To przekierowuje osobno stdout i stderr i wysyła oddzielne kopie stdout i stderr do wywołującego (którym może być twój terminal).

  • W zsh nie przejdzie do następnej instrukcji, dopóki tees nie zakończą się .

  • W bashu może się okazać, że kilka ostatnich wierszy wyniku pojawi się po każdej następnej instrukcji.

W obu przypadkach właściwe bity trafiają we właściwe miejsca.


Wyjaśnienie

Oto skrypt (przechowywany w ./example):

#! /usr/bin/env bash
the_cmd()
{
    echo out;
    1>&2 echo err;
}

the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )

Oto sesja:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Oto jak to działa:

  1. Oba teeprocesy są uruchamiane, ich stdins są przypisane do deskryptorów plików. Ponieważ są zawarte w podstawieniach procesów , ścieżki do tych deskryptorów plików są zastępowane w poleceniu wywołującym, więc teraz wygląda to mniej więcej tak:

the_cmd 1> /proc/self/fd/13 2> /proc/self/fd/14

  1. the_cmd działa, zapisując stdout do pierwszego deskryptora pliku i stderr do drugiego.

  2. W przypadku basha, po the_cmdzakończeniu, natychmiast pojawia się następująca instrukcja (jeśli dzwoniącym jest twój terminal, pojawi się zachęta).

  3. W przypadku zsh, po the_cmdzakończeniu, powłoka czeka na teezakończenie obu procesów przed przejściem dalej. Więcej na ten temat tutaj .

  4. Pierwszy teeproces, który czyta ze standardowego the_cmdwyjścia, zapisuje kopię tego standardowego wyjścia z powrotem do wywołującego, ponieważ to właśnie teerobi. Jego dane wyjściowe nie są przekierowywane, więc zwracają je z powrotem do dzwoniącego bez zmian

  5. Drugi teeproces jest stdoutprzekierowywany do wywołującego stderr(co jest dobre, ponieważ jego stdin czyta ze the_cmdstandardowego polecenia). Więc kiedy zapisuje na swoje standardowe wyjście, te bity trafiają na stderr wywołującego.

Dzięki temu stderr jest oddzielony od stdout zarówno w plikach, jak i w danych wyjściowych polecenia.

Jeśli pierwszy trójnik zapisze jakiekolwiek błędy, pojawią się one zarówno w pliku stderr, jak iw stderr polecenia, jeśli drugi trójnik zapisze jakiekolwiek błędy, pojawią się one tylko w stderr terminala.

MatrixManAtYrService
źródło
To wygląda na naprawdę przydatne i czego chcę. Nie jestem jednak pewien, jak powtórzyć użycie nawiasów (jak pokazano w pierwszym wierszu) w skrypcie wsadowym systemu Windows. ( teejest dostępny w danym systemie). Pojawia się błąd „Proces nie może uzyskać dostępu do pliku, ponieważ jest używany przez inny proces”.
Agi Hammerthief
To rozwiązanie, podobnie jak większość innych proponowanych do tej pory rozwiązań, jest podatne na wyścigi. Oznacza to, że gdy bieżący skrypt zakończy pracę i powróci, albo do zachęty użytkownika, albo do jakiegoś skryptu wywołującego wyższego poziomu, koszulka, która działa w tle, nadal będzie działać i może wyemitować kilka ostatnich linii na ekran i do plik dziennika późno (to znaczy do ekranu po monicie i do pliku dziennika po spodziewanym zakończeniu pliku dziennika).
Don Hatch
2
@DonHatch Czy możesz zaproponować rozwiązanie, które naprawi ten problem?
pylipp
Byłbym również zainteresowany przypadkiem testowym, który uwidacznia wyścig. Nie chodzi o to, że wątpię, ale trudno jest próbować tego uniknąć, ponieważ nie widziałem tego.
MatrixManAtYrService
@pylipp Nie mam rozwiązania. Byłbym bardzo zainteresowany jednym.
Don Hatch
4

aby przekierować stderr na stdout, dołącz to w poleceniu: 2>&1 Do wysyłania do terminala i logowania do pliku powinieneś użyćtee

Oba razem wyglądałyby tak:

 mycommand 2>&1 | tee mylogfile.log

EDYCJA: Aby osadzić w swoim skrypcie, zrobiłbyś to samo. Więc twój scenariusz

#!/bin/sh
whatever1
whatever2
...
whatever3

skończy jako

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log
flolo
źródło
2
Zauważ, że rozróżnienie między stdout i stderr zostanie utracone, ponieważ tee wypisze wszystko na stdout.
Flimm,
4

EDYCJA: Widzę, że wykoleiłem się i ostatecznie odpowiedziałem na inne pytanie niż zadane. Odpowiedź na prawdziwe pytanie znajduje się u dołu odpowiedzi Paula Tomblina. (Jeśli z jakiegoś powodu chcesz ulepszyć to rozwiązanie, aby osobno przekierowywać stdout i stderr, możesz użyć techniki, którą tutaj opisuję).


Szukałem odpowiedzi, która zachowuje rozróżnienie między stdout a stderr. Niestety wszystkie odpowiedzi udzielone do tej pory, które zachowują to rozróżnienie, są podatne na rasy: ryzykują, że programy zobaczą niepełne dane wejściowe, jak wskazałem w komentarzach.

Myślę, że w końcu znalazłem odpowiedź, która zachowuje to rozróżnienie, nie jest podatna na rasy i nie jest też strasznie skrzypiąca.

Pierwszy blok konstrukcyjny: aby zamienić stdout i stderr:

my_command 3>&1 1>&2 2>&3-

Drugi element konstrukcyjny: gdybyśmy chcieli filtrować (np. Tee) tylko stderr, moglibyśmy to osiągnąć, zamieniając stdout i stderr, filtrując, a następnie zamieniając z powrotem:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

Teraz reszta jest prosta: możemy dodać filtr stdout, albo na początku:

{ { my_command | stdout_filter;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

lub na końcu:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter

Aby przekonać się, że oba powyższe polecenia działają, użyłem następującego:

alias my_command='{ echo "to stdout"; echo "to stderr" >&2;}'
alias stdout_filter='{ sleep 1; sed -u "s/^/teed stdout: /" | tee stdout.txt;}'
alias stderr_filter='{ sleep 2; sed -u "s/^/teed stderr: /" | tee stderr.txt;}'

Wynik to:

...(1 second pause)...
teed stdout: to stdout
...(another 1 second pause)...
teed stderr: to stderr

a mój znak zachęty pojawia się natychmiast po „ teed stderr: to stderr”, zgodnie z oczekiwaniami.

Przypis o zsh :

Powyższe rozwiązanie działa w bashu (i może w kilku innych powłokach, nie jestem pewien), ale nie działa w zsh. Istnieją dwa powody niepowodzenia w zsh:

  1. składnia 2>&3-nie jest rozumiana przez zsh; to musi zostać przepisane jako2>&3 3>&-
  2. w zsh (w przeciwieństwie do innych powłok), jeśli przekierujesz deskryptor pliku, który jest już otwarty, w niektórych przypadkach (nie do końca rozumiem, jak decyduje), zamiast tego zachowuje się wbudowany w tee. Aby tego uniknąć, przed przekierowaniem musisz zamknąć każdy plik fd.

Na przykład moje drugie rozwiązanie musi zostać przepisane dla zsh as {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(które działa również w bash, ale jest okropnie rozwlekłe).

Z drugiej strony możesz skorzystać z tajemniczego, wbudowanego ukrytego teeinga zsh, aby uzyskać znacznie krótsze rozwiązanie dla zsh, które w ogóle nie obsługuje tee:

my_command >&1 >stdout.txt 2>&2 2>stderr.txt

(Nie zgadłbym na podstawie dokumentów, które znalazłem, że >&1i 2>&2są tym, co wywołuje ukryte teeing zsh; Odkryłem to metodą prób i błędów).

Don Hatch
źródło
Bawiłem się tym w bashu i działa dobrze. Tylko ostrzeżenie dla użytkowników zsh z nawykiem zakładania kompatybilności (jak ja), tam zachowuje się inaczej: gist.github.com/MatrixManAtYrService/ ...
MatrixManAtYrService
@MatrixManAtYrService Wydaje mi się, że zrozumiałem sytuację zsh i okazuje się, że w zsh jest dużo lepsze rozwiązanie. Zobacz moją edycję „Przypis o zsh”.
Don Hatch
Dzięki za tak szczegółowe wyjaśnienie rozwiązania. Czy wiesz również, jak pobrać kod powrotu, używając funkcji function ( my_function) w zagnieżdżonym filtrowaniu stdout / stderr? Zrobiłem, { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filterale dziwne jest tworzenie pliku jako wskaźnika błędu ...
pylipp
@pylipp Nie mam od ręki. Możesz zadać to jako osobne pytanie (być może z prostszym potokiem).
Don Hatch
2

Użyj scriptpolecenia w swoim skrypcie (skrypt man 1)

Utwórz skrypt powłoki (2 wiersze), który konfiguruje skrypt (), a następnie wywołuje polecenie exit.

Część 1: wrap.sh

#!/bin/sh
script -c './realscript.sh'
exit

Część 2: realscript.sh

#!/bin/sh
echo 'Output'

Wynik:

~: sh wrap.sh 
Script started, file is typescript
Output
Script done, file is typescript
~: cat typescript 
Script started on fr. 12. des. 2008 kl. 18.07 +0100
Output

Script done on fr. 12. des. 2008 kl. 18.07 +0100
~:
gnud
źródło
1

Użyj programu tee i dup stderr do wyjścia na standardowe wyjście.

 program 2>&1 | tee > logfile
tvanfosson
źródło
1

Stworzyłem skrypt o nazwie „RunScript.sh”. Zawartość tego skryptu to:

${APP_HOME}/${1}.sh ${2} ${3} ${4} ${5} ${6} 2>&1 | tee -a ${APP_HOME}/${1}.log

Nazywam to tak:

./RunScript.sh ScriptToRun Param1 Param2 Param3 ...

To działa, ale wymaga uruchamiania skryptów aplikacji za pośrednictwem zewnętrznego skryptu. To trochę niezdarne.

JPLemme
źródło
9
Stracisz grupowanie argumentów zawierających spacje z 1 $ 2 $ 3 $ ... , powinieneś użyć (z cudzysłowami): "$ @"
NVRAM
1

Rok później mam stary skrypt bash do rejestrowania czegokolwiek. Na przykład
teelog make ...loguje się do wygenerowanej nazwy dziennika (i zobacz też sztuczkę rejestrowania zagnieżdżonych plików make).

#!/bin/bash
me=teelog
Version="2008-10-9 oct denis-bz"

Help() {
cat <<!

    $me anycommand args ...

logs the output of "anycommand ..." as well as displaying it on the screen,
by running
    anycommand args ... 2>&1 | tee `day`-command-args.log

That is, stdout and stderr go to both the screen, and to a log file.
(The Unix "tee" command is named after "T" pipe fittings, 1 in -> 2 out;
see http://en.wikipedia.org/wiki/Tee_(command) ).

The default log file name is made up from "command" and all the "args":
    $me cmd -opt dir/file  logs to `day`-cmd--opt-file.log .
To log to xx.log instead, either export log=xx.log or
    $me log=xx.log cmd ...
If "logdir" is set, logs are put in that directory, which must exist.
An old xx.log is moved to /tmp/\$USER-xx.log .

The log file has a header like
    # from: command args ...
    # run: date pwd etc.
to show what was run; see "From" in this file.

Called as "Log" (ln -s $me Log), Log anycommand ... logs to a file:
    command args ... > `day`-command-args.log
and tees stderr to both the log file and the terminal -- bash only.

Some commands that prompt for input from the console, such as a password,
don't prompt if they "| tee"; you can only type ahead, carefully.

To log all "make" s, including nested ones like
    cd dir1; \$(MAKE)
    cd dir2; \$(MAKE)
    ...
export MAKE="$me make"

!
  # See also: output logging in screen(1).
    exit 1
}


#-------------------------------------------------------------------------------
# bzutil.sh  denisbz may2008 --

day() {  # 30mar, 3mar
    /bin/date +%e%h  |  tr '[A-Z]' '[a-z]'  |  tr -d ' '
}

edate() {  # 19 May 2008 15:56
    echo `/bin/date "+%e %h %Y %H:%M"`
}

From() {  # header  # from: $*  # run: date pwd ...
    case `uname` in Darwin )
        mac=" mac `sw_vers -productVersion`"
    esac
    cut -c -200 <<!
${comment-#} from: $@
${comment-#} run: `edate`  in $PWD `uname -n` $mac `arch` 

!
    # mac $PWD is pwd -L not -P real
}

    # log name: day-args*.log, change this if you like --
logfilename() {
    log=`day`
    [[ $1 == "sudo" ]]  &&  shift
    for arg
    do
        log="$log-${arg##*/}"  # basename
        (( ${#log} >= 100 ))  &&  break  # max len 100
    done
            # no blanks etc in logfilename please, tr them to "-"
    echo $logdir/` echo "$log".log  |  tr -C '.:+=[:alnum:]_\n' - `
}

#-------------------------------------------------------------------------------
case "$1" in
-v* | --v* )
    echo "$0 version: $Version"
    exit 1 ;;
"" | -* )
    Help
esac

    # scan log= etc --
while [[ $1 == [a-zA-Z_]*=* ]]; do
    export "$1"
    shift
done

: ${logdir=.}
[[ -w $logdir ]] || {
    echo >&2 "error: $me: can't write in logdir $logdir"
    exit 1
    }
: ${log=` logfilename "$@" `}
[[ -f $log ]]  &&
    /bin/mv "$log" "/tmp/$USER-${log##*/}"


case ${0##*/} in  # basename
log | Log )  # both to log, stderr to caller's stderr too --
{
    From "$@"
    "$@"
} > $log  2> >(tee /dev/stderr)  # bash only
    # see http://wooledge.org:8000/BashFAQ 47, stderr to a pipe
;;

* )
#-------------------------------------------------------------------------------
{
    From "$@"  # header: from ... date pwd etc.

    "$@"  2>&1  # run the cmd with stderr and stdout both to the log

} | tee $log
    # mac tee buffers stdout ?

esac
denis
źródło
Wiem, że to już późno, aby dodać komentarz, ale musiałem tylko podziękować za ten skrypt. Bardzo przydatne i dobrze udokumentowane!
stephenmm
Dzięki @stephenmm; to nigdy nie jest za późno, aby powiedzieć „przydatna” lub „mogłaby być lepsza”.
denis