Для удобства читателя этот рецепт здесь
- может быть повторно использован как 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
считается опасным. Однако в этом случае это не более зло, чем использование "$@"
(для выполнения произвольных команд). Однако, пожалуйста, не забудьте использовать точное и правильное цитирование, как показано здесь (иначе это становится очень и очень опасным ).
ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)