Перенаправить STDERR и STDOUT в разные переменные без временных файлов.

Вопрос или проблема

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 функции 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, таких как процесс замещения, и часто не обходится без компромиссов. Тем не менее, знание этих техник позволяет создавать более элегантные и производительные скрипты, которые учитывают специфические требования вашей системы и окружающей среды. Важно помнить, что ни одно решение не является универсальным, и его выбор следует делать на основе анализа конкретной задачи и ее ограничений.

Оцените материал
Добавить комментарий

Капча загружается...