it-swarm.com.de

Wie wird ein Standardfehler in einer Variablen in einem Bash-Skript gespeichert?

Nehmen wir an, ich habe ein Skript wie das folgende:

nutzlos.sh

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

Und ich habe ein anderes Shell-Skript:

auchUseless.sh

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

Ich möchte "This Is Error" oder einen beliebigen anderen stderr von useless.sh in eine Variable einfangen. Nennen wir es FEHLER.

Beachten Sie, dass ich stdout für etwas verwende. Ich möchte stdout weiterhin verwenden, daher ist das Umleiten von stderr in stdout in diesem Fall nicht hilfreich.

Im Grunde möchte ich das tun

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

aber das funktioniert offensichtlich nicht.

Ich weiß auch, dass ich das könnte

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

aber das ist hässlich und unnötig.

Wenn hier keine Antworten auftauchen, muss ich das leider tun.

Ich hoffe, es gibt einen anderen Weg.

Hat jemand eine bessere Idee?

146
psycotica0

Es wäre netter, die Fehlerdatei so zu erfassen:

ERROR=$(</tmp/Error)

Die Shell erkennt dies und muss 'cat' nicht ausführen, um die Daten abzurufen.

Die größere Frage ist schwer. Ich glaube nicht, dass es einen einfachen Weg gibt. Sie müssen die gesamte Pipeline in die Sub-Shell integrieren und schließlich die endgültige Standardausgabe an eine Datei senden, damit Sie die Fehler zur Standardausgabe umleiten können.

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

Beachten Sie, dass das Semikolon benötigt wird (in klassischen Shells - Bourne, Korn - sicherlich; wahrscheinlich auch in Bash). Der '{}' führt eine E/A-Umleitung über die eingeschlossenen Befehle aus. Wie geschrieben, würde es auch Fehler von sed erfassen.

WARNUNG: Formal ungeprüfter Code - Verwendung auf eigene Gefahr.

73

auchUseless.sh

Auf diese Weise können Sie die Ausgabe Ihres useless.sh-Skripts über einen Befehl wie sed übergeben und die stderr in einer Variablen mit dem Namen error speichern. Das Ergebnis der Pipe wird an stdout gesendet, um angezeigt oder in einen anderen Befehl geleitet zu werden.

Es werden einige zusätzliche Dateideskriptoren eingerichtet, um die dafür erforderlichen Umleitungen zu verwalten.

#!/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 an stdout, stdout an/dev/null umgeleitet und dann mit den Backticks oder $() den umgeleiteten stderr erfassen

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

Für diese Frage gibt es viele Duplikate, von denen viele ein etwas einfacheres Verwendungsszenario haben, in dem Sie nicht stderr und stdout und den Exit-Code gleichzeitig erfassen möchten.

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

funktioniert für das allgemeine Szenario, in dem Sie entweder eine korrekte Ausgabe im Erfolgsfall oder eine Diagnosemeldung auf stderr im Fehlerfall erwarten.

Beachten Sie, dass die Steueranweisungen der Shell $? bereits unter der Haube prüfen. also alles was aussieht

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

ist nur eine ungeschickte, unidiomatische Art zu sagen

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

So habe ich es gemacht:

#
# $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
}

Anwendungsbeispiel:

captureStderr err "./useless.sh"

echo -$err-

Es tut verwendet eine temporäre Datei. Aber zumindest das hässliche Zeug ist in eine Funktion gehüllt.

3
tfga

Zum Wohle des Lesers ist dieses Rezept hier

  • kann als oneliner wiederverwendet werden, um stderr in eine Variable einzufangen
  • ermöglicht weiterhin den Zugriff auf den Rückkehrcode des Befehls
  • Verzichtet auf einen temporären Dateideskriptor 3 (der natürlich von Ihnen geändert werden kann)
  • Und macht diese temporären Dateideskriptoren nicht für den inneren Befehl verfügbar

Wenn Sie stderr von einigen command in var fangen möchten, können Sie dies tun

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

Danach haben Sie alles:

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

Wenn command einfach ist (nicht so etwas wie a | b), können Sie den inneren {} weglassen:

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

Eingewickelt in eine einfach wiederverwendbare bash- Funktion (benötigt wahrscheinlich Version 3 und höher für local -n):

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

