Як зберігати стандартну помилку в змінній


182

Скажімо, у мене такий сценарій:

марно.ш

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

У мене є ще один сценарій оболонки:

такожUseless.sh

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

Я хочу зафіксувати "Це помилка" або будь-який інший stderr з useless.sh, в змінну. Назвемо це ПОМИЛКА.

Зауважте, що я використовую stdout для чогось. Я хочу продовжувати використовувати stdout, тому перенаправлення stderr в stdout не корисне в цьому випадку.

Отже, в основному, я хочу це зробити

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

але це очевидно не працює.

Я також знаю, що міг би зробити

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

але це потворно і непотрібно.

На жаль, якщо тут не знайдеться жодної відповіді, це я маю робити.

Я сподіваюся, що існує інший спосіб.

Хтось має кращі ідеї?


4
Для чого саме потрібно використовувати stdout? Ви просто хочете переглянути його на консолі? Або ви захоплюєте / переспрямовуєте його вихід? Якщо це лише консоль, ви перенаправляєте stdout на консоль, а stderr - на stdout, щоб захопити:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Тім Керстен,

Відповіді:


91

Було б акуратніше зафіксувати файл помилки таким чином:

ERROR=$(</tmp/Error)

Оболонка розпізнає це і не потребує запуску ' cat', щоб отримати дані.

Питання велике - важко. Я не думаю, що існує простий спосіб це зробити. Вам доведеться зібрати весь конвеєр у підкольору, врешті-решт відправивши його остаточний стандартний вихід у файл, щоб ви могли перенаправляти помилки на стандартний вихід.

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

Зауважте, що напівколона потрібна (у класичних оболонках - Борн, Корн - точно; напевно, і в Баша). {}Перенаправлення ' ' робить введення / виведення над доданими командами. Як було написано, воно також захоплювало б помилки sed.

Попередження: формально неперевірений код - використання на власний ризик.


1
Я сподівався, що буде якийсь справді божевільний трюк, якого я не знав, але схоже, це все. Дякую.
psycotica0

9
Якщо вам не потрібен стандартний вихід, ви можете перенаправити його на /dev/nullзамість outfile(якщо ви такі, як я, ви знайшли це запитання через Google, і не маєте тих же вимог, що й ОП)
Марк Ейріх,

2
Відповідь без тимчасових файлів дивіться тут .
Том Хейл

1
Ось спосіб це зробити без перенаправлення до файлів; він грає з обмінювати stdoutі stderrвперед і назад. Але будьте обережні , як тут сказано: У баші, було б краще не вважати, що дескриптор файлу 3 не використовується " .
Golar Ramblar

69

такожUseless.sh

Це дозволить вам передавати висновок useless.shсценарію через таку команду, як sedі зберегти stderrзмінну з назвою error. Результат труби надсилається на stdoutпоказ або переводиться в іншу команду.

Він встановлює пару додаткових дескрипторів файлів для управління перенаправленнями, необхідними для цього.

#!/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
Добре використовувати техніку "exec" для встановлення та закриття дескрипторів файлів. Закриття насправді не потрібне, якщо сценарій закінчується відразу після цього.
Джонатан Леффлер

3
Як би я захоплював stderrі stdoutзмінні, і змінні?
Gingi

Відмінно. Це допомагає мені реалізувати dry_runфункцію, яка може надійно вибирати між повторенням своїх аргументів та їх запуском, незалежно від того, чи пересувається команда в сухому режимі до якогось іншого файлу.
Mihai Danila

1
@ t00bs: readне приймає вхід з труби. Ви можете використовувати інші методи, щоб досягти того, що ви намагаєтесь продемонструвати.
Призупинено до подальшого повідомлення.

2
Може бути простіше, з: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn

64

Перенаправляємо stderr на stdout, stdout на / dev / null, а потім використовуємо backticks або $()захоплюємо перенаправлений stderr:

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

8
Це причина, що я включив трубу в свій приклад. Я все ще хочу стандартний вихід, і я хочу, щоб він робив інші речі, ходив в інші місця.
psycotica0

