Захват 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 решение

эта версия использует подсхемы и работает без tempfiles. (Для 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 of dummy to stdout, такие, что внешний улов может собрать 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 to stderr. 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, вам нужно сделать это обычным способом с tempfiles. (Есть способы сделать это таким образом, что прерывание оболочки обычно не оставляет мусора, но это сложно и заслуживает собственного ответ.)

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"

в интересах читателя вот решение, используя tempfiles.

вопрос не в том, чтобы использовать tempfiles. Однако это может быть связано с нежелательным загрязнением /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}";
}

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

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

  • catch_1() догоняет stdout (FD 1) в переменную и перемещается stderr to stdout, такой, что следующий ("слева") 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)