Come memorizzare l'errore standard in una variabile


182

Diciamo che ho una sceneggiatura come la seguente:

useless.sh

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

E ho un altro script di shell:

alsoUseless.sh

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

Voglio catturare "This Is Error", o qualsiasi altro stderr da useless.sh, in una variabile. Chiamiamolo ERRORE.

Nota che sto usando stdout per qualcosa. Voglio continuare a utilizzare stdout, quindi reindirizzare stderr a stdout non è utile, in questo caso.

Quindi, fondamentalmente, voglio fare

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

ma ovviamente non funziona.

So anche che potrei fare

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

ma è brutto e inutile.

Sfortunatamente, se non si presentano risposte qui, è quello che dovrò fare.

Spero che ci sia un altro modo.

Qualcuno ha idee migliori?


4
Per cosa usare esattamente stdout? Vuoi semplicemente vederlo sulla console? Oppure stai acquisendo / reindirizzando il suo output? Se è solo per la console reindirizzare stdout a console e stderr a stdout per catturarlo:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Risposte:


91

Sarebbe più ordinato acquisire il file di errore in questo modo:

ERROR=$(</tmp/Error)

La shell lo riconosce e non deve eseguire ' cat' per ottenere i dati.

La domanda più grande è difficile. Non penso che ci sia un modo semplice per farlo. Dovresti compilare l'intera pipeline nella sotto-shell, eventualmente inviando l'output standard finale a un file, in modo da poter reindirizzare gli errori all'output standard.

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

Nota che il punto e virgola è necessario (nelle conchiglie classiche - Bourne, Korn - di sicuro; probabilmente anche in Bash). ' {}' Esegue il reindirizzamento I / O sui comandi allegati. Come scritto, catturerebbe anche errori da sed.

ATTENZIONE: codice ufficialmente non testato: utilizzare a proprio rischio.


1
Avevo sperato che ci sarebbe stato un trucco davvero pazzo che non conoscevo, ma sembra che sia così. Grazie.
psycotica0,

9
Se non hai bisogno dell'output standard, puoi reindirizzarlo /dev/nullinvece di outfile(Se sei come me, hai trovato questa domanda tramite Google e non hai gli stessi requisiti del PO)
Mark Eirich

2
Per una risposta senza file temporanei, vedere qui .
Tom Hale,

1
Ecco un modo per farlo senza reindirizzarlo ai file; gioca con lo scambio stdoute stderravanti e indietro. Ma attenzione , come si dice qui : In bash, sarebbe meglio non presumere che il descrittore di file 3 non sia usato " .
Golar Ramblar,

69

alsoUseless.sh

Ciò ti consentirà di reindirizzare l'output del tuo useless.shscript tramite un comando come sede salvare il stderrin una variabile denominata error. Il risultato del tubo viene inviato stdoutper la visualizzazione o per il piping in un altro comando.

Imposta un paio di descrittori di file extra per gestire i reindirizzamenti necessari per fare ciò.

#!/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
È una buona tecnica usare 'exec' per impostare e chiudere i descrittori di file. La chiusura non è davvero necessaria se lo script esce immediatamente dopo.
Jonathan Leffler,

3
Come vorrei acquisire sia stderre stdoutin variabili?
Gingi,

Eccellente. Questo mi aiuta a implementare una dry_runfunzione che può scegliere in modo affidabile tra l'eco dei suoi argomenti e la loro esecuzione, indipendentemente dal fatto che il comando che viene eseguito a secco venga convogliato in qualche altro file.
Mihai Danila,

1
@ t00bs: readnon accetta input da una pipe. Puoi utilizzare altre tecniche per ottenere ciò che stai cercando di dimostrare.
In pausa fino a nuovo avviso.

2
Potrebbe essere più semplice, con: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn

64

Reindirizzato stderr su stdout, stdout su / dev / null, quindi utilizzare i backtick o $()per acquisire lo stderr reindirizzato:

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

8
Questo è il motivo per cui ho incluso la pipe nel mio esempio. Voglio ancora l'output standard e voglio che faccia altre cose, che vada in altri posti.
psycotica0

