Est-il possible de stocker ou capturer stdout et stderr dans différentes variables, sans utiliser de fichier temporaire? En ce moment, je fais cela pour obtenir stdout dans out
et stderr dans err
lors de l'exécution de some_command
, mais j'aimerais éviter le fichier temporaire.
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< error_file)
rm $error_file
Ok, c'est devenu un peu moche, mais voici une solution:
unset t_std t_err
eval "$( (echo std; echo err >&2) \
2> >(readarray -t t_err; typeset -p t_err) \
> >(readarray -t t_std; typeset -p t_std) )"
où (echo std; echo err >&2)
doit être remplacé par la commande réelle. La sortie de stdout est enregistrée dans le tableau $t_std
Ligne par ligne en omettant les sauts de ligne (le -t
) Et stderr dans $t_err
.
Si vous n'aimez pas les tableaux, vous pouvez le faire
unset t_std t_err
eval "$( (echo std; echo err >&2 ) \
2> >(t_err=$(cat); typeset -p t_err) \
> >(t_std=$(cat); typeset -p t_std) )"
qui imite à peu près le comportement de var=$(cmd)
à l'exception de la valeur de $?
qui nous amène à la dernière modification:
unset t_std t_err t_ret
eval "$( (echo std; echo err >&2; exit 2 ) \
2> >(t_err=$(cat); typeset -p t_err) \
> >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"
Ici, $?
Est conservé dans $t_ret
Testé sur Debian Wheezy en utilisant GNU bash
, Version 4.2.37 (1) -release (i486-pc-linux- gnu).
Jonathan a la réponse . Pour référence, c'est l'astuce ksh93. (nécessite une version non ancienne).
function out {
echo stdout
echo stderr >&2
}
x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values
produit
x=stderr
y=stdout
La syntaxe ${ cmds;}
N'est qu'une substitution de commandes qui ne crée pas de sous-shell. Les commandes sont exécutées dans l'environnement Shell actuel. L'espace au début est important ({
Est un mot réservé).
Stderr du groupe de commandes interne est redirigé vers stdout (afin qu'il s'applique à la substitution interne). Ensuite, la sortie standard de out
est affectée à y
, et le stderr redirigé est capturé par x
, sans la perte habituelle de y
à la substitution d'une commande. sous-coquille.
Ce n'est pas possible dans d'autres shells, car toutes les constructions qui capturent la sortie nécessitent de placer le producteur dans une sous-coque, qui dans ce cas, inclurait l'affectation.
mise à jour: Maintenant également pris en charge par mksh.
Cette commande définit à la fois les valeurs stdout (stdval) et stderr (errval) dans le shell en cours d'exécution:
eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"
à condition que cette fonction soit définie:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }
Remplacez execcommand par la commande capturée, que ce soit "ls", "cp", "df", etc.
Tout cela est basé sur l'idée que nous pourrions convertir toutes les valeurs capturées en une ligne de texte à l'aide de la fonction setval, puis setval est utilisé pour capturer chaque valeur dans cette structure:
execcommand 2> CaptureErr > CaptureOut
Convertissez chaque valeur de capture en un appel setval:
execcommand 2> >(setval errval) > >(setval stdval)
Enveloppez tout dans un appel d'exécution et faites-le écho:
echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"
Vous obtiendrez les appels de déclaration que chaque setval crée:
declare -- stdval="I'm std"
declare -- errval="I'm err"
Pour exécuter ce code (et obtenir le jeu de variables), utilisez eval:
eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"
et enfin faire écho aux vars définis:
echo "std out is : |$stdval| std err is : |$errval|
Il est également possible d'inclure la valeur de retour (sortie).
Un exemple de script bash complet ressemble à ceci:
#!/bin/bash --
# The only function to declare:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }
# a dummy function with some example values:
function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; }
# Running a command to capture all values
# change execcommand to dummy or any other command to test.
eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )"
echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"
C'est pour attraper stdout et stderr dans différentes variables. Si vous voulez seulement attraper
stderr
, en laissantstdout
tel quel, il existe une solution meilleure et plus courte .
Pour somme tout haut pour le bénéfice du lecteur, voici un
bash
SolutionCette version utilise des sous-coquilles et fonctionne sans tempfile
s. (Pour une version tempfile
qui fonctionne sans sous-shell, voir mon autre réponse .)
: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
{ __1="$("${@:3}")"; } 2>&1;
ret=$?;
printf '%q=%q\n' "$1" "$__1" >&2;
exit $ret
)"
ret="$?";
printf '%s=%q\n' "$2" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}
Exemple d'utilisation:
dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}
catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n data \n\n'
printf 'ret=%q\n' "$?"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"
cela imprime
ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n data '
Il peut donc être utilisé sans y réfléchir plus profondément. Mettez simplement catch VAR1 VAR2
Devant tout command args..
Et vous avez terminé.
Certains if cmd args..; then
Deviendront if catch VAR1 VAR2 cmd args..; then
. Vraiment rien de complexe.
Q: Comment ça marche?
Il enveloppe simplement les idées des autres réponses ici dans une fonction, de sorte qu'il peut facilement être réutilisé.
catch()
utilise essentiellement eval
pour définir les deux variables. Ceci est similaire à https://stackoverflow.com/a/18086548
Considérez un appel de catch out err dummy 1 2a 3b
:
sautons le eval "$({
et le __2="$(
pour l'instant. J'y reviendrai plus tard.
__1="$("$("${@:3}")"; } 2>&1;
exécute dummy 1 2 3
et stocke son stdout
dans __1
pour une utilisation ultérieure. Donc __1
Devient 2a
. Il redirige également stderr
de dummy
vers stdout
, de sorte que la capture externe peut rassembler stdout
ret=$?;
Capture le code de sortie, qui est 1
printf '%q=%q\n' "$1" "$__1" >&2;
Renvoie ensuite out=2a
Vers stderr
. stderr
est utilisé ici, car le stdout
actuel a déjà repris le rôle de stderr
de la commande dummy
.
exit $ret
Transmet ensuite le code de sortie (1
) À l'étape suivante.
Passons maintenant à la __2="$( ... )"
extérieure:
Cela capture stdout
de ce qui précède, qui est le stderr
de l'appel dummy
, dans la variable __2
. (Nous pourrions réutiliser __1
Ici, mais j'ai utilisé __2
Pour le rendre moins déroutant.). Donc __2
Devient 3b
ret="$?";
Récupère le code retour (retourné) 1
(De dummy
) à nouveau
printf '%s=%q\n' "$2" "$__2" >&2;
Renvoie ensuite err=3a
Vers stderr
. stderr
est à nouveau utilisé, car il était déjà utilisé pour sortir l'autre variable out=2a
.
printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to
catch`.
Veuillez noter que, comme optimisation, nous aurions pu écrire ces 2 printf
comme un seul comme printf '%s=%q\n( exit %q )
"$ __ 2" "$ ret" `également.
Alors qu'avons-nous jusqu'à présent?
Nous avons écrit à stderr:
out=2a
err=3b
( exit 1 )
où out
vient de $1
, 2a
vient de stdout
de dummy
, err
vient de $2
, 3b
Provient de stderr
de dummy
, et 1
Provient du code de retour de dummy
.
Veuillez noter que %q
Au format printf
prend soin de la citation, de telle sorte que le Shell voit les arguments (uniques) appropriés quand il s'agit de eval
. 2a
Et 3b
Sont si simples qu'ils sont copiés littéralement.
Passons maintenant à la eval "$({ ... } 2>&1 )";
extérieure:
Cela exécute tout ce qui précède qui génère les 2 variables et le exit
, l'attrape (à cet effet le 2>&1
) Et l'analyse dans le shell actuel en utilisant eval
.
De cette façon, les 2 variables sont définies ainsi que le code retour.
Q: Il utilise eval
ce qui est mauvais. Est-ce donc sûr?
printf %q
N'a pas de bogues, il devrait être sûr. Mais vous devez toujours être très prudent, pensez à Shellshock.Q: Des bugs?
Aucun bogue évident n'est connu, à l'exception des suivants:
Comme d'habitude $(echo $'\n\n\n\n')
avale tous les sauts de ligne , pas seulement le dernier. Il s'agit d'une exigence POSIX. Si vous avez besoin d'obtenir les LF indemnes, ajoutez simplement un caractère de fin à la sortie et supprimez-le ensuite comme dans la recette suivante (regardez le x
de fin qui permet de lire un lien logiciel pointant vers un fichier qui se termine sur un $'\n'
):
target="$(readlink -e "$file")x"
target="${target%x}"
Les variables shell ne peuvent pas porter l'octet NUL ($'\0'
). Ils sont simplement ignorés s'ils se produisent dans stdout
ou stderr
.
La commande donnée s'exécute dans un sous-sous-shell. Il n'a donc pas accès à $PPID
, Ni ne peut modifier les variables Shell. Vous pouvez catch
une fonction Shell, même les fonctions internes, mais celles-ci ne pourront pas modifier les variables Shell (car tout ce qui s'exécute dans $( .. )
ne peut pas le faire). Donc, si vous devez exécuter une fonction dans le shell actuel et attraper son stderr/stdout, vous devez le faire de la manière habituelle avec tempfile
s. (Il existe des moyens de le faire de telle sorte que l'interruption du shell ne laisse normalement pas de débris, mais c'est complexe et mérite sa propre réponse.)
Q: version Bash?
printf %q
)Q: Cela semble toujours aussi gênant.
ksh
beaucoup plus proprement. Cependant, je n'ai pas l'habitude de ksh
, je laisse donc à d'autres le soin de créer une recette similaire facile à réutiliser pour ksh
.Q: Pourquoi ne pas utiliser ksh
alors?
bash
Q: Le script peut être amélioré
Q: Il y a une faute de frappe. : catch STDOUT STDERR cmd args..
Doit lire # catch STDOUT STDERR cmd args..
:
Apparaît dans bash -x
Tandis que les commentaires sont avalés en silence. Vous pouvez donc voir où se trouve l'analyseur si vous avez une faute de frappe dans la définition de la fonction. C'est une vieille astuce de débogage. Mais attention, vous pouvez facilement créer des effets secondaires soignés dans les arguments de :
.Edit: Ajout de quelques ;
De plus pour faciliter la création d'une doublure simple à partir de catch()
. Et ajouté une section comment cela fonctionne.
Techniquement, les canaux nommés ne sont pas des fichiers temporaires et personne ici ne les mentionne. Ils ne stockent rien dans le système de fichiers et vous pouvez les supprimer dès que vous les connectez (vous ne les verrez donc jamais):
#!/bin/bash -e
foo () {
echo stdout1
echo stderr1 >&2
sleep 1
echo stdout2
echo stderr2 >&2
}
rm -f stdout stderr
mkfifo stdout stderr
foo >stdout 2>stderr & # blocks until reader is connected
exec {fdout}<stdout {fderr}<stderr # unblocks `foo &`
rm stdout stderr # filesystem objects are no longer needed
stdout=$(cat <&$fdout)
stderr=$(cat <&$fderr)
echo $stdout
echo $stderr
exec {fdout}<&- {fderr}<&- # free file descriptors, optional
Vous pouvez avoir plusieurs processus d'arrière-plan de cette façon et collecter de manière asynchrone leurs stdouts et stderrs à un moment opportun, etc.
Si vous en avez besoin pour un seul processus, vous pouvez tout aussi bien utiliser des nombres fd codés en dur comme 3 et 4, au lieu de {fdout}/{fderr}
syntaxe (qui trouve un fd gratuit pour vous).
N'aimait pas l'eval, voici donc une solution qui utilise des astuces de redirection pour capturer la sortie du programme dans une variable, puis analyse cette variable pour extraire les différents composants. L'indicateur -w définit la taille du bloc et influe sur l'ordre des messages std-out/err au format intermédiaire. 1 donne une résolution potentiellement élevée au détriment des frais généraux.
#######
# runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later.
# limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output.
# example:
# var=$(keepBoth ls . notHere)
# echo ls had the exit code "$(extractOne r "$var")"
# echo ls had the stdErr of "$(extractOne e "$var")"
# echo ls had the stdOut of "$(extractOne o "$var")"
keepBoth() {
(
prefix(){
( set -o pipefail
base64 -w 1 - | (
while read c
do echo -E "$1" "$c"
done
)
)
}
( (
"$@" | prefix o >&3
echo ${PIPESTATUS[0]} | prefix r >&3
) 2>&1 | prefix e >&1
) 3>&1
)
}
extractOne() { # extract
echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode -
}
En résumé, je crois que la réponse est "non". La capture $( ... )
capture uniquement la sortie standard vers la variable; il n'y a aucun moyen d'obtenir l'erreur standard capturée dans une variable distincte. Donc, ce que vous avez est aussi soigné que possible.
Qu'en est-il de ... = D
GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
GET_STDERR=""
GET_STDOUT=""
unset t_std t_err
eval "$( (eval $1) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )"
GET_STDERR=$t_err
GET_STDOUT=$t_std
}
get_stderr_stdout "command"
echo "$GET_STDERR"
echo "$GET_STDOUT"
Pour le bénéfice du lecteur, voici une solution utilisant tempfile
s.
La question n'était pas d'utiliser tempfile
s. Cependant, cela peut être dû à la pollution indésirable de /tmp/
Avec tempfile au cas où le Shell meurt. Dans le cas de kill -9
Certains trap 'rm "$tmpfile1" "$tmpfile2"' 0
Ne se déclenchent pas.
Si vous êtes dans une situation où vous pouvez utiliser tempfile
, mais que vous souhaitez ne jamais laisser de débris derrière , voici une recette.
Encore une fois, il est appelé catch()
(comme ma autre réponse ) et a la même syntaxe d'appel:
catch stdout stderr command args..
# Wrappers to avoid polluting the current Shell's environment with variables
: catch_read returncode FD variable
catch_read()
{
eval "$3=\"\`cat <&$2\`\"";
# You can use read instead to skip some fork()s.
# However read stops at the first NUL byte,
# also does no \n removal and needs bash 3 or above:
#IFS='' read -ru$2 -d '' "$3";
return $1;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "$1";
"${@:3}" 66<&-;
catch_read $? 66 "$2";
} 2>&1 >"$1" 66<"$1";
}
: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}
Ce qu'il fait:
Il crée deux tempfile
pour stdout
et stderr
. Cependant, il les supprime presque immédiatement, de sorte qu'ils ne sont disponibles que très peu de temps.
catch_1()
capture stdout
(FD 1) dans une variable et déplace stderr
vers stdout
, de sorte que la prochaine ("gauche") catch_1
peut attraper ça.
Le traitement dans catch
se fait de droite à gauche, donc le catch_1
De gauche est exécuté en dernier et attrape stderr
.
Le pire qui puisse arriver est que certains fichiers temporaires apparaissent sur /tmp/
, Mais ils sont toujours vides dans ce cas. (Ils sont retirés avant d'être remplis.). Habituellement, cela ne devrait pas poser de problème, car sous Linux, tmpfs prend en charge environ 128 Ko de fichiers par Go de mémoire principale.
La commande donnée peut également accéder à toutes les variables Shell locales et les modifier. Vous pouvez donc appeler une fonction Shell qui a des effets secondaires!
Cela ne bifurque que deux fois pour l'appel tempfile
.
Bugs:
Manque une bonne gestion des erreurs en cas d'échec de tempfile
.
Cela fait la suppression habituelle \n
Du Shell. Voir le commentaire dans catch_read()
.
Vous ne pouvez pas utiliser le descripteur de fichier 66
Pour diriger les données vers votre commande. Si vous en avez besoin, utilisez un autre descripteur pour la redirection, comme 42
(Notez que les très anciens shells ne proposent que des FD jusqu'à 9).
Cela ne peut pas gérer les octets NUL ($'\0'
) Dans stdout
et stderr
. (NUL est simplement ignoré. Pour la variante read
tout derrière un NUL est ignoré.)
Pour info:
Une solution de contournement, qui est hacky mais peut-être plus intuitive que certaines des suggestions de cette page, consiste à baliser les flux de sortie, à les fusionner et à les diviser ensuite en fonction des balises. Par exemple, nous pouvons étiqueter stdout avec un préfixe "STDOUT":
function someCmd {
echo "I am stdout"
echo "I am stderr" 1>&2
}
ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1)
OUT=$(echo "$ALL" | grep "^STDOUT" | sed -e 's/^STDOUT//g')
ERR=$(echo "$ALL" | grep -v "^STDOUT")
`` ''
Si vous savez que stdout et/ou stderr sont de forme restreinte, vous pouvez créer une balise qui n'entre pas en conflit avec leur contenu autorisé.
Voici une variation plus simple qui n'est pas tout à fait ce que l'OP voulait, mais qui ne ressemble à aucune des autres options. Vous pouvez obtenir ce que vous voulez en réorganisant les descripteurs de fichiers.
Commande de test:
%> cat xx.sh
#!/bin/bash
echo stdout
>&2 echo stderr
qui en soi:
%> ./xx.sh
stdout
stderr
Maintenant, imprimez stdout, capturez stderr dans une variable et connectez stdout dans un fichier
%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out
stdout
%> echo
$err
stderr
Ou connectez stdout et capturez stderr dans une variable:
export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr
Vous avez eu l'idée.
ATTENTION: PAS (encore?) FONCTIONNANT!
Ce qui suit semble être une piste possible pour le faire fonctionner sans créer de fichiers temporaires et également sur POSIX sh uniquement; il nécessite cependant base64 et en raison du codage/décodage peut ne pas être aussi efficace et utiliser également une mémoire "plus grande".
Le principal problème est cependant que tout semble racé. Essayez d'utiliser un exe comme:
exe () {cat /usr/share/hunspell/de_DE.dic cat /usr/share/hunspell/en_GB.dic> & 2}
et vous verrez que par exemple des parties de la ligne encodée en base64 se trouvent en haut du fichier, des parties à la fin et les éléments stderr non décodés au milieu.
Eh bien, même si l'idée ci-dessous ne peut pas être mise en œuvre (ce que je suppose), elle peut servir d'anti-exemple pour les personnes qui pourraient croire à tort qu'elle pourrait être mise en œuvre de cette façon.
Idée (ou anti-exemple):
#!/bin/sh
exe()
{
echo out1
echo err1 >&2
echo out2
echo out3
echo err2 >&2
echo out4
echo err3 >&2
echo -n err4 >&2
}
r="$( { exe | base64 -w 0 ; } 2>&1 )"
echo RAW
printf '%s' "$r"
echo RAW
o="$( printf '%s' "$r" | tail -n 1 | base64 -d )"
e="$( printf '%s' "$r" | head -n -1 )"
unset r
echo
echo OUT
printf '%s' "$o"
echo OUT
echo
echo ERR
printf '%s' "$e"
echo ERR
donne (avec le correctif stderr-newline):
$ ./ggg
RAW
err1
err2
err3
err4
b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW
OUT
out1
out2
out3
out4OUT
ERR
err1
err2
err3
err4ERR
(Au moins sur le tableau de bord et bash de Debian)
Si la commande 1) aucun effet secondaire avec état et 2) est bon marché sur le plan des calculs, la solution la plus simple consiste à l'exécuter deux fois. J'ai principalement utilisé cela pour le code qui s'exécute pendant la séquence de démarrage lorsque vous ne savez pas encore si le disque va fonctionner. Dans mon cas, c'était un minuscule some_command
donc il n'y a pas eu de perte de performances pour avoir exécuté deux fois, et la commande n'a eu aucun effet secondaire.
Le principal avantage est qu'il est propre et facile à lire. Les solutions ici sont assez intelligentes, mais je détesterais être celle qui doit maintenir un script contenant les solutions les plus compliquées. Je recommanderais l'approche simple à exécuter deux fois si votre scénario fonctionne avec cela, car il est beaucoup plus propre et plus facile à entretenir.
Exemple:
output=$(getopt -o '' -l test: -- "$@")
errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
echo "Option Error: $errout"
fi
Encore une fois, ce n'est correct que parce que getopt n'a aucun effet secondaire. Je sais que les performances sont sûres car mon code parent l'appelle moins de 100 fois pendant tout le programme, et l'utilisateur ne remarquera jamais 100 appels getopt contre 200 appels getopt.