Как сохранить стандартную ошибку в переменной - PullRequest
154 голосов
/ 07 июня 2009

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

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`

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

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

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

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

Ответы [ 15 ]

0 голосов
/ 13 июня 2019

Для удобства читателя этот рецепт здесь

  • может быть повторно использован как 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 считается опасным. Однако в этом случае это не более зло, чем использование "$@" (для выполнения произвольных команд). Однако, пожалуйста, не забудьте использовать точное и правильное цитирование, как показано здесь (иначе это становится очень очень опасно ).
0 голосов
/ 01 октября 2018

1001 * 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.


Спасибо за этот ответ за общую идею.

0 голосов
/ 29 декабря 2017

Для Устранение ошибок Ваши команды:

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
}

Вдохновленный в Бережливом производстве:

0 голосов
/ 14 ноября 2015

В зш:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )
0 голосов
/ 31 мая 2012

Если вы хотите обойти использование временного файла, вы можете использовать процесс подстановки. Я еще не совсем заставил его работать. Это была моя первая попытка:

$ .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, к сожалению, я, похоже, не могу управлять им так, как я хочу.

...