Come ottengo sia STDOUT che STDERR per andare al terminale e un file di registro?


105

Ho uno script che verrà eseguito in modo interattivo da utenti non tecnici. Lo script scrive gli aggiornamenti di stato su STDOUT in modo che l'utente possa essere sicuro che lo script sia in esecuzione correttamente.

Voglio che sia STDOUT che STDERR siano reindirizzati al terminale (in modo che l'utente possa vedere che lo script funziona e vedere se c'è stato un problema). Voglio anche che entrambi i flussi vengano reindirizzati a un file di registro.

Ho visto un sacco di soluzioni in rete. Alcuni non funzionano e altri sono orribilmente complicati. Ho sviluppato una soluzione praticabile (che inserirò come risposta), ma è kludgy.

La soluzione perfetta sarebbe una singola riga di codice che potrebbe essere incorporata all'inizio di qualsiasi script che invia entrambi i flussi sia al terminale che a un file di registro.

EDIT: Reindirizzare STDERR a STDOUT e reindirizzare il risultato a tee funziona, ma dipende dagli utenti che si ricordano di reindirizzare e reindirizzare l'output. Voglio che la registrazione sia automatica e a prova di errore (motivo per cui mi piacerebbe poter incorporare la soluzione nello script stesso).


Per gli altri lettori: domanda simile: stackoverflow.com/questions/692000/...
pevik

1
Sono infastidito dal fatto che tutti (incluso me!) Ad eccezione di @JasonSydes siano stati deragliati e abbiano risposto a una domanda diversa. E la risposta di Jason è inaffidabile, come ho commentato. Mi piacerebbe vedere una risposta davvero affidabile alla domanda che hai posto (ed enfatizzato nel tuo EDIT).
Don Hatch,

Oh aspetta, lo riprendo. La risposta accettata di @PaulTromblin risponde. Non ho letto abbastanza a fondo.
Don Hatch,

Risposte:


169

Usa "tee" per reindirizzare a un file e allo schermo. A seconda della shell che usi, devi prima reindirizzare stderr a stdout usando

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

o

./a.out |& tee output

In csh, c'è un comando incorporato chiamato "script" che catturerà tutto ciò che va sullo schermo in un file. Lo si avvia digitando "script", quindi facendo tutto ciò che si desidera acquisire, quindi premere control-D per chiudere il file di script. Non conosco un equivalente per sh / bash / ksh.

Inoltre, poiché hai indicato che questi sono i tuoi script sh che puoi modificare, puoi eseguire il reindirizzamento internamente circondando l'intero script con parentesi graffe o parentesi, come

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file

4
Non sapevo che potessi mettere tra parentesi i comandi negli script della shell. Interessante.
Jamie

1
Apprezzo anche la scorciatoia Bracket! Per qualche motivo, 2>&1 | tee -a filenamenon stavo salvando stderr nel file dal mio script, ma ha funzionato bene quando ho copiato il comando e incollato nel terminale! Il trucco della staffa funziona bene, però.
Ed Brannin

8
Si noti che la distinzione tra stdout e stderr andrà persa, poiché tee stampa tutto su stdout.
Flimm

2
FYI: Il comando 'script' è disponibile nella maggior parte delle distribuzioni (fa parte del pacchetto util-linux)
SamWN

2
@Flimm, c'è un modo (un altro modo) per mantenere la distinzione tra stdout e stderr?
Gabriel

20

Circa mezzo decennio dopo ...

Credo che questa sia la "soluzione perfetta" cercata dall'OP.

Ecco una riga che puoi aggiungere all'inizio del tuo script Bash:

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

Ecco un piccolo script che ne dimostra l'uso:

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

(Nota: funziona solo con Bash, non lo farà lavoro con / bin / sh.)

Adattato da qui ; l'originale, da quello che posso dire, non ha catturato STDERR nel file di registro. Risolto con una nota da qui .


3
Si noti che la distinzione tra stdout e stderr andrà persa, poiché tee stampa tutto su stdout.
Flimm

@Flimm stderr potrebbe essere reindirizzato a un diverso processo tee che potrebbe nuovamente essere reindirizzato a stderr.
jarno

@Flimm, ho scritto il suggerimento di Jarno qui: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService

