Comment obtenir à la fois STDOUT et STDERR pour accéder au terminal et à un fichier journal?

104

J'ai un script qui sera exécuté de manière interactive par des utilisateurs non techniques. Le script écrit les mises à jour d'état dans STDOUT afin que l'utilisateur puisse être sûr que le script s'exécute correctement.

Je veux que STDOUT et STDERR soient redirigés vers le terminal (afin que l'utilisateur puisse voir que le script fonctionne ainsi que voir s'il y avait un problème). Je veux également que les deux flux soient redirigés vers un fichier journal.

J'ai vu un tas de solutions sur le net. Certains ne fonctionnent pas et d'autres sont horriblement compliqués. J'ai développé une solution viable (que je vais entrer comme réponse), mais c'est kludgy.

La solution parfaite serait une seule ligne de code qui pourrait être incorporée au début de tout script qui envoie les deux flux à la fois au terminal et à un fichier journal.

EDIT: Rediriger STDERR vers STDOUT et envoyer le résultat vers le tee fonctionne, mais cela dépend du fait que les utilisateurs se souviennent de rediriger et de diriger la sortie. Je veux que la journalisation soit infaillible et automatique (c'est pourquoi j'aimerais pouvoir intégrer la solution dans le script lui-même.)

JPLemme
la source
Pour les autres lecteurs: question similaire: stackoverflow.com/questions/692000/…
pevik
1
Je suis ennuyé que tout le monde (y compris moi!) Sauf @JasonSydes ait déraillé et ait répondu à une question différente. Et la réponse de Jason n'est pas fiable, comme je l'ai commenté. J'aimerais voir une vraie réponse fiable à la question que vous avez posée (et soulignée dans votre EDIT).
Don Hatch
Oh attendez, je le reprends. La réponse acceptée de @PaulTromblin y répond. Je n'ai pas lu assez loin.
Don Hatch

Réponses:

167

Utilisez "tee" pour rediriger vers un fichier et l'écran. Selon le shell que vous utilisez, vous devez d'abord rediriger stderr vers stdout en utilisant

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

ou

./a.out |& tee output

Dans csh, il existe une commande intégrée appelée "script" qui capturera tout ce qui va à l'écran dans un fichier. Vous le démarrez en tapant "script", puis en faisant ce que vous voulez capturer, puis appuyez sur control-D pour fermer le fichier de script. Je ne connais pas d'équivalent pour sh / bash / ksh.

