Захват stdout и stderr в различные переменные
можно ли хранить или захватить stdout и stderr в различные переменные, без использования временного файла? Сейчас я делаю это, чтобы получить stdout в out
и stderr в err
при работе some_command
, но я бы
как избежать временного файла.
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< error_file)
rm $error_file
13 ответов
Хорошо, это немного уродливо, но вот решение:
unset t_std t_err
eval "$( (echo std; echo err >&2) \
2> >(readarray -t t_err; typeset -p t_err) \
> >(readarray -t t_std; typeset -p t_std) )"
здесь (echo std; echo err >&2)
необходимо заменить фактической командой. Выход stdout сохраняется в массиве $t_std
строка за строкой, опуская новые строки (-t
) и stderr на $t_err
.
Если вам не нравятся массивы, вы можете сделать
unset t_std t_err
eval "$( (echo std; echo err >&2 ) \
2> >(t_err=$(cat); typeset -p t_err) \
> >(t_std=$(cat); typeset -p t_std) )"
, который в значительной степени имитирует поведение var=$(cmd)
за исключением стоимости $?
что приводит нас к последняя модификация:
unset t_std t_err t_ret
eval "$( (echo std; echo err >&2; exit 2 ) \
2> >(t_err=$(cat); typeset -p t_err) \
> >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"
здесь $?
сохранена в $t_ret
протестировано на Debian wheezy с помощью GNU bash
, версия 4.2.37 (1)-release (i486-pc-linux-gnu).
Джонатан ответ. Для справки, это трюк ksh93. (требуется не древняя версия).
function out {
echo stdout
echo stderr >&2
}
x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values
производит
x=stderr
y=stdout
The ${ cmds;}
синтаксис - это просто подстановка команд, которая не создает подрешетку. Команды выполняются в текущей среде shell. Пространство в начале важно ({
- это зарезервированное слово).
Stderr внутренней группы команд перенаправляется на stdout (так что он относится к внутренней подстановке). Далее, stdout out
назначена y
, и перенаправленный stderr захватывается x
, без обычной потери y
в команду по подуровень.
это невозможно в других оболочках, потому что все конструкции, которые захватывают выход, требуют помещения производителя в подрешетку, которая в этом случае будет включать назначение.
обновление: теперь также поддерживается mksh.
эта команда устанавливает значения stdout (stdval) и stderr (errval) в текущей запущенной оболочке:
eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"
при условии, что эта функция была определена:
function setval { printf -v "" "%s" "$(cat)"; declare -p ""; }
измените execcommand на захваченную команду, будь то" ls"," cp"," df " и т. д.
все это основано на идее, что мы могли бы преобразовать все захваченные значения в текстовую строку с помощью функции setval, затем setval используется для захвата каждого значения в этом структура:
execcommand 2> CaptureErr > CaptureOut
преобразуйте каждое значение захвата в вызов setval:
execcommand 2> >(setval errval) > >(setval stdval)
оберните все внутри вызова execute и повторите его:
echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"
вы получите вызовы declare, которые создает каждый setval:
declare -- stdval="I'm std"
declare -- errval="I'm err"
чтобы выполнить этот код (и получить набор vars), используйте eval:
eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"
и, наконец, Эхо набора vars:
echo "std out is : |$stdval| std err is : |$errval|
также можно включить значение return (exit).
Ля полный пример сценария bash выглядит так:
#!/bin/bash --
# The only function to declare:
function setval { printf -v "" "%s" "$(cat)"; declare -p ""; }
# a dummy function with some example values:
function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; }
# Running a command to capture all values
# change execcommand to dummy or any other command to test.
eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )"
echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"
до sum все до для удобства читателя, вот
Легкие Многоразовые bash
решение
эта версия использует подсхемы и работает без tempfile
s. (Для A tempfile
версия, которая работает без подоболочек, см. мой другой ответ.)
: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
{ __1="$("${@:3}")"; } 2>&1;
ret=$?;
printf '%q=%q\n' "" "$__1" >&2;
exit $ret
)"
ret="$?";
printf '%s=%q\n' "" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}
пример использования:
dummy()
{
echo "" >&2
echo "" >&1
return ""
}
catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n data \n\n'
printf 'ret=%q\n' "$?"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"
это выводит
ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n data '
так его можно использовать без более глубокого думать об этом. Просто положите catch VAR1 VAR2
перед command args..
и вы сделали.
некоторые if cmd args..; then
станет if catch VAR1 VAR2 cmd args..; then
. На самом деле ничего сложного.
Обсуждение
Q: Как это работает?
это просто обертывания идей из других ответов здесь в функцию, такой, что его можно легко resused.
catch()
в основном использует eval
для установки двух переменных. Это похоже на https://stackoverflow.com/a/18086548
рассмотрим вызов catch out err dummy 1 2a 3b
:
- давайте
eval "$({
и__2="$(
сейчас. Я приду к этому позже. __1="$("$("${@:3}")"; } 2>&1;
выполняетdummy 1 2 3
и магазины этоstdout
на__1
для последующего использования. Так что__1
становится2a
. Он также перенаправляетstderr
ofdummy
tostdout
, такие, что внешний улов может собратьstdout
ret=$?;
ловит код выхода, который составляет1
printf '%q=%q\n' "" "$__1" >&2;
затем выводитout=2a
доstderr
.stderr
используется здесь, как текущийstdout
уже взял на себя рольstderr
на .exit $ret
затем пересылает код выхода (1
) к следующему этапу.
теперь к внешнему __2="$( ... )"
:
этот ловит
stdout
выше, что являетсяstderr
наdummy
вызова, в переменную__2
. (Мы могли бы повторно использовать__1
здесь, но я использовал__2
чтобы сделать его менее запутанным.). Так что__2
становится3b
ret="$?";
ловит (возвращенный) код возврата1
(отdummy
) сноваprintf '%s=%q\n' "" "$__2" >&2;
затем выводитerr=3a
tostderr
.stderr
is используется снова, так как он уже использовался для вывода другой переменнойout=2a
.printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to
catch`.
обратите внимание, что в качестве оптимизации мы могли бы написать эти 2 printf
как один как printf '%s=%q\n( exit %q )
"$__2" "$ret "' также.
Итак, что у нас есть до сих пор?
мы имеем следующее написанное к stderr:
out=2a
err=3b
( exit 1 )
здесь out
с ,
2a
от stdout
of dummy
, err
С ,
3b
С stderr
of dummy
и 1
из кода возврата от dummy
.
обратите внимание:%q
в формате printf
заботится о цитировании, так что оболочка видит правильные (одиночные) аргументы, когда дело доходит до eval
. 2a
и 3b
настолько просты, что копируются буквально.
теперь к наружному eval "$({ ... } 2>&1 )";
:
это выполняет все выше вывода 2 переменные и exit
, ловит его (поэтому 2>&1
) и анализирует его в текущей оболочке, используя eval
.
таким образом, устанавливаются 2 переменные и код возврата.
Q: он использует eval
что есть зло. Так это безопасно?
- пока
printf %q
не имеет ошибок, это должно быть безопасно. Но вы всегда должны быть очень осторожны, подумайте о контуженных.
Q: Жуки?
-
очевидные ошибки не известны, за исключением следующих:
- для ловли большого выхода нужна большая память и процессор, так как все идет в переменные и должно быть проанализировано оболочкой. Так что используйте его с умом.
-
как обычно
$(echo $'\n\n\n\n')
глотает все linefeeds, а не только последний. Это требование POSIX. Если вам нужно получить LFs невредимым, просто добавьте некоторый трейлинг-символ к выходу и удалите его после этого, как в следующем рецепте (посмотрите на трейлингx
что позволяет читать софтлинк, указывающий на файл, который заканчивается на$'\n'
):target="$(readlink -e "$file")x" target="${target%x}"
Shell-переменные не могут нести байт NUL (
$''
). Они просто игнорируются, если они происходят вstdout
илиstderr
.
данная команда выполняется в под-подрешетке. Таким образом, он не имеет доступа к
$PPID
, и он не может изменить переменная оболочки. Ты можешь!--88--> функция оболочки, даже встроенные, но они не смогут изменять переменные оболочки (как все работает в$( .. )
не может этого сделать). Поэтому, если вам нужно запустить функцию в текущей оболочке и поймать ее stderr/stdout, вам нужно сделать это обычным способом сtempfile
s. (Есть способы сделать это таким образом, что прерывание оболочки обычно не оставляет мусора, но это сложно и заслуживает собственного ответ.)
Q: версия Bash?
- я думаю, вам нужно Bash 4 и выше (из-за
printf %q
)
Q: это все еще выглядит так неудобно.
- право. еще один ответ здесь показывает, как это можно сделать в
ksh
гораздо чистоплотнее. Однако я не привык кksh
, поэтому я оставляю его другим, чтобы создать аналогичный простой в повторном использовании рецепт дляksh
.
Q: почему бы не использовать ksh
тогда?
- потому что это
bash
решение
Q: скрипт можно улучшить
- конечно, вы можете выжать некоторые байты и создать меньшее или более непонятное решение. Просто пойти на это ;)
Q: есть опечатка. : catch STDOUT STDERR cmd args..
читать # catch STDOUT STDERR cmd args..
- на самом деле это предназначено.
:
появляетсяbash -x
в то время как комментарии молча проглатываются. Так что вы можете увидеть, где парсер, если у вас есть опечатка в определении функции. Это старый отладочный трюк. Но будьте осторожны немного, вы можете легко создать некоторые аккуратные побочные эффекты в аргументах:
.
Edit: добавлена еще пара ;
сделать его более легким создать одиночн-вкладыш из catch()
. И добавлен раздел как это работает.
технически, именованные каналы не являются временными файлами, и никто здесь их не упоминает. Они ничего не хранят в файловой системе, и вы можете удалить их, как только вы подключите их (так что вы никогда не увидите их):
#!/bin/bash -e
foo () {
echo stdout1
echo stderr1 >&2
sleep 1
echo stdout2
echo stderr2 >&2
}
rm -f stdout stderr
mkfifo stdout stderr
foo >stdout 2>stderr & # blocks until reader is connected
exec {fdout}<stdout {fderr}<stderr # unblocks `foo &`
rm stdout stderr # filesystem objects are no longer needed
stdout=$(cat <&$fdout)
stderr=$(cat <&$fderr)
echo $stdout
echo $stderr
exec {fdout}<&- {fderr}<&- # free file descriptors, optional
таким образом, вы можете иметь несколько фоновых процессов и асинхронно собирать их stdouts и stderrs в удобное время и т. д.
Если вам это нужно только для одного процесса, вы можете также использовать жестко закодированные числа fd, такие как 3 и 4, вместо {fdout}/{fderr}
синтаксис (который находит для вас свободный fd).
не понравился eval, поэтому вот решение, которое использует некоторые трюки перенаправления для захвата вывода программы в переменную, а затем анализирует эту переменную для извлечения различных компонентов. Флаг-w задает размер фрагмента и влияет на порядок сообщений std-out / err в промежуточном формате. 1 дает потенциально высокое разрешение за счет накладных расходов.
#######
# runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later.
# limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output.
# example:
# var=$(keepBoth ls . notHere)
# echo ls had the exit code "$(extractOne r "$var")"
# echo ls had the stdErr of "$(extractOne e "$var")"
# echo ls had the stdOut of "$(extractOne o "$var")"
keepBoth() {
(
prefix(){
( set -o pipefail
base64 -w 1 - | (
while read c
do echo -E "" "$c"
done
)
)
}
( (
"$@" | prefix o >&3
echo ${PIPESTATUS[0]} | prefix r >&3
) 2>&1 | prefix e >&1
) 3>&1
)
}
extractOne() { # extract
echo "" | grep "^" | cut --delimiter=' ' --fields=2 | base64 --decode -
}
кратко, Я считаю, что ответ "Нет". Захват $( ... )
только захватывает стандартный вывод в переменную; нет способа получить стандартную ошибку, захваченную в отдельную переменную. Итак, то, что у вас есть, так же аккуратно, как и получается.
о чем... =D
GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
GET_STDERR=""
GET_STDOUT=""
unset t_std t_err
eval "$( (eval ) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )"
GET_STDERR=$t_err
GET_STDOUT=$t_std
}
get_stderr_stdout "command"
echo "$GET_STDERR"
echo "$GET_STDOUT"
в интересах читателя вот решение, используя tempfile
s.
вопрос не в том, чтобы использовать tempfile
s. Однако это может быть связано с нежелательным загрязнением /tmp/
С tempfile в случае, если оболочка умирает. В случае kill -9
некоторые trap 'rm "$tmpfile1" "$tmpfile2"' 0
не срабатывает.
если вы находитесь в ситуации, где вы можете использовать tempfile
, но хотите никогда не оставляйте мусора за, вот рецепт.
снова это называется catch()
(как мой другого ответа) и имеет тот же синтаксис вызова:
catch stdout stderr command args..
# Wrappers to avoid polluting the current shell's environment with variables
: catch_read returncode FD variable
catch_read()
{
eval "=\"\`cat <&\`\"";
# You can use read instead to skip some fork()s.
# However read stops at the first NUL byte,
# also does no \n removal and needs bash 3 or above:
#IFS='' read -ru -d '' "";
return ;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "";
"${@:3}" 66<&-;
catch_read $? 66 "";
} 2>&1 >"" 66<"";
}
: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}
что он делает:
он создает два
tempfile
s дляstdout
иstderr
. Однако он почти сразу удаляет их, так что они только вокруг в течение очень короткого времени.catch_1()
догоняетstdout
(FD 1) в переменную и перемещаетсяstderr
tostdout
, такой, что следующий ("слева")catch_1
улавливаете.обработка
catch
делается справа налево, так слеваcatch_1
выполняется последним и ловитstderr
.
худшее, что может случиться, что некоторые временные файлы появляются на /tmp/
, но в этом случае они всегда пусты. (Они удаляются до заполнения.). Обычно это не должно быть проблемой, так как под Linux tmpfs поддерживает примерно 128K файлов на Гб main память.
данная команда может получить доступ и изменить все локальные переменные оболочки. Таким образом, вы можете вызвать функцию оболочки, которая имеет sideffects!
это только вилки дважды для
tempfile
звонок.
ошибки:
отсутствует хорошая обработка ошибок в случае
tempfile
не удается.это обычный
\n
удаления оболочки. См. комментарий вcatch_read()
.нельзя использовать файловый дескриптор
66
для передачи данных в вашу команду. Если вам это нужно, используйте другой дескриптор для перенаправления, например42
(обратите внимание, что очень старые оболочки предлагают только FDs до 9).это не может обрабатывать байты NUL (
$''
) вstdout
иstderr
. (NUL просто игнорируется. Дляread
вариант все за нулем игнорируемый.)
FYI:
- Unix позволяет нам получать доступ к удаленным файлам, если вы сохраняете некоторую ссылку на них (например, открытую файловую ручку). Таким образом, мы можем открыть, а затем удалить их.
Если команда 1) нет побочных эффектов с состоянием и 2) является вычислительно дешевой, самое простое решение-просто запустить ее дважды. Я в основном использовал это для кода, который запускается во время загрузки, когда вы еще не знаете, будет ли диск работать. В моем случае это был крошечный some_command
таким образом, не было никакого хита производительности для запуска дважды, и команда не имела побочных эффектов.
главным образом преимущество что это чисто и легко для того чтобы прочитать. Решения здесь довольно умные, но я не хотелось бы быть тем, кто должен поддерживать скрипт, содержащий более сложные решения. Я бы рекомендовал простой подход run-it-twice, если ваш сценарий работает с этим, так как он намного чище и проще в обслуживании.
пример:
output=$(getopt -o '' -l test: -- "$@")
errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
echo "Option Error: $errout"
fi
опять же, это только нормально, потому что getopt не имеет побочных эффектов. Я знаю, что это безопасно для производительности, потому что мой родительский код вызывает это менее 100 раз в течение всей программы, и пользователь никогда не заметит 100 getopt звонки против 200 вызовов getopt.
вот более простой вариант, который не совсем то, что OP хотел, но в отличие от любого другого варианта. Вы можете получить все, что хотите, переставив файловые дескрипторы.
командная тест:
%> cat xx.sh
#!/bin/bash
echo stdout
>&2 echo stderr
что само по себе делает:
%> ./xx.sh
stdout
stderr
теперь распечатайте stdout, захватите stderr в переменную и войдите в stdout в файл
%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out
stdout
%> echo
$err
stderr
или log stdout & capture stderr для переменной:
export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr
вы поняли идею.
один обходной путь, который является хакерским, но, возможно, более интуитивным, чем некоторые из предложений на этой странице, - это пометить выходные потоки, объединить их и разделить впоследствии на основе тегов. Например, мы можем пометить stdout префиксом "STDOUT":
function someCmd {
echo "I am stdout"
echo "I am stderr" 1>&2
}
ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1)
OUT=$(echo "$ALL" | grep "^STDOUT" | sed -e 's/^STDOUT//g')
ERR=$(echo "$ALL" | grep -v "^STDOUT")
``
Если вы знаете, что stdout и/или stderr в ограниченном виде, вы можете придумать тег, который не конфликтует с их допустимое содержание.
предупреждение :нет (пока? Работает!
следующее кажется возможным привести к его работе без создания каких-либо временных файлов, а также только на POSIX sh; это требует base64, однако и из-за кодирования/декодирования может быть не так эффективно и использовать также "большую" память.
- даже в простом случае он уже потерпел бы неудачу, когда последняя строка stderr не имеет новой строки. Это может быть исправлено, по крайней мере, в некоторых случаях с заменой exe на " { exe; echo >&2;}", т. е. добавление новой строки.
-
основная проблема заключается в том, что все кажется колоритным. Попробуйте использовать exe, как:
exe() { cat / usr / share/hunspell / de_DE.ДВС-синдром cat / usr / share/hunspell / en_GB.dic >&2 }
и вы увидите, что, например, части закодированной строки base64 находятся в верхней части файла, части в конце и не декодированные stderr-материалы в середине.
Ну, даже если идея ниже не может быть работая (что я предполагаю), он может служить анти-примером для людей, которые могут ошибочно полагать, что его можно заставить работать так.
идея (или анти-пример):
#!/bin/sh
exe()
{
echo out1
echo err1 >&2
echo out2
echo out3
echo err2 >&2
echo out4
echo err3 >&2
echo -n err4 >&2
}
r="$( { exe | base64 -w 0 ; } 2>&1 )"
echo RAW
printf '%s' "$r"
echo RAW
o="$( printf '%s' "$r" | tail -n 1 | base64 -d )"
e="$( printf '%s' "$r" | head -n -1 )"
unset r
echo
echo OUT
printf '%s' "$o"
echo OUT
echo
echo ERR
printf '%s' "$e"
echo ERR
дает (с исправлением stderr-newline):
$ ./ggg
RAW
err1
err2
err3
err4
b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW
OUT
out1
out2
out3
out4OUT
ERR
err1
err2
err3
err4ERR
(по крайней мере, на тире и bash Debian)