Wie bringe ich STDOUT und STDERR dazu, zum Terminal und zu einer Protokolldatei zu gelangen?

103

Ich habe ein Skript, das von nicht technischen Benutzern interaktiv ausgeführt wird. Das Skript schreibt Statusaktualisierungen in STDOUT, damit der Benutzer sicher sein kann, dass das Skript ordnungsgemäß ausgeführt wird.

Ich möchte, dass sowohl STDOUT als auch STDERR zum Terminal umgeleitet werden (damit der Benutzer sehen kann, dass das Skript funktioniert und ob ein Problem aufgetreten ist). Ich möchte auch, dass beide Streams in eine Protokolldatei umgeleitet werden.

Ich habe eine Reihe von Lösungen im Internet gesehen. Einige funktionieren nicht und andere sind schrecklich kompliziert. Ich habe eine praktikable Lösung entwickelt (die ich als Antwort eingeben werde), aber sie ist klobig.

Die perfekte Lösung wäre eine einzelne Codezeile, die in den Anfang eines Skripts eingefügt werden könnte, das beide Streams sowohl an das Terminal als auch an eine Protokolldatei sendet.

BEARBEITEN: Das Umleiten von STDERR zu STDOUT und das Weiterleiten des Ergebnisses an das Tee funktioniert, hängt jedoch davon ab, dass die Benutzer daran denken, die Ausgabe umzuleiten und weiterzuleiten. Ich möchte, dass die Protokollierung narrensicher und automatisch ist (weshalb ich die Lösung in das Skript selbst einbetten möchte.)

JPLemme
quelle
Für andere Leser: ähnliche Frage: stackoverflow.com/questions/692000/…
pevik
1
Ich ärgere mich, dass alle (einschließlich ich!) Mit Ausnahme von @JasonSydes entgleist sind und eine andere Frage beantwortet haben. Und Jasons Antwort ist unzuverlässig, wie ich kommentierte. Ich würde gerne eine wirklich zuverlässige Antwort auf die Frage sehen, die Sie gestellt (und in Ihrer EDIT hervorgehoben) haben.
Don Hatch
Oh warte, ich nehme es zurück. Die akzeptierte Antwort von @PaulTromblin beantwortet sie. Ich habe nicht weit genug hineingelesen.
Don Hatch

Antworten:

165

Verwenden Sie "tee", um zu einer Datei und dem Bildschirm umzuleiten. Abhängig von der verwendeten Shell müssen Sie stderr zuerst mit stdout umleiten

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

oder

./a.out |& tee output

In csh gibt es einen integrierten Befehl namens "script", der alles, was auf dem Bildschirm angezeigt wird, in einer Datei erfasst. Sie starten es, indem Sie "script" eingeben, dann alles tun, was Sie erfassen möchten, und dann Strg-D drücken, um die Skriptdatei zu schließen. Ich kenne kein Äquivalent für sh / bash / ksh.

Da Sie angegeben haben, dass dies Ihre eigenen sh-Skripte sind, die Sie ändern können, können Sie die Umleitung auch intern durchführen, indem Sie das gesamte Skript mit geschweiften Klammern oder Klammern umgeben

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file
Paul Tomblin
quelle
4
Ich wusste nicht, dass Sie Befehle in die Shell-Skripte einschließen können. Interessant.
Jamie
1
Ich schätze auch die Bracket-Verknüpfung! Aus irgendeinem Grund 2>&1 | tee -a filenamewurde stderr nicht aus meinem Skript in die Datei gespeichert, aber es funktionierte einwandfrei, als ich den Befehl kopierte und in das Terminal einfügte! Der Bracket-Trick funktioniert jedoch einwandfrei.
Ed Brannin
8
Beachten Sie, dass die Unterscheidung zwischen stdout und stderr verloren geht, da Tee alles auf stdout druckt.
Flimm
2
Zu Ihrer Information: Der Befehl 'script' ist in den meisten Distributionen verfügbar (er ist Teil des util-linux-Pakets)
SamWN
2
@Flimm, gibt es eine Möglichkeit (eine andere Möglichkeit), die Unterscheidung zwischen stdout und stderr beizubehalten?
Gabriel
20

Ein halbes Jahrzehnt später ...

Ich glaube, dies ist die "perfekte Lösung", die das OP sucht.

Hier ist ein Einzeiler, den Sie oben in Ihr Bash-Skript einfügen können:

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

Hier ist ein kleines Skript, das seine Verwendung demonstriert:

#!/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

(Hinweis: Dies funktioniert nur mit Bash. Es funktioniert nicht mit / bin / sh.)

Von hier aus angepasst ; Soweit ich weiß, hat das Original STDERR nicht in der Protokolldatei abgefangen. Mit einer Notiz von hier behoben .

Jason Sydes
quelle
3
Beachten Sie, dass die Unterscheidung zwischen stdout und stderr verloren geht, da Tee alles auf stdout druckt.
Flimm
@Flimm stderr könnte zu einem anderen Tee-Prozess umgeleitet werden, der wiederum zu stderr umgeleitet werden könnte.
Jarno
@ Flimm, ich schrieb jarnos Vorschlag hier: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService
1
Diese Lösung ist, wie die meisten anderen bisher vorgeschlagenen Lösungen, renngefährdet. Das heißt, wenn das aktuelle Skript abgeschlossen ist und entweder zur Eingabeaufforderung des Benutzers oder zu einem übergeordneten aufrufenden Skript zurückkehrt, wird das im Hintergrund ausgeführte Tee weiterhin ausgeführt und gibt möglicherweise die letzten Zeilen an den Bildschirm und an aus Die Protokolldatei wird verspätet angezeigt (dh auf dem Bildschirm nach der Eingabeaufforderung und auf der Protokolldatei, nachdem die Protokolldatei voraussichtlich vollständig ist).
Don Hatch
1
Dies ist jedoch die einzige bisher vorgeschlagene Antwort, die sich tatsächlich mit der Frage befasst!
Don Hatch
9

Das Muster

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

Dadurch werden sowohl stdout als auch stderr separat umgeleitet und separate Kopien von stdout und stderr an den Anrufer (möglicherweise Ihr Terminal) gesendet.

  • In zsh wird erst mit der nächsten Anweisung fortgefahren, wenn die tees beendet sind.

  • In bash stellen Sie möglicherweise fest, dass die letzten Zeilen der Ausgabe nach der nächsten Anweisung erscheinen .

In beiden Fällen gehen die richtigen Bits an die richtigen Stellen.


Erläuterung

Hier ist ein Skript (gespeichert in ./example):

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

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

Hier ist eine Sitzung:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

So funktioniert das:

  1. Beide teeProzesse werden gestartet, ihre Standards werden Dateideskriptoren zugewiesen. Da sie in Prozessersetzungen eingeschlossen sind , werden die Pfade zu diesen Dateideskriptoren im aufrufenden Befehl ersetzt. Jetzt sieht es ungefähr so ​​aus:

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

  1. the_cmd wird ausgeführt und schreibt stdout in den ersten Dateideskriptor und stderr in den zweiten.

  2. Im Bash-Fall wird the_cmdnach Abschluss sofort die folgende Anweisung ausgeführt (wenn Ihr Terminal der Anrufer ist, wird Ihre Eingabeaufforderung angezeigt).

  3. Im zsh-Fall the_cmdwartet die Shell teenach Abschluss auf den Abschluss beider Prozesse, bevor sie fortfährt. Mehr dazu hier .

  4. Der erste teeProzess, der aus the_cmdder Standardausgabe liest , schreibt eine Kopie dieser Standardausgabe zurück an den Anrufer, da dies der teeFall ist. Die Ausgänge werden nicht umgeleitet, sodass sie unverändert zum Anrufer zurückkehren

  5. Der zweite teeProzess wird stdoutan den Anrufer weitergeleitet stderr(was gut ist, da stdin aus the_cmddem stderr liest ). Wenn es also in seine Standardausgabe schreibt, gehen diese Bits in die Standardausgabe des Aufrufers.

Dadurch wird stderr sowohl in den Dateien als auch in der Ausgabe des Befehls von stdout getrennt.

Wenn das erste Tee Fehler schreibt, werden sie sowohl in der stderr-Datei als auch im stderr des Befehls angezeigt. Wenn das zweite Tee Fehler schreibt, werden sie nur im stderr des Terminals angezeigt.

MatrixManAtYrService
quelle
Das sieht wirklich nützlich aus und was ich will. Ich bin mir jedoch nicht sicher, wie ich die Verwendung von Klammern (wie in der ersten Zeile gezeigt) in einem Windows-Stapelskript replizieren soll. ( teeist auf dem betreffenden System verfügbar.) Der Fehler, den ich erhalte, lautet "Der Prozess kann nicht auf die Datei zugreifen, da sie von einem anderen Prozess verwendet wird."
Agi Hammerthief
Diese Lösung ist, wie die meisten anderen bisher vorgeschlagenen Lösungen, renngefährdet. Das heißt, wenn das aktuelle Skript abgeschlossen ist und entweder zur Eingabeaufforderung des Benutzers oder zu einem übergeordneten aufrufenden Skript zurückkehrt, wird das im Hintergrund ausgeführte Tee weiterhin ausgeführt und gibt möglicherweise die letzten Zeilen an den Bildschirm und an aus Die Protokolldatei wird verspätet angezeigt (dh auf dem Bildschirm nach der Eingabeaufforderung und auf der Protokolldatei, nachdem die Protokolldatei voraussichtlich vollständig ist).
Don Hatch
2
@DonHatch Können Sie eine Lösung vorschlagen, die dieses Problem behebt?
Pylipp
Ich würde mich auch für einen Testfall interessieren, der das Rennen deutlich macht. Es ist nicht so, dass ich zweifelhaft bin, aber es ist schwer zu versuchen, es zu vermeiden, weil ich es nicht gesehen habe.
MatrixManAtYrService
@pylipp Ich habe keine Lösung. Ich würde mich sehr für einen interessieren.
Don Hatch
4

Das Umleiten von stderr zu stdout hängt dies auf Ihren Befehl an: 2>&1 Für die Ausgabe an das Terminal und die Anmeldung in einer Datei sollten Sie verwendentee

Beide zusammen würden so aussehen:

 mycommand 2>&1 | tee mylogfile.log

BEARBEITEN: Zum Einbetten in Ihr Skript würden Sie dasselbe tun. Also dein Drehbuch

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

würde enden als

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log
flolo
quelle
2
Beachten Sie, dass die Unterscheidung zwischen stdout und stderr verloren geht, da Tee alles auf stdout druckt.
Flimm
4

EDIT: Ich sehe, ich wurde entgleist und beantwortete eine andere Frage als die gestellte. Die Antwort auf die eigentliche Frage befindet sich am Ende von Paul Tomblins Antwort. (Wenn Sie diese Lösung verbessern möchten, um stdout und stderr aus irgendeinem Grund getrennt umzuleiten, können Sie die hier beschriebene Technik verwenden.)


Ich wollte eine Antwort, die die Unterscheidung zwischen stdout und stderr bewahrt. Leider sind alle bisher gegebenen Antworten, die diese Unterscheidung bewahren, rassenanfällig: Sie riskieren, dass Programme unvollständige Eingaben sehen, wie ich in den Kommentaren ausgeführt habe.

Ich denke, ich habe endlich eine Antwort gefunden, die die Unterscheidung bewahrt, nicht rassenanfällig ist und auch nicht besonders fummelig.

Erster Baustein: stdout und stderr tauschen:

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

Zweiter Baustein: Wenn wir nur stderr filtern wollten (z. B. tee), könnten wir dies erreichen, indem wir stdout & stderr austauschen, filtern und dann zurück tauschen:

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

Jetzt ist der Rest einfach: Wir können entweder am Anfang einen Standardfilter hinzufügen:

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

oder am Ende:

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

Um mich davon zu überzeugen, dass beide oben genannten Befehle funktionieren, habe ich Folgendes verwendet:

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;}'

Ausgabe ist:

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

und meine Eingabeaufforderung kommt teed stderr: to stderrerwartungsgemäß sofort nach dem " " zurück.

Fußnote zu zsh :