Per i comandi che inviano output solo a stderr, il modo semplice per catturarlo è, ad esempioPY_VERSION="$(python --version 2>&1)"
John Mark,

9

Ci sono molti duplicati per questa domanda, molti dei quali hanno uno scenario di utilizzo leggermente più semplice in cui non si desidera catturare contemporaneamente stderr e stdout e il codice di uscita.

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

funziona per lo scenario comune in cui si prevede un corretto output in caso di successo o un messaggio diagnostico su stderr in caso di errore.

Si noti che le dichiarazioni di controllo della shell già esaminano $?sotto il cofano; quindi qualsiasi cosa assomigli

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

è solo un modo goffo e unidiomatico di dire

if cmd; then ...

Questo ha funzionato per me: my_service_status = $ (service my_service status 2> & 1) Grazie !!
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
commandè una cattiva scelta qui, in quanto esiste effettivamente un built-in con quel nome. Potrebbe renderlo yourCommando tale, per essere più esplicito.
Charles Duffy,

4

Per il beneficio del lettore, questa ricetta qui

  • può essere riutilizzato come oneliner per catturare stderr in una variabile
  • dà ancora accesso al codice di ritorno del comando
  • Sacrifica un descrittore di file temporaneo 3 (che può essere modificato da te ovviamente)
  • E non espone questi descrittori di file temporanei al comando interno

Se si desidera catturare stderrdi alcuni commandin varsi può fare

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

Dopo hai tutto:

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

Se commandè semplice (non qualcosa del genere a | b) puoi lasciare l'interno {}lontano:

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

Avvolto in una semplice funzione riutilizzabile bash(probabilmente necessita della versione 3 e successive per local -n):

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

Ha spiegato:

  • local -nalias "$ 1" (che è la variabile per catch-stderr)
  • 3>&1 usa il descrittore di file 3 per salvare lì punti stdout
  • { command; } (o "$ @") quindi esegue il comando all'interno della cattura dell'output $(..)
  • Si prega di notare che l'ordine esatto è importante qui (farlo nel modo sbagliato mescola erroneamente i descrittori di file):
    • 2>&1reindirizza stderralla cattura dell'output$(..)
    • 1>&3reindirizza stdoutlontano dall'output catturando $(..)indietro su "esterno" stdoutche è stato salvato nel descrittore di file 3. Nota che stderrfa ancora riferimento a dove FD 1 puntava prima: All'acquisizione output$(..)
    • 3>&-quindi chiude il descrittore di file 3 poiché non è più necessario, in modo tale che commandall'improvviso non compaiano alcuni descrittori di file aperti sconosciuti. Si noti che la shell esterna ha ancora FD 3 aperto, ma commandnon lo vedrà.
    • Quest'ultimo è importante, perché alcuni programmi come lvmlamentano descrittori di file imprevisti. E si lvmlamenta di stderr: proprio quello che cattureremo!

Puoi prendere qualsiasi altro descrittore di file con questa ricetta, se ti adatti di conseguenza. Tranne il descrittore di file 1 ovviamente (qui la logica di reindirizzamento sarebbe errata, ma per il descrittore di file 1 puoi semplicemente usare var=$(command)come al solito).

Nota che questo sacrifica il descrittore di file 3. Se ti capita di aver bisogno di quel descrittore di file, sentiti libero di cambiare il numero. Ma attenzione, alcune shell (dagli anni '80) potrebbero comprendere 99>&1come argomento 9seguito da 9>&1(questo non è un problema bash).

Si noti inoltre che non è particolarmente facile rendere questo FD 3 configurabile tramite una variabile. Questo rende le cose molto illeggibili:

: 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>&-";
}

Nota di sicurezza: i primi 3 argomenti catch-var-from-fd-by-fdnon devono essere presi da una terza parte. Dagli sempre esplicitamente in modo "statico".

Quindi no-no-no catch-var-from-fd-by-fd $var $fda $fdb $command, non farlo mai!

Se ti capita di passare un nome di variabile variabile, almeno fallo come segue: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

