¿Cómo consigo que STDOUT y STDERR vayan a la terminal y a un archivo de registro?

104

Tengo un script que será ejecutado de forma interactiva por usuarios no técnicos. El script escribe actualizaciones de estado en STDOUT para que el usuario pueda estar seguro de que el script se está ejecutando correctamente.

Quiero que tanto STDOUT como STDERR sean redirigidos al terminal (para que el usuario pueda ver que el script está funcionando y ver si hubo un problema). También quiero que ambas secuencias se redirijan a un archivo de registro.

He visto un montón de soluciones en la red. Algunos no funcionan y otros son horriblemente complicados. Desarrollé una solución viable (que ingresaré como respuesta), pero es torpe.

La solución perfecta sería una sola línea de código que podría incorporarse al principio de cualquier script que envíe ambos flujos tanto al terminal como a un archivo de registro.

EDITAR: Redirigir STDERR a STDOUT y canalizar el resultado a tee funciona, pero depende de que los usuarios recuerden redirigir y canalizar la salida. Quiero que el registro sea infalible y automático (por eso me gustaría poder insertar la solución en el script).

JPLemme
fuente
Para otros lectores: pregunta similar: stackoverflow.com/questions/692000/…
pevik
1
Me molesta que todos (¡incluido yo!), Excepto @JasonSydes, se descarrilaron y respondieron una pregunta diferente. Y la respuesta de Jason no es confiable, como comenté. Me encantaría ver una respuesta realmente confiable a la pregunta que ha hecho (y enfatizada en su EDITAR).
Don Hatch
Oh, espera, lo retiro. La respuesta aceptada de @PaulTromblin sí la responde. No leí lo suficiente.
Don Hatch

Respuestas:

167

Utilice "tee" para redirigir a un archivo y la pantalla. Dependiendo del shell que use, primero debe redirigir stderr a stdout usando

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

o

./a.out |& tee output

En csh, hay un comando incorporado llamado "script" que capturará todo lo que vaya a la pantalla en un archivo. Comience escribiendo "script", luego haga lo que quiera capturar, luego presione control-D para cerrar el archivo de script. No conozco un equivalente para sh / bash / ksh.

Además, dado que ha indicado que estos son sus propios scripts sh que puede modificar, puede hacer la redirección internamente rodeando todo el script con llaves o corchetes, como

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file
Paul Tomblin
fuente
4
No sabía que podía poner entre corchetes los comandos en los scripts de shell. Interesante.
Jamie
1
¡También aprecio el atajo de Bracket! Por alguna razón, 2>&1 | tee -a filenameno estaba guardando stderr en el archivo de mi script, ¡pero funcionó bien cuando copié el comando y lo pegué en la terminal! Sin embargo, el truco del corchete funciona bien.
Ed Brannin
8
Tenga en cuenta que la distinción entre stdout y stderr se perderá, ya que tee imprime todo en stdout.
Flimm
2
FYI: El comando 'script' está disponible en la mayoría de las distribuciones (es parte del paquete util-linux)
SamWN
2
@Flimm, ¿hay alguna forma (alguna otra forma) de mantener la distinción entre stdout y stderr?
Gabriel
20

Aproximadamente media década después ...

Creo que esta es la "solución perfecta" que busca el OP.

Aquí hay una línea que puede agregar a la parte superior de su script Bash:

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

Aquí hay un pequeño script que demuestra su 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: Esto sólo funciona con Bash Lo hará. No trabajo con / bin / sh).

Adaptado de aquí ; el original, por lo que puedo decir, no captó STDERR en el archivo de registro. Corregido con una nota de aquí .

Jason Sydes
fuente
3
Tenga en cuenta que la distinción entre stdout y stderr se perderá, ya que tee imprime todo en stdout.
Flimm
@Flimm stderr podría redirigirse a diferentes procesos de tee que nuevamente podrían redirigirse a stderr.
jarno
@Flimm, escribí la sugerencia de Jarno aquí: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService
1
Esta solución, como la mayoría de las otras soluciones propuestas hasta ahora, es propensa a las carreras. Es decir, cuando el script actual se completa y regresa, ya sea al indicador del usuario o algún script de llamada de nivel superior, el tee, que se está ejecutando en segundo plano, seguirá ejecutándose y puede emitir las últimas líneas a la pantalla y a el archivo de registro tarde (es decir, a la pantalla después de la solicitud, y al archivo de registro después de que se espera que el archivo de registro esté completo).
Don Hatch
1
Sin embargo, esta es la única respuesta propuesta hasta ahora que realmente aborda la pregunta.
Don Hatch
9