Erklärt:

  • local -n Aliase "$ 1" (das ist die Variable für catch-stderr)
  • 3>&1 verwendet den Dateideskriptor 3, um die Standardpunkte zu speichern
  • { command; } (oder "$ @") führt dann den Befehl innerhalb der Ausgabe aus und erfasst $(..)
  • Bitte beachten Sie, dass die genaue Reihenfolge hier wichtig ist (falsches Vorgehen mischt die Dateideskriptoren falsch):
    • 2>&1 leitet stderr zur Ausgabe um, die $(..) erfasst
    • 1>&3 leitet stdout von der Ausgabeerfassung $(..) zurück zu dem "äußeren" stdout, der in Dateideskriptor 3 gespeichert wurde. Beachten Sie, dass stderr weiterhin auf verweist Wobei FD 1 zuvor zeigte: Auf den Ausgang, der $(..) aufzeichnet
    • 3>&- schließt dann den Dateideskriptor 3, da dieser nicht mehr benötigt wird, so dass command nicht plötzlich einen unbekannten offenen Dateideskriptor anzeigt. Beachten Sie, dass in der äußeren Shell noch FD 3 geöffnet ist, command es jedoch nicht erkennt.
    • Letzteres ist wichtig, da sich einige Programme wie lvm über unerwartete Dateideskriptoren beschweren. Und lvm beschwert sich bei stderr - genau das, was wir einfangen werden!

Sie können mit diesem Rezept jeden anderen Dateideskriptor abfangen, wenn Sie ihn entsprechend anpassen. Ausgenommen natürlich Dateideskriptor 1 (hier wäre die Umleitungslogik falsch, aber für Dateideskriptor 1 können Sie einfach wie gewohnt var=$(command) verwenden).

Beachten Sie, dass dies den Dateideskriptor 3 opfert. Wenn Sie diesen Dateideskriptor benötigen, können Sie die Nummer jederzeit ändern. Beachten Sie jedoch, dass einige Shells (aus den 1980er Jahren) 99>&1 möglicherweise als Argument 9 gefolgt von 9>&1 verstehen (dies ist kein Problem für bash).

Beachten Sie auch, dass es nicht besonders einfach ist, diesen FD 3 über eine Variable konfigurierbar zu machen. Dies macht die Dinge sehr unlesbar:

: 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 '"[email protected]"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}

Sicherheitshinweis: Die ersten 3 Argumente zu catch-var-from-fd-by-fd dürfen nicht von Dritten übernommen werden. Geben Sie sie immer explizit "statisch" an.

Also nein-nein-nein catch-var-from-fd-by-fd $var $fda $fdb $command, mach das niemals!

Wenn Sie zufällig eine Variable Variablenname übergeben, gehen Sie mindestens wie folgt vor: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

Dies schützt Sie immer noch nicht vor jedem Exploit, hilft jedoch, häufig auftretende Skriptfehler zu erkennen und zu vermeiden.

Anmerkungen:

  • catch-var-from-fd-by-fd var 2 3 cmd.. ist dasselbe wie catch-stderr var cmd..
  • shift || return ist nur eine Möglichkeit, hässliche Fehler zu vermeiden, falls Sie vergessen, die richtige Anzahl von Argumenten anzugeben. Möglicherweise wäre das Beenden der Shell eine andere Möglichkeit (dies erschwert jedoch das Testen über die Befehlszeile).
  • Die Routine wurde so geschrieben, dass sie leichter zu verstehen ist. Man kann die Funktion so umschreiben, dass sie exec nicht benötigt, aber dann wird es wirklich hässlich.
  • Diese Routine kann auch für nonbash umgeschrieben werden, sodass local -n nicht erforderlich ist. Dann können Sie jedoch keine lokalen Variablen verwenden und es wird extrem hässlich!
  • Beachten Sie auch, dass die evals auf sichere Weise verwendet werden. Normalerweise wird eval als gefährlich angesehen. In diesem Fall ist es jedoch nicht schlimmer als die Verwendung von "[email protected]" (um beliebige Befehle auszuführen). Bitte achten Sie jedoch darauf, dass Sie die genauen und korrekten Anführungszeichen wie hier angegeben verwenden (sonst wird es sehr sehr gefährlich).
2
Tino

Dies ist ein interessantes Problem, auf das ich gehofft hatte, dass es eine elegante Lösung gab. Leider habe ich eine Lösung, die Mr. Leffler ähnelt, aber ich füge hinzu, dass Sie aus einer Bash-Funktion für eine bessere Lesbarkeit unbrauchbar machen können:

 #!/bin/bash 

 Funktion nutzlos {
 /tmp/useless.sh | sed 's/Output/Useless /' 
}

 ERROR = $ (unbrauchbar) 
 echo $ ERROR 

