Speichern von Standardfehlern in einer Variablen


179

Angenommen, ich habe ein Skript wie das folgende:

nutzlos.sh

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

Und ich habe noch ein Shell-Skript:

auchUseless.sh

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

Ich möchte "This Is Error" oder einen anderen stderr aus nutzlos.sh in einer Variablen erfassen. Nennen wir es FEHLER.

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

Also im Grunde möchte ich tun

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

aber das funktioniert offensichtlich nicht.

Ich weiß auch, dass ich es tun 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 bessere Ideen?


4
Wofür genau soll stdout verwendet werden? Möchten Sie es einfach auf der Konsole anzeigen? Oder erfassen / leiten Sie die Ausgabe um? Wenn es nur um die Konsole geht, leiten Sie stdout zur Konsole und stderr zur stdout um, um es zu erfassen:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Antworten:


90

Es wäre ordentlicher, die Fehlerdatei folgendermaßen zu erfassen:

ERROR=$(</tmp/Error)

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

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

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

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

WARNUNG: Formal nicht getesteter Code - Verwendung auf eigenes Risiko.


1
Ich hatte gehofft, dass es einen wirklich verrückten Trick geben würde, den ich nicht kannte, aber es sieht so aus, als wäre es das. Vielen Dank.
Psycotica0

9
Wenn Sie die Standardausgabe nicht benötigen, können Sie sie /dev/nullstattdessen umleiten zu outfile(Wenn Sie wie ich sind, haben Sie diese Frage über Google gefunden und haben nicht die gleichen Anforderungen wie das OP)
Mark Eirich

2
Eine Antwort ohne temporäre Dateien finden Sie hier .
Tom Hale

1
Hier ist eine Möglichkeit, dies zu tun, ohne es in Dateien umzuleiten. es spielt mit tauschen stdoutund stderrhin und her. Aber Vorsicht , wie hier gesagt wird: In Bash ist es besser, nicht anzunehmen, dass Dateideskriptor 3 nicht verwendet wird " .
Golar Ramblar

69

auchUseless.sh

Auf diese Weise können Sie die Ausgabe Ihres useless.shSkripts über einen Befehl wie beispielsweise sedleiten und stderrin einer Variablen mit dem Namen speichern error. Das Ergebnis der Pipe wird stdoutzur Anzeige gesendet oder an einen anderen Befehl weitergeleitet.

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

4
Es ist eine gute Technik, 'exec' zum Festlegen und Schließen von Dateideskriptoren zu verwenden. Das Schließen wird nicht wirklich benötigt, wenn das Skript unmittelbar danach beendet wird.
Jonathan Leffler

3
Wie würde ich beide stderrund stdoutin Variablen erfassen ?
Gingi

Ausgezeichnet. Dies hilft mir, eine dry_runFunktion zu implementieren , die zuverlässig zwischen dem Echo ihrer Argumente und deren Ausführung wählen kann, unabhängig davon, ob der Befehl, der trocken ausgeführt wird, an eine andere Datei weitergeleitet wird.
Mihai Danila

1
@ t00bs: Akzeptiert readkeine Eingaben von einer Pipe. Sie können andere Techniken verwenden, um das zu erreichen, was Sie demonstrieren möchten.
Bis auf weiteres angehalten.

2
Könnte einfacher sein, mit: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn

64

Umgeleitetes stderr nach stdout, stdout nach / dev / null und dann die Backticks verwenden oder $()das umgeleitete stderr erfassen:

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

7
Aus diesem Grund habe ich die Pipe in mein Beispiel aufgenommen. Ich möchte immer noch die Standardausgabe, und ich möchte, dass sie andere Dinge tut und an andere Orte geht.
Psycotica0

Für Befehle , den Sendeausgang auf stderr nur, erfaßt die einfache Art und Weise ist es zum BeispielPY_VERSION="$(python --version 2>&1)"
John Mark

9

