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

235

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 le déclarer dans le script lui-même

j'ai essayé

exec | tee foo.log

mais ça n'a pas marché.

Vitaly Kushner
la source
3
Votre question est mal formulée. Lorsque vous appelez 'exec> foo.log', la sortie standard du script est le fichier foo.log. Je pense que vous voulez dire que vous voulez que la sortie pour aller à foo.log et au téléscripteur, puisque va foo.log est va stdout.
William Pursell
ce que j'aimerais faire, c'est utiliser le | sur le «exec». ce serait parfait pour moi, c'est-à-dire "exec | tee foo.log", malheureusement, vous ne pouvez pas utiliser la redirection de tuyau sur l'appel exec
Vitaly Kushner

Réponses:

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

Notez que ce n'est bashpas le cas sh. Si vous appelez le script avec sh myscript.sh, vous obtiendrez une erreur dans le sens de syntax error near unexpected token '>'.

Si vous travaillez avec des interruptions de signal, vous souhaiterez peut-être utiliser l' tee -ioption pour éviter de perturber la sortie si un signal se produit. (Merci à JamesThomasMoon1979 pour le commentaire.)


Les outils qui modifient leur sortie selon qu'ils écrivent dans un tuyau ou un terminal (en lsutilisant des couleurs et une sortie en colonnes, par exemple) détecteront la construction ci-dessus comme signifiant qu'ils sortent vers un tuyau.

Il existe des options pour appliquer la colorisation / la mise en colonnes (par exemple ls -C --color=always). Notez que cela entraînera également l'écriture des codes couleur dans le fichier journal, ce qui le rendra moins lisible.

DevSolar
la source
5
Tee sur la plupart des systèmes est tamponné, donc la sortie peut ne pas arriver avant la fin du script. De plus, étant donné que ce tee s'exécute dans un sous-shell, et non dans un processus enfant, l'attente ne peut pas être utilisée pour synchroniser la sortie avec le processus appelant. Ce que vous voulez, c'est une version non tamponnée de tee similaire à bogomips.org/rainbows.git/commit/…
14
@Barry: POSIX spécifie que teene doit pas mettre en tampon sa sortie. S'il fait un tampon sur la plupart des systèmes, il est cassé sur la plupart des systèmes. C'est un problème d' teeimplémentations, pas de ma solution.
DevSolar
3
@Sebastian: execest très puissant, mais aussi très impliqué. Vous pouvez "sauvegarder" la sortie standard actuelle vers un autre descripteur de fichier, puis la récupérer plus tard. Google "bash exec tutorial", il y a beaucoup de choses avancées là-bas.
DevSolar
2
@AdamSpiers: Je ne sais pas non plus de quoi Barry parlait. Bash's execest documenté pour ne pas démarrer de nouveaux processus, >(tee ...)est une substitution standard nommée pipe / process, et &la redirection n'a bien sûr rien à voir avec l'arrière-plan ...? :-)
DevSolar
11
Je suggère de passer -ià tee. Sinon, les interruptions de signal (interruptions) perturberont la sortie standard du script principal. Par exemple, si vous en avez un trap 'echo foo' EXITpuis appuyez sur ctrl+c, vous ne verrez pas " foo ". Je modifierais donc la réponse à exec &> >(tee -ia file).
JamesThomasMoon1979
174

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

./script.sh >/dev/null

ne sortira pas barsur le terminal, uniquement sur le fichier journal, et

./script.sh 2>/dev/null

affichera à la fois fooet barvers le terminal. De toute évidence, ce n'est pas le comportement auquel un utilisateur normal est susceptible de s'attendre. Cela peut être résolu en utilisant deux processus de départ 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 voulez ce comportement, vous devez ajouter

>foo.log

en haut du script.)

La spécification POSIX.1-2008 detee(1) requiert que la sortie ne soit pas mise en mémoire tampon, c'est-à-dire même pas mise en mémoire tampon de ligne, donc dans ce cas, il est possible que STDOUT et STDERR puissent se retrouver sur la même ligne de foo.log; cependant, cela pourrait également se produire sur le terminal, de sorte que le fichier journal sera un reflet fidèle de ce qui pourrait être vu sur le terminal, sinon un miroir exact de celui-ci. Si vous souhaitez que les lignes STDOUT soient clairement séparées des lignes STDERR, envisagez d'utiliser deux fichiers journaux, éventuellement avec des préfixes de date sur chaque ligne pour permettre un réassemblage chronologique plus tard.

Adam Spires
la source
Pour une raison quelconque, dans mon cas, lorsque le script est exécuté à partir d'un appel système c) (), les deux sous-processus tee continuent d'exister même après la sortie du script principal. J'ai donc dû ajouter des pièges comme celui-ci: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
Je suggère de passer -ià tee. Sinon, les interruptions de signal (interruptions) perturberont la sortie standard du script. Par exemple, si vous trap 'echo foo' EXITappuyez sur ctrl+c, vous ne verrez pas " foo ". Je modifierais donc la réponse à exec > >(tee -ia foo.log).
JamesThomasMoon1979
J'ai fait quelques petits scripts "sourceable" sur cette base. Peut les utiliser dans un script comme . logou . log foo.log: sam.nipl.net/sh/log sam.nipl.net/sh/log-a
Sam Watkins
1
Le problème avec cette méthode est que les messages vont STDOUTapparaître en premier comme un lot, puis les messages vont STDERRapparaître. Ils ne sont pas entrelacés comme prévu.
CMCDragonkai
28

