Comment stocker l'erreur standard dans une variable


190

Disons que j'ai un script comme celui-ci:

inutile.sh

echo "This Is Error" 1>&2
echo "This Is Output" 

Et j'ai un autre script shell:

aussiUseless.sh

./useless.sh | sed 's/Output/Useless/'

Je veux capturer "This Is Error", ou tout autre stderr de useless.sh, dans une variable. Appelons cela ERREUR.

Notez que j'utilise stdout pour quelque chose. Je veux continuer à utiliser stdout, donc la redirection de stderr vers stdout n'est pas utile, dans ce cas.

Donc, en gros, je veux faire

./useless.sh 2> $ERROR | ...

mais cela ne fonctionne évidemment pas.

Je sais aussi que je pourrais faire

./useless.sh 2> /tmp/Error
ERROR=`cat /tmp/Error`

mais c'est moche et inutile.

Malheureusement, si aucune réponse n'apparaît ici, c'est ce que je vais devoir faire.

J'espère qu'il y a un autre moyen.

Quelqu'un a-t-il de meilleures idées?


4
Pour quoi exactement vouloir utiliser stdout? Voulez-vous simplement le voir sur la console? Ou êtes-vous en train de capturer / rediriger sa sortie? Si c'est juste vers la console, vous redirigez stdout vers console et stderr vers stdout pour le capturer:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Réponses:


93

Il serait plus simple de capturer le fichier d'erreur ainsi:

ERROR=$(</tmp/Error)

Le shell reconnaît cela et n'a pas besoin d'exécuter « cat» pour obtenir les données.

La plus grande question est difficile. Je ne pense pas qu'il y ait un moyen facile de le faire. Vous auriez à construire le pipeline entier dans le sous-shell, en envoyant finalement sa sortie standard finale à un fichier, afin que vous puissiez rediriger les erreurs vers la sortie standard.

ERROR=$( { ./useless.sh | sed s/Output/Useless/ > outfile; } 2>&1 )

Notez que le point-virgule est nécessaire (dans les shells classiques - Bourne, Korn - bien sûr; probablement dans Bash aussi). Le ' {}' effectue la redirection d'E / S sur les commandes incluses. Tel qu'il est écrit, il capturerait également les erreurs sed.

AVERTISSEMENT: code formellement non testé - utilisation à vos propres risques.


1
J'avais espéré qu'il y aurait un truc vraiment fou que je ne connaissais pas, mais on dirait que c'est ça. Merci.
psycotica0

9
Si vous n'avez pas besoin de la sortie standard, vous pouvez la rediriger vers /dev/nullau lieu de outfile(Si vous êtes comme moi, vous avez trouvé cette question via Google et n'avez pas les mêmes exigences que l'OP)
Mark Eirich

2
Pour une réponse sans fichiers temporaires, cliquez ici .
Tom Hale

1
Voici un moyen de le faire sans le rediriger vers des fichiers; il joue avec les échanges stdoutet les stderrallers-retours. Mais attention , comme il est dit ici : en bash, il vaudrait mieux ne pas supposer que le descripteur de fichier 3 est inutilisé " .
Golar Ramblar

70

aussiUseless.sh

Cela vous permettra de diriger la sortie de votre useless.shscript via une commande telle que sedet d'enregistrer le stderrdans une variable nommée error. Le résultat du tube est envoyé à stdoutpour affichage ou pour être redirigé vers une autre commande.

Il met en place quelques descripteurs de fichiers supplémentaires pour gérer les redirections nécessaires pour ce faire.

#!/bin/bash

exec 3>&1 4>&2 #set up extra file descriptors

error=$( { ./useless.sh | sed 's/Output/Useless/' 2>&4 1>&3; } 2>&1 )

echo "The message is \"${error}.\""

exec 3>&- 4>&- # release the extra file descriptors

5
C'est une bonne technique d'utiliser 'exec' pour définir et fermer les descripteurs de fichiers. La fermeture n'est pas vraiment nécessaire si le script se termine immédiatement après.
Jonathan Leffler

3
Comment capturerais-je à la fois stderret stdoutdans les variables?
Gingi

Excellent. Cela m'aide à implémenter une dry_runfonction qui peut choisir de manière fiable entre faire écho à ses arguments et les exécuter, que la commande exécutée à sec soit dirigée vers un autre fichier.
Mihai Danila

