假设我有一个像下面这样的脚本:

useless.sh

echo "This Is Error" 1>&2
echo "This Is Output" 

我有另一个shell脚本:

alsoUseless.sh

./useless.sh | sed 's/Output/Useless/'

我想捕捉“这是错误”,或任何其他stderr从无用的。sh,到一个变量。 我们称它为ERROR。

注意,我使用了stdout。我想继续使用stdout,所以在这种情况下,将stderr重定向到stdout没有帮助。

所以,基本上,我想做

./useless.sh 2> $ERROR | ...

但这显然行不通。

我也知道我能做到

./useless.sh 2> /tmp/Error
ERROR=`cat /tmp/Error`

但这是丑陋和不必要的。

不幸的是,如果这里没有答案,这就是我要做的。

我希望还有别的办法。

有人有更好的主意吗?


当前回答

为了方便读者,这是食谱

可以重新使用作为联机捕获stderr到一个变量 仍然允许访问命令的返回代码 牺牲一个临时文件描述符3(当然可以由您更改) 并且不将此临时文件描述符公开给内部命令

如果你想将某些命令的stderr捕获到var中,你可以这样做

{ var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;

之后你就拥有了一切:

echo "command gives $? and stderr '$var'";

如果命令很简单(不是像| b那样),你可以去掉内部的{}:

{ var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;

包装成一个简单的可重用bash函数(可能需要版本3及以上的local -n):

: catch-stderr var cmd [args..]
catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }

解释道:

local -n aliases "$1" (which is the variable for catch-stderr) 3>&1 uses file descriptor 3 to save there stdout points { command; } (or "$@") then executes the command within the output capturing $(..) Please note that the exact order is important here (doing it the wrong way shuffles the file descriptors wrongly): 2>&1 redirects stderr to the output capturing $(..) 1>&3 redirects stdout away from the output capturing $(..) back to the "outer" stdout which was saved in file descriptor 3. Note that stderr still refers to where FD 1 pointed before: To the output capturing $(..) 3>&- then closes the file descriptor 3 as it is no more needed, such that command does not suddenly has some unknown open file descriptor showing up. Note that the outer shell still has FD 3 open, but command will not see it. The latter is important, because some programs like lvm complain about unexpected file descriptors. And lvm complains to stderr - just what we are going to capture!

如果进行相应的调整,则可以使用此配方捕获任何其他文件描述符。当然除了文件描述符1(这里的重定向逻辑是错误的,但是对于文件描述符1,您可以像往常一样使用var=$(命令))。

注意,这牺牲了文件描述符3。如果您恰好需要该文件描述符,可以随意更改数字。但是请注意,一些shell(从20世纪80年代开始)可能将99>&1理解为参数9,后面跟着9>&1(这对bash来说没有问题)。

还要注意的是,通过一个变量来配置这个fd3并不是特别容易。这使得内容非常难以阅读:

: catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
catch-var-from-fd-by-fd()
{
local -n v="$1";
local fd1="$2" fd2="$3";
shift 3 || return;

eval exec "$fd2>&1";
v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}

安全注意:catch-var-from-fd-by-fd的前3个参数不能来自第三方。始终以“静态”方式明确地给出它们。 所以不-不-不catch-var-from-fd-by-fd $var $fda $fdb $命令,永远不要这样做! 如果你碰巧传递了一个变量名,至少要这样做: Local -n var="$var";Catch-var-from-fd-by-fd var 3 5 $命令 这仍然不能保护您免受所有攻击,但至少有助于检测和避免常见的脚本错误。

注:

catch-var-from-fd-by-fd var 2 3 cmd.. is the same as catch-stderr var cmd.. shift || return is just some way to prevent ugly errors in case you forget to give the correct number of arguments. Perhaps terminating the shell would be another way (but this makes it hard to test from commandline). The routine was written such, that it is more easy to understand. One can rewrite the function such that it does not need exec, but then it gets really ugly. This routine can be rewritten for non-bash as well such that there is no need for local -n. However then you cannot use local variables and it gets extremely ugly! Also note that the evals are used in a safe fashion. Usually eval is considerered dangerous. However in this case it is no more evil than using "$@" (to execute arbitrary commands). However please be sure to use the exact and correct quoting as shown here (else it becomes very very dangerous).

其他回答

# command receives its input from stdin.
# command sends its output to stdout.
exec 3>&1
stderr="$(command </dev/stdin 2>&1 1>&3)"
exitcode="${?}"
echo "STDERR: $stderr"
exit ${exitcode}

重定向stderr到stdout, stdout到/dev/null,然后使用反勾号或$()捕获重定向的stderr:

ERROR=$(./useless.sh 2>&1 >/dev/null)

我认为你想要捕获stderr, stdout和exitcode,如果这是你的意图,你可以使用这段代码:

## Capture error when 'some_command() is executed
some_command_with_err() {
    echo 'this is the stdout'
    echo 'this is the stderr' >&2
    exit 1
}

run_command() {
    {
        IFS=$'\n' read -r -d '' stderr;
        IFS=$'\n' read -r -d '' stdout;
        IFS=$'\n' read -r -d '' stdexit;
    } < <((printf '\0%s\0%d\0' "$(some_command_with_err)" "${?}" 1>&2) 2>&1)
    stdexit=${stdexit:-0};
}

echo 'Run command:'
if ! run_command; then
    ## Show the values
    typeset -p stdout stderr stdexit
else
    typeset -p stdout stderr stdexit
fi

这个脚本捕获标准错误、标准输出以及退出代码。

但是Teo它是如何工作的呢?

首先,我们使用printf '\0%s\0%d\0'捕获标准输出和退出代码。它们由\0分隔,也就是“空字节”。

在此之后,我们通过执行:1>&2将printf重定向到stderr,然后使用2>&1将所有重定向回stdout。因此,标准输出看起来像:

"<stderr>\0<stdout>\0<exitcode>\0"

将printf命令封装在<(…)执行流程替换。进程替换允许使用文件名引用进程的输入或输出。这意味着<(…)将输出(printf '\0%s\0%d\0' "$(some_command_with_err)"“$ {?}" 1>&2) 2>&1到命令组的stdin中使用第一个<。

Then, we can capture the piped stdout from the stdin of the command group with read. This command reads a line from the file descriptor stdin and split it into fields. Only the characters found in $IFS are recognized as word delimiters. $IFS or Internal Field Separator is a variable that determines how Bash recognizes fields, or word boundaries, when it interprets character strings. $IFS defaults to whitespace (space, tab, and newline), but may be changed, for example, to parse a comma-separated data file. Note that $* uses the first character held in $IFS.

## Shows whitespace as a single space, ^I(horizontal tab), and newline, and display "$" at end-of-line.
echo "$IFS" | cat -vte
# Output:
# ^I$
# $

## Reads commands from string and assign any arguments to pos params
bash -c 'set w x y z; IFS=":-;"; echo "$*"'
# Output:
# w:x:y:z

for l in $(printf %b 'a b\nc'); do echo "$l"; done
# Output: 
# a
# b
# c

IFS=$'\n'; for l in $(printf %b 'a b\nc'); do echo "$l"; done
# Output: 
# a b
# c

这就是为什么我们将IFS=$'\n'(换行符)定义为分隔符。 我们的脚本使用read -r -d ",其中read -r不允许反斜杠转义任何字符,并且-d "继续直到读取第一个字符",而不是换行符。

最后,用你的脚本文件替换some_command_with_err,你就可以捕获和处理stderr、stdout以及exitcode了。

我是这样做的:

#
# $1 - name of the (global) variable where the contents of stderr will be stored
# $2 - command to be executed
#
captureStderr()
{
    local tmpFile=$(mktemp)

    $2 2> $tmpFile

    eval "$1=$(< $tmpFile)"

    rm $tmpFile
}

使用示例:

captureStderr err "./useless.sh"

echo -$err-

它确实使用了一个临时文件。但至少丑陋的东西被包裹在一个函数中。

$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr