Jak przechowywać standardowy błąd w zmiennej


182

Powiedzmy, że mam skrypt podobny do następującego:

useless.sh

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

Mam inny skrypt powłoki:

alsoUseless.sh

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

Chcę przechwycić „This Is Error” lub dowolny inny błąd standardowy z useless.sh do zmiennej. Nazwijmy to BŁĄDEM.

Zauważ, że do czegoś używam standardowego wyjścia. Chcę nadal używać stdout, więc przekierowanie stderr na stdout nie jest pomocne w tym przypadku.

Więc w zasadzie chcę to zrobić

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

ale to oczywiście nie działa.

Wiem też, że mógłbym to zrobić

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

ale to brzydkie i niepotrzebne.

Niestety, jeśli nie pojawią się tutaj żadne odpowiedzi, to będę musiał zrobić.

Mam nadzieję, że jest inny sposób.

Czy ktoś ma lepsze pomysły?


4
Po co dokładnie chcieć używać stdout? Czy chcesz po prostu obejrzeć go na konsoli? A może przechwytujesz / przekierowujesz to wyjście? Jeśli to tylko do konsoli przekierować stdout do konsoli i stderr do stdout, żeby go uchwycić:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Odpowiedzi:


91

Byłoby lepiej przechwycić plik błędu w ten sposób:

ERROR=$(</tmp/Error)

Powłoka rozpoznaje to i nie musi uruchamiać ' cat', aby uzyskać dane.

Większe pytanie jest trudne. Nie sądzę, aby można było to łatwo zrobić. Musiałbyś zbudować cały potok w powłoce podrzędnej, ostatecznie wysyłając końcowe standardowe wyjście do pliku, aby można było przekierować błędy na standardowe wyjście.

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

Zwróć uwagę, że średnik jest potrzebny (w klasycznych muszlach - Bourne, Korn - na pewno; prawdopodobnie również w Bash). Symbol „ {}” wykonuje przekierowanie we / wy przez dołączone polecenia. Jak napisano, wychwytuje również błędy sed.

OSTRZEŻENIE: Formalnie niesprawdzony kod - używaj na własne ryzyko.


1
Miałem nadzieję, że będzie jakaś naprawdę szalona sztuczka, której nie znałem, ale wygląda na to, że to jest to. Dzięki.
psycotica0

9
Jeśli nie potrzebujesz standardowego wyjścia, możesz go przekierować /dev/nullzamiast outfile(Jeśli jesteś podobny do mnie, znalazłeś to pytanie przez Google i nie masz takich samych wymagań jak OP)
Mark Eirich

2
Aby uzyskać odpowiedź bez plików tymczasowych, zobacz tutaj .
Tom Hale

1
Oto sposób na zrobienie tego bez przekierowywania do plików; to gra z ciężkich stdouti stderrprzodu i do tyłu. Ale uważaj , jak tutaj jest powiedziane: W bash, byłoby lepiej nie zakładać, że deskryptor 3 nieużywane” .
Golar Ramblar

69

alsoUseless.sh

Umożliwi to przesłanie danych wyjściowych useless.shskryptu za pomocą polecenia takiego jak sedi zapisanie stderrw zmiennej o nazwie error. Wynik potoku jest wysyłany do stdoutwyświetlenia lub do innego polecenia.

Tworzy kilka dodatkowych deskryptorów plików, aby zarządzać przekierowaniami potrzebnymi w tym celu.

#!/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
Dobrą techniką jest używanie „exec” do ustawiania i zamykania deskryptorów plików. Zamknięcie nie jest naprawdę potrzebne, jeśli skrypt kończy się natychmiast po tym.
Jonathan Leffler

3
Jak przechwycić zarówno stderri stdoutw zmiennych?
Gingi

Doskonały. Pomaga mi to zaimplementować dry_runfunkcję, która może niezawodnie wybierać między powtarzaniem swoich argumentów a ich uruchamianiem, niezależnie od tego, czy polecenie, które jest uruchamiane na sucho, jest przesyłane potokiem do innego pliku.
Mihai Danila

1
@ t00bs: readnie przyjmuje danych wejściowych z potoku. Możesz użyć innych technik, aby osiągnąć to, co próbujesz pokazać.
Wstrzymano do odwołania.

2
Może być prostsze, z: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn

64

Przekierowano stderr na stdout, stdout na / dev / null, a następnie użyj backticks lub $()do przechwycenia przekierowanego stderr:

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

8
To jest powód, dla którego umieściłem fajkę w moim przykładzie. Nadal chcę standardowego wyjścia i chcę, aby robił inne rzeczy, chodził w inne miejsca.
psycotica0

