Подтвердить что ты не робот

Захват stdout и stderr в разные переменные

Можно ли хранить или записывать stdout и stderr в разные переменные без использования временного файла? Прямо сейчас я делаю это для out stdout и stderr в err при запуске some_command, но я бы хотел избежать временного файла.

error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< error_file)
rm $error_file
4b9b3361

Ответ 1

Хорошо, это немного уродливо, но вот решение:

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).

Ответ 2

У Джонатана есть ответ. Для справки, это трюк ksh93. (требуется не-древняя версия).

function out {
    echo stdout
    echo stderr >&2
}

x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values

производит

x=stderr
y=stdout

Синтаксис ${ cmds;} - это просто подстановка команды, которая не создает подоболочку. Команды выполняются в текущей среде оболочки. Пространство в начале важно ({ - зарезервированное слово).

Stderr внутренней группы команд перенаправляется на stdout (так, чтобы он применялся к внутренней подстановке). Затем stdout out присваивается y, а перенаправленный stderr захватывается x, без обычной потери y в подзаголовок подстановки команд.

Это невозможно в других оболочках, потому что все конструкции, которые захватывают выходные данные, должны помещать производителя в подоболочку, которая в этом случае будет включать назначение.

update: теперь также поддерживается mksh.

Ответ 3

Эта команда устанавливает значения stdout (stdval) и stderr (errval) в текущей рабочей оболочке:

eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"

при условии, что эта функция определена:

function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

Измените execcommand на захваченную команду, будь то "ls", "cp", "df" и т.д.


Все это основано на идее, что мы можем преобразовать все полученные значения в текстовую строку с помощью функции setval, тогда setval используется для захвата каждого значения в этой структуре:

execcommand 2> CaptureErr > CaptureOut

Преобразование каждого значения захвата в вызов setval:

execcommand 2> >(setval errval) > >(setval stdval)

Оберните все внутри вызова выполнения и эхо:

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) )"

и, наконец, эхо-набор значений:

echo "std out is : |$stdval| std err is : |$errval|

Можно также включить значение return (exit).
Полный пример bash script выглядит следующим образом:

#!/bin/bash --

# The only function to declare:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

# 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|"

Ответ 4

Это для отлова stdout и stderr в разных переменных. Если вы хотите поймать только stderr, оставив stdout как есть, есть лучшее и более короткое решение.

Для того, чтобы подвести все вверх для удобства читателя, вот

Простое многоразовое решение для bash

Эта версия использует подоболочки и работает без tempfile s. (Для версии tempfile которая работает без подоболочек, см. Мой другой ответ.)

: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
  { __1="$("${@:3}")"; } 2>&1;
  ret=$?;
  printf '%q=%q\n' "$1" "$__1" >&2;
  exit $ret
  )"
ret="$?";
printf '%s=%q\n' "$2" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}

Пример использования:

dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}

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 cmd args..; then станет if catch VAR1 VAR2 cmd args..; then if catch VAR1 VAR2 cmd args..; then. На самом деле ничего сложного.

обсуждение

Q: Как это работает?

Он просто оборачивает идеи из других ответов здесь в функцию, так что ее можно легко использовать повторно.

catch() основном использует eval для установки двух переменных. Это похоже на fooobar.com/questions/4703/...

Рассмотрим вызов catch out err dummy 1 2a 3b:

  • давайте пока пропустим eval "$({ и __2="$( я вернусь к этому позже).

  • __1="$("$("${@:3}")"; } 2>&1; выполняет dummy 1 2 3 и сохраняет ее стандартный stdout в __1 для дальнейшего использования. Таким образом, __1 становится 2a. Он также перенаправляет stderr dummy к stdout, так что внешний улов может собрать stdout

  • ret=$?; ловит код выхода, который равен 1

  • printf '%q=%q\n' "$1" "$__1" >&2; затем выводит out=2a в stderr. Здесь используется stderr, так как текущий stdout уже взял на себя роль stderr команды dummy.

  • exit $ret затем перенаправляет код выхода (1) на следующий этап.

Теперь к внешнему __2="$(... )":

  • Это ловит stdout из вышеперечисленных, который является stderr из dummy вызова, в переменную __2. (Мы могли бы повторно использовать __1 здесь, но я использовал __2 чтобы сделать его менее запутанным.). Таким образом, __2 становится 3b

  • ret="$?"; ловит (возвращенный) код возврата 1 (из dummy) снова

  • printf '%s=%q\n' "$2" "$__2" >&2; затем выводит err=3a в stderr. Снова используется stderr, так как он уже использовался для вывода другой переменной out=2a.

  • printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. я 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 printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. я 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 перехвата '.

Обратите внимание, что в качестве оптимизации мы могли бы записать эти 2 printf как один, например printf '%s=%q\n( exit %q ) "$ __ 2" "$ ret"'.