El patrón

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

Esto redirige tanto stdout como stderr por separado, y envía copias separadas de stdout y stderr a la persona que llama (que podría ser su terminal).

  • En zsh, no pasará a la siguiente instrucción hasta que el tees haya terminado.

  • En bash, puede encontrar que las últimas líneas de salida aparecen después de cualquier declaración que venga a continuación.

En cualquier caso, los bits correctos van a los lugares correctos.


Explicación

Aquí hay un script (almacenado en ./example):

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

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

Aquí tienes una sesión:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Así es como funciona:

  1. teeSe inician ambos procesos, sus stdins se asignan a descriptores de archivo. Debido a que están encerrados en sustituciones de procesos , las rutas a esos descriptores de archivo se sustituyen en el comando de llamada, por lo que ahora se ve así:

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

  1. the_cmd se ejecuta, escribiendo stdout en el primer descriptor de archivo y stderr en el segundo.

  2. En el caso de bash, una vez que the_cmdfinaliza, la siguiente declaración ocurre inmediatamente (si su terminal es la persona que llama, verá aparecer su mensaje).

  3. En el caso de zsh, una vez que the_cmdfinaliza, el shell espera teea que terminen ambos procesos antes de continuar. Más sobre esto aquí .

  4. El primer teeproceso, que está leyendo desde the_cmdla salida estándar, escribe una copia de esa salida estándar en la persona que llama porque eso es lo que teehace. Sus salidas no se redirigen, por lo que vuelven a la persona que llama sin cambios

  5. El segundo teeproceso lo stdoutredirige a la persona stderrque llama (lo cual es bueno, porque stdin está leyendo de the_cmdstderr). Entonces, cuando escribe en su stdout, esos bits van al stderr de la persona que llama.

Esto mantiene stderr separado de stdout tanto en los archivos como en la salida del comando.

Si el primer tee escribe algún error, aparecerán tanto en el archivo stderr como en el stderr del comando, si el segundo tee escribe algún error, solo aparecerán en el stderr del terminal.

MatrixManAtYrService
fuente
Esto parece realmente útil y lo que quiero. Sin embargo, no estoy seguro de cómo replicar el uso de corchetes (como se muestra en la primera línea) en un script por lotes de Windows. ( teeestá disponible en el sistema en cuestión). El error que aparece es "El proceso no puede acceder al archivo porque está siendo utilizado por otro proceso".
Agi Hammerthief
Esta solución, como la mayoría de las otras soluciones propuestas hasta ahora, es propensa a las carreras. Es decir, cuando el script actual se completa y regresa, ya sea al indicador del usuario o algún script de llamada de nivel superior, el tee, que se está ejecutando en segundo plano, seguirá ejecutándose y puede emitir las últimas líneas a la pantalla y a el archivo de registro tarde (es decir, a la pantalla después de la solicitud, y al archivo de registro después de que se espera que el archivo de registro esté completo).
Don Hatch
2
@DonHatch ¿Puede proponer una solución que corrija este problema?
pylipp
También me interesaría un caso de prueba que haga evidente la carrera. No es que tenga dudas, pero es difícil intentar evitarlo porque no lo he visto suceder.
MatrixManAtYrService
@pylipp No tengo una solución. Estaría muy interesado en uno.
Don Hatch
4

para redirigir stderr a stdout agregue esto a su comando: 2>&1 Para enviar a la terminal e iniciar sesión en el archivo, debe usartee

Ambos juntos se verían así:

 mycommand 2>&1 | tee mylogfile.log

EDITAR: para incrustarlo en su script, haría lo mismo. Entonces tu guión

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

terminaría como

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log
flolo
fuente
2
Tenga en cuenta que la distinción entre stdout y stderr se perderá, ya que tee imprime todo en stdout.
Flimm
4

EDITAR: Veo que me descarrilé y terminé respondiendo una pregunta diferente a la que me hicieron. La respuesta a la pregunta real está al final de la respuesta de Paul Tomblin. (Si desea mejorar esa solución para redirigir stdout y stderr por separado por alguna razón, puede usar la técnica que describo aquí).