W przypadku poleceń, które wysyłają dane wyjściowe tylko do stderr, prostym sposobem ich przechwycenia jest na przykładPY_VERSION="$(python --version 2>&1)"
John Mark

9

Istnieje wiele duplikatów tego pytania, z których wiele ma nieco prostszy scenariusz użycia, w którym nie chcesz przechwytywać stderr i stdout oraz kodu zakończenia w tym samym czasie.

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

działa dla typowego scenariusza, w którym w przypadku sukcesu oczekuje się prawidłowego wyniku lub komunikatu diagnostycznego na stderr w przypadku niepowodzenia.

Zauważ, że instrukcje kontrolne powłoki już sprawdzają $?pod maską; więc wszystko, co wygląda

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

jest po prostu niezdarnym, jednostajnym sposobem powiedzenia

if cmd; then ...

To zadziałało dla mnie: my_service_status = $ (stan usługi my_service 2> & 1) Dzięki !!
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
commandto zły wybór, ponieważ w rzeczywistości istnieje wbudowany plik o tej nazwie. Może to yourCommandlub coś takiego, żeby być bardziej wyraźnym.
Charles Duffy

4

Dla dobra czytelnika ten przepis tutaj

  • może być ponownie użyty jako oneliner do przechwytywania stderr do zmiennej
  • nadal daje dostęp do kodu zwrotnego polecenia
  • Poświęca tymczasowy deskryptor pliku 3 (który możesz oczywiście zmienić)
  • I nie ujawnia deskryptorów plików tymczasowych poleceniu wewnętrznemu

Jeśli chcesz złapać stderrniektórych commanddo varmożna zrobić

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

Potem masz wszystko:

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

Jeśli commandjest to proste (nie coś w rodzaju a | b), możesz zostawić wnętrze z {}daleka:

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

Zapakowany w łatwą funkcję wielokrotnego użytku bash(prawdopodobnie potrzebuje wersji 3 i nowszych dla local -n):

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

Wyjaśnione:

  • local -naliasy „$ 1” (czyli zmienna dla catch-stderr)
  • 3>&1 używa deskryptora pliku 3, aby zapisać tam punkty wyjścia
  • { command; } (lub „$ @”) wykonuje polecenie w ramach przechwytywania danych wyjściowych $(..)
  • Zwróć uwagę, że ważna jest tutaj dokładna kolejność (zrobienie tego w niewłaściwy sposób powoduje nieprawidłowe tasowanie deskryptorów plików):
    • 2>&1przekierowuje stderrdo przechwytywania danych wyjściowych$(..)
    • 1>&3przekierowuje stdoutz przechwytywania wyjścia z $(..)powrotem do „zewnętrznego”, stdoutktóre zostało zapisane w deskryptorze pliku 3. Zauważ, że stderrnadal odnosi się do miejsca, w którym wskazał wcześniej FD 1: Do przechwytywania wyjścia$(..)
    • 3>&-następnie zamyka deskryptor pliku 3, ponieważ nie jest już potrzebny, tak że commandnagle nie pojawia się jakiś nieznany deskryptor otwartego pliku. Zwróć uwagę, że zewnętrzna powłoka nadal ma otwarty FD 3, ale commandgo nie zobaczy.
    • To ostatnie jest ważne, ponieważ niektóre programy lvmnarzekają na nieoczekiwane deskryptory plików. I lvmnarzeka stderr- właśnie to, co uchwycimy!

Możesz złapać dowolny inny deskryptor pliku z tym przepisem, jeśli odpowiednio się dostosujesz. Z wyjątkiem oczywiście deskryptora pliku 1 (tutaj logika przekierowania byłaby błędna, ale dla deskryptora pliku 1 można po prostu użyć var=$(command)jak zwykle).

Zauważ, że ten deskryptor pliku poświęca 3. Jeśli potrzebujesz tego deskryptora pliku, możesz zmienić jego numer. Należy jednak pamiętać, że niektóre powłoki (z lat 80. XX wieku) mogą być rozumiane 99>&1jako argument, 9po którym następuje 9>&1(nie stanowi to problemu bash).

Należy również zauważyć, że nie jest szczególnie łatwe skonfigurowanie tego FD 3 za pomocą zmiennej. To sprawia, że ​​rzeczy są bardzo nieczytelne:

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

Uwaga dotycząca bezpieczeństwa: pierwsze 3 argumenty catch-var-from-fd-by-fdnie mogą pochodzić od strony trzeciej. Zawsze podawaj je wyraźnie w „statyczny” sposób.

Więc nie catch-var-from-fd-by-fd $var $fda $fdb $command, nie, nie, nigdy tego nie rób!

