web-dev-qa-db-fra.com

rediriger la copie de stdout vers le fichier journal à partir du script bash lui-même

Je sais comment rediriger stdout vers un fichier:

exec > foo.log
echo test

cela mettra le "test" dans le fichier foo.log.

Maintenant, je veux rediriger la sortie dans le fichier journal ET le garder sur stdout

c'est-à-dire que cela peut être fait trivialement en dehors du script:

script | tee foo.log

mais je veux faire le déclarer dans le script lui-même

J'ai essayé

exec | tee foo.log

mais ça n'a pas marché.

221
Vitaly Kushner
#!/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

Notez que ceci est bash, pas sh. Si vous appelez le script avec sh myscript.sh, vous obtiendrez une erreur du type syntax error near unexpected token '>'.

Si vous travaillez avec des interruptions de signal, vous pouvez utiliser l'option tee -i pour éviter toute perturbation de la sortie si un signal se produisait. (Merci à JamesThomasMoon1979 pour le commentaire.)


Les outils qui modifient leur sortie en fonction de leur écriture dans un canal ou un terminal (ls à l'aide de couleurs et d'une sortie en colonnes, par exemple) détecteront la construction ci-dessus comme signifiant qu'ils s'affichent dans un canal.

Il existe des options pour appliquer la coloration/mise en colonnes (par exemple ls -C --color=always). Notez que les codes de couleur seront également écrits dans le fichier journal, ce qui le rendra moins lisible.

281
DevSolar

La réponse acceptée ne conserve pas STDERR en tant que descripteur de fichier séparé. Cela signifie

./script.sh >/dev/null

n’émettra pas bar sur le terminal, mais uniquement sur le fichier journal, et

./script.sh 2>/dev/null

émettra à la fois foo et bar sur le terminal. Clairement, ce n'est pas le comportement qu'un utilisateur normal est susceptible de s'attendre. Ce problème peut être résolu en utilisant deux processus tee distincts, tous deux ajoutés au même fichier journal:

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

(Notez que ce qui précède ne tronque pas initialement le fichier journal. Si vous souhaitez ce comportement, vous devez ajouter

>foo.log

en haut du script.)

La spécification POSIX.1-2008 de tee(1) nécessite que la sortie ne soit pas tamponnée, c'est-à-dire qu'elle ne soit même pas mise en mémoire tampon, il est donc possible que STDOUT et STDERR se retrouvent sur le même ligne de foo.log; Toutefois, cela pourrait également se produire sur le terminal. Le fichier journal reflétera fidèlement ce que pourrait afficher sur le terminal, si ce n’est un miroir exact. de cela. Si vous souhaitez que les lignes STDOUT soient bien séparées des lignes STDERR, envisagez d'utiliser deux fichiers journaux, avec éventuellement des préfixes d'horodatage sur chaque ligne pour permettre un réassemblage chronologique ultérieurement.

163
Adam Spiers

Solution pour busybox, shells macOS bash et non bash

La réponse acceptée est certainement le meilleur choix pour bash. Je travaille dans un environnement Busybox sans accès à bash et il ne comprend pas la syntaxe exec > >(tee log.txt). Il ne fait pas non plus correctement exec >$PIPE, essayant de créer un fichier ordinaire portant le même nom que le canal nommé, qui échoue et se bloque.

Espérons que cela serait utile à quelqu'un d'autre qui n'a pas de bash.

De même, pour toute personne utilisant un canal nommé, rm $PIPE est sécurisé, car cela dissocie le canal du système de fichiers VFS, mais les processus qui l'utilisent conservent toujours une référence jusqu'à ce qu'ils soient terminés.

Notez que l'utilisation de $ * n'est pas nécessairement sûre.

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

Dans votre fichier de script, placez toutes les commandes entre parenthèses, comme suit:

(
echo start
ls -l
echo end
) | tee foo.log
18
WReach

Un moyen facile de créer un journal de script bash dans syslog. La sortie du script est disponible à la fois via /var/log/syslog et via stderr. syslog ajoutera des métadonnées utiles, y compris des horodatages.

Ajoutez cette ligne en haut:

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

Vous pouvez également envoyer le journal dans un fichier séparé:

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

Cela nécessite moreutils (pour la commande ts, qui ajoute des horodatages).

13
Tobu

En utilisant la réponse acceptée, mon script renvoyait exceptionnellement tôt (juste après 'exec>> (tee ...)'), laissant le reste de mon script s'exécuter en arrière-plan. N'ayant pas réussi à obtenir cette solution, j'ai trouvé une autre solution au problème:

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

# Rest of my script

Cela rend la sortie du script du processus, en passant par le canal, dans le sous-processus en arrière-plan de 'tee' qui enregistre tout sur un disque et sur la sortie standard du script.

Notez que 'exec &>' redirige à la fois stdout et stderr, nous pourrions les rediriger séparément si nous le souhaitons, ou passer à 'exec>' si nous voulons simplement stdout.

Même si le tuyau est supprimé du système de fichiers au début du script, il continuera à fonctionner jusqu'à la fin du processus. Nous ne pouvons simplement pas le référencer en utilisant le nom du fichier après la ligne de commande.

9
fgunger

Bash 4 a une commande coproc qui établit un canal nommé vers une commande et vous permet de communiquer à travers elle.

1
Dennis Williamson

Je ne peux pas dire que je suis à l'aise avec l'une des solutions basées sur exec. Je préfère utiliser le tee directement, alors je demande au script de s'appeler lui-même avec le tee:

# 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

Cela vous permet de faire ceci:

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

Vous pouvez personnaliser cela, par exemple. make tee = false la valeur par défaut à la place, demandez à TEE de conserver le fichier journal à la place, etc. Je suppose que cette solution est similaire à celle de jbarlow, mais qu'elle est plus simple, peut-être que la mienne a des limitations que je n'ai pas encore rencontrées.

0
Oliver