Так что у нас так далеко?

Мы написали следующее для stderr:

out=2a
err=3b
( exit 1 )

где out - от $1, 2a - от stdout dummy, err - от $2, 3b - от stderr dummy, а 1 - от кода возврата от dummy.

Обратите внимание, что %q в формате printf заботится о кавычках, так что оболочка видит правильные (одиночные) аргументы, когда дело доходит до eval. 2a и 3b настолько просты, что они копируются буквально.

Теперь к внешнему eval "$({... } 2>&1 )"; :

Выполняется все вышеперечисленное, которое выводит 2 переменные и exit, перехватывает их (для этого 2>&1) и анализирует их в текущей оболочке, используя eval.

Таким образом, устанавливаются 2 переменные и код возврата.

Q: Он использует eval который является злом. Так это безопасно?

  • Пока у printf %q нет ошибок, это должно быть безопасно. Но вы всегда должны быть очень осторожны, просто подумайте о ShellShock.

Q: ошибки?

  • Никаких очевидных ошибок не известно, кроме следующих:

    • Для захвата больших выходных данных требуется большая память и процессор, так как все идет в переменные и оболочка должна анализировать их обратно. Так что используйте это с умом.
    • Как обычно, $(echo $'\n\n\n\n') поглощает все переводы строк, а не только последний. Это требование POSIX. Если вам нужно получить LF без повреждений, просто добавьте какой-либо завершающий символ в вывод и удалите его впоследствии, как в следующем рецепте (посмотрите на конечный x который позволяет читать мягкую ссылку, указывающую на файл, заканчивающийся на $'\n'):

      target="$(readlink -e "$file")x"
      target="${target%x}"
      
    • Переменные оболочки не могут содержать байт NUL ($'\0'). Они просто игнорируют, если они происходят в stdout или stderr.

  • Данная команда выполняется в под-оболочке. Таким образом, он не имеет доступа к $PPID и не может изменять переменные оболочки. Вы можете catch функцию оболочки, даже встроенную, но они не смогут изменять переменные оболочки (так как все, что выполняется в $(.. ) не может этого сделать). Поэтому, если вам нужно запустить функцию в текущей оболочке и перехватить ее как 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 то время как комментарии молча проглатываются. Таким образом, вы можете увидеть, где находится анализатор, если у вас есть опечатка в определении функции. Это старый прием отладки. Но остерегайтесь немного, вы можете легко создать некоторые аккуратные побочные эффекты в аргументах :

Изменение: добавили еще пару ; чтобы было проще создать однострочную версию из catch(). И добавил раздел, как это работает.

Ответ 5

Технически, именованные каналы не являются временными файлами, и никто здесь не упоминает их. Они ничего не хранят в файловой системе, и вы можете удалить их, как только вы их соедините (так что вы их никогда не увидите):

#!/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 в удобное время и т.д.

Если вам нужно это только для одного процесса, вы можете просто использовать hardcoded fd numbers, например 3 и 4, вместо синтаксиса {fdout}/{fderr} (который находит для вас бесплатный fd).

Ответ 6

Не понравилось eval, поэтому вот решение, которое использует некоторые приемы перенаправления для захвата вывода программы в переменную, а затем анализирует эту переменную для извлечения различных компонентов. Флаг -w устанавливает размер порции и влияет на порядок сообщений std-out/err в промежуточном формате. 1 дает потенциально высокое разрешение за счет накладных расходов.

#######                                                                                                                                                                                                                          
# runs "[email protected]" 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 "$1" "$c"                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
          done                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
        )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
      )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    ( (                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
        "[email protected]" | prefix o >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
        echo  ${PIPESTATUS[0]} | prefix r >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                           
      ) 2>&1 | prefix e >&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
    ) 3>&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
  )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

extractOne() { # extract                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

Ответ 7

Вкратце, я считаю, что ответ "Нет". Запись $( ... ) только фиксирует стандартный вывод переменной; нет способа получить стандартную ошибку, захваченную в отдельную переменную. Итак, у вас есть такая же аккуратная, как и она.

Ответ 8

Как насчет... = D

GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
    GET_STDERR=""
    GET_STDOUT=""
    unset t_std t_err
    eval "$( (eval $1) 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"

Ответ 9

В интересах читателя здесь используется решение с использованием tempfile s.

Вопрос заключался не в использовании tempfile s. Однако это может быть связано с нежелательным загрязнением /tmp/ с временным файлом в случае, если оболочка умирает. В случае kill -9 некоторая trap 'rm "$tmpfile1" "$tmpfile2"' 0 не срабатывает.

Если вы находитесь в ситуации, когда вы можете использовать tempfile, но хотите, чтобы никогда не оставлял мусор за, вот рецепт.

Снова он называется catch() (как мой другой ответ) и имеет тот же синтаксис вызова:

catch stdout stderr command args..

# Wrappers to avoid polluting the current shell environment with variables

: catch_read returncode FD variable
catch_read()
{
eval "$3=\"\`cat <&$2\`\"";
# 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$2 -d '' "$3";
return $1;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "$1";
"${@:3}" 66<&-;
catch_read $? 66 "$2";
} 2>&1 >"$1" 66<"$1";
}

