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

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`

但这是丑陋和不必要的。

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

我希望还有别的办法。

有人有更好的主意吗?


当前回答

重复Tom Hale的回答,我发现可以将重定向瑜伽包装到一个函数中,以便于重用。例如:

#!/bin/sh

capture () {
    { captured=$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
}

# Example usage; capturing dialog's output without resorting to temp files
# was what motivated me to search for this particular SO question
capture dialog --menu "Pick one!" 0 0 0 \
        "FOO" "Foo" \
        "BAR" "Bar" \
        "BAZ" "Baz"
choice=$captured

clear; echo $choice

几乎可以肯定,可以进一步简化。还没有进行特别彻底的测试,但它似乎可以同时使用bash和ksh。


EDIT:捕获函数的另一个版本,它将捕获的STDERR输出存储到用户指定的变量中(而不是依赖于全局$ capturing),灵感来自Léa Gris的答案,同时保留了上述实现的ksh(和zsh)兼容性:

capture () {
    if [ "$#" -lt 2 ]; then
        echo "Usage: capture varname command [arg ...]"
        return 1
    fi
    typeset var captured; captured="$1"; shift
    { read $captured <<<$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
}

和用法:

capture choice dialog --menu "Pick one!" 0 0 0 \
        "FOO" "Foo" \
        "BAR" "Bar" \
        "BAZ" "Baz"

clear; echo $choice

其他回答

alsoUseless.sh

这将允许您通过sed等命令来输出无用的.sh脚本的输出,并将stderr保存在名为error的变量中。管道的结果被发送到stdout以供显示或传输到另一个命令中。

它设置了两个额外的文件描述符来管理执行此操作所需的重定向。

#!/bin/bash

exec 3>&1 4>&2 #set up extra file descriptors

error=$( { ./useless.sh | sed 's/Output/Useless/' 2>&4 1>&3; } 2>&1 )

echo "The message is \"${error}.\""

exec 3>&- 4>&- # release the extra file descriptors

这篇文章帮助我想出了一个类似的解决方案:

MESSAGE=`{ echo $ERROR_MESSAGE | format_logs.py --level=ERROR; } 2>&1`

然后只要MESSAGE不是空字符串,我们就把它传递给其他东西。这将让我们知道我们的format_logs.py是否因某种python异常而失败。

我认为你想要捕获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了。

这样捕获错误文件会更简洁:

ERROR=$(</tmp/Error)

shell可以识别这一点,并且不需要运行'cat'来获取数据。

更大的问题很难。我不认为有什么简单的方法可以做到。您必须将整个管道构建到子shell中,最终将其最终标准输出发送到一个文件,以便将错误重定向到标准输出。

ERROR=$( { ./useless.sh | sed s/Output/Useless/ > outfile; } 2>&1 )

注意,分号是必需的(在经典shell中,Bourne, Korn,这是肯定的;可能在Bash中也是如此)。'{}'对所包含的命令进行I/O重定向。如前所述,它也会从sed捕获错误。

警告:正式未经测试的代码-使用风险自负。

为了方便读者,这是食谱

可以重新使用作为联机捕获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).