Questo comunque non ti proteggerà da ogni exploit, ma almeno aiuterà a rilevare ed evitare errori di scripting comuni.

Appunti:

  • catch-var-from-fd-by-fd var 2 3 cmd.. equivale a catch-stderr var cmd..
  • shift || returnè solo un modo per prevenire brutti errori nel caso in cui si dimentichi di fornire il numero corretto di argomenti. Forse terminare la shell sarebbe un altro modo (ma questo rende difficile testare dalla riga di comando).
  • La routine è stata scritta in modo tale che è più facile da capire. Si può riscrivere la funzione in modo che non sia necessaria exec, ma diventa davvero brutta.
  • Questa routine può essere riscritta per non bashcosì bene che non è necessario local -n. Tuttavia, non è possibile utilizzare le variabili locali e diventa estremamente brutto!
  • Si noti inoltre che le evals sono utilizzate in modo sicuro. Di solito evalè considerato pericoloso. Tuttavia, in questo caso non è più malvagio che usare "$@"(per eseguire comandi arbitrari). Tuttavia, assicurati di utilizzare il preventivo esatto e corretto come mostrato qui (altrimenti diventa molto molto pericoloso ).

3

Ecco come l'ho fatto:

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

Esempio di utilizzo:

captureStderr err "./useless.sh"

echo -$err-

Si fa utilizzare un file temporaneo. Ma almeno le cose brutte sono racchiuse in una funzione.


@ShadowWizard Poco dubbio dalla mia parte. In francese, il colon è di solito preceduto da uno spazio. Applico erroneamente questa stessa regola con le risposte in inglese . Dopo aver verificato questo , so che non commetterò più questo errore.
Stephan,

Saluti @Stephan, anche questo è stato discusso qui . :)
Shadow Wizard is Ear For You,

1
Ci sono modi più sicuri per farlo rispetto all'uso eval. Ad esempio, printf -v "$1" '%s' "$(<tmpFile)"non rischia di eseguire codice arbitrario se la TMPDIRvariabile è stata impostata su un valore dannoso (o il nome della variabile di destinazione contiene tale valore).
Charles Duffy,

1
Allo stesso modo, rm -- "$tmpFile"è più robusto di rm $tmpFile.
Charles Duffy,

2

Questo è un problema interessante al quale speravo ci fosse una soluzione elegante. Purtroppo, finisco con una soluzione simile a Mr. Leffler, ma aggiungerò che puoi chiamare inutile dall'interno di una funzione Bash per una migliore leggibilità:

#! / Bin / bash

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

ERROR = $ (inutile)
echo $ ERRORE

Tutti gli altri tipi di reindirizzamento dell'output devono essere supportati da un file temporaneo.


2

POSIX

STDERR può essere catturato con un po 'di magia di reindirizzamento:

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

Si noti che il piping di STDOUT del comando (qui ls) viene eseguito all'interno { }. Se stai eseguendo un comando semplice (ad esempio, non una pipe), potresti rimuovere queste parentesi graffe interne.

Non è possibile eseguire il pipe all'esterno del comando poiché il piping crea una subshell in bashe zshe l'assegnazione alla variabile nella subshell non sarebbe disponibile per la shell corrente.

bash

In bash, sarebbe meglio non presumere che il descrittore di file 3 non sia usato:

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

Nota che non funziona zsh.


Grazie a questa risposta per l'idea generale.