: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}

Что он делает:

  • Создает два tempfile для stdout и stderr. Однако он почти сразу удаляет их, так что они находятся только в течение очень короткого времени.

  • catch_1() перехватывает stdout (FD 1) в переменную и перемещает stderr в stdout, так что следующий ( "левый" ) catch_1 может поймать это.

  • Обработка в catch выполняется справа налево, поэтому левый catch_1 выполняется последним и ловит stderr.

Хуже всего то, что некоторые временные файлы появляются на /tmp/, но в любом случае они всегда пусты. (Они удаляются, прежде чем они заполняются.). Обычно это не должно быть проблемой, так как под Linux tmpfs поддерживает примерно 128 тыс. Файлов на ГБ основной памяти.

  • Данная команда может также получить доступ и изменить все локальные переменные оболочки. Таким образом, вы можете вызвать функцию оболочки, которая имеет побочные эффекты!

  • Это только два раза для вызова tempfile.

Ошибка:

  • Отсутствует хорошая обработка ошибок в случае неудачи tempfile.

  • Это обычное удаление \n оболочки. См. Комментарий в catch_read().

  • Вы не можете использовать дескриптор файла 66 для передачи данных в вашу команду. Если вам это нужно, используйте другой дескриптор для перенаправления, например 42 (обратите внимание, что очень старые оболочки только предлагают FD до 9).

  • Это не может обрабатывать байты NUL ($'\0') в stdout и stderr. (NUL просто игнорируется. Для варианта read все, что стоит за NUL, игнорируется.)

FYI:

  • Unix позволяет нам получать доступ к удаленным файлам, если вы сохраняете некоторую ссылку на них (например, открытую дескриптор файла). Таким образом мы можем открыть, а затем удалить их.

Ответ 10

Если команда 1) не имеет побочных эффектов с состоянием и 2) является недорогой, проще всего просто запустить ее дважды. В основном я использовал это для кода, который запускается во время загрузки, когда вы еще не знаете, будет ли диск работать. В моем случае это был крошечный some_command, поэтому не было производительности для запуска дважды, и команда не имела побочных эффектов.

Основное преимущество заключается в том, что это чисто и легко читается. Решения здесь довольно умны, но я бы не хотел быть тем, кто должен поддерживать script, содержащий более сложные решения. Я бы рекомендовал простой подход "два раза в два", если ваш сценарий работает с этим, так как он намного чище и проще в обслуживании.

Пример:

output=$(getopt -o '' -l test: -- "[email protected]")
errout=$(getopt -o '' -l test: -- "[email protected]" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
        echo "Option Error: $errout"
fi

Опять же, это нормально, потому что getopt не имеет побочных эффектов. Я знаю, что это безопасно, потому что мой родительский код вызывает это менее 100 раз в течение всей программы, и пользователь никогда не заметит 100 вызовов getopt против 200 вызовов getopt.

Ответ 11

Здесь более простое изменение, которое не совсем то, что хотел OP, но в отличие от любого другого варианта. Вы можете получить все, что захотите, переставив дескрипторы файлов.

Команда тестирования:

%> cat xx.sh  
#!/bin/bash
echo stdout
>&2 echo stderr

который сам по себе делает:

%> ./xx.sh
stdout
stderr

Теперь напечатайте stdout, запишите stderr в переменную и log stdout в файл

%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out    
stdout
%> echo
$err 
stderr

Или запишите stdout и запишите stderr в переменную:

export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr

Вы получаете идею.

Ответ 12

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

Ответ 13

ПРЕДУПРЕЖДЕНИЕ: НЕ (еще?) РАБОТА!

Следующее, кажется, может привести к его работе без создания временных файлов, а также только для POSIX sh; он требует base64, однако из-за кодирования/декодирования может быть не так эффективно и использовать также "большую" память.

  • Даже в простом случае это уже не сработает, когда в последней строке stderr нет новой строки. Это может быть исправлено, по крайней мере, в некоторых случаях с заменой exe на "{exe; echo > & 2;}", то есть добавление новой строки.
  • Основная проблема заключается в том, что все кажется ярким. Попробуйте использовать exe, например:

    EXE() {   cat/usr/share/hunspell/de_DE.dic   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

(По крайней мере, на тире Debian и bash)