web-dev-qa-db-fra.com

Comment stocker l'erreur standard dans une variable dans un script Bash

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 souhaite continuer à utiliser stdout, il est donc inutile de rediriger stderr vers stdout.

Donc, fondamentalement, 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, s'il n'y a pas de réponses, 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?

146
psycotica0

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

ERROR=$(</tmp/Error)

Le shell le reconnaît et il n’est pas nécessaire 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 devez créer l'intégralité du pipeline dans le sous-shell, en envoyant éventuellement sa sortie standard finale dans un fichier, afin de pouvoir 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 coquilles classiques - Bourne, Korn - bien sûr; probablement aussi dans Bash). Le '{}' effectue la redirection E/S sur les commandes incluses. Tel qu'écrit, il capturerait aussi les erreurs de sed.

AVERTISSEMENT: Code non encore testé - utilisation à vos risques et périls.

73
Jonathan Leffler

aussiUseless.sh

Cela vous permettra de canaliser la sortie de votre script useless.sh par le biais d'une commande telle que sed et d'enregistrer stderr dans une variable nommée error. Le résultat du canal est envoyé à stdout pour être affiché ou pour être dirigé vers une autre commande.

Il configure quelques descripteurs de fichier supplémentaires pour gérer les redirections nécessaires à cette fin.

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

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

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

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 dans lequel vous attendez une sortie appropriée en cas de succès ou 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 unidiomatic de dire

if cmd; then ...
6
tripleee
# 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}
5
human9

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 utilise utilise un fichier temporaire. Mais au moins le truc laid est enveloppé dans une fonction.

3
tfga

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

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

Si vous voulez attraper stderr d'une certaine command dans var, vous pouvez le faire

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

Ensuite, vous avez tout:

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

Si command est simple (pas quelque chose comme a | b), vous pouvez laisser le {} intérieur absent:

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

Enveloppé dans une fonction bash- facilement réutilisable (nécessite probablement la version 3 et les versions ultérieures pour local -n):

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

A expliqué:

  • local -n alias "$ 1" (qui est la variable pour catch-stderr)
  • 3>&1 utilise le descripteur de fichier 3 pour enregistrer ses points stdout
  • { command; } (ou "$ @") exécute ensuite la commande dans la capture de sortie $(..)
  • Veuillez noter que l'ordre exact est important ici (le faire dans le mauvais sens mélange les descripteurs de fichiers à tort):
    • 2>&1 redirige stderr vers la capture de sortie $(..)
    • 1>&3 redirige stdout loin de la capture de sortie $(..) vers le "extérieur" stdout enregistré dans le descripteur de fichier 3. Notez que stderr fait toujours référence à où FD 1 pointait avant: vers la sortie capturée $(..)
    • 3>&- ferme ensuite le descripteur de fichier 3 car il n'est plus nécessaire, de sorte que command ne présente pas soudainement un descripteur de fichier ouvert inconnu. Notez que le shell externe a toujours FD 3 ouvert, mais command ne le verra pas.
    • Ce dernier point est important, car certains programmes comme lvm se plaignent de descripteurs de fichier inattendus. Et lvm se plaint de stderr - exactement ce que nous allons capturer!

