Как сохранить стандартную ошибку в переменной


182

Допустим, у меня есть скрипт, подобный следующему:

useless.sh

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

И у меня есть другой сценарий оболочки:

alsoUseless.sh

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

Я хочу записать «This Is Error» или любой другой stderr из useless.sh в переменную. Давайте назовем это ОШИБКА.

Обратите внимание, что я использую стандартный вывод для чего-то. Я хочу продолжать использовать stdout, поэтому перенаправление stderr в stdout в этом случае бесполезно.

Итак, в основном, я хочу сделать

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

но это, очевидно, не работает.

Я также знаю, что я мог сделать

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

но это безобразно и ненужно.

К сожалению, если здесь не будет ответов, это то, что я собираюсь сделать.

Я надеюсь, что есть другой способ.

У кого-нибудь есть идеи получше?


4
Для чего именно нужно использовать стандартный вывод? Вы просто хотите посмотреть это на консоли? Или вы захватываете / перенаправляете его вывод? Если это просто консоль, вы перенаправляете стандартный вывод на консоль, а стандартный вывод на стандартный вывод: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 )

Обратите внимание, что точка с запятой необходима (в классических оболочках - Bourne, Korn - наверняка; вероятно, в Bash тоже). ' {}' Выполняет перенаправление ввода / вывода поверх вложенных команд. Как написано, это будет захватывать ошибки от sedслишком.

ВНИМАНИЕ: Формально непроверенный код - используйте на свой страх и риск.


1
Я надеялся, что будет какой-то действительно сумасшедший трюк, которого я не знал, но, похоже, вот оно. Спасибо.
psycotica0

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

2
Ответ без временных файлов смотрите здесь .
Том Хейл,

1
Вот способ сделать это, не перенаправляя его в файлы; это играет с обменом stdoutи stderrвперед и назад. Но будьте осторожны , как здесь сказано: в bash было бы лучше не предполагать, что файловый дескриптор 3 не используется " .
Golar Ramblar

69

alsoUseless.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в переменных?
Гинги

Превосходно. Это помогает мне реализовать dry_runфункцию, которая может надежно выбирать между отображением своих аргументов и их выполнением, независимо от того, передается ли команда, выполняемая без пробега, в какой-либо другой файл.
Михай Данила

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

2
Может быть проще: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Джоселин

64

Перенаправить stderr в stdout, stdout в / dev / null, а затем использовать обратные клавиши или $()для захвата перенаправленного stderr:

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

8
Вот почему я включил трубу в моем примере. Я все еще хочу стандартный вывод, и я хочу, чтобы он делал другие вещи, ходил в другие места.
psycotica0

Для команд, которые отправляют вывод только в stderr, простой способ перехвата этого, например,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 = $ (service my_service status 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 для сохранения там стандартных точек
  • { 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. Однако тогда вы не можете использовать локальные переменные, и это становится ужасно!
  • Также обратите внимание, что evalони используются безопасным образом. Обычно 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 Мало сомнений на моей стороне. Во французском языке двоеточию обычно предшествует пробел. Я ошибочно применяю это правило к английским ответам. После проверки этого , я знаю , что я не буду делать эту ошибку снова.
Стефан

@ Стефан ура, это также обсуждалось здесь . :)
Shadow Wizard - это ухо для тебя

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

1
Точно так же rm -- "$tmpFile"является более надежным, чем rm $tmpFile.
Чарльз Даффи

2

Это интересная проблема, на которую я надеялся найти элегантное решение. К сожалению, я получаю решение, похожее на мистер Леффлер, но добавлю, что вы можете бесполезно вызывать функцию из Bash для улучшения читабельности:

#! / Bin / Баш

функция бесполезна {
    /tmp/useless.sh | sed 's / Output / Бесполезный /'
}

ERROR = $ (бесполезный)
echo $ ERROR

Все другие виды перенаправления вывода должны быть поддержаны временным файлом.


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 в качестве временного хранилища в стандартном алгоритме подкачки.

Если вы хотите захватить И распечатать, используйте teeдля создания дубликата. В этом случае выходные данные teeбудут записываться, $()а не переходить на консоль, но stderr (of tee) будет по-прежнему идти на консоль, поэтому мы используем его в качестве второго вывода для teeспециального файла, /dev/fd/2поскольку teeожидается путь к файлу, а не fd число.

ПРИМЕЧАНИЕ. Это очень много перенаправлений в одну строку, и порядок имеет значение. $()захватывает стандартный вывод teeв конце конвейера, а сам конвейер направляет стандартный вывод ./useless.shв стандартный поток teeПОСЛЕ того, как мы поменялись стандартными и стандартными ./useless.sh.

Используя стандартный вывод ./useless.sh

ОП сказал, что он все еще хотел использовать (а не только печатать) стандартный вывод ./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. Позвольте мне опубликовать отдельное решение.
tripleee


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.