Для команд, вихід відправити тільки потік помилок, простий спосіб , щоб захопити це, наприкладPY_VERSION="$(python --version 2>&1)"
Джон Марк

9

Для цього питання існує багато дублікатів, багато з яких мають дещо простіший сценарій використання, коли ви не хочете одночасно фіксувати stderr та stdout та вихідний код.

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

працює для загального сценарію, де ви очікуєте або належного виводу у випадку успіху, або діагностичного повідомлення на stderr у разі відмови.

Зауважте, що контрольні заяви оболонки вже розглядаються $?під кришкою; так що все, що схоже

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

це просто незграбний, однозначний спосіб сказати

if cmd; then ...

Це працювало для мене: my_service_status = $ (стан служби my_service 2> & 1) Дякую !!
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тут поганий вибір, оскільки насправді вбудована така назва. Можна зробити так yourCommandчи інакше, щоб бути більш чітким.
Чарльз Даффі

4

На користь читача цей рецепт тут

  • може бути використаний повторно як oneliner для лову stderr у змінну
  • як і раніше надає доступ до коду повернення команди
  • Пожертвує тимчасовим дескриптором файлів 3 (який ви, звичайно, можете змінити)
  • І не виставляє цей тимчасовий дескриптор файлу внутрішній команді

Якщо ви хочете , щоб зловити stderrдеяких commandв varви можете зробити

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

Згодом у вас все є:

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

Якщо commandце просто (не щось на кшталт a | b), ви можете залишити внутрішнє {}:

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

Згорнутий у просту функцію для багаторазового використання bash(ймовірно, потрібна версія 3 і вище для local -n):

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

Пояснили:

  • local -nпсевдоніми "$ 1" (що є змінною для catch-stderr)
  • 3>&1 використовує дескриптор файлів 3 для збереження там точок stdout
  • { command; } (або "$ @") потім виконує команду в заході виводу $(..)
  • Зверніть увагу, що тут важливий точний порядок (неправильний спосіб переміщення дескрипторів файлів неправильно):
    • 2>&1перенаправляє stderrна захоплення виходу$(..)
    • 1>&3переспрямовує stdoutвід виходу, що захоплює $(..)назад, до "зовнішнього", stdoutякий був збережений у дескрипторі файлів 3. Зауважте, що stderrвсе ще посилається на те, де FD 1 вказував раніше: На вихід захоплення$(..)
    • 3>&-потім закриває дескриптор файлу 3, оскільки він більше не потрібен, таким чином, щоб commandраптом не з’явився якийсь невідомий дескриптор відкритого файлу. Зауважте, що зовнішня оболонка все ще має FD 3 відкритою, але commandїї не побачить.
    • Останнє є важливим, оскільки деякі програми на зразок lvmскаржаться на несподівані дескриптори файлів. І lvmскаржиться stderr- просто те, що ми збираємось захопити!

Ви можете зловити будь-який інший дескриптор файлу за цим рецептом, якщо ви адаптуєтесь відповідно. За винятком дескриптора файлів 1, звичайно (тут логіка перенаправлення була б неправильною, але для дескриптора файлів 1 ви можете просто використовувати var=$(command)як завжди).

Зауважте, що це жертвує дескриптором файлів 3. Якщо вам потрібен цей дескриптор файлу, не соромтеся змінити число. Але майте на увазі, що деякі снаряди (починаючи з 1980-х) могли зрозуміти99>&1 аргументи, 9за якими слід 9>&1(це не проблема bash).

Також зауважте, що зробити FD 3, що може бути налаштований за допомогою змінної, не особливо легко. Це робить речі дуже нечитабельними:

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

Примітка безпеки: перші 3 аргументи catch-var-from-fd-by-fdне слід брати від третьої сторони. Завжди надайте їх явно "статичним" способом.

Тож ні-ні-ні catch-var-from-fd-by-fd $var $fda $fdb $command, ніколи цього не робіть!

Якщо вам трапиться ім'я змінної змінної, принаймні виконайте наступне: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