De plus, puisque vous avez indiqué qu'il s'agit de vos propres scripts sh que vous pouvez modifier, vous pouvez effectuer la redirection en interne en entourant tout le script d'accolades ou de crochets, comme

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file
Paul Tomblin
la source
4
Je ne savais pas que vous pouviez mettre entre parenthèses les commandes dans les scripts shell. Intéressant.
Jamie
1
J'apprécie également le raccourci Bracket! Pour une raison quelconque, 2>&1 | tee -a filenameje n'ai pas enregistré stderr dans le fichier à partir de mon script, mais cela a bien fonctionné lorsque j'ai copié la commande et l'ai collée dans le terminal! L'astuce du support fonctionne bien, cependant.
Ed Brannin
8
Notez que la distinction entre stdout et stderr sera perdue, car tee imprime tout sur stdout.
Flimm
2
FYI: La commande 'script' est disponible dans la plupart des distributions (elle fait partie du paquet util-linux)
SamWN
2
@Flimm, y a-t-il un moyen (d'une autre manière) de conserver la distinction entre stdout et stderr?
Gabriel
20

Près d'une demi-décennie plus tard ...

Je pense que c'est la «solution parfaite» recherchée par le PO.

Voici une seule ligne que vous pouvez ajouter en haut de votre script Bash:

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

Voici un petit script démontrant son utilisation:

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

(Remarque: cela ne fonctionne qu'avec Bash. Cela ne fonctionnera pas avec / bin / sh.)

Adapté d' ici ; l'original n'a pas, d'après ce que je peux dire, attraper STDERR dans le fichier journal. Corrigé avec une note d' ici .

Jason Sydes
la source
3
Notez que la distinction entre stdout et stderr sera perdue, car tee imprime tout sur stdout.
Flimm
@Flimm stderr pourrait être redirigé vers un processus de tee différent qui pourrait à nouveau être redirigé vers stderr.
jarno
@Flimm, j'ai écrit la suggestion de jarno ici: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService
1
Cette solution, comme la plupart des autres solutions proposées jusqu'à présent, est sujette à la race. C'est-à-dire que lorsque le script actuel se termine et retourne, soit à l'invite de l'utilisateur, soit à un script d'appel de niveau supérieur, le tee, qui s'exécute en arrière-plan, sera toujours en cours d'exécution et peut émettre les dernières lignes à l'écran et à le fichier journal en retard (c'est-à-dire à l'écran après l'invite et au fichier journal une fois que le fichier journal est censé être terminé).
Don Hatch
1
Cependant, c'est la seule réponse proposée jusqu'à présent qui répond réellement à la question!
Don Hatch
9

Le motif

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

Cela redirige à la fois stdout et stderr séparément, et il envoie des copies séparées de stdout et stderr à l'appelant (qui pourrait être votre terminal).

  • Dans zsh, il ne passera pas à l'instruction suivante tant que les tees ne seront pas terminés.

  • Dans bash, vous constaterez peut-être que les dernières lignes de sortie apparaissent après la déclaration suivante.

Dans les deux cas, les bons bits vont aux bons endroits.


Explication

Voici un script (stocké dans ./example):

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

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

Voici une session:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Voici comment ça fonctionne:

  1. Les deux teeprocessus sont lancés, leurs stdins sont affectés à des descripteurs de fichiers. Comme ils sont inclus dans des substitutions de processus , les chemins vers ces descripteurs de fichiers sont substitués dans la commande appelante, donc maintenant cela ressemble à ceci:

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

  1. the_cmd s'exécute, écrivant stdout dans le premier descripteur de fichier et stderr dans le second.

  2. Dans le cas bash, une fois the_cmdterminé, l'instruction suivante se produit immédiatement (si votre terminal est l'appelant, alors vous verrez votre invite apparaître).

  3. Dans le cas de zsh, une fois the_cmdterminé, le shell attend la fin des deux teeprocessus avant de continuer. Plus d'informations ici .

  4. Le premier teeprocessus, qui lit depuis the_cmdle stdout de, écrit une copie de ce stdout vers l'appelant parce que c'est ce que teefait. Ses sorties ne sont pas redirigées, donc elles le ramènent à l'appelant inchangé

  5. Le deuxième teeprocessus est stdoutredirigé vers l'appelant stderr(ce qui est bien, car c'est stdin qui lit depuis the_cmdle stderr de). Ainsi, quand il écrit dans sa sortie stdout, ces bits vont dans le stderr de l'appelant.

Cela permet de séparer stderr de stdout à la fois dans les fichiers et dans la sortie de la commande.

Si le premier tee écrit des erreurs, elles apparaîtront à la fois dans le fichier stderr et dans le stderr de la commande, si le second tee écrit des erreurs, elles n'apparaîtront que dans le stderr du terminal.

MatrixManAtYrService
la source
Cela semble vraiment utile et ce que je veux. Je ne suis pas sûr de savoir comment reproduire l'utilisation de crochets (comme indiqué dans la première ligne) dans un script Windows Batch, cependant. ( teeest disponible sur le système en question.) L'erreur que j'obtiens est "Le processus ne peut pas accéder au fichier car il est utilisé par un autre processus."
Agi Hammerthief
Cette solution, comme la plupart des autres solutions proposées jusqu'à présent, est sujette à la race. C'est-à-dire que lorsque le script actuel se termine et retourne, soit à l'invite de l'utilisateur, soit à un script d'appel de niveau supérieur, le tee, qui s'exécute en arrière-plan, sera toujours en cours d'exécution et peut émettre les dernières lignes à l'écran et à le fichier journal en retard (c'est-à-dire à l'écran après l'invite et au fichier journal une fois que le fichier journal est censé être terminé).
Don Hatch
2
@DonHatch Pouvez-vous proposer une solution qui corrige ce problème?
pylipp
Je serais également intéressé par un cas de test qui rend la course évidente. Ce n'est pas que je doute, mais il est difficile d'essayer de l'éviter parce que je ne l'ai pas vu arriver.
MatrixManAtYrService
@pylipp Je n'ai pas de solution. Je serais très intéressé par un.
Don Hatch
4

le pour rediriger stderr vers stdout, ajoutez ceci à votre commande: 2>&1 Pour la sortie vers le terminal et la connexion au fichier, vous devez utilisertee

Les deux ensemble ressembleraient à ceci:

 mycommand 2>&1 | tee mylogfile.log

EDIT: Pour l'intégration dans votre script, vous feriez de même. Donc votre script

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

finirait comme

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log
flolo
la source
2
Notez que la distinction entre stdout et stderr sera perdue, car tee imprime tout sur stdout.
Flimm
4

EDIT: Je vois que j'ai déraillé et j'ai fini par répondre à une question différente de celle posée. La réponse à la vraie question se trouve au bas de la réponse de Paul Tomblin. (Si vous souhaitez améliorer cette solution pour rediriger stdout et stderr séparément pour une raison quelconque, vous pouvez utiliser la technique que je décris ici.)


Je voulais une réponse qui préserve la distinction entre stdout et stderr. Malheureusement, toutes les réponses données jusqu'à présent qui préservent cette distinction sont sujettes à la race: elles risquent que les programmes voient des contributions incomplètes, comme je l'ai souligné dans mes commentaires.

Je pense que j'ai finalement trouvé une réponse qui préserve la distinction, n'est pas sujette à la race et n'est pas non plus terriblement délicate.

Premier bloc de construction: pour permuter stdout et stderr:

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

Deuxième bloc de construction: si nous voulions filtrer (par exemple tee) uniquement stderr, nous pourrions le faire en échangeant stdout et stderr, filtrer, puis inverser:

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

Maintenant, le reste est simple: nous pouvons ajouter un filtre stdout, soit au début:

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

ou à la fin:

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

Pour me convaincre que les deux commandes ci-dessus fonctionnent, j'ai utilisé ce qui suit:

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 sortie est:

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

et mon invite revient immédiatement après le " teed stderr: to stderr", comme prévu.

Note de bas de page sur zsh :

La solution ci-dessus fonctionne dans bash (et peut-être dans d'autres shells, je ne suis pas sûr), mais cela ne fonctionne pas dans zsh. Il y a deux raisons pour lesquelles il échoue dans zsh:

  1. la syntaxe 2>&3-n'est pas comprise par zsh; qui doit être réécrit comme2>&3 3>&-
  2. dans zsh (contrairement à d'autres shells), si vous redirigez un descripteur de fichier qui est déjà ouvert, dans certains cas (je ne comprends pas complètement comment il décide), il effectue à la place un comportement de type tee intégré. Pour éviter cela, vous devez fermer chaque fd avant de le rediriger.

Ainsi, par exemple, ma deuxième solution doit être réécrite pour zsh comme {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(qui fonctionne aussi dans bash, mais est terriblement verbeuse).

D'un autre côté, vous pouvez profiter du mystérieux départ implicite intégré de zsh pour obtenir une solution beaucoup plus courte pour zsh, qui ne fonctionne pas du tout:

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

(Je n'aurais pas deviné d'après les documents que j'ai trouvé que le >&1et 2>&2sont la chose qui déclenche le départ implicite de zsh; je l'ai découvert par essais et erreurs.)

Don Hatch
la source
J'ai joué avec ça dans bash et ça marche bien. Juste un avertissement pour les utilisateurs de zsh avec une habitude de compatibilité en supposant (comme moi), il se comporte différemment là - bas: gist.github.com/MatrixManAtYrService/...
MatrixManAtYrService
@MatrixManAtYrService Je crois avoir une idée de la situation zsh, et il s'avère qu'il existe une solution beaucoup plus soignée dans zsh. Voir ma modification "Note de bas de page sur zsh".
Don Hatch
Merci d'avoir expliqué la solution avec autant de détails. Savez-vous également comment récupérer le code de retour lors de l'utilisation d'une fonction ( my_function) dans le filtrage stdout / stderr imbriqué? Je l'ai fait { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filtermais ça fait bizarre de créer un fichier comme indicateur d'échec ...
pylipp
@pylipp je ne suis pas désinvolte. Vous pourriez poser cette question séparément (peut-être avec un pipeline plus simple).
Don Hatch le
2

Utilisez la scriptcommande dans votre script (script man 1)

Créez un shellscript wrapper (2 lignes) qui configure script (), puis appelle exit.

Partie 1: wrap.sh

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

Partie 2: realscript.sh

#!/bin/sh
echo 'Output'

Résultat:

~: 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
la source
1

Utilisez le programme tee et dup stderr pour stdout.

 program 2>&1 | tee > logfile
Tvanfosson
la source
1

J'ai créé un script appelé "RunScript.sh". Le contenu de ce script est:

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

Je l'appelle comme ça:

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

Cela fonctionne, mais cela nécessite que les scripts de l'application soient exécutés via un script externe. C'est un peu kludgy.

JPLemme
la source
9
Vous perdrez le groupement d'arguments contenant des espaces avec $ 1 $ 2 $ 3 ... , vous devriez utiliser (w / quotes): "$ @"
NVRAM
1

Un an plus tard, voici un ancien script bash pour enregistrer quoi que ce soit. Par exemple,
teelog make ...enregistre sous un nom de journal généré (et voyez aussi l'astuce pour journaliser les makes imbriqués .)

#!/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
la source
Je sais que c'est trop tard pour ajouter un commentaire mais je devais juste dire merci pour ce script. Très utile et bien documenté!
stephenmm
Merci @stephenmm; il n'est jamais trop tard pour dire «utile» ou «pourrait être amélioré».
denis