164 votes

Comment puis-je envoyer à la fois STDOUT et STDERR vers le terminal et un fichier journal ?

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

Je veux à la fois rediriger STDOUT et STDERR vers le terminal (pour que l'utilisateur puisse voir que le script fonctionne ainsi que voir s'il y a un problème). Je veux également rediriger les deux flux vers un fichier journal.

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

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

MODIFIER : Rediriger STDERR vers STDOUT et diriger le résultat vers tee fonctionne, mais cela dépend du fait que les utilisateurs se souviennent de rediriger et d'acheminer la sortie. Je veux que le journal soit idiot et automatique (c'est pourquoi j'aimerais être capable d'incorporer la solution dans le script lui-même.)

0 votes

Pour les autres lecteurs : question similaire : stackoverflow.com/questions/692000/…

244voto

Paul Tomblin Points 83687

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 y a 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 en appuyant sur control-D pour fermer le fichier script. Je ne connais pas d'équivalent pour sh/bash/ksh.

Aussi, puisque vous avez indiqué que ce sont vos propres scripts sh que vous pouvez modifier, vous pouvez faire la redirection internalement en entourant le script entier avec des accolades ou des crochets, comme

#!/bin/sh
{
    ... ce que vous aviez dans votre script auparavant
} 2>&1 | tee output.file

9 votes

Je ne savais pas que vous pouviez mettre des commandes entre crochets dans les scripts shell. Intéressant.

2 votes

Je trouve également pratique le raccourci de crochet! Pour une raison inconnue, 2>&1 | tee -a filename ne sauvait pas stderr dans le fichier à partir de mon script, mais ça fonctionnait bien lorsque j'ai copié la commande et que je l'ai collée dans le terminal! En revanche, l'astuce du crochet fonctionne bien.

16 votes

Notez que la distinction entre stdout et stderr sera perdue, car tee imprime tout sur stdout.

29voto

Jason Sydes Points 111

Approchant la moitié d'une décennie plus tard...

Je crois que c'est la "solution parfaite" recherchée par l'OP.

Voici une ligne que vous pouvez ajouter au début 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 de STDOUT
echo test_stdout

# Test redirection de STDERR
ls test_stderr___ce_fichier_n'existe_pas

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

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

7 votes

Notez que la distinction entre stdout et stderr sera perdue, car tee imprime tout sur stdout.

0 votes

@Flimm stderr pourrait être redirigé vers un autre processus tee qui, à son tour, pourrait être redirigé vers stderr.

0 votes

@Flimm, j'ai écrit la suggestion de jarno ici : stackoverflow.com/a/53051506/1054322

28voto

Le Modèle

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

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

  • Dans zsh, il n'avancera pas vers l'instruction suivante tant que les tee n'auront pas terminé.

  • Dans bash, vous pourriez remarquer que les dernières lignes de sortie apparaissent après n'importe quelle instruction suivante.

Dans tous les cas, les bons morceaux 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 processus tee sont lancés, leurs stdins sont assignés à des descripteurs de fichiers. Parce qu'ils sont inclus dans des substitutions de processus, les chemins vers ces descripteurs de fichiers sont substitués dans la commande appelante, donc maintenant ça ressemble à quelque chose comme ça:

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

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

  2. Dans le cas de bash, une fois que the_cmd a terminé, 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 que the_cmd a terminé, le shell attend que les deux processus tee aient terminé avant de passer à la suite. Plus d'informations à ce sujet ici.

  4. Le premier processus tee, qui lit depuis stdout de the_cmd, écrit une copie de ce stdout à l'appelant car c'est ce que fait tee. Ses sorties ne sont pas redirigées, donc elles retournent à l'appelant inchangées.

  5. Le second processus tee a son stdout redirigé vers le stderr de l'appelant (ce qui est bien, car son stdin lit depuis stderr de the_cmd). Donc lorsque ça écrit sur son stdout, ces bits vont au stderr de l'appelant.

Cela garde stderr séparé 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.

0 votes

