redirigir COPIA de stdout al archivo de registro desde el script bash mismo

235

Sé cómo redirigir stdout a un archivo:

exec > foo.log
echo test

esto colocará la 'prueba' en el archivo foo.log.

Ahora quiero redirigir el resultado al archivo de registro Y mantenerlo en stdout

es decir, se puede hacer trivialmente desde fuera del script:

script | tee foo.log

pero quiero declararlo dentro del script mismo

Lo intenté

exec | tee foo.log

Pero no funcionó.

Vitaly Kushner
fuente
3
Tu pregunta está mal formulada. Cuando invocas 'exec> foo.log', la salida estándar del script es el archivo foo.log. Creo que significa que desea que la salida vaya a foo.log y al TTY, ya que va a foo.log se va a la salida estándar.
William Pursell
lo que me gustaría hacer es usar el | en el 'ejecutivo'. eso sería perfecto para mí, es decir, "exec | tee foo.log", desafortunadamente no puede usar la redirección de canalizaciones en la llamada ejecutiva
Vitaly Kushner

Respuestas:

297
#!/usr/bin/env bash

# Redirect stdout ( > ) into a named pipe ( >() ) running "tee"
exec > >(tee -i logfile.txt)

# Without this, only stdout would be captured - i.e. your
# log file would not contain any error messages.
# SEE (and upvote) the answer by Adam Spiers, which keeps STDERR
# as a separate stream - I did not want to steal from him by simply
# adding his answer to mine.
exec 2>&1

echo "foo"
echo "bar" >&2

Tenga en cuenta que esto es bash, no sh. Si invoca el script con sh myscript.sh, obtendrá un error en la línea de syntax error near unexpected token '>'.

Si está trabajando con trampas de señal, es posible que desee utilizar la tee -iopción para evitar la interrupción de la salida si se produce una señal. (Gracias a JamesThomasMoon1979 por el comentario).


Herramientas que cambian su salida dependiendo de si escriben en una tubería o en un terminal (ls usando colores y salida en columnas, por ejemplo) detectarán la construcción anterior como que significa que salen a una tubería.

Hay opciones para aplicar la coloración / columna (p ls -C --color=always. Ej .). Tenga en cuenta que esto dará como resultado que los códigos de color se escriban también en el archivo de registro, lo que lo hará menos legible.

DevSolar
fuente
55
Tee en la mayoría de los sistemas está almacenado en búfer, por lo que la salida puede no llegar hasta que el script haya finalizado. Además, dado que este tee se ejecuta en un subshell, no en un proceso secundario, la espera no se puede usar para sincronizar la salida con el proceso de llamada. Lo que quieres es una versión sin amortiguación
14
@Barry: POSIX especifica que teeno se debe almacenar en búfer su salida. Si se amortigua en la mayoría de los sistemas, se rompe en la mayoría de los sistemas. Ese es un problema de las teeimplementaciones, no de mi solución.
DevSolar
3
@Sebastian: execes muy poderoso, pero también muy involucrado. Puede "hacer una copia de seguridad" de la salida estándar actual en un descriptor de archivo diferente y luego recuperarla más adelante. Google "tutorial de bash exec", hay muchas cosas avanzadas por ahí.
DevSolar
2
@ AdamSpiers: Tampoco estoy seguro de qué trataba Barry. Bash's execestá documentado para no iniciar nuevos procesos, >(tee ...)es una sustitución estándar de tubería / proceso, y &en la redirección, por supuesto, no tiene nada que ver con el fondo ...? :-)
DevSolar
11
Sugiero pasar -ia tee. De lo contrario, las interrupciones de señal (trampas) interrumpirán stdout en el script principal. Por ejemplo, si tiene un trap 'echo foo' EXITy luego presiona ctrl+c, no verá " foo ". Entonces modificaría la respuesta a exec &> >(tee -ia file).
JamesThomasMoon1979
174

La respuesta aceptada no conserva STDERR como un descriptor de archivo separado. Eso significa

./script.sh >/dev/null

no saldrá bara la terminal, solo al archivo de registro, y

./script.sh 2>/dev/null

dará salida a ambos fooy baral terminal. Claramente, ese no es el comportamiento que un usuario normal probablemente espera. Esto se puede solucionar mediante el uso de dos procesos tee separados, ambos anexados al mismo archivo de registro:

#!/bin/bash

# See (and upvote) the comment by JamesThomasMoon1979 
# explaining the use of the -i option to tee.
exec >  >(tee -ia foo.log)
exec 2> >(tee -ia foo.log >&2)

echo "foo"
echo "bar" >&2

(Tenga en cuenta que lo anterior no trunca inicialmente el archivo de registro; si desea ese comportamiento, debe agregar

>foo.log

al principio del guión)