Quería una respuesta que conservara la distinción entre stdout y stderr. Desafortunadamente, todas las respuestas dadas hasta ahora que preservan esa distinción son propensas a la raza: corren el riesgo de que los programas vean una entrada incompleta, como señalé en los comentarios.

Creo que finalmente encontré una respuesta que conserva la distinción, que no es propensa a la raza y tampoco es terriblemente complicada.

Primer bloque de construcción: para intercambiar stdout y stderr:

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

Segundo bloque de construcción: si quisiéramos filtrar (por ejemplo, tee) solo stderr, podríamos lograrlo cambiando stdout y stderr, filtrando y luego volviendo a intercambiar:

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

Ahora el resto es fácil: podemos agregar un filtro de salida estándar, ya sea al principio:

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

o al final:

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

Para convencerme de que los dos comandos anteriores funcionan, utilicé lo siguiente:

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

La salida es:

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

y mi mensaje vuelve inmediatamente después de " teed stderr: to stderr", como se esperaba.

Nota al pie sobre zsh :

La solución anterior funciona en bash (y tal vez en otros shells, no estoy seguro), pero no funciona en zsh. Hay dos razones por las que falla en zsh:

  1. 2>&3-zsh no entiende la sintaxis ; que tiene que ser reescrito como2>&3 3>&-
  2. en zsh (a diferencia de otros shells), si redirige un descriptor de archivo que ya está abierto, en algunos casos (no entiendo completamente cómo decide), en su lugar, realiza un comportamiento integrado en forma de tee. Para evitar esto, debe cerrar cada fd antes de redirigirlo.

Entonces, por ejemplo, mi segunda solución debe reescribirse para zsh como {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(que también funciona en bash, pero es muy detallado).

Por otro lado, puede aprovechar el misterioso tee implícito incorporado de zsh para obtener una solución mucho más corta para zsh, que no ejecuta tee en absoluto:

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

(No habría adivinado por los documentos que encontré que >&1y 2>&2son lo que desencadena el tee implícito de zsh; lo descubrí por prueba y error).

Don Hatch
fuente
Jugué con esto en bash y funciona bien. Solo una advertencia para los usuarios de zsh con el hábito de asumir la compatibilidad (como yo), se comporta de manera diferente allí: gist.github.com/MatrixManAtYrService/…
MatrixManAtYrService
@MatrixManAtYrService Creo que tengo un control sobre la situación de zsh, y resulta que hay una solución mucho mejor en zsh. Vea mi edición "Nota al pie sobre zsh".
Don Hatch
Gracias por explicar la solución con tanto detalle. ¿También sabe cómo recuperar el código de retorno cuando utiliza una función ( my_function) en el filtrado stdout / stderr anidado? Lo hice, { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filterpero se siente extraño crear un archivo como indicador de falla ...
pylipp
@pylipp No lo hago de improviso. Puede hacer eso como una pregunta separada (quizás con una canalización más simple).
Don Hatch
2

Use el scriptcomando en su script (script man 1)

Cree un shellscript contenedor (2 líneas) que configure script () y luego llame a exit.

Parte 1: wrap.sh

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

Parte 2: realscript.sh

#!/bin/sh
echo 'Output'

Resultado:

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

Utilice el programa tee y dup stderr para stdout.

 program 2>&1 | tee > logfile
tvanfosson
fuente
1

Creé un script llamado "RunScript.sh". El contenido de este script es:

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

Lo llamo así:

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

Esto funciona, pero requiere que los scripts de la aplicación se ejecuten a través de un script externo. Es un poco torpe.

JPLemme
fuente
9
Perderá la agrupación de argumentos que contienen espacios en blanco con $ 1 $ 2 $ 3 ... , debe usar (con comillas): "$ @"
NVRAM
1

Un año después, aquí hay un antiguo script de bash para registrar cualquier cosa. Por ejemplo,
teelog make ...registros a un nombre de registro generado (y vea el truco para registrar correos electrónicos anidados maketambién).

#!/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
fuente
Sé que es demasiado tarde para agregar un comentario, pero solo tenía que decir gracias por este guión. ¡Muy útil y bien documentado!
stephenmm
Gracias @stephenmm; es que nunca es demasiado tarde para decir "útil" o "podría mejorarse".
denis