Вопрос или проблема
func() {
echo 'hello'
echo 'This is an error' >&2
}
a=$(func)
b=???
Я хочу перенаправить stderr в переменную b
без создания временного файла.
echo $b
# вывод должен быть: "This is an error"
Решение, которое работает, но с временным файлом:
touch temp.txt
exec 3< temp.txt
a=$(func 2> temp.txt);
cat <&3
rm temp.txt
Итак, вопрос: как перенаправить stderr
функции bash func
в переменную b
без необходимости создания временного файла?
В Linux и с оболочками, которые реализуют here-документы с временными файлами на запись (например, zsh
или bash
версий до 5.1), вы можете сделать:
{
out=$(
chmod u+w /dev/fd/3 && # необходимо для bash5.0
ls /dev/null /x 2> /dev/fd/3
)
status=$?
err=$(cat<&3)
} 3<<EOF
EOF
printf '%s=<%s>\n' out "$out" err "$err" status "$status"
(где ls /dev/null /x
— это пример команды, которая выводит что-то как на stdout, так и на stderr).
С zsh
вы также можете сделать:
(){ out=$(ls /dev/null /x 2> $1) status=$? err=$(<$1);} =(:)
(где =(cmd)
— это форма подстановки процесса, которая использует временные файлы, а (){ code; } args
— анонимные функции).
В любом случае, вы захотите использовать временные файлы. Любое решение, использующее каналы, будет подвержено взаимным блокировкам в случае большого объема вывода. Вы могли бы читать stdout и stderr через два отдельных канала и использовать select()
/poll()
и некоторые чтения в цикле, чтобы читать данные по мере поступления из двух каналов без создания блокировок, но это было бы достаточно сложно, и насколько мне известно, только zsh
имеет встроенную поддержку select()
, а только yash
— сырой интерфейс к pipe()
(подробнее об этом на Чтение / запись в один и тот же дескриптор файла с помощью перенаправления в оболочке).
Другой подход мог бы заключаться в хранении одного из потоков во временной памяти вместо временного файла. Например (синтаксис zsh
или bash
):
{
IFS= read -rd '' err
IFS= read -rd '' out
IFS= read -rd '' status
} < <({ out=$(ls /dev/null /x); } 2>&1; printf '\0%s' "$out" "$?")
(при условии, что команда не выводит никаких NUL)
Обратите внимание, что $err
будет включать завершающий символ новой строки.
Другие подходы могли бы заключаться в том, чтобы по-разному декорировать stdout и stderr и удалять декорирование при чтении:
out= err= status=
while IFS= read -r line; do
case $line in
(out:*) out=$out${line#out:}$'\n';;
(err:*) err=$err${line#err:}$'\n';;
(status:*) status=${line#status:};;
esac
done < <(
{
{
ls /dev/null /x |
grep --label=out --line-buffered -H '^' >&3
echo >&3 "status:${PIPESTATUS[0]}" # $pipestatus[1] в zsh
} 2>&1 |
grep --label=err --line-buffered -H '^'
} 3>&1
)
Это предполагает использование GNU grep
и то, что строки достаточно короткие. С строками больше PIPEBUF (4K на Linux), строки вывода двух grep
могут в итоге перепутаться вместе в сегментах.
Ну, захват stderr в одну переменную и stdout в другую без временного файла весьма непростая задача.
Вот пример, который работает
func() {
echo 'hello'
echo 'This is an error' >&2
}
result=$(
{ stdout=$(func) ; } 2>&1
echo -e "mysuperuniqueseparator\n"
echo -e "${stdout}\n"
)
var_out=${result#*mysuperuniqueseparator$'\n'}
var_err=${result%$'\n'mysuperuniqueseparator*}
Мне не нравится это, потому что это грязный способ: перенаправьте stderr в stdout и положите оба в одну переменную с разделителем между ними, а затем разделите на две части.
Плюс:
Очевидно, это не надежно, потому что либо стандартный вывод, либо стандартная ошибка команды могут содержать любой разделитель, который вы используете.
Взято отсюда http://mywiki.wooledge.org/BashFAQ/002
Нет временных файлов/_FIFO и никаких смешных evals/дескрипторов файла и т. д.:
x=$((echo 'это stdout'; echo 'это stderr' 1>&2; exit 123) 2> >(sed -r 's/^/2/g') 1> >(sed -r 's/^/1/g'))
echo $? ### код выхода сохраняется
# 123
echo "$x" | sed '/^2/d;s/^1//g' ### stdout
# это stdout
echo "$x" | sed '/^1/d;s/^2//g' ### stderr
# это stderr
Примечание: может быть неэффективно для больших объемов данных.
.
Ответ или решение
Редиректирование вывода ошибок (STDERR) и стандартного вывода (STDOUT) в разные переменные без использования временных файлов – это задача, которая может показаться сложной из-за ограничений командных оболочек, таких как Bash. В этой статье мы подробно разберем такой способ, который не использует временные файлы, и обсудим различные подходы к решению.
Теория
В UNIX-подобных системах стандартный вывод, стандартный вывод ошибок и стандартный ввод обозначаются файловыми дескрипторами 1, 2 и 0 соответственно. Вопрос заключается в том, как разделить эти потоки данных так, чтобы можно было сохранить их в разные переменные для дальнейшей обработки. При этом часто используется конструкция с перенаправлением в файловый дескриптор или временный файл, что нежелательно в некоторых случаях из-за потенциальных проблем с безопасностью, производительностью или ограничениями использования.
Пример решения без временных файлов
Для решения данной задачи можно использовать механизм процесса замещения (Process Substitution), доступный в некоторых оболчках, например, в zsh или в bash начиная с версии 4.0. Этот подход позволяет направлять вывод команд напрямую в скрипты через анонимные именованные каналы, что предотвращает необходимость использования временных файлов.
Пример простого Bash-скрипта, который реализует данное решение, выглядит следующим образом:
func() {
echo 'hello'
echo 'This is an error' >&2
}
result=$(
# Вся функция выполняется, её stdout и stderr объединяются
{
stdout=$(func) # STDOUT помечается как переменная
} 2>&1 # STDERR также направляется на STDOUT
echo '---STDOUT END---' # Уникальный разделитель
echo "$stdout"
echo '---STDERR DATA---'
)
# Делим поток на две переменные по заданному разделителю
var_out="${result%%---STDERR DATA---*}"
var_err="${result##*---STDOUT END---}"
Здесь мы используем уникальные строки-разделители, чтобы отделить стандартный вывод от вывода ошибок в рассмотренном решении. Теперь можно разложить переменную result
на var_out
и var_err
.
Применение и ограничения
Хотя это решение и работает в некоторых случаях, у него есть свои ограничения. Основное заключается в том, что команда stdout
или stderr
не должна содержать строки разделителей. Кроме того, данный способ не может гарантировать корректность при больших объемах данных: в таком случае может возникнуть блокировка, так как Bash не поддерживает ненадежные операции чтения/записи из канала.
Эти ограничения могут быть решены с помощью более сложных подходов, например, использования конструкций select()
или poll()
для чтения данных по мере их поступления из обоих потоков через разделенные каналы. Однако подобное решение требует значительно больше работы, и такая проработанность доступна только в более продвинутых оболочках, таких как zsh
.
Заключение
Отказ от временных файлов для разделения STDERR и STDOUT на переменные требует использования тонкостей механизмов перенаправления Bash, таких как процесс замещения, и часто не обходится без компромиссов. Тем не менее, знание этих техник позволяет создавать более элегантные и производительные скрипты, которые учитывают специфические требования вашей системы и окружающей среды. Важно помнить, что ни одно решение не является универсальным, и его выбор следует делать на основе анализа конкретной задачи и ее ограничений.