1
@ t00bs: readn'accepte pas l'entrée d'un tube. Vous pouvez utiliser d'autres techniques pour réaliser ce que vous essayez de démontrer.
Suspendu jusqu'à nouvel ordre.

2
Pourrait être plus simple, avec: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn

69

Redirigé stderr vers stdout, stdout vers / dev / null, puis utilisez les backticks ou $()pour capturer le stderr redirigé:

ERROR=$(./useless.sh 2>&1 >/dev/null)

8
C'est la raison pour laquelle j'ai inclus le tuyau dans mon exemple. Je veux toujours la sortie standard, et je veux qu'elle fasse d'autres choses, aille ailleurs.
psycotica0

Pour les commandes qui envoient une sortie uniquement à stderr, le moyen simple de le capturer est, par exemplePY_VERSION="$(python --version 2>&1)"
John Mark

10

Il y a beaucoup de doublons pour cette question, dont beaucoup ont un scénario d'utilisation légèrement plus simple où vous ne voulez pas capturer stderr et stdout et le code de sortie en même temps.

if result=$(useless.sh 2>&1); then
    stdout=$result
else
    rc=$?
    stderr=$result
fi

fonctionne pour le scénario courant où vous attendez soit une sortie correcte en cas de succès, soit un message de diagnostic sur stderr en cas d'échec.

Notez que les instructions de contrôle du shell examinent déjà $?sous le capot; donc tout ce qui ressemble

cmd
if [ $? -eq 0 ], then ...

est juste une façon maladroite et unidiomatique de dire

if cmd; then ...

Cela a fonctionné pour moi: my_service_status = $ (service my_service status 2> & 1) Merci !!
JRichardsz

6
# command receives its input from stdin.
# command sends its output to stdout.
exec 3>&1
stderr="$(command </dev/stdin 2>&1 1>&3)"
exitcode="${?}"
echo "STDERR: $stderr"
exit ${exitcode}

2
commandest un mauvais choix ici, dans la mesure où il existe en fait un builtin portant ce nom. Pourrait le rendre yourCommandou tel, pour être plus explicite.
Charles Duffy

5

Pour le bénéfice du lecteur, cette recette ici

  • peut être réutilisé comme oneliner pour capturer stderr dans une variable
  • donne toujours accès au code retour de la commande
  • Sacrifie un descripteur de fichier temporaire 3 (qui peut être modifié par vous bien sûr)
  • Et n'expose pas ces descripteurs de fichiers temporaires à la commande interne

Si vous voulez attraper stderrde certains commanden varvous pouvez faire

{ var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;

Ensuite, vous avez tout:

echo "command gives $? and stderr '$var'";

Si commandest simple (pas quelque chose comme a | b) vous pouvez laisser l'intérieur {}loin:

{ var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;

Enveloppé dans une fonction réutilisable facile bash(nécessite probablement la version 3 et supérieure pour local -n):

: catch-stderr var cmd [args..]
catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }

Expliqué:

  • local -nalias "$ 1" (qui est la variable pour catch-stderr)
  • 3>&1 utilise le descripteur de fichier 3 pour y enregistrer les points stdout
  • { command; } (ou "$ @") exécute alors la commande dans la capture de sortie $(..)
  • Veuillez noter que l'ordre exact est important ici (le faire dans le mauvais sens mélange mal les descripteurs de fichiers):
    • 2>&1redirige stderrvers la capture de sortie$(..)
    • 1>&3redirige stdoutloin de la capture de sortie $(..)vers le "externe" stdoutqui a été enregistré dans le descripteur de fichier 3. Notez que stderrfait toujours référence à l'endroit où FD 1 pointait auparavant: à la capture de sortie$(..)
    • 3>&-puis ferme le descripteur de fichier 3 car il n'est plus nécessaire, de sorte que commandtout à coup un descripteur de fichier ouvert inconnu n'apparaisse pas. Notez que la coque externe a toujours FD 3 ouvert, mais commandne le verra pas.
    • Ce dernier est important, car certains programmes comme se lvmplaignent de descripteurs de fichiers inattendus. Et se lvmplaint de stderr- exactement ce que nous allons capturer!

Vous pouvez attraper n'importe quel autre descripteur de fichier avec cette recette, si vous vous adaptez en conséquence. Sauf le descripteur de fichier 1 bien sûr (ici la logique de redirection serait fausse, mais pour le descripteur de fichier 1, vous pouvez simplement l'utiliser var=$(command)comme d'habitude).

Notez que cela sacrifie le descripteur de fichier 3. Si vous avez besoin de ce descripteur de fichier, n'hésitez pas à changer le numéro. Mais sachez que certains shells (des années 1980) pourraient être interprétés 99>&1comme un argument 9suivi de 9>&1(ce n'est pas un problème pour bash).

