Je pense qu'avant de dire "vous ne pouvez pas" faire quelque chose, les gens devraient au moins essayer de le faire de leurs propres mains
Solution simple et propre, sans utiliser eval
ou tout ce qui est exotique
1. Une version minimale
{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(some_command)" 1>&2) 2>&1)
Exige : printf
, read
2. Un test simple
Un script factice pour la production de stdout
y stderr
: useless.sh
#!/bin/bash
#
# useless.sh
#
echo "This is stderr" 1>&2
echo "This is stdout"
Le script réel qui va capturer stdout
y stderr
: capture.sh
#!/bin/bash
#
# capture.sh
#
{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(./useless.sh)" 1>&2) 2>&1)
echo 'Here is the captured stdout:'
echo "${CAPTURED_STDOUT}"
echo
echo 'And here is the captured stderr:'
echo "${CAPTURED_STDERR}"
echo
Sortie de capture.sh
Here is the captured stdout:
This is stdout
And here is the captured stderr:
This is stderr
3. Comment cela fonctionne
La commande
(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1
envoie la sortie standard de some_command
a printf '\0%s\0'
créant ainsi la chaîne de caractères \0${stdout}\n\0
(où \0
es un NUL
octet et \n
est un caractère de nouvelle ligne) ; la chaîne \0${stdout}\n\0
est ensuite redirigé vers l'erreur standard, où l'erreur standard de some_command
était déjà présente, composant ainsi la chaîne ${stderr}\n\0${stdout}\n\0
qui est ensuite redirigé vers la sortie standard.
Ensuite, la commande
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
commence à lire la chaîne de caractères ${stderr}\n\0${stdout}\n\0
jusqu'au premier NUL
et enregistre le contenu dans ${CAPTURED_STDERR}
. Ensuite, la commande
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
continue à lire la même chaîne jusqu'à la prochaine NUL
et enregistre le contenu dans ${CAPTURED_STDOUT}
.
4. Le rendre incassable
La solution ci-dessus repose sur un NUL
octet pour le délimiteur entre stderr
y stdout
par conséquent, il ne fonctionnera pas si pour une raison quelconque stderr
contient d'autres NUL
octets.
Bien que cela ne devrait jamais se produire, il est possible de rendre le script complètement incassable en supprimant tous les possibles NUL
octets de stdout
y stderr
avant de passer les deux sorties à read
(désinfection) - NUL
Les octets se perdent de toute façon, car il n'est pas possible de les stocker dans des variables shell :
{
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
} < <((printf '\0%s\0' "$((some_command | tr -d '\0') 3>&1- 1>&2- 2>&3- | tr -d '\0')" 1>&2) 2>&1)
Exige : printf
, read
, tr
EDIT
J'ai supprimé un autre exemple de propagation de l'état de sortie au shell courant, car, comme Andy a fait remarquer dans les commentaires, il n'était pas aussi "incassable" qu'il était censé l'être (puisqu'il n'utilisait pas de système de contrôle de la qualité de l'air). printf
pour mettre en mémoire tampon l'un des flux). Pour mémoire, je colle le code problématique ici :
Préservation de l'état de sortie (toujours insécable)
La variante suivante propage également l'état de sortie de l'utilisateur. some_command
au shell actuel :
{
IFS= read -r -d '' CAPTURED_STDOUT;
IFS= read -r -d '' CAPTURED_STDERR;
(IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr -d '\0'; printf '\0'; } 2>&1- 1>&4- | tr -d '\0' 1>&4-) 3>&1- | xargs printf '\0%s\0' 1>&4-) 4>&1-)
Exige : printf
, read
, tr
, xargs
Andy a ensuite soumis la "modification suggérée" suivante pour capturer le code de sortie :
Solution simple et propre permettant d'économiser la valeur de sortie
Nous pouvons ajouter à la fin de stderr
un troisième élément d'information, un autre NUL
plus le exit
le statut de la commande. Il sera affiché après stderr
mais avant stdout
{
IFS= read -r -d '' CAPTURED_STDERR;
IFS= read -r -d '' CAPTURED_EXIT;
IFS= read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\n\0' "$(some_command; printf '\0%d' "${?}" 1>&2)" 1>&2) 2>&1)
Sa solution semble fonctionner, mais présente le problème mineur que l'état de sortie devrait être placé comme le dernier fragment de la chaîne, de sorte que nous puissions lancer exit "${CAPTURED_EXIT}"
entre parenthèses et ne pas polluer la portée globale, comme j'avais essayé de le faire dans l'exemple supprimé. L'autre problème est que, comme la sortie de son fichier interne printf
est immédiatement ajouté à la stderr
de some_command
nous ne pouvons plus assainir les possibles NUL
octets en stderr
car parmi ceux-ci, il y a aussi notre NUL
délimiteur.
5. Préservation de l'état de sortie - le schéma directeur (sans assainissement)
Après avoir réfléchi un peu à l'approche ultime, j'ai trouvé une solution qui utilise printf
pour le cache les deux stdout
et le code de sortie comme deux arguments différents, afin qu'ils n'interfèrent jamais.
La première chose que j'ai faite a été d'esquisser un moyen de communiquer l'état de sortie au troisième argument de la fonction printf
et il s'agissait d'une opération très facile à réaliser dans sa forme la plus simple (c'est-à-dire sans désinfection).
{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
(IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(some_command)" "${?}" 1>&2) 2>&1)
Exige : exit
, printf
, read
6. Préservation de l'état de sortie avec sanitization - incassable (réécrit)
Les choses se gâtent cependant lorsque nous essayons d'introduire la désinfection. Lancement de tr
pour assainir les flux écrase en fait notre état de sortie précédent, donc apparemment la seule solution est de rediriger ce dernier vers un descripteur séparé avant qu'il ne soit perdu, de le garder là jusqu'à ce que tr
fait son travail deux fois, puis le redirige à sa place.
Après quelques redirections assez acrobatiques entre les descripteurs de fichiers, voici ce que j'ai obtenu.
Le code ci-dessous est une réécriture de l'exemple que j'ai supprimé. Il nettoie également les éventuels NUL
octets dans les flux, de sorte que read
peut toujours fonctionner correctement.
{
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
(IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ some_command; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
Exige : exit
, printf
, read
, tr
Cette solution est vraiment robuste. Le code de sortie est toujours gardé séparé dans un descripteur différent jusqu'à ce qu'il atteigne printf
directement comme un argument distinct.
7. La solution ultime - une fonction d'usage général avec statut de sortie
Nous pouvons également transformer le code ci-dessus en une fonction d'usage général.
# SYNTAX:
# catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch() {
{
IFS=$'\n' read -r -d '' "${1}";
IFS=$'\n' read -r -d '' "${2}";
(IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
}
Exige : cat
, exit
, printf
, read
, tr
Avec le catch
nous pouvons lancer le snippet suivant,
catch MY_STDOUT MY_STDERR './useless.sh'
echo "The \`./useless.sh\` program exited with code ${?}"
echo
echo 'Here is the captured stdout:'
echo "${MY_STDOUT}"
echo
echo 'And here is the captured stderr:'
echo "${MY_STDERR}"
echo
et obtenir le résultat suivant :
The `./useless.sh` program exited with code 0
Here is the captured stdout:
This is stderr 1
This is stderr 2
And here is the captured stderr:
This is stdout 1
This is stdout 2
8. Ce qui se passe dans les derniers exemples
Voici une schématisation rapide :
-
some_command
est lancé : on a alors some_command
's stdout
sur le descripteur 1, some_command
's stderr
sur le descripteur 2 et some_command
Le code de sortie de l'utilisateur est redirigé vers le descripteur 3.
-
stdout
est acheminé vers tr
(assainissement)
-
stderr
est échangé avec stdout
(en utilisant temporairement le descripteur 4) et transmis en pipeline à tr
(assainissement)
- le code de sortie (descripteur 3) est échangé avec
stderr
(maintenant descripteur 1) et transmis à exit $(cat)
-
stderr
(maintenant le descripteur 3) est redirigé vers le descripteur 1, fin développée comme second argument de printf
- le code de sortie de
exit $(cat)
est capturé par le troisième argument de printf
- la sortie de
printf
est redirigée vers le descripteur 2, où stdout
était déjà présent
- la concaténation de
stdout
et la sortie de printf
est acheminé vers read
9. La version #1 conforme à POSIX (cassable)
Substitutions de processus (le < <()
) ne sont pas conformes à la norme POSIX (bien qu'ils de facto sont). Dans un shell qui ne prend pas en charge l'option < <()
syntaxe, la seule façon d'arriver au même résultat est de passer par la <<EOF … EOF
syntaxe. Malheureusement, cela ne nous permet pas d'utiliser NUL
comme délimiteurs, parce que ceux-ci sont automatiquement supprimés avant d'atteindre read
. Nous devons utiliser un autre délimiteur. Le choix naturel se porte sur le CTRL+Z
(caractère ASCII n° 26). Voici un cassable (les sorties ne doivent jamais contenir le CTRL+Z
sinon ils seront mélangés).
_CTRL_Z_=$'\cZ'
{
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDERR;
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDOUT;
(IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; exit ${_ERRNO_});
} <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(some_command)" "${?}" 1>&2) 2>&1)
EOF
Exige : exit
, printf
, read
10. La version #2 conforme à POSIX (incassable, mais pas aussi bonne que la version non-POSIX)
Et voici sa version incassable, directement en forme de fonction (si l'un ou l'autre stdout
o stderr
contiennent CTRL+Z
caractères, le flux sera tronqué, mais ne sera jamais échangé avec un autre descripteur).
_CTRL_Z_=$'\cZ'
# SYNTAX:
# catch_posix STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch_posix() {
{
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${1}";
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${2}";
(IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; return ${_ERRNO_});
} <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(((({ ${3}; echo "${?}" 1>&3-; } | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 4>&2- 2>&1- | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
EOF
}
Exige : cat
, cut
, exit
, printf
, read
, tr