puoi spiegare questa riga con i dettagli? non ho capito 1> & $ tmp; {errore = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Thiago Conrado,

1

Questo post mi ha aiutato a trovare una soluzione simile per i miei scopi:

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

Quindi fino a quando il nostro MESSAGGIO non è una stringa vuota, lo passiamo ad altre cose. Questo ci farà sapere se il nostro format_logs.py non è riuscito con qualche tipo di eccezione di Python.


1

Cattura e stampa stderr

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

Abbattersi

Puoi usare $()per catturare stdout, ma invece vuoi catturare stderr. Quindi scambiate stdout e stderr. Utilizzo di fd 3 come memoria temporanea nell'algoritmo di scambio standard.

Se vuoi catturare E stampare usa teeper fare un duplicato. In questo caso l'output di teeverrà catturato $()invece di andare alla console, ma stderr (of tee) andrà comunque alla console, quindi lo usiamo come secondo output per teetramite il file speciale /dev/fd/2poiché si teeaspetta un percorso di file anziché un fd numero.

NOTA: si tratta di moltissimi reindirizzamenti in una sola riga e l'ordine conta. $()sta afferrando lo stdout teealla fine della pipeline e la stessa pipeline dirige stdout ./useless.shallo stdin di teeDOPO che abbiamo scambiato stdin e stdout ./useless.sh.

Utilizzando stdout di ./useless.sh

L'OP ha detto che voleva ancora usare (non solo stampare) stdout, come ./useless.sh | sed 's/Output/Useless/'.

Nessun problema, fallo PRIMA di scambiare stdout e stderr. Consiglio di spostarlo in una funzione o file (anche-useless.sh) e chiamarlo al posto di ./useless.sh nella riga sopra.

Tuttavia, se si desidera CATTURARE stdout E stderr, penso che si debba $()ricorrere a file temporanei perché lo farà solo uno alla volta e crea una sottostruttura dalla quale non è possibile restituire variabili.


1

Iterando un po 'sulla risposta di Tom Hale, ho trovato possibile avvolgere lo yoga di reindirizzamento in una funzione per un riutilizzo più facile. Per esempio:

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

È quasi certamente possibile semplificarlo ulteriormente. Non sono stati testati in modo particolarmente accurato, ma sembra funzionare sia con bash che con ksh.


0

Se si desidera ignorare l'uso di un file temporaneo, è possibile utilizzare la sostituzione del processo. Non sono ancora riuscito a farlo funzionare. Questo era il mio primo tentativo:

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

Poi ho provato

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

però

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

Quindi la sostituzione del processo sta facendo generalmente la cosa giusta ... sfortunatamente, ogni volta che avvolgo STDIN dentro >( )con qualcosa nel $()tentativo di catturarlo in una variabile, perdo il contenuto di $(). Penso che ciò $()avvenga perché avvia un processo secondario che non ha più accesso al descrittore di file in / dev / fd di proprietà del processo padre.

La sostituzione del processo mi ha permesso di lavorare con un flusso di dati che non si trova più in STDERR, sfortunatamente non riesco a manipolarlo nel modo che desidero.


1
Se lo facessi ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" ), vedresti l'output di ERROR. Il problema è che la sostituzione del processo viene eseguita in una sotto-shell, quindi il valore impostato nella sotto-shell non influisce sulla shell genitore.
Jonathan Leffler,

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

3
Sembra una buona idea, ma su Mac OSX 10.8.5, stampaa=> b=>stderr
Heath Borders

3
Sono d'accordo con @HeathBorders; questo non produce l'output mostrato. Il problema qui è che aviene valutato e assegnato in una shell secondaria e l'assegnazione nella shell secondaria non influisce sulla shell padre. (Testato su Ubuntu 14.04 LTS e su Mac OS X 10.10.1.)
Jonathan Leffler

Lo stesso in Windows GitBash. Quindi non funziona. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby,

Non funziona su SLE 11.4nessuno dei due e produce l'effetto descritto da @JonathanLeffler
smarber

Mentre questo codice può rispondere alla domanda, fornendo un contesto aggiuntivo riguardo al perché e / o al modo in cui questo codice risponde alla domanda migliora il suo valore a lungo termine.
β.εηοιτ.βε

0

In zsh:

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

0

Per la correzione degli errori dei comandi:

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
}

Ispirato alla produzione snella:


La soluzione idiomatica è affidare l'incarico all'interno di if. Vorrei pubblicare una soluzione separata.
triplo,


0

Migliorare la risposta di YellowApple :

Questa è una funzione di Bash per catturare stderr in qualsiasi variabile

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"

test:

bash stderr_capture_example.sh

Produzione:

 stderr_capture_example.sh

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

Questa funzione può essere utilizzata per acquisire la scelta restituita di un dialogcomando.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.