Це все одно не захистить вас від кожного подвигу, але, принаймні, допоможе виявити та уникнути поширених помилок сценарію.

Примітки:

  • catch-var-from-fd-by-fd var 2 3 cmd.. те саме, що catch-stderr var cmd..
  • shift || returnце лише певний спосіб запобігти некрасиві помилки, якщо ви забудете дати правильну кількість аргументів. Можливо, закінчення оболонки було б іншим способом (але це ускладнює тестування з командного рядка).
  • Порядок був написаний таким, що його легше зрозуміти. Можна переписати функцію так, що вона не потрібнаexec , але тоді вона стає справді некрасивою.
  • Цю процедуру можна переписати на bashте, що не потребує такої потребиlocal -n . Однак тоді ви не можете використовувати локальні змінні, і це стає надзвичайно негарно!
  • Також зауважте, що evals використовуються безпечно. Зазвичай evalвважається небезпечним. Однак у цьому випадку це не більше зла, ніж використання "$@"(для виконання довільних команд). Однак будь ласка, використовуйте точне та правильне цитування, як показано тут (інакше це стає дуже небезпечно ).

3

Ось як я це зробив:

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

Приклад використання:

captureStderr err "./useless.sh"

echo -$err-

Це робить використовувати тимчасовий файл. Але принаймні потворні речі загорнуті у функцію.


@ShadowWizard Мало сумнівів на моєму боці. У французькій мові товстій кишці зазвичай передує пробіл. Я помилково застосовую це саме правило з англійською відповідями. Перевіривши це , я знаю, що більше не помиляюся.
Стефан

@Stephan привіт, це також обговорювалося тут . :)
Shadow Wizard is Ear For You

1
Є безпечніші способи зробити це, ніж використовувати eval. Наприклад, printf -v "$1" '%s' "$(<tmpFile)"не ризикує виконувати довільний код, якщо для вашої TMPDIRзмінної було встановлено зловмисне значення (або назва вашої змінної містить таке значення).
Чарльз Даффі

1
Так само rm -- "$tmpFile"є більш надійним, ніж rm $tmpFile.
Чарльз Даффі

2

Це цікава проблема, на яку я сподівався, що буде елегантне рішення. На жаль, я закінчую рішення, подібне до містера Леффлера, але я додам, що ви можете викликати марність зсередини функції Bash для поліпшення читабельності:

#! / бін / баш

функція марна {
    /tmp/useless.sh | sed 's / вихід / марно /'
}

ПОМИЛКА = $ (марно)
echo $ ПОМИЛКА

Усі інші види перенаправлення виводу повинні бути підкріплені тимчасовим файлом.


2

POSIX

STDERR можна захопити за допомогою деякої магії перенаправлення:

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

Зауважте, що трубопровід STDOUT команди (тут ls) робиться всередині всередині{ } . Якщо ви виконуєте просту команду (наприклад, не трубу), ви можете видалити ці внутрішні дужки.

Ви не можете виконувати передачу даних за межами команди, оскільки трубопровід перетворює підклітку в bashі zsh, а присвоєння змінній в підпакеті не буде доступним для поточної оболонки.

баш

В bash, було б краще не вважати, що дескриптор файлу 3 не використовується:

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

Зауважте, що це не працює zsh.


Завдяки цій відповіді за загальну ідею.