1
Questa soluzione, come la maggior parte delle altre soluzioni proposte finora, è soggetta a gare. Cioè, quando lo script corrente viene completato e ritorna, o al prompt dell'utente o ad uno script chiamante di livello superiore, il tee, che è in esecuzione in background, sarà ancora in esecuzione e potrebbe emettere le ultime righe sullo schermo e su il file di registro in ritardo (ovvero, allo schermo dopo il prompt e al file di registro dopo che si prevede che il file di registro sia completo).
Don Hatch,

1
Tuttavia, questa è l'unica risposta proposta finora che affronta effettivamente la domanda!
Don Hatch,

9

Il modello

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

Questo reindirizza sia stdout che stderr separatamente e invia copie separate di stdout e stderr al chiamante (che potrebbe essere il tuo terminale).

  • In zsh, non procederà all'istruzione successiva finché le tees non saranno terminate.

  • In bash, potresti scoprire che le ultime righe di output appaiono dopo qualsiasi istruzione successiva.

In entrambi i casi, i bit giusti vanno al posto giusto.


Spiegazione

Ecco uno script (memorizzato 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 )

Ecco una sessione:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Ecco come funziona:

  1. Entrambi i teeprocessi vengono avviati, i loro standard vengono assegnati ai descrittori di file. Poiché sono racchiusi nelle sostituzioni di processo , i percorsi a quei descrittori di file vengono sostituiti nel comando chiamante, quindi ora assomiglia a questo:

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

  1. the_cmd viene eseguito, scrivendo stdout nel primo descrittore di file e stderr nel secondo.

  2. Nel caso bash, una volta the_cmdterminato, la seguente istruzione viene eseguita immediatamente (se il tuo terminale è il chiamante, vedrai apparire il tuo prompt).

  3. Nel caso zsh, una volta the_cmdterminato, la shell attende che entrambi i teeprocessi finiscano prima di proseguire. Maggiori informazioni su questo qui .

  4. Il primo teeprocesso, che sta leggendo dallo the_cmdstdout di, scrive una copia di quello stdout al chiamante perché è quello che teefa. I suoi output non vengono reindirizzati, quindi tornano al chiamante senza modifiche

  5. Il secondo teeprocesso è stato stdoutreindirizzato al chiamante stderr(il che è positivo, perché lo stdin sta leggendo dallo the_cmdstderr di). Quindi, quando scrive nel suo stdout, quei bit vanno allo stderr del chiamante.

Ciò mantiene stderr separato da stdout sia nei file che nell'output del comando.

Se il primo tee scrive degli errori, verranno visualizzati sia nel file stderr che nello stderr del comando, se il secondo tee scrive degli errori, verranno visualizzati solo nello stderr del terminale.


Questo sembra davvero utile e quello che voglio. Tuttavia, non sono sicuro di come replicare l'uso delle parentesi (come mostrato nella prima riga) in uno script batch di Windows. ( teeè disponibile sul sistema in questione.) L'errore che ottengo è "Il processo non può accedere al file perché è utilizzato da un altro processo."
Agi Hammerthief

Questa soluzione, come la maggior parte delle altre soluzioni proposte finora, è soggetta a gare. Cioè, quando lo script corrente viene completato e ritorna, o al prompt dell'utente o uno script chiamante di livello superiore, il tee, che è in esecuzione in background, sarà ancora in esecuzione e potrebbe emettere le ultime righe sullo schermo e su il file di registro in ritardo (ovvero, allo schermo dopo il prompt e al file di registro dopo che si prevede che il file di registro sia completo).
Don Hatch

2
@DonHatch Puoi proporre una soluzione che modifichi questo problema?
pylipp

Sarei anche interessato a un test case che renda evidente la gara. Non è che io abbia dubbi, ma è difficile tentare di evitarlo perché non l'ho visto accadere.
MatrixManAtYrService

@pylipp Non ho una soluzione. Sarei molto interessato a uno.
Don Hatch,

4

per reindirizzare stderr a stdout, aggiungi questo al tuo comando: 2>&1 Per l'output sul terminale e l'accesso al file dovresti usaretee

Entrambi insieme sarebbero simili a questo:

 mycommand 2>&1 | tee mylogfile.log

EDIT: per l'incorporamento nel tuo script faresti lo stesso. Quindi il tuo copione

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

finirebbe come

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log

2
Si noti che la distinzione tra stdout e stderr andrà persa, poiché tee stampa tutto su stdout.
Flimm

4

EDIT: Vedo che sono stato deragliato e ho finito per rispondere a una domanda diversa da quella posta. La risposta alla vera domanda è in fondo alla risposta di Paul Tomblin. (Se vuoi migliorare quella soluzione per reindirizzare stdout e stderr separatamente per qualche motivo, potresti usare la tecnica che descrivo qui.)


Volevo una risposta che preservasse la distinzione tra stdout e stderr. Sfortunatamente tutte le risposte date finora che conservano tale distinzione sono inclini alla corsa: rischiano che i programmi vedano input incompleti, come ho sottolineato nei commenti.

Penso di aver finalmente trovato una risposta che preserva la distinzione, non è incline alla razza e non è nemmeno troppo complicata.

Primo blocco di costruzione: per scambiare stdout e stderr:

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

Secondo elemento costitutivo: se volessimo filtrare (ad esempio tee) solo stderr, potremmo farlo scambiando stdout e stderr, filtrando e poi scambiando di nuovo:

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

Ora il resto è facile: possiamo aggiungere un filtro stdout, o all'inizio:

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

o alla fine:

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

Per convincermi che entrambi i comandi precedenti funzionino, ho utilizzato quanto segue:

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

L'output è:

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

e il mio messaggio ritorna immediatamente dopo il " teed stderr: to stderr", come previsto.

Nota a piè di pagina su zsh :

La soluzione di cui sopra funziona in bash (e forse alcune altre shell, non ne sono sicuro), ma non funziona in zsh. Ci sono due ragioni per cui fallisce in zsh:

  1. la sintassi 2>&3-non è compresa da zsh; che deve essere riscritto come2>&3 3>&-
  2. in zsh (a differenza di altre shell), se reindirizzi un descrittore di file che è già aperto, in alcuni casi (non capisco completamente come decide) fa invece un comportamento tipo tee incorporato. Per evitare ciò, è necessario chiudere ogni fd prima di reindirizzarlo.

Quindi, ad esempio, la mia seconda soluzione deve essere riscritta per 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(che funziona anche in bash, ma è terribilmente prolissa).

D'altra parte, puoi sfruttare il misterioso tee implicito incorporato di zsh per ottenere una soluzione molto più breve per zsh, che non esegue affatto tee:

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

(Non avrei immaginato dai documenti di aver scoperto che >&1e 2>&2sono la cosa che fa scattare il tee implicito di zsh; l'ho scoperto per tentativi ed errori.)


Ho giocato con questo in bash e funziona bene. Solo un avvertimento per gli utenti zsh con l'abitudine di presumere la compatibilità (come me), si comporta in modo diverso lì: gist.github.com/MatrixManAtYrService/…
MatrixManAtYrService

@ MatrixManAtYrService Credo di aver capito la situazione zsh, e si scopre che c'è una soluzione molto più ordinata in zsh. Vedere la mia modifica "Nota a piè di pagina su zsh".
Don Hatch

Grazie per aver spiegato la soluzione in modo così dettagliato. Sai anche come recuperare il codice di ritorno quando usi una funzione ( my_function) nel filtro stdout / stderr annidato? L'ho fatto, { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filterma sembra strano creare un file come indicatore di errore ...
pylipp

@pylipp io non improvvisamente. Potresti chiederlo come domanda separata (magari con una pipeline più semplice).
Don Hatch il

2

Utilizzare il script comando nel tuo script (man 1 script)

Crea uno shellscript wrapper (2 righe) che imposta script () e quindi chiama exit.

Parte 1: wrap.sh

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

Parte 2: realscript.sh

#!/bin/sh
echo 'Output'

Risultato:

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

1

Usa il programma tee e dup stderr su stdout.

 program 2>&1 | tee > logfile

1

Ho creato uno script chiamato "RunScript.sh". Il contenuto di questo script è:

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

Lo chiamo così:

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

Funziona, ma richiede che gli script dell'applicazione vengano eseguiti tramite uno script esterno. È un po 'disordinato.


9
Perderai il raggruppamento di argomenti contenenti spazi bianchi con $ 1 $ 2 $ 3 ... , dovresti usare (senza virgolette): "$ @"
NVRAM

1

Un anno dopo, ecco un vecchio script bash per registrare qualsiasi cosa. Ad esempio,
teelog make ...registra in un nome di registro generato (e guarda anche il trucco per registrare i messaggi nidificati 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

So che è molto tardi per aggiungere un commento, ma dovevo solo dire grazie per questo script. Molto utile e ben documentato!
stephenmm

Grazie @stephenmm; non è mai troppo tardi per dire "utile" o "potrebbe essere migliorato".
denis
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.