Jeśli zdarzy ci się przekazać zmienną nazwę zmiennej, przynajmniej zrób to w następujący sposób: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

To nadal nie ochroni Cię przed każdym exploitem, ale przynajmniej pomoże wykryć i uniknąć typowych błędów skryptów.

Uwagi:

  • catch-var-from-fd-by-fd var 2 3 cmd.. jest taki sam jak catch-stderr var cmd..
  • shift || returnto tylko sposób na uniknięcie brzydkich błędów w przypadku, gdy zapomnisz podać poprawną liczbę argumentów. Być może zamknięcie powłoki byłoby innym sposobem (ale utrudnia to testowanie z wiersza poleceń).
  • Rutyna została napisana tak, aby była łatwiejsza do zrozumienia. Można przepisać funkcję tak, że nie potrzebuje exec, ale wtedy robi się naprawdę brzydka.
  • Ta procedura może zostać zmieniona bashtak dobrze, że nie ma takiej potrzeby local -n. Jednak wtedy nie możesz używać zmiennych lokalnych i robi się to wyjątkowo brzydkie!
  • Należy również pamiętać, że evalsą one używane w bezpieczny sposób. Zwykle evaljest uważany za niebezpieczny. Jednak w tym przypadku nie jest to bardziej złe niż używanie "$@"(do wykonywania dowolnych poleceń). Jednak pamiętaj, aby używać dokładnych i poprawnych cytatów, jak pokazano tutaj (w przeciwnym razie staje się to bardzo niebezpieczne ).

3

Oto jak to zrobiłem:

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

Przykład użycia:

captureStderr err "./useless.sh"

echo -$err-

To robi użyć pliku tymczasowego. Ale przynajmniej brzydkie rzeczy są opakowane w funkcję.


@ShadowWizard Małe wątpliwości po mojej stronie. W języku francuskim dwukropek jest zwykle poprzedzony spacją. Błędnie stosuję tę samą zasadę do odpowiedzi w języku angielskim . Po sprawdzeniu tego wiem, że więcej nie popełnię tego błędu.
Stephan,

@Stephan pozdrawiam, zostało to również omówione tutaj . :)
Shadow Wizard is Ear For You

1
Są na to bezpieczniejsze sposoby niż używanie eval. Na przykład printf -v "$1" '%s' "$(<tmpFile)"nie ryzykuje uruchomienia dowolnego kodu, jeśli TMPDIRzmienna została ustawiona na złośliwą wartość (lub nazwa zmiennej docelowej zawiera taką wartość).
Charles Duffy,

1
Podobnie rm -- "$tmpFile"jest bardziej wytrzymały niż rm $tmpFile.
Charles Duffy,

2

To ciekawy problem, na który liczyłem na eleganckie rozwiązanie. Niestety, kończy się na rozwiązaniu podobnym do pana Lefflera, ale dodam, że można wywołać bezużyteczną funkcję z wnętrza Bash, aby poprawić czytelność:

#! / bin / bash

funkcja bezużyteczna {
    /tmp/useless.sh | sed 's / Wyjście / Bezużyteczne /'
}

BŁĄD = $ (bezużyteczne)
echo $ ERROR

Wszystkie inne rodzaje przekierowań wyjścia muszą być poparte plikiem tymczasowym.


2

POSIX

STDERR można przechwycić za pomocą magii przekierowań:

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

Zauważ, że orurowanie STDOUT polecenia (tutaj ls) odbywa się wewnątrz najbardziej wewnętrznego { }. Jeśli wykonujesz proste polecenie (np. Nie potok), możesz usunąć te wewnętrzne nawiasy klamrowe.

Nie możesz potokować poza polecenie, ponieważ potok tworzy podpowłokę w bashi zsh, a przypisanie do zmiennej w podpowłoce nie byłoby dostępne dla bieżącej powłoki.

grzmotnąć

W programie bashlepiej nie zakładać, że deskryptor pliku 3 jest nieużywany:

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

Zauważ, że to nie działa w zsh.


Dzięki tej odpowiedzi za ogólny pomysł.