Notez également qu'il n'est pas particulièrement simple de rendre ce FD 3 configurable via une variable. Cela rend les choses très illisibles:

: catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
catch-var-from-fd-by-fd()
{
local -n v="$1";
local fd1="$2" fd2="$3";
shift 3 || return;

eval exec "$fd2>&1";
v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}

Note de sécurité: les 3 premiers arguments de catch-var-from-fd-by-fdne doivent pas provenir d'un tiers. Donnez-les toujours explicitement de façon «statique».

Alors non catch-var-from-fd-by-fd $var $fda $fdb $command, non , ne fais jamais ça!

Si vous passez un nom de variable, faites-le au moins comme suit: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

Cela ne vous protégera toujours pas contre tous les exploits, mais aidera au moins à détecter et à éviter les erreurs de script courantes.

Remarques:

  • catch-var-from-fd-by-fd var 2 3 cmd.. est le même que catch-stderr var cmd..
  • shift || returnest juste un moyen d'éviter de vilaines erreurs au cas où vous oublieriez de donner le nombre correct d'arguments. Peut-être que terminer le shell serait une autre façon (mais cela rend le test difficile à partir de la ligne de commande).
  • La routine a été écrite de telle sorte qu'elle est plus facile à comprendre. On peut réécrire la fonction de telle sorte qu'elle n'en ait pas besoin exec, mais alors ça devient vraiment moche.
  • Cette routine peut également être réécrite pour non- bashainsi que ce n'est pas nécessaire local -n. Cependant, vous ne pouvez pas utiliser de variables locales et cela devient extrêmement moche!
  • Notez également que les evals sont utilisés de manière sûre. evalEst généralement considéré comme dangereux. Cependant, dans ce cas, ce n'est pas plus mal que d'utiliser "$@"(pour exécuter des commandes arbitraires). Cependant, assurez-vous d'utiliser la citation exacte et correcte comme indiqué ici (sinon cela devient très très dangereux ).

4

POSIX

STDERR peut être capturé avec une certaine magie de redirection:

$ { error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&3 ; } 2>&1); } 3>&1
lrwxrwxrwx 1 rZZt rZZt 7 Aug 22 15:44 /bin -> usr/bin/

$ echo $error
ls: cannot access '/XXXX': No such file or directory

Notez que le piping de STDOUT de la commande (ici ls) se fait à l'intérieur du plus profond { }. Si vous exécutez une commande simple (par exemple, pas un tube), vous pouvez supprimer ces accolades internes.

Vous ne pouvez pas canaliser en dehors de la commande car la canalisation crée un sous-shell dans bashet zsh, et l'affectation à la variable dans le sous-shell ne serait pas disponible pour le shell actuel.

frapper

Dans bash, il serait préférable de ne pas supposer que le descripteur de fichier 3 est inutilisé:

{ error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1; 
exec {tmp}>&-  # With this syntax the FD stays open

Notez que cela ne fonctionne pas dans zsh.


Merci à cette réponse pour l'idée générale.


pouvez-vous expliquer cette ligne avec des détails? n'a pas compris 1> & $ tmp; {erreur = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Thiago Conrado

1
@ThiagoConrado Je suppose tmpque dans ce cas, il s'agit simplement d'une variable qui stocke un descripteur de fichier dont vous savez qu'il n'est pas utilisé. Par exemple, si tmp=3then 1>&$tmpdevenait 1>&3et que la commande serait la même que celle expliquée précédemment (elle stockerait stdout( 1) dans le descripteur de fichier 3, que stderr( 2) irait stdoutet serait stocké dans la errorvariable, et enfin le contenu diffusé dans le descripteur de fichier 3revient au descripteur de fichier 1, c'est-à-dire stdout, à cause de {tmp}>&1cela se transforme en 3>&1, si j'ai bien compris).
Lucas Basquerotto

3

Voici comment je l'ai fait:

#
# $1 - name of the (global) variable where the contents of stderr will be stored
# $2 - command to be executed
#
captureStderr()
{
    local tmpFile=$(mktemp)

    $2 2> $tmpFile

    eval "$1=$(< $tmpFile)"

    rm $tmpFile
}

Exemple d'utilisation:

captureStderr err "./useless.sh"

echo -$err-

Il n'utiliser un fichier temporaire. Mais au moins, le truc laid est enveloppé dans une fonction.


@ShadowWizard Petit doute de mon côté. En français, les deux points sont généralement précédés d'un espace. J'applique par erreur cette même règle avec des réponses en anglais . Après avoir vérifié cela , je sais que je ne referai plus cette erreur.
Stephan

@Stephan cheers, cela a également été discuté ici . :)
Shadow Wizard est l'oreille pour vous