Solution pour les boîtiers busybox, 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 exec > >(tee log.txt)syntaxe. Il ne fonctionne pas non plus exec >$PIPEcorrectement, essayant de créer un fichier ordinaire avec le même nom que le canal nommé, qui échoue et se bloque.

J'espère que cela serait utile à quelqu'un d'autre qui n'a pas bash.

En outre, pour toute personne utilisant un canal nommé, cela est sûr rm $PIPE, car cela dissocie le canal du VFS, mais les processus qui l'utilisent conservent toujours un compte de 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
jbarlow
la source
C'est la seule solution que j'ai vue jusqu'à présent qui fonctionne sur mac
Mike Baglio Jr.
19

Dans votre fichier de script, mettez toutes les commandes entre parenthèses, comme ceci:

(
echo start
ls -l
echo end
) | tee foo.log
WReach
la source
5
pédantiquement, pourrait également utiliser des accolades ( {})
glenn jackman
eh bien oui, j'ai considéré cela, mais ce n'est pas la redirection de la sortie standard du shell, son genre de triche, vous exécutez en fait un sous-shell et faites une redirection de piper régulière dessus. travaille la pensée. Je suis divisé avec cela et la solution "tail -f foo.log &". attendra un peu pour voir si une meilleure surface. sinon va probablement se régler;)
Vitaly Kushner
8
{} exécute une liste dans l'environnement shell actuel. () exécute une liste dans un environnement de sous-shell.
Zut. Je vous remercie. La réponse acceptée là-haut n'a pas fonctionné pour moi, essayant de planifier un script à exécuter sous MingW sur un système Windows. Il se plaignait, je crois, d'une substitution de processus non mise en œuvre. Cette réponse a très bien fonctionné, après le changement suivant, pour capturer à la fois stderr et stdout: `` `-) | tee foo.log +) 2> & 1 | tee foo.log
Jon Carter
14

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

Ajoutez cette ligne en haut:

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

Vous pouvez également envoyer le journal dans un fichier distinct:

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

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

Tobu
la source
10

En utilisant la réponse acceptée, mon script revenait exceptionnellement tôt (juste après 'exec>> (tee ...)'), laissant le reste de mon script en arrière-plan. Comme je n'ai pas réussi à faire fonctionner cette solution à ma façon, j'ai trouvé une autre solution / 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 fait que la sortie du script passe du processus, par le canal, au sous-processus en arrière-plan de «tee» qui enregistre tout sur le disque et sur la sortie standard du script.

Notez que 'exec &>' redirige à la fois stdout et stderr, nous pouvons les rediriger séparément si nous le souhaitons, ou changer en 'exec>' si nous voulons juste stdout.

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

fgunger
la source
Réponse similaire à la seconde idée de David Z . Jetez un œil à ses commentaires. +1 ;-)
olibre
Fonctionne bien. Je ne comprends pas la $logfilepartie de tee < ${logfile}.pipe $logfile &. Plus précisément, j'ai essayé de modifier cela pour capturer les lignes de journal de commande développées complètes (de set -x) dans un fichier tout en affichant uniquement les lignes sans diriger «+» dans stdout en modifiant (tee | grep -v '^+.*$') < ${logfile}.pipe $logfile &mais en recevant un message d'erreur concernant $logfile. Pouvez-vous expliquer la teeligne un peu plus en détail?
Chris Johnson
J'ai testé cela et il semble que cette réponse ne préserve pas STDERR (elle est fusionnée avec STDOUT), donc si vous comptez sur les flux séparés pour la détection d'erreur ou une autre redirection, vous devriez regarder la réponse d'Adam.
HeroCC
2

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

En pause jusqu'à nouvel ordre.
la source
1

Je ne peux pas dire que je suis à l'aise avec l'une des solutions basées sur exec. Je préfère utiliser directement tee, donc je fais appeler le script avec tee lorsque demandé:

# 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 faire de tee = false la valeur par défaut à la place, faire en sorte que TEE conserve le fichier journal, etc. Je suppose que cette solution est similaire à celle de jbarlow, mais plus simple, peut-être que la mienne a des limites que je n'ai pas encore rencontrées.

Oliver
la source
-1

Aucun de ces deux n'est une solution parfaite, mais voici quelques choses que vous pouvez essayer:

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

ou

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

Le second laisserait un fichier pipe assis en cas de problème avec votre script, ce qui pourrait ou non être un problème (c'est-à-dire que vous pourriez le rmfaire dans le shell parent par la suite).

David Z
la source
1
tail laissera un processus en cours dans le 2ème script, le té bloquera, ou vous devrez l'exécuter avec & dans ce cas, il laissera le processus comme dans le 1er.
Vitaly Kushner
@Vitaly: oups, j'ai oublié le fond tee- j'ai édité. Comme je l'ai dit, ni l'un ni l'autre n'est une solution parfaite, mais les processus d'arrière-plan seront tués lorsque leur shell parent se terminera, vous n'avez donc pas à vous soucier qu'ils monopolisent les ressources pour toujours.
David Z
1
Yikes: ceux-ci semblent attrayants, mais la sortie de tail -f va également à foo.log. Vous pouvez résoudre ce problème en exécutant tail -f avant l'exec, mais la queue est toujours exécutée après la fin du parent. Vous devez le tuer explicitement, probablement dans un piège 0.
William Pursell
Ouais. Si le script est en arrière-plan, il laisse des processus partout.