Cela semble très utile et correspond à ce que je veux. Je ne suis pas sûr de comment reproduire l'utilisation de parenthèses (comme indiqué dans la première ligne) dans un script Batch Windows, cependant. (tee est disponible sur le système en question.) L'erreur que je reçois est "Le processus ne peut pas accéder au fichier car il est utilisé par un autre processus."

1 votes

Cette solution, comme la plupart des autres solutions proposées jusqu'à présent, est sujette aux courses. Autrement dit, lorsque le script actuel se termine et retourne, que ce soit à l'invite de l'utilisateur ou à un script d'appel de niveau supérieur, le tee, qui s'exécute en arrière-plan, continuera de tourner, et pourrait émettre les dernières lignes à l'écran et dans le fichier journal tardivement (c'est-à-dire à l'écran après l'invite, et dans le fichier journal après qu'on s'attendait à ce qu'il soit complet).

2 votes

@DonHatch Pouvez-vous proposer une solution qui corrige ce problème ?

7voto

flolo Points 8757

Pour rediriger stderr vers stdout, ajoutez ceci à votre commande : 2>&1 Pour afficher à l'écran et enregistrer dans un fichier, vous devriez utiliser tee

En les combinant, cela ressemblerait à ceci :

 mycommand 2>&1 | tee mylogfile.log

MODIFIER : Pour intégrer dans votre script, vous feriez la même chose. Ainsi, votre script

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

se terminerait comme suit

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log

3 votes

Notez que la distinction entre stdout et stderr sera perdue, car tee imprime tout sur stdout.

7voto

Don Hatch Points 458

EDIT: Je constate que je me suis égaré et j'ai fini par répondre à une question différente de celle posée. La réponse à la vraie question se trouve en 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 pourriez utiliser la technique que je décris ici.)


J'ai cherché 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 aux courses : elles risquent que les programmes voient une entrée incomplète, comme je l'ai souligné dans les commentaires.

Je pense avoir enfin trouvé une réponse qui préserve la distinction, qui ne provoque pas de courses, et qui n'est pas trop compliquée non plus.

Premier bloc de construction : pour échanger stdout et stderr :

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

Deuxième bloc de construction : si nous voulions filtrer (par exemple avec tee) seulement stderr, nous pourrions y parvenir en échangeant stdout&stderr, en filtrant, puis en échangeant à nouveau :

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

Maintenant, le reste est facile : 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-

soit à 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 "vers stdout"; echo "vers 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 :

...(pause d'1 seconde)...
teed stdout: vers stdout
...(autre pause de 1 seconde)...
teed stderr: vers stderr

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

Note à propos de zsh :

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

  1. la syntaxe 2>&3- n'est pas comprise par zsh ; elle doit être réécrite en 2>&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 réalise un comportement similaire à tee. 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 (ce qui fonctionne également dans bash, mais est assez verbeux).

En revanche, vous pouvez profiter de l'écriture mystérieuse intégrée de zsh pour obtenir une solution beaucoup plus courte pour zsh, qui n'exécute pas tee 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és, que >1 et 2>&2 sont ce qui déclenche le teeing implicite de zsh ; j'ai découvert cela par tâtonnement.)

0 votes

J'ai joué avec ça dans bash et ça fonctionne bien. Juste un avertissement pour les utilisateurs de zsh qui ont l'habitude de supposer la compatibilité (comme moi), cela se comporte différemment là-bas: gist.github.com/MatrixManAtYrService/…

0 votes

@MatrixManAtYrService Je crois avoir une bonne maîtrise de la situation zsh, et il s'avère qu'il existe une solution bien plus propre en zsh. Consultez ma modification "Note de bas de page sur zsh".

0 votes

Merci d'avoir expliqué la solution en détail. Savez-vous également comment récupérer le code de retour lors de l'utilisation d'une fonction (my_function) dans le filtrage imbriqué stdout/stderr? J'ai fait { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter mais cela semble étrange de créer un fichier comme indicateur d'échec...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X