czy możesz wyjaśnić tę kwestię szczegółami? nie zrozumiał 1> & $ tmp; {błąd = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Thiago Conrado

1

Ten post pomógł mi znaleźć podobne rozwiązanie do własnych celów:

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

Dopóki nasz MESSAGE nie jest pustym łańcuchem, przekazujemy go innym rzeczom. Dzięki temu będziemy wiedzieć, czy nasz format_logs.py nie powiódł się z powodu jakiegoś wyjątku w Pythonie.


1

Przechwyć i wydrukuj stderr

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

Awaria

Możesz użyć $()do przechwycenia stdout, ale zamiast tego chcesz przechwycić stderr. Więc zamieniasz stdout i stderr. Używanie fd 3 jako tymczasowej pamięci w standardowym algorytmie wymiany.

Jeśli chcesz przechwycić ORAZ wydrukować, użyj, teeaby zrobić duplikat. W tym przypadku dane wyjściowe programu teebędą przechwytywane przez $()zamiast trafiać do konsoli, ale stderr (of tee) nadal będzie trafiać do konsoli, więc używamy go jako drugiego wyjścia za teepośrednictwem specjalnego pliku, /dev/fd/2ponieważ teeoczekuje ścieżki do pliku zamiast fd numer.

UWAGA: To bardzo dużo przekierowań w jednej linii, a kolejność ma znaczenie. $()przechwytuje stdout z teena końcu potoku, a sam potok kieruje stdout z ./useless.shdo stdin z teeAFTER zamieniliśmy stdin i stdout na ./useless.sh.

Korzystanie ze standardowego wyjścia ./useless.sh

OP powiedział, że nadal chce używać (nie tylko drukowania) standardowego wyjścia, na przykład ./useless.sh | sed 's/Output/Useless/'.

Żaden problem, po prostu zrób to PRZED zamianą stdout i stderr. Zalecam przeniesienie go do funkcji lub pliku (także-useless.sh) i wywołanie tego zamiast ./useless.sh w powyższym wierszu.

Jeśli jednak chcesz przechwycić stdout i stderr, myślę, że musisz wrócić do plików tymczasowych, ponieważ $()będzie to robić tylko jeden naraz i tworzy podpowłokę, z której nie możesz zwracać zmiennych.


1

Powtarzając nieco odpowiedź Toma Hale'a, odkryłem, że możliwe jest zawinięcie jogi przekierowania w funkcję ułatwiającą ponowne użycie. Na przykład:

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

Niemal na pewno możliwe jest dalsze uproszczenie tego. Nie był testowany szczególnie dokładnie, ale wydaje się, że działa zarówno z bash, jak i ksh.


0

Jeśli chcesz ominąć użycie pliku tymczasowego, możesz użyć podstawiania procesów. Jeszcze nie udało mi się tego zrobić. To była moja pierwsza próba:

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

Potem spróbowałem

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

jednak

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

Tak więc podstawianie procesu jest ogólnie słuszne ... niestety, ilekroć zawijam >( )coś w STDIN, $()próbując przechwycić to do zmiennej, tracę zawartość $(). Myślę, że dzieje się tak, ponieważ $()uruchamia proces podrzędny, który nie ma już dostępu do deskryptora pliku w / dev / fd, którego właścicielem jest proces nadrzędny.

Zastępowanie procesów dało mi możliwość pracy ze strumieniem danych, którego nie ma już w STDERR, niestety wydaje mi się, że nie jestem w stanie manipulować nim tak, jak chcę.


1
Jeśli tak ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" ), zobaczysz wynik ERROR. Problem polega na tym, że podstawianie procesu odbywa się w powłoce podrzędnej, więc wartość ustawiona w powłoce podrzędnej nie wpływa na powłokę macierzystą.
Jonathan Leffler

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

3
Wygląda na to, że to dobry pomysł, ale w systemie Mac OSX 10.8.5 drukujea=> b=>stderr
Heath Borders

3
Zgadzam się z @HeathBorders; nie daje to pokazanego wyniku. Problem polega na tym, że ajest oceniany i przypisywany w powłoce podrzędnej, a przypisanie w powłoce podrzędnej nie wpływa na powłokę macierzystą. (Testowane na Ubuntu 14.04 LTS oraz Mac OS X 10.10.1.)
Jonathan Leffler,

To samo w Windows GitBash. Więc to nie działa. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby

Nie działa na SLE 11.4też i daje efekt opisany przez @JonathanLeffler
smarber

Chociaż ten kod może odpowiedzieć na pytanie, zapewnia dodatkowy kontekst dotyczący tego, dlaczego i / lub jak ten kod odpowiada, poprawia jego długoterminową wartość.
β.εηοιτ.βε

0

W zsh:

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

0

W celu sprawdzenia błędów poleceń:

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
}

Zainspirowani Lean Manufacturing:


Idiomatycznym rozwiązaniem jest umieszczenie przypisania wewnątrz pliku if. Pozwólcie, że opublikuję osobne rozwiązanie.
tripleee


0

Poprawa odpowiedzi YellowApple :

Jest to funkcja Bash do przechwytywania stderr do dowolnej zmiennej

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"

Testowanie:

bash stderr_capture_example.sh

Wynik:

 stderr_capture_example.sh

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

Ta funkcja może służyć do przechwytywania zwróconego wyboru dialogpolecenia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.