Die obige Lösung funktioniert in Bash (und vielleicht einigen anderen Shells, da bin ich mir nicht sicher), aber in Zsh funktioniert sie nicht. Es gibt zwei Gründe, warum es in zsh fehlschlägt:

  1. Die Syntax 2>&3-wird von zsh nicht verstanden. das muss umgeschrieben werden als2>&3 3>&-
  2. Wenn Sie in zsh (im Gegensatz zu anderen Shells) einen bereits geöffneten Dateideskriptor umleiten, führt er in einigen Fällen (ich verstehe nicht ganz, wie er sich entscheidet) stattdessen ein integriertes tee-ähnliches Verhalten aus. Um dies zu vermeiden, müssen Sie jedes fd schließen, bevor Sie es umleiten.

So muss zum Beispiel meine zweite Lösung für zsh as neu geschrieben werden {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(was auch in bash funktioniert, aber schrecklich ausführlich ist).

Auf der anderen Seite können Sie das mysteriöse integrierte implizite Abschlagen von zsh nutzen, um eine viel kürzere Lösung für zsh zu erhalten, bei der überhaupt kein Abschlag ausgeführt wird:

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

(Ich hätte aus den Dokumenten nicht erraten, dass das >&1und das 2>&2sind, was das implizite Abschlagen von zsh auslöst. Ich habe das durch Ausprobieren herausgefunden.)

Don Hatch
quelle
Ich habe damit in Bash rumgespielt und es funktioniert gut. Nur eine Warnung für zsh-Benutzer mit der Angewohnheit, Kompatibilität anzunehmen (wie ich), verhält es sich dort anders: gist.github.com/MatrixManAtYrService/…
MatrixManAtYrService
@MatrixManAtYrService Ich glaube, ich habe die zsh-Situation in den Griff bekommen, und es stellt sich heraus, dass es in zsh eine viel sauberere Lösung gibt. Siehe meine Bearbeitung "Fußnote über zsh".
Don Hatch
Vielen Dank, dass Sie die Lösung so ausführlich erklärt haben. Wissen Sie auch, wie Sie den Rückkehrcode abrufen, wenn Sie eine Funktion ( my_function) in der verschachtelten stdout / stderr-Filterung verwenden? Ich habe { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filteres getan, aber es fühlt sich komisch an, eine Datei als Indikator für einen Fehler zu erstellen ...
Pylipp
@pylipp Ich nicht ohne weiteres. Sie können dies als separate Frage stellen (möglicherweise mit einer einfacheren Pipeline).
Don Hatch
2

Verwenden Sie den scriptBefehl in Ihrem Skript (Man 1-Skript).

Erstellen Sie ein Wrapper-Shellscript (2 Zeilen), das script () einrichtet und dann exit aufruft.

Teil 1: wrap.sh

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

Teil 2: realscript.sh

#!/bin/sh
echo 'Output'

Ergebnis:

~: 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
quelle
1

Verwenden Sie das Tee-Programm und dup stderr, um zu stdout.

 program 2>&1 | tee > logfile
Tvanfosson
quelle
1

Ich habe ein Skript namens "RunScript.sh" erstellt. Der Inhalt dieses Skripts ist:

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

Ich nenne es so:

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

Dies funktioniert, erfordert jedoch, dass die Skripte der Anwendung über ein externes Skript ausgeführt werden. Es ist ein bisschen klobig.

JPLemme
quelle
9
Sie verlieren die Gruppierung von Argumenten mit Leerzeichen mit $ 1 $ 2 $ 3 ... , sollten Sie verwenden (mit Anführungszeichen): "$ @"
NVRAM
1

Ein Jahr später ist hier ein altes Bash-Skript zum Protokollieren von Daten. Beispiel:
teelog make ...Protokolle unter einem generierten Protokollnamen (und siehe auch den Trick zum Protokollieren verschachtelter makes).

#!/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
quelle
Ich weiß, dass es viel zu spät ist, einen Kommentar hinzuzufügen, aber ich musste mich nur für dieses Skript bedanken. Sehr nützlich und gut dokumentiert!
Stephenmm
Danke @stephenmm; Es ist nie zu spät zu sagen, "nützlich" oder "könnte verbessert werden".
Denis