Es gibt viele Duplikate für diese Frage, von denen viele ein etwas einfacheres Verwendungsszenario haben, in dem Sie nicht gleichzeitig stderr und stdout und den Exit-Code 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 im Erfolgsfall entweder eine ordnungsgemäße Ausgabe oder im Fehlerfall eine Diagnosemeldung auf stderr erwarten.

Beachten Sie, dass die Steueranweisungen der Shell bereits $?unter der Haube geprüft werden . also alles was aussieht

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

ist nur eine ungeschickte, unidiomatische Art zu sagen

if cmd; then ...

Das hat bei mir funktioniert: my_service_status = $ (service my_service status 2> & 1) Danke !!
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}

1
commandist hier eine schlechte Wahl, da es tatsächlich einen eingebauten Namen gibt. Könnte es yourCommandoder so machen, um expliziter zu sein.
Charles Duffy

4

Zum Nutzen des Lesers dieses Rezept hier

  • kann als Oneliner wiederverwendet werden, um stderr in eine Variable zu fangen
  • gibt weiterhin Zugriff auf den Rückkehrcode des Befehls
  • Opfert einen temporären Dateideskriptor 3 (der natürlich von Ihnen geändert werden kann)
  • Und setzt diese temporären Dateideskriptoren nicht dem inneren Befehl aus

Wenn Sie fangen wollen stderreinige commandin varkönnen Sie tun

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

Danach haben Sie alles:

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

Wenn commandes einfach ist (nicht so ähnlich a | b), können Sie das Innere {}weglassen:

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