Alle anderen Arten der Ausgabeumleitung müssen durch eine temporäre Datei gesichert werden. 

2
FD Gonthier

Erfassen und Drucken von Dateien

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

Nervenzusammenbruch

Sie können $() verwenden, um stdout zu erfassen, Sie möchten jedoch stattdessen stderr erfassen. Sie tauschen also stdout und stderr. Verwenden von fd 3 als temporärer Speicher im Standard-Swap-Algorithmus.

Wenn Sie UND erfassen möchten, verwenden Sie tee, um ein Duplikat zu erstellen. In diesem Fall wird die Ausgabe von tee von $() erfasst und nicht zur Konsole geleitet. Stderr (von tee) wird jedoch weiterhin in die Konsole gehen. Daher verwenden wir tee als zweite Ausgabe über die spezielle Datei /dev/fd/2, da tee ein erwartet Dateipfad statt fd-Nummer.

HINWEIS: Dies ist eine sehr große Anzahl von Weiterleitungen in einer einzigen Zeile, und die Reihenfolge ist wichtig. $() greift das stdout von tee am Ende der Pipeline und die Pipeline selbst leitet stdout von ./useless.sh an das stdin von tee, nachdem wir stdin und stdout gegen ./useless.sh ausgetauscht haben.

stdout von ./useless.sh verwenden

Das OP sagte, er wollte immer noch stdout verwenden (nicht nur drucken), wie ./useless.sh | sed 's/Output/Useless/'.

Kein Problem, bevor Sie stdout und stderr austauschen. Ich empfehle, es in eine Funktion oder Datei (auch-useless.sh) zu verschieben und diese anstelle von ./useless.sh in der Zeile oben aufzurufen.

Wenn Sie jedoch stdout UND stderr erfassen möchten, müssen Sie auf temporäre Dateien zurückgreifen, da $() jeweils nur eine ausführt und eine Subshell erstellt, aus der Sie keine Variablen zurückgeben können.

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

Dieser Beitrag hat mir geholfen, eine ähnliche Lösung für meine eigenen Zwecke zu finden:

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

Solange unsere NACHRICHT keine leere Zeichenfolge ist, geben wir sie an andere Sachen weiter. Dies lässt uns wissen, ob unser format_logs.py mit einer Art Python-Ausnahme fehlgeschlagen ist.

1
palmbardier

Wenn Sie die Verwendung einer temporären Datei umgehen möchten, können Sie möglicherweise die Prozessersetzung verwenden. Ich habe es noch nicht ganz zum Laufen gebracht. Dies war mein erster Versuch:

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

Dann habe ich es versucht

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

Jedoch

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

Die Prozessersetzung ist also im Allgemeinen das Richtige ... Unglücklicherweise, wenn ich STDIN in >( ) mit etwas in $() einwickle, um die Variable zu erfassen, verliere ich den Inhalt von $(). Ich denke, das liegt daran, dass $() einen Unterprozess startet, der keinen Zugriff mehr auf den Dateideskriptor in/dev/fd hat, der dem übergeordneten Prozess gehört.

Die Prozessersetzung hat mir die Fähigkeit erarbeitet, mit einem Datenstrom zu arbeiten, der nicht mehr in STDERR enthalten ist. Leider kann ich ihn leider nicht so manipulieren, wie ich möchte.

0

In zsh:

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

POSIX

STDERR kann mit etwas Umleitungsmagie erfasst werden:

$ { 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

Beachten Sie, dass das Piping von STDOUT des Befehls (hier ls) im innersten {} erfolgt. Wenn Sie einen einfachen Befehl ausführen (z. B. keine Pipe), können Sie diese inneren Klammern entfernen.

Sie können keine Pipe außerhalb des Befehls ausführen, da Piping in bash und zsh eine Subshell erstellt und die Zuweisung zu der Variablen in der Subshell für die aktuelle Shell nicht verfügbar ist.

bash

In bash sollte man nicht davon ausgehen, dass der Dateideskriptor 3 nicht verwendet wird:

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

Beachten Sie, dass dies in zsh nicht funktioniert.


Danke an diese Antwort für die allgemeine Idee.

0
Tom Hale

Für error proofing Ihre Befehle:

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 in Lean Manufacturing:

Eine einfache Lösung

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

Wird herstellen:

This Is Output
-
This Is Error
0
Karl Morrison