1
Il existe des moyens plus sûrs que d'utiliser eval. Par exemple, printf -v "$1" '%s' "$(<tmpFile)"ne risque pas d'exécuter du code arbitraire si votre TMPDIRvariable a été définie sur une valeur malveillante (ou si votre nom de variable de destination contient une telle valeur).
Charles Duffy

1
De même, rm -- "$tmpFile"est plus robuste que rm $tmpFile.
Charles Duffy

2

C'est un problème intéressant auquel j'espérais qu'il y avait une solution élégante. Malheureusement, je me retrouve avec une solution similaire à M. Leffler, mais j'ajouterai que vous pouvez appeler inutile de l'intérieur d'une fonction Bash pour une meilleure lisibilité:

#! / bin / bash

fonction inutile {
    /tmp/useless.sh | sed 's / Sortie / Inutile /'
}

ERREUR = $ (inutile)
echo $ ERROR

Tout autre type de redirection de sortie doit être sauvegardé par un fichier temporaire.


1

Cet article m'a aidé à trouver une solution similaire pour mes propres besoins:

MESSAGE=`{ echo $ERROR_MESSAGE | format_logs.py --level=ERROR; } 2>&1`

Ensuite, tant que notre MESSAGE n'est pas une chaîne vide, nous le passons à d'autres choses. Cela nous permettra de savoir si notre format_logs.py a échoué avec une sorte d'exception python.


1

Capturer et imprimer stderr

ERROR=$( ./useless.sh 3>&1 1>&2 2>&3 | tee /dev/fd/2 )

Panne

Vous pouvez utiliser $()pour capturer stdout, mais vous souhaitez capturer stderr à la place. Vous permutez donc stdout et stderr. Utilisation de fd 3 comme stockage temporaire dans l'algorithme d'échange standard.

Si vous voulez capturer ET imprimer, utilisez teepour faire une copie. Dans ce cas, la sortie de teesera capturée par $()plutôt que d'aller à la console, mais stderr (of tee) ira toujours à la console, nous l'utilisons donc comme deuxième sortie pour teevia le fichier spécial /dev/fd/2car teeattend un chemin de fichier plutôt qu'un fd nombre.

REMARQUE: c'est énormément de redirections sur une seule ligne et l'ordre est important. $()saisit le stdout de teeà la fin du pipeline et le pipeline lui-même achemine stdout de ./useless.shvers le stdin de teeAPRÈS que nous ayons échangé stdin et stdout pour ./useless.sh.

Utilisation de stdout de ./useless.sh

L'OP a déclaré qu'il souhaitait toujours utiliser (pas seulement imprimer) stdout, comme ./useless.sh | sed 's/Output/Useless/'.

Pas de problème, faites-le juste AVANT d'échanger stdout et stderr. Je recommande de le déplacer dans une fonction ou un fichier (également-useless.sh) et de l'appeler à la place de ./useless.sh dans la ligne ci-dessus.

Cependant, si vous voulez CAPTURER stdout ET stderr, alors je pense que vous devez vous rabattre sur des fichiers temporaires car vous $()n'en faites qu'un à la fois et cela crée un sous-shell à partir duquel vous ne pouvez pas retourner de variables.


1

En répétant un peu la réponse de Tom Hale, j'ai trouvé qu'il était possible d'envelopper le yoga de redirection dans une fonction pour une réutilisation plus facile. Par exemple:

#!/bin/sh