Vous pouvez attraper tout autre descripteur de fichier avec cette recette, si vous vous adaptez en conséquence. Excepté 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 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 comprendre 99>&1 comme argument 9 suivi de 9>&1 (ce n'est pas un problème pour bash).

Notez également qu’il n’est pas particulièrement facile de rendre ce FD 3 configurable au moyen d’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-fd ne doivent pas provenir d'une 3ème partie. Toujours leur donner explicitement de manière "statique".

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

Si vous passez un nom de variable, procédez 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 vous aidera au moins à détecter et à éviter les erreurs de script courantes.

Remarques:

  • catch-var-from-fd-by-fd var 2 3 cmd.. est identique à catch-stderr var cmd..
  • shift || return n'est qu'un moyen d'éviter les erreurs laides si vous oubliez de donner le nombre correct d'arguments. Terminer le Shell serait peut-être un autre moyen (mais cela rend difficile le test depuis la ligne de commande).
  • La routine était telle qu’elle est plus facile à comprendre. On peut réécrire la fonction de telle sorte qu'elle n'ait pas besoin de exec, mais elle devient vraiment laide.
  • Cette routine peut être réécrite pour non -bash, de sorte que local -n n'est pas nécessaire. Cependant, vous ne pouvez pas utiliser de variables locales et cela devient extrêmement laid!
  • Notez également que les evals sont utilisés de manière sécurisée. Habituellement, eval est considéré comme dangereux. Cependant, dans ce cas, il n’est pas plus diabolique que d’utiliser "$@" (pour exécuter des commandes arbitraires). Cependant, veillez à utiliser les citations exactes et correctes comme indiqué ici (sinon, il devient très très dangereux).
2
Tino

C’est un problème intéressant auquel j’espérais une solution élégante. Malheureusement, je me retrouve avec une solution similaire à celle de M. Leffler, mais j'ajouterai que vous pouvez appeler inutilisable depuis l'intérieur d'une fonction Bash pour améliorer la lisibilité:

 #!/bin/bash 

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

 ERROR = $ (inutilisable) 
 echo $ ERROR 

Tous les autres types de redirection de sortie doivent être sauvegardés par un fichier temporaire. 

2
FD Gonthier

Capture AND Print 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. Donc, vous échangez stdout et stderr. Utilisation de fd 3 comme stockage temporaire dans l’algorithme de permutation standard.

Si vous souhaitez capturer ET imprimer, utilisez tee pour créer une copie. Dans ce cas, la sortie de tee sera capturée par $() plutôt que d'aller à la console, mais stderr (de tee) ira toujours vers la console, nous l'utilisons donc comme deuxième sortie de tee via le fichier spécial /dev/fd/2 puisque tee attend un chemin du fichier plutôt qu'un nombre fd.

NOTE: Il y a énormément de redirections sur une seule ligne et l'ordre compte. $() saisit le stdout de tee à la fin du pipeline et le pipeline lui-même achemine la stdout de ./useless.sh vers le stdin de tee APRÈS que nous ayons échangé stdin et stdout pour ./useless.sh.

Utilisation de la sortie standard de ./useless.sh

L'OP a déclaré qu'il souhaitait toujours utiliser (pas seulement imprimer) la sortie standard, telle que ./useless.sh | sed 's/Output/Useless/'.

Pas de problème, faites-le AVANT de permuter stdout et stderr. Je recommande de le déplacer dans une fonction ou un fichier (also-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 les fichiers temporaires car $() n'en fera qu'un à la fois et crée un sous-shell à partir duquel vous ne pouvez pas retourner de variables.

1
SensorSmith
$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr
1
Mario Wolff

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 transmettons à d'autres éléments. Cela nous permettra de savoir si notre format_logs.py a échoué avec une sorte d’exception python.

1
palmbardier

Si vous souhaitez ignorer l'utilisation d'un fichier temporaire, vous pouvez éventuellement utiliser la substitution de processus. Je n'ai pas encore réussi à le faire fonctionner. 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

Toutefois

$ ./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'emballe STDIN dans >( ) avec quelque chose dans $() dans le but 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 de/dev/fd qui appartient au processus parent.

La substitution de processus m'a permis de travailler avec un flux de données qui n'est plus dans STDERR. Malheureusement, il semble que je ne sois pas capable de le manipuler comme je le souhaite.

0

En zsh:

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

POSIX

STDERR peut être capturé avec une 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 la tuyauterie de STDOUT de la commande (ici ls) est réalisée à l'intérieur du {} le plus interne. Si vous exécutez une commande simple (par exemple, pas un tuyau), vous pouvez supprimer ces accolades internes.

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

bash

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.

0
Tom Hale

Pour error proofing 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
}

Inspired dans la fabrication sans gaspillage:

0

Une solution simple

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

Produira:

This Is Output
-
This Is Error
0
Karl Morrison