Захват и stdout и stderr в Bash

Я знаю этот синтаксис

var=`myscript.sh`

или

var=$(myscript.sh)

Захват результата (stdout) myscript.sh в var. Я мог бы перенаправить stderr в stdout, если бы захотел захватить оба. Как сохранить каждую из них для разделения переменных?

Мой вариант использования здесь, если код возврата отличен от нуля, я хочу откликнуть stderr и подавить в противном случае. Могут быть другие способы сделать это, но этот подход кажется, что он будет работать, если это действительно возможно.

Ответ 1

Невозможно записать оба без временного файла.

Вы можете записать stderr в переменную и передать stdout на экран пользователя (образец из здесь):

exec 3>&1                    # Save the place that stdout (1) points to.
output=$(command 2>&1 1>&3)  # Run command.  stderr is captured.
exec 3>&-                    # Close FD #3.

# Or this alternative, which captures stderr, letting stdout through:
{ output=$(command 2>&1 1>&3-) ;} 3>&1

Но невозможно зафиксировать как stdout, так и stderr:

То, что вы не можете сделать, это захват stdout в одной переменной и stderr в другой, используя только перенаправления FD. Вы должны использовать временный файл (или именованный канал) для достижения этого.

Ответ 2

Там действительно уродливый способ захватить stderr и stdout в двух отдельных переменных без временных файлов (если вам нравится сантехника), используя замещение процесса, source и declare соответственно. Я назову вашу команду banana. Вы можете имитировать такую ​​команду с помощью функции:

banana() {
    echo "banana to stdout"
    echo >&2 "banana to stderr"
}

Предполагаю, что вы хотите стандартный вывод banana в переменной bout и стандартную ошибку banana в переменной berr. Здесь волшебство, которое достигнет этого (Bash ≥4 только):

. <({ berr=$({ bout=$(banana); } 2>&1; declare -p bout >&2); declare -p berr; } 2>&1)

Итак, что здесь происходит?

Пусть начнется с самого внутреннего термина:

bout=$(banana)

Это стандартный способ присвоить bout стандартный вывод banana, стандартная ошибка отображается на вашем терминале.

Тогда:

{ bout=$(banana); } 2>&1

по-прежнему будет назначать bout stdout banana, но stderr banana отображается на терминале через stdout (благодаря перенаправлению 2>&1.

Тогда:

{ bout=$(banana); } 2>&1; declare -p bout >&2

будет делать, как указано выше, но также будет отображать на терминале (через stderr) содержимое bout с помощью встроенного declare: это будет повторно использовано в ближайшее время.

Тогда:

berr=$({ bout=$(banana); } 2>&1; declare -p bout >&2); declare -p berr

назначит berr stderr banana и отобразит содержимое berr с помощью declare.

В этот момент вы будете иметь на экране терминала:

declare -- bout="banana to stdout"
declare -- berr="banana to stderr"

с линией

declare -- bout="banana to stdout"

отображается через stderr.

Окончательное перенаправление:

{ berr=$({ bout=$(banana); } 2>&1; declare -p bout >&2); declare -p berr; } 2>&1

будет отображаться предыдущее через stdout.

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


Вы также указали код возврата команды. Измените banana на:

banana() {
    echo "banana to stdout"
    echo >&2 "banana to stderr"
    return 42
}

Мы также будем иметь код возврата banana в переменной bret так:

. <({ berr=$({ bout=$(banana); bret=$?; } 2>&1; declare -p bout bret >&2); declare -p berr; } 2>&1)

Вы можете обойтись без источников и подстановки процесса, используя также eval (и он работает с Bash < 4 тоже):

eval "$({ berr=$({ bout=$(banana); bret=$?; } 2>&1; declare -p bout bret >&2); declare -p berr; } 2>&1)"

И все это безопасно, потому что только те, что мы source ing или eval ing, получены из declare -p и всегда будут правильно экранированы.


Конечно, если вы хотите вывод в массиве (например, с mapfile, если вы используете Bash ≥4-иначе замените mapfile на цикл while - read), адаптация проста.

Например:

banana() {
    printf 'banana to stdout %d\n' {1..10}
    echo >&2 'banana to stderr'
    return 42
}

. <({ berr=$({ mapfile -t bout < <(banana); } 2>&1; declare -p bout >&2); declare -p berr; } 2>&1)

и с кодом возврата:

. <({ berr=$({ mapfile -t bout< <(banana; bret=$?; declare -p bret >&3); } 3>&2 2>&1; declare -p bout >&2); declare -p berr; } 2>&1)

Ответ 3

Вы можете сделать:

OUT=$(myscript.sh 2> errFile)
ERR=$(<errFile)

Теперь $OUT будет иметь стандартный вывод ваших script и $ERR ошибок с вашим script.

Ответ 4

Простой, но не элегантный способ: перенаправить stderr во временный файл и затем прочитать его:

TMP=$(mktemp)
var=$(myscript.sh 2> "$TMP")
err=$(cat "$TMP")
rm "$TMP"

Ответ 5

Пока я не нашел способ захвата stderr и stdout для разделения переменных в bash, я отправляю обе те же переменные с...

result=$( { grep "JUNK" ./junk.txt; } 2>&1 )

... затем я проверяю статус выхода "$?" и действую соответствующим образом на данные в $result.

Ответ 6

# NAME
#   capture - capture the stdout and stderr output of a command
# SYNOPSIS
#   capture <result> <error> <command>
# DESCRIPTION
#   This shell function captures the stdout and stderr output of <command> in
#   the shell variables <result> and <error>.
# ARGUMENTS
#   <result>  - the name of the shell variable to capture stdout
#   <error>   - the name of the shell variable to capture stderr
#   <command> - the command to execute
# ENVIRONMENT
#   The following variables are mdified in the caller context:
#    - <result>
#    - <error>
# RESULT
#   Retuns the exit code of <command>.
# SOURCE
capture ()
{
    # Name of shell variable to capture the stdout of command.
    result=$1
    shift

    # Name of shell variable to capture the stderr of command.
    error=$1
    shift

    # Local AWK program to extract the error, the result, and the exit code
    # parts of the captured output of command.
    local evaloutput='
        {
            output [NR] = $0
        }
        END \
        {
            firstresultline = NR - output [NR - 1] - 1
            if (Var == "error") \
            {
                for (i = 1; i < firstresultline; ++ i)
                {
                    printf ("%s\n", output [i])
                }
            }
            else if (Var == "result") \
            {
                for (i = firstresultline; i < NR - 1; ++ i)
                {
                    printf ("%s\n", output [i])
                }
            }
            else \
            {
                printf ("%d", output [NR])
            }
        }'

    # Capture the stderr and stdout output of command, as well as its exit code.
    local output="$(
    {
        local stdout
        stdout="$($*)"
        local exitcode=$?
        printf "\n%s\n%d\n%d\n" \
               "$stdout" "$(echo "$stdout" | wc -l)" "$exitcode"
    } 2>&1)"

    # extract the stderr, the stdout, and the exit code parts of the captured
    # output of command.
    printf -v $error "%s" \
                     "$(echo "$output" | gawk -v Var="error" "$evaloutput")"
    printf -v $result "%s" \
                      "$(echo "$output" | gawk -v Var="result" "$evaloutput")"
    return $(echo "$output" | gawk "$evaloutput")
}