capture () {
    { captured=$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
}

# Example usage; capturing dialog's output without resorting to temp files
# was what motivated me to search for this particular SO question
capture dialog --menu "Pick one!" 0 0 0 \
        "FOO" "Foo" \
        "BAR" "Bar" \
        "BAZ" "Baz"
choice=$captured

clear; echo $choice

Il est presque certainement possible de simplifier cela davantage. Je n'ai pas été testé de manière particulièrement approfondie, mais il semble fonctionner à la fois avec bash et ksh.


0

Si vous souhaitez contourner l'utilisation d'un fichier temporaire, vous pourrez peut-être utiliser la substitution de processus. Je ne l'ai pas encore tout à fait fonctionné. C'était ma première tentative:

$ .useless.sh 2> >( ERROR=$(<) )
-bash: command substitution: line 42: syntax error near unexpected token `)'
-bash: command substitution: line 42: `<)'

Puis j'ai essayé

$ ./useless.sh 2> >( ERROR=$( cat <() )  )
This Is Output
$ echo $ERROR   # $ERROR is empty

pourtant

$ ./useless.sh 2> >( cat <() > asdf.txt )
This Is Output
$ cat asdf.txt
This Is Error

Donc, la substitution de processus fait généralement la bonne chose ... malheureusement, chaque fois que j'enveloppe STDIN >( )avec quelque chose $()dans une tentative de capturer cela dans une variable, je perds le contenu de $(). Je pense que c'est parce que $()lance un sous-processus qui n'a plus accès au descripteur de fichier dans / dev / fd qui appartient au processus parent.

La substitution de processus m'a donné la possibilité de travailler avec un flux de données qui n'est plus dans STDERR, malheureusement je ne semble pas être capable de le manipuler comme je le souhaite.


1
Si vous le faisiez, ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" )vous verriez la sortie de ERROR. Le problème est que la substitution de processus est exécutée dans un sous-shell, donc la valeur définie dans le sous-shell n'affecte pas le shell parent.
Jonathan Leffler

0
$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr

3
Cela semble être une bonne idée, mais sur Mac OSX 10.8.5, il imprimea=> b=>stderr
Heath Borders

3
Je suis d'accord avec @HeathBorders; cela ne produit pas la sortie indiquée. Le problème ici est qu'il aest évalué et assigné dans un sous-shell, et l'affectation dans le sous-shell n'affecte pas le shell parent. (Testé sur Ubuntu 14.04 LTS ainsi que Mac OS X 10.10.1.)
Jonathan Leffler

La même chose dans Windows GitBash. Donc, ça ne marche pas. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby

Ne fonctionne pas sur SLE 11.4non plus et produit l'effet décrit par @JonathanLeffler
smarber

Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire concernant la raison et / ou la manière dont ce code répond à la question améliore sa valeur à long terme.
β.εηοιτ.βε

0

Dans zsh:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )

0

Pour vérifier les erreurs de vos commandes:

execute [INVOKING-FUNCTION] [COMMAND]

execute () {
    function="${1}"
    command="${2}"
    error=$(eval "${command}" 2>&1 >"/dev/null")

    if [ ${?} -ne 0 ]; then
        echo "${function}: ${error}"
        exit 1
    fi
}

Inspiré de la fabrication Lean:


La solution idiomatique consiste à insérer l'affectation dans le fichier if. Permettez-moi de publier une solution distincte.
tripleee

0

Une solution simple

{ ERROR=$(./useless.sh 2>&1 1>&$out); } {out}>&1
echo "-"
echo $ERROR

Produira:

This Is Output
-
This Is Error

0

Améliorer la réponse de YellowApple :

Ceci est une fonction Bash pour capturer stderr dans n'importe quelle variable

stderr_capture_example.sh:

#!/usr/bin/env bash

# Capture stderr from a command to a variable while maintaining stdout
# @Args:
# $1: The variable name to store the stderr output
# $2: Vararg command and arguments
# @Return:
# The Command's Returnn-Code or 2 if missing arguments
function capture_stderr {
  [ $# -lt 2 ] && return 2
  local stderr="$1"
  shift
  {
    printf -v "$stderr" '%s' "$({ "$@" 1>&3; } 2>&1)"
  } 3>&1
}

# Testing with a call to erroring ls
LANG=C capture_stderr my_stderr ls "$0" ''

printf '\nmy_stderr contains:\n%s' "$my_stderr"

Essai:

bash stderr_capture_example.sh

Production:

 stderr_capture_example.sh

my_stderr contains:
ls: cannot access '': No such file or directory

Cette fonction peut être utilisée pour capturer le choix retourné d'une dialogcommande.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.