Eingewickelt in eine einfach wiederverwendbare Funktion bash(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="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }

Erklärt:

  • local -nAliase "$ 1" (das ist die Variable für catch-stderr)
  • 3>&1 verwendet den Dateideskriptor 3, um dort Standardpunkte zu speichern
  • { command; } (oder "$ @") führt dann den Befehl innerhalb der Ausgabeerfassung aus $(..)
  • Bitte beachten Sie, dass die genaue Reihenfolge hier wichtig ist (wenn Sie die Dateideskriptoren falsch machen, werden die Dateideskriptoren falsch gemischt):
    • 2>&1leitet stderrzur Ausgabeerfassung weiter$(..)
    • 1>&3leitet stdoutvon der Ausgabeerfassung $(..)zurück zu der "äußeren", stdoutdie in Dateideskriptor 3 gespeichert wurde. Beachten Sie, dass sich stderrimmer noch darauf bezieht, wo FD 1 zuvor gezeigt hat: Zur Ausgabeerfassung$(..)
    • 3>&-schließt dann den Dateideskriptor 3, da er nicht mehr benötigt wird, so dass commandnicht plötzlich ein unbekannter offener Dateideskriptor angezeigt wird. Beachten Sie, dass in der Außenhülle FD 3 noch geöffnet ist, es jedoch commandnicht angezeigt wird.
    • Letzteres ist wichtig, da sich einige Programme gerne lvmüber unerwartete Dateideskriptoren beschweren. Und lvmbeschwert sich bei stderr- genau das, was wir einfangen werden!

Sie können mit diesem Rezept jeden anderen Dateideskriptor abfangen, wenn Sie ihn entsprechend anpassen. Außer Dateideskriptor 1 natürlich (hier wäre die Umleitungslogik falsch, aber für Dateideskriptor 1 können Sie einfach var=$(command)wie gewohnt 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 Muscheln (aus den 1980er Jahren) möglicherweise 99>&1als Argument verstanden werden, 9gefolgt von 9>&1(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. Das 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 '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}

Sicherheitshinweis: Die ersten 3 Argumente catch-var-from-fd-by-fddürfen nicht von Dritten stammen. Geben Sie sie immer explizit "statisch".

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

Wenn Sie zufällig einen variablen Variablennamen ü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 aber zumindest dabei, häufige Skriptfehler zu erkennen und zu vermeiden.

Anmerkungen:

  • catch-var-from-fd-by-fd var 2 3 cmd.. ist das gleiche wie catch-stderr var cmd..
  • shift || returnDies ist nur eine Möglichkeit, hässliche Fehler zu vermeiden, falls Sie vergessen, die richtige Anzahl von Argumenten anzugeben. Vielleicht 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 nicht benötigt wird exec, aber dann wird sie wirklich hässlich.
  • Diese Routine kann auch für Nicht-Routine umgeschrieben werden, bashsodass keine Notwendigkeit besteht local -n. 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. Wird normalerweise evalals gefährlich angesehen. In diesem Fall ist es jedoch nicht böser als die Verwendung "$@"(zum Ausführen beliebiger Befehle). Bitte achten Sie jedoch darauf, das genaue und korrekte Zitat wie hier gezeigt zu verwenden (sonst wird es sehr, sehr gefährlich ).

3

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 wird eine temporäre Datei verwendet. Aber zumindest das hässliche Zeug ist in eine Funktion eingewickelt.


@ ShadowWizard Wenig Zweifel auf meiner Seite. Im Französischen steht vor dem Doppelpunkt normalerweise ein Leerzeichen. Ich wende diese Regel fälschlicherweise mit englischen Antworten an. Nachdem ich dies überprüft habe , weiß ich, dass ich diesen Fehler nicht noch einmal machen werde.
Stephan

@Stephan prost, hat dies auch diskutiert hier . :)
Schatten-Assistent ist Ohr für Sie

1
Es gibt sicherere Möglichkeiten, dies zu tun als die Verwendung eval. Es besteht beispielsweise printf -v "$1" '%s' "$(<tmpFile)"kein Risiko, beliebigen Code auszuführen, wenn Ihre TMPDIRVariable auf einen schädlichen Wert festgelegt wurde (oder der Name Ihrer Zielvariablen einen solchen Wert enthält).
Charles Duffy

1
Ebenso rm -- "$tmpFile"ist robuster als rm $tmpFile.
Charles Duffy

2

Dies ist ein interessantes Problem, für das ich gehofft habe, dass es eine elegante Lösung gibt. Leider habe ich eine ähnliche Lösung wie Herr Leffler, aber ich möchte hinzufügen, dass Sie innerhalb einer Bash-Funktion für eine bessere Lesbarkeit nutzlos aufrufen können:

#! / bin / bash

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

FEHLER = $ (nutzlos)
echo $ ERROR

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


2

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 die Weiterleitung von STDOUT des Befehls (hier ls) innerhalb des Innersten erfolgt { }. Wenn Sie einen einfachen Befehl ausführen (z. B. keine Pipe), können Sie diese inneren Klammern entfernen.

Sie können nicht außerhalb des Befehls weiterleiten , da durch Piping eine Unterschale in bashund erstellt zshwird und die Zuweisung zur Variablen in der Unterschale für die aktuelle Shell nicht verfügbar ist.

Bash

In bashist es besser, nicht anzunehmen, 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 nicht funktioniert zsh.


Vielen Dank an diese Antwort für die allgemeine Idee.


1

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 Personen weiter. Dadurch erfahren wir, ob unsere format_logs.py mit einer Python-Ausnahme fehlgeschlagen ist.


1

Erfassen UND Drucken stderr

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

Nervenzusammenbruch

Sie können $()stdout erfassen, möchten aber stattdessen stderr erfassen. Sie tauschen also stdout und stderr aus. Verwendung von fd 3 als temporärer Speicher im Standard-Swap-Algorithmus.

Wenn Sie erfassen UND drucken möchten, verwenden Sie tee, um ein Duplikat zu erstellen. In diesem Fall wird die Ausgabe von teeerfasst $()und nicht an die Konsole gesendet, aber stderr (of tee) wird weiterhin an die Konsole gesendet, sodass wir diese als zweite Ausgabe teeüber die spezielle Datei verwenden, /dev/fd/2da teeein Dateipfad anstelle eines fd erwartet wird Nummer.

HINWEIS: Das sind sehr viele Weiterleitungen in einer einzelnen Zeile, und die Reihenfolge ist wichtig. $()greift nach dem Standard von teeam Ende der Pipeline und die Pipeline selbst leitet den Standard von ./useless.shzum Standard von teeAFTER, nachdem wir Standard und Standard ausgetauscht haben ./useless.sh.

Verwenden von stdout von ./useless.sh

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

Kein Problem, tun Sie es einfach, bevor Sie stdout und stderr tauschen. Ich empfehle, es in eine Funktion oder Datei (also-useless.sh) zu verschieben und dies anstelle von ./useless.sh in der obigen Zeile aufzurufen.

Wenn Sie jedoch stdout UND stderr erfassen möchten, müssen Sie meiner Meinung nach auf temporäre Dateien zurückgreifen, da jeweils $()nur eine ausgeführt wird und eine Unterschale erstellt wird, von der Sie keine Variablen zurückgeben können.


1

Ich habe ein wenig über Tom Hales Antwort nachgedacht und festgestellt, dass es möglich ist, das Umleitungs-Yoga in eine Funktion zu verpacken, um die Wiederverwendung zu vereinfachen. Beispielsweise:

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

Es ist mit ziemlicher Sicherheit möglich, dies weiter zu vereinfachen. Ich habe nicht besonders gründlich getestet, aber es scheint sowohl mit Bash als auch mit Ksh zu funktionieren.


0

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 macht also im Allgemeinen das Richtige ... Leider verliere ich den Inhalt von , wenn ich STDIN >( )mit etwas einpacke $(), um dies in einer Variablen zu erfassen $(). Ich denke, das liegt daran, dass $()ein Unterprozess gestartet wird, der keinen Zugriff mehr auf den Dateideskriptor in / dev / fd hat, der dem übergeordneten Prozess gehört.

Durch die Prozessersetzung habe ich die Möglichkeit erhalten, mit einem Datenstrom zu arbeiten, der nicht mehr in STDERR enthalten ist. Leider kann ich ihn nicht so bearbeiten, wie ich es möchte.


1
Wenn Sie dies tun ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" )würden, würden Sie die Ausgabe von sehen ERROR. Das Problem besteht darin, dass die Prozessersetzung in einer Unter-Shell ausgeführt wird, sodass der in der Unter-Shell festgelegte Wert die übergeordnete Shell nicht beeinflusst.
Jonathan Leffler

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

3
Das sieht nach einer guten Idee aus, aber unter Mac OS X 10.8.5 wird es gedruckta=> b=>stderr
Heath Borders

3
Ich stimme @HeathBorders zu. Dies erzeugt nicht die gezeigte Ausgabe. Das Problem hierbei ist, dass aes in einer Sub-Shell ausgewertet und zugewiesen wird und die Zuweisung in der Sub-Shell die übergeordnete Shell nicht beeinflusst. (Getestet unter Ubuntu 14.04 LTS sowie Mac OS X 10.10.1.)
Jonathan Leffler

Das gleiche gilt für Windows GitBash. Es funktioniert also nicht. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby

Funktioniert auch nicht SLE 11.4und erzeugt den von @JonathanLeffler
smarber

Während dieser Code die Frage möglicherweise beantwortet, verbessert die Bereitstellung eines zusätzlichen Kontexts darüber, warum und / oder wie dieser Code die Frage beantwortet, ihren langfristigen Wert.
β.εηοιτ.βε

0

In zsh:

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

0

Zum Fehlerprüfen Ihrer 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
}

Inspiriert von Lean Manufacturing:


Die idiomatische Lösung besteht darin, die Zuordnung innerhalb der if. Lassen Sie mich eine separate Lösung veröffentlichen.
Tripleee


0

Verbesserung der Antwort von YellowApple :

Dies ist eine Bash-Funktion zum Erfassen von stderr in einer beliebigen Variablen

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"

Testen:

bash stderr_capture_example.sh

Ausgabe:

 stderr_capture_example.sh

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

Diese Funktion kann verwendet werden, um die zurückgegebene Auswahl eines dialogBefehls zu erfassen .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.