Ви можете пояснити цей рядок деталями? не зрозумів 1> & $ tmp; {error = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Тіаго Конрадо

1

Ця публікація допомогла мені придумати подібне рішення для власних цілей:

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

Тоді, поки наш ПОВІДОМЛЕННЯ не є порожнім рядком, ми передаємо його іншим матеріалам. Це дозволить нам знати, якщо наш format_logs.py не вдався за винятком python.


1

Захоплення та друк stderr

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

Зламатися

Можна використовувати $() для зйомки stdout, але замість цього потрібно захопити stderr. Так ви поміняєте stdout і stderr. Використання fd 3 в якості тимчасового зберігання в стандартному алгоритмі swap.

Якщо ви хочете зафіксувати І надрукувати, скористайтеся, teeщоб зробити дублікат. У цьому випадку висновок teeбуде зафіксовано, $()а не піде на консоль, але stderr (of tee) все одно перейде до консолі, тому ми використовуємо це як другий вихід для teeспеціального файлу, /dev/fd/2оскільки teeочікує шлях файлу, а не fd число.

ПРИМІТКА. Це надзвичайно багато переспрямувань в одному рядку, і порядок має значення. $()є захоплення stdout teeв кінці трубопроводу, а сам трубопровід прокладає stdout ./useless.shдо stdin teeПІСЛЯ ми замінили stdin і stdout на ./useless.sh.

Використання stdout ./useless.sh

ОП заявив, що все ще хоче використовувати (не тільки друк) stdout, як ./useless.sh | sed 's/Output/Useless/'.

Немає проблем, просто зробіть це ПЕРЕД ЗАМІНЮВАННЯм stdout та stderr. Я рекомендую перемістити його у функцію або файл (також-useless.sh) і викликати його замість ./useless.sh у рядку вище.

Однак, якщо ви хочете CAPTURE stdout AND stderr, то, я думаю, вам доведеться повернутися до тимчасових файлів, тому що вони $()будуть робити лише один за одним, і це зробить підзаголовок, з якого ви не можете повернути змінні.


1

Трохи повторивши відповідь Тома Хейла, я знайшов можливим перетворити йогу на перенаправлення у функцію для легшого використання. Наприклад:

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

Це майже напевно можливо спростити це далі. Не перевірений особливо ретельно, але, здається, працює і з bash, і з ksh.


0

Якщо ви хочете обійти використання тимчасового файлу, можливо, ви зможете використовувати підстановку процесу. Я ще не дуже змусив його працювати. Це була моя перша спроба:

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

Потім я спробував

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

Однак

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

Тож підміна процесу робить, як правило, правильну справу ... на жаль, кожного разу, коли я загортаю STDIN всередину >( )чимось $(), намагаючись захопити це до змінної, я втрачаю вміст $(). Я думаю, що це тому$() запускає підпроцес, який більше не має доступу до дескриптора файлів в / dev / fd, який належить батьківському процесу.

Заміна процесів дозволила мені працювати з потоком даних, якого більше немає в STDERR, на жаль, я, здається, не в змозі маніпулювати ним так, як мені хочеться.


1
Якби ви це зробили, ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" )тоді ви побачили б вихід ERROR. Проблема полягає в тому, що підміна процесу запускається в під-оболонці, тому значення, встановлене в під-оболонці, не впливає на батьківську оболонку.
Джонатан Леффлер

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

3
Це виглядає як гарна ідея, але на Mac OSX 10.8.5 він друкуєa=> b=>stderr
Heath Borders

3
Я згоден з @HeathBorders; це не дає показаного результату. Проблема тут полягає в тому, що aвона оцінюється і призначається в підколонці, а присвоєння в під оболонці не впливає на батьківську оболонку. (Тестовано на Ubuntu 14.04 LTS, а також на Mac OS X 10.10.1.)
Джонатан Леффлер

Те саме в Windows GitBash. Отже, це не працює. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Кірбі

Не працює SLE 11.4ні на одному, і створює ефект, описаний @JonathanLeffler
smarber

Хоча цей код може відповісти на питання, надаючи додатковий контекст стосовно того, чому та / або як цей код відповідає на питання, покращує його довгострокове значення.
β.εηοιτ.βε


0

Для перевірки помилок ваших команд:

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
}

Натхненний художнім виробництвом:


Ідіоматичне рішення полягає в тому, щоб виконувати завдання всередині if. Дозвольте опублікувати окреме рішення.
трійка


0

Покращення відповіді на YellowApple :

Це функція Bash для захоплення stderr в будь-яку змінну

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"

Тестування:

bash stderr_capture_example.sh

Вихід:

 stderr_capture_example.sh

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

Ця функція може бути використана для збору повернутого вибору dialogкоманди.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.