La especificación POSIX.1-2008 detee(1) requiere que la salida no esté almacenada, es decir, ni siquiera esté almacenada en línea, por lo que en este caso es posible que STDOUT y STDERR puedan terminar en la misma línea de foo.log; sin embargo, eso también podría suceder en el terminal, por lo que el archivo de registro será un fiel reflejo de lo que se puede ver en el terminal, si no un espejo exacto del mismo. Si desea que las líneas STDOUT se separen limpiamente de las líneas STDERR, considere usar dos archivos de registro, posiblemente con prefijos de sello de fecha en cada línea para permitir el reensamblaje cronológico más adelante.

Adam Spires
fuente
Por alguna razón, en mi caso, cuando el script se ejecuta desde una llamada del sistema c-program (), los dos subprocesos tee continúan existiendo incluso después de que el script principal sale. Así que tuve que agregar trampas como esta:exec > >(tee -a $LOG) trap "kill -9 $! 2>/dev/null" EXIT exec 2> >(tee -a $LOG >&2) trap "kill -9 $! 2>/dev/null" EXIT
alveko
15
Sugiero pasar -ia tee. De lo contrario, las interrupciones de señal (trampas) interrumpirán stdout en el script. Por ejemplo, si usted trap 'echo foo' EXITy luego presiona ctrl+c, no verá " foo ". Entonces modificaría la respuesta a exec > >(tee -ia foo.log).
JamesThomasMoon1979
Hice algunos pequeños guiones "fuentebles" basados ​​en esto. Puede usarlos en un script como . logo . log foo.log: sam.nipl.net/sh/log sam.nipl.net/sh/log-a
Sam Watkins
1
El problema con este método es que los mensajes STDOUTaparecerán primero como un lote y luego STDERRaparecerán los mensajes . No están intercalados como se espera habitualmente.
CMCDragonkai
28

Solución para busybox, macOS bash y shells que no son bash

La respuesta aceptada es sin duda la mejor opción para bash. Estoy trabajando en un entorno de Busybox sin acceso a bash, y no entiende la exec > >(tee log.txt)sintaxis. Tampoco lo haceexec >$PIPE correctamente, al intentar crear un archivo ordinario con el mismo nombre que la canalización con nombre, que falla y se bloquea.

Esperemos que esto sea útil para alguien más que no tenga bash.

Además, para cualquiera que use una tubería con nombre, es seguro rm $PIPE , porque eso desvincula la tubería del VFS, pero los procesos que la usan aún mantienen un recuento de referencia en ella hasta que finalizan.

Tenga en cuenta que el uso de $ * no es necesariamente seguro.

#!/bin/sh

if [ "$SELF_LOGGING" != "1" ]
then
    # The parent process will enter this branch and set up logging

    # Create a named piped for logging the child's output
    PIPE=tmp.fifo
    mkfifo $PIPE

    # Launch the child process with stdout redirected to the named pipe
    SELF_LOGGING=1 sh $0 $* >$PIPE &

    # Save PID of child process
    PID=$!

    # Launch tee in a separate process
    tee logfile <$PIPE &

    # Unlink $PIPE because the parent process no longer needs it
    rm $PIPE    

    # Wait for child process, which is running the rest of this script
    wait $PID

    # Return the error code from the child process
    exit $?
fi

# The rest of the script goes here
jbarlow
fuente
Esta es la única solución que he visto hasta ahora que funciona en Mac
Mike Baglio Jr. el
19

Dentro de su archivo de script, coloque todos los comandos entre paréntesis, de esta manera:

(
echo start
ls -l
echo end
) | tee foo.log
WReach
fuente
55
pedantemente, también podría utilizar llaves ( {})
Jackman Glenn
bueno, sí, lo consideré, pero esto no es una redirección del stdout de shell actual, es una especie de trampa, en realidad estás ejecutando una subshell y haciendo una redirección de piper regular en ella. funciona el pensamiento Estoy dividido con esto y la solución "tail -f foo.log &". esperará un poco para ver si puede haber una mejor superficie. si no es probable que se vaya a resolver;)
Vitaly Kushner
8
{} ejecuta una lista en el entorno actual del shell. () ejecuta una lista en un entorno de subshell.
Maldición. Gracias. La respuesta aceptada allí no funcionó para mí, tratando de programar un script para que se ejecute bajo MingW en un sistema Windows. Creo que se quejó de la sustitución no implementada del proceso. Esta respuesta funcionó bien, después del siguiente cambio, para capturar tanto stderr como stdout: `` `-) | tee foo.log +) 2> & 1 | tee foo.log
Jon Carter
14

Manera fácil de hacer un registro de script bash en syslog. La salida del script está disponible tanto a través /var/log/syslogcomo a través de stderr. syslog agregará metadatos útiles, incluidas las marcas de tiempo.

Agregue esta línea en la parte superior:

exec &> >(logger -t myscript -s)

Alternativamente, envíe el registro a un archivo separado:

exec &> >(ts |tee -a /tmp/myscript.output >&2 )

Esto requiere moreutils(para el tscomando, que agrega marcas de tiempo).

Tobu
fuente
10

Usando la respuesta aceptada, mi script siguió regresando excepcionalmente temprano (justo después de 'exec>> (tee ...)') dejando el resto de mi script ejecutándose en segundo plano. Como no pude lograr que esa solución funcionara a mi manera, encontré otra solución / solución al problema:

# Logging setup
logfile=mylogfile
mkfifo ${logfile}.pipe
tee < ${logfile}.pipe $logfile &
exec &> ${logfile}.pipe
rm ${logfile}.pipe

# Rest of my script

Esto hace que la salida del script pase del proceso, a través de la tubería hacia el proceso de fondo secundario de 'tee' que registra todo en el disco y en el stdout original del script.

Tenga en cuenta que 'exec &>' redirige tanto stdout como stderr, podríamos redirigirlos por separado si lo deseamos, o cambiar a 'exec>' si solo queremos stdout.

Incluso si la tubería se elimina del sistema de archivos al comienzo del script, continuará funcionando hasta que finalice el proceso. Simplemente no podemos hacer referencia a él usando el nombre del archivo después de la línea rm.

Fgunger
fuente
Respuesta similar a la segunda idea de David Z . Echa un vistazo a sus comentarios. +1 ;-)
olibre
Funciona bien. No entiendo la $logfileparte de tee < ${logfile}.pipe $logfile &. Específicamente, traté de alterar esto para capturar líneas de registro de comando expandidas completas (desde set -x) al archivo mientras solo mostraba líneas sin llevar '+' en stdout al cambiar (tee | grep -v '^+.*$') < ${logfile}.pipe $logfile &pero recibí un mensaje de error al respecto $logfile. ¿Puedes explicar la teelínea con un poco más de detalle?
Chris Johnson
Probé esto y parece que esta respuesta no conserva STDERR (se fusionó con STDOUT), por lo que si confía en que las secuencias están separadas para la detección de errores u otra redirección, debe mirar la respuesta de Adam.
HeroCC
2

Bash 4 tiene un coproccomando que establece una canalización con nombre a un comando y le permite comunicarse a través de él.

Pausado hasta nuevo aviso.
fuente
1

No puedo decir que me sienta cómodo con ninguna de las soluciones basadas en exec. Prefiero usar tee directamente, por lo que hago que el script se llame a sí mismo con tee cuando lo solicite:

# my script: 

check_tee_output()
{
    # copy (append) stdout and stderr to log file if TEE is unset or true
    if [[ -z $TEE || "$TEE" == true ]]; then 
        echo '-------------------------------------------' >> log.txt
        echo '***' $(date) $0 $@ >> log.txt
        TEE=false $0 $@ 2>&1 | tee --append log.txt
        exit $?
    fi 
}

check_tee_output $@

rest of my script

Esto le permite hacer esto:

your_script.sh args           # tee 
TEE=true your_script.sh args  # tee 
TEE=false your_script.sh args # don't tee
export TEE=false
your_script.sh args           # tee

Puede personalizar esto, por ejemplo, hacer que tee = false sea el valor predeterminado en su lugar, hacer que TEE retenga el archivo de registro, etc. Supongo que esta solución es similar a la de jbarlow, pero más simple, tal vez la mía tiene limitaciones que aún no he encontrado.

Oliver
fuente
-1

Ninguno de estos es una solución perfecta, pero aquí hay un par de cosas que puedes probar:

exec >foo.log
tail -f foo.log &
# rest of your script

o

PIPE=tmp.fifo
mkfifo $PIPE
exec >$PIPE
tee foo.log <$PIPE &
# rest of your script
rm $PIPE

El segundo dejaría un archivo de tubería alrededor si algo sale mal con su script, lo que puede o no ser un problema (es decir, tal vez podría rmhacerlo en el shell principal después).

David Z
fuente
1
tail dejará atrás un proceso en ejecución en el segundo script que bloqueará, o deberá ejecutarlo con & en cuyo caso dejará el proceso como en el primero.
Vitaly Kushner
@Vitaly: ¡Vaya! Olvidé el fondo tee. He editado. Como dije, ninguna de las dos es una solución perfecta, pero los procesos en segundo plano se anularán cuando finalice su shell principal, por lo que no tiene que preocuparse de que acaparen los recursos para siempre.
David Z
1
Yikes: estos se ven atractivos, pero la salida de tail -f también va a foo.log. Puede arreglar eso ejecutando tail -f antes del exec, pero la cola todavía se sigue ejecutando después de que el padre termina. Necesitas matarlo explícitamente, probablemente en una trampa 0.
William Pursell
Yeap Si el script está en segundo plano, deja procesos por todas partes.