如何将标准错误存储在变量中

254

假设我有一个如下所示的脚本:

useless.sh

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

我还有另一个shell脚本:

alsoUseless.sh

./useless.sh | sed 's/Output/Useless/'
我想将"useless.sh"的"This Is Error"或其他标准错误信息捕获到一个变量中,命名为ERROR。
请注意,我正在使用stdout做一些事情。在这种情况下,将stderr重定向到stdout是没有帮助的。
因此,基本上,我想要做的是:
./useless.sh 2> $ERROR | ...

但显然那行不通。

我也知道我可以这样做

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

但那样很丑陋,也是不必要的。

不幸的是,如果这里没有答案的话,我就只能那么做了。

我希望还有其他的方法。

有人有更好的想法吗?


6
你想要使用标准输出(stdout)来做什么?你只是想在控制台上查看它吗?还是你正在捕获/重定向其输出?如果只是为了在控制台上输出,你可以将stdout重定向到控制台,将stderr重定向到stdout以进行捕获:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX) - Tim Kersten
对于更加平凡的情况,您想要捕获标准输出和标准错误输出,请参见例如 https://stackoverflow.com/questions/37115949/output-not-captured-in-bash-variable?noredirect=1 - tripleee
@psycotica0,看看这个,可能会有帮助 https://dev59.com/mHNA5IYBdhLWcg3wcNXF#70108786 - Teocci
20个回答

132

更好的做法是这样捕获错误文件:

ERROR=$(</tmp/Error)

Shell会识别这个,不必运行'cat'来获取数据。

更大的问题很难。我认为没有简单的方法可以解决它。您将不得不在子shell中构建整个管道,并最终将其标准输出发送到文件,以便您可以将错误重定向到标准输出。

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

注意在经典的shell(Bourne、Korn)中需要分号;Bash可能也需要。 '{}'会对括号内的命令进行I/O重定向。按照写法,它还会捕获sed的错误。

警告: 此代码未经正式测试 - 使用需谨慎。


1
我曾希望有一些我不知道的非常疯狂的技巧,但看起来这就是它了。 - psycotica0
12
如果您不需要标准输出,可以将其重定向到/dev/null而不是outfile(如果您和我一样是通过谷歌找到这个问题的,那么您可能没有与提问者相同的要求)。 - Mark Eirich
2
有关不使用临时文件的答案,请参见此处 - Tom Hale
1
这里有一种方法可以在不将其重定向到文件的情况下完成它;它通过交换stdoutstderr来回进行操作。但是要注意,正如这里所说:“在bash中,最好不要假设文件描述符3未使用”。 - Golar Ramblar

104

将标准错误重定向到标准输出,将标准输出重定向到 /dev/null,然后使用反引号或 $() 捕获重定向的标准错误:

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

18
这就是我在示例中包含管道的原因。我仍然想要标准输出,而且我希望它可以做其他事情,去其他地方。 - psycotica0
1
对于仅将输出发送到stderr的命令,捕获它的简单方法是: PY_VERSION="$(python --version 2>&1)" - John Mark

83

alsoUseless.sh

这将允许您通过类似于sed的命令管道传输您的useless.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

5
使用 'exec' 来设置和关闭文件描述符是一个好的技巧。如果脚本紧接着退出,那么关闭操作并不是必要的。 - Jonathan Leffler
6
我该如何将stderrstdout都存储到变量中? - Gingi
这在Zsh中很容易实现,但在Bash中无法使用此技术。stdout=$(echo good; echo bad >&2) 2>&1 | read stderr; echo "stdout=>$stdout"; echo "stderr=>$stderr"stdout=>goodstderr=>bad - Bruce
1
@t00bs:read 不接受管道输入。您可以使用其他技术来实现您想要演示的内容。 - Dennis Williamson
4
可以更简单,使用以下命令:error=$( ./useless.sh | sed 's/Output/Useless/' 2>&1 1>&3 )。 - Jocelyn
显示剩余2条评论

30

这个问题有很多重复的提问,其中许多使用场景稍微简单一些,您不需要同时捕获stderr和stdout和退出码。

if result=$(useless.sh 2>&1); then
    stdout=$result
else
    rc=$?
    stderr=$result
fi

适用于常见场景,其中您希望在成功的情况下得到正确的输出,或在失败的情况下在stderr上获得诊断信息。

请注意,Shell的控制语句已经在内部检查了$?; 因此,任何看起来像

cmd
if [ $? -eq 0 ], then ...

仅是说话笨拙、不太得体的表达方式。

if cmd; then ...

1
这个方法对我行得通:my_service_status=$(service my_service status 2>&1) 谢谢!! - JRichardsz

20

为了读者的方便,这里提供一个配方:

  • 可作为一行命令重用,将stderr捕获到变量中
  • 仍然可以访问命令的返回代码
  • 牺牲了临时文件描述符3(当然可以由您更改)
  • 并不会将该临时文件描述符暴露给内部命���

如果您想将某个命令的stderr捕获到var中,可以执行以下操作:

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

接下来你就拥有了所有:

echo "command gives $? and stderr '$var'";
如果command是简单的(不像a | b这样的语句),你可以省略内部的{}

如果“command”很简单(不像“a | 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 将"$1"(即catch-stderr变量)定义为别名(alias)
  • 3>&1 使用文件描述符3保存标准输出(stdout)
  • { command; } (或"$@")然后在捕获输出的$(..)中执行命令
  • 请注意,确切的顺序在这里很重要(错误的方式会打乱文件描述符的排序):
    • 2>&1 重定向stderr到捕获输出的$(..)
    • 1>&3stdout重定向回“外部”stdout,它是保存在文件描述符3中的。注意,stderr仍然指向之前FD 1所指向的位置:$(..)
    • 3>&- 然后关闭文件描述符3,因为它不再需要,以便于command不会突然出现某些未知的打开文件描述符。请注意,外壳仍然保持FD 3处于打开状态,但command将看不到它。
    • 后者很重要,因为一些程序(如lvm)会抱怨出现了意外的文件描述符。而我们正要捕获stderr

如果您进行相应的调整,可以使用此示例捕获任何其他文件描述符。当然,除了文件描述符1之外(这里重定向逻辑会出错,但对于文件描述符1,您可以像平常一样使用var=$(command))。

请注意,这牺牲了文件描述符3。如果您需要该文件描述符,则可以更改编号。但是,请注意,一些(来自20世纪80年代的)shell可能将99>&1解释为参数9,然后再是9>&1(对于bash来说没有问题)。

还要注意,通过变量使FD 3可配置化并不特别容易。这使得事情变得非常难以阅读:

: 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的前三个参数。始终以“静态”的方式明确给出它们。

因此,不要使用catch-var-from-fd-by-fd $var $fda $fdb $command,永远不要这样做!

如果您必须传递一个变量变量名,请至少按以下方式执行:local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

这仍然不能保护您免受所有攻击,但至少有助于检测和避免常见的脚本错误。

注:

  • catch-var-from-fd-by-fd var 2 3 cmd..catch-stderr var cmd..相同。
  • shift || return只是一种防止忘记提供正确数量参数时出现丑陋错误的方法。也许终止shell是另一种方法(但这使得从命令行测试变得困难)。
  • 该程序被编写得更易于理解。可以重写函数,使其不需要exec,但那样会变得非常丑陋。
  • 此例程也可以为非bash重写,使其不需要local -n。但是,那样您将无法使用本地变量,并且会变得极其丑陋!
  • 还要注意,eval是以安全的方式使用的。通常,eval被认为是危险的。但在这种情况下,它不比使用"$@"(执行任意命令)更危险。但请务必使用如此精确和正确的引号(否则会变得非常非常危险)。

我认为它没有提供对命令返回代码的访问。 - Johan Boulé

8
# 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}

3
这里使用command并不是一个好选择,因为实际上已经有一个叫做这个名字的内置函数了。更明确的做法可能是将其改为yourCommand等。 - Charles Duffy
退出码始终为零。 - Johan Boulé
真的有必要对/dev/stdin做任何操作吗?由于在您的命令中未涉及FD=0,因此我不确定会得到什么好处。 - nhed

6

POSIX

使用一些重定向技巧可以捕获STDERR:

$ { error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&3 ; } 2>&1); } 3>&1
lrwxrwxrwx 1 rZZt rZZt 7 Aug 22 15:44 /bin -> usr/bin/

$ echo $error
ls: cannot access '/XXXX': No such file or directory

请注意,管道命令(此处为ls)的STDOUT是在最内层的{}内完成的。如果执行的是简单命令(例如,不是管道命令),则可以删除这些内部大括号。
不能在命令外使用管道,因为在bashzsh中,管道会创建一个子shell,并且子shell中对变量的赋值不会在当前shell中生效。

bash

bash中,最好不要假设文件描述符3未被使用:
{ error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1; 
exec {tmp}>&-  # With this syntax the FD stays open

请注意,这在zsh中不起作用。
感谢此答案提供的一般思路。

你能详细解释一下这行代码吗?我不理解 1>&$tmp; { error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1; - Thiago Conrado
2
@ThiagoConrado 我假设在这种情况下,tmp只是一个存储未使用文件描述符的变量。例如,如果tmp=3,那么1>&$tmp将变成1>&3,并且该命令与之前解释的相同(它将把stdout(1)存储在文件描述符3中,然后stderr(2)会进入stdout并存储在error变量中,最后流式传输到文件描述符3的内容返回到文件描述符1,也就是stdout,因为{tmp}>&1变成了3>&1,如果我理解正确的话)。 - Lucas Basquerotto

6

一个简单的解决方案

{ ERROR=$(./useless.sh 2>&1 1>&$out); } {out}>&1
echo "-"
echo $ERROR

将产生:

This Is Output
-
This Is Error

1
我喜欢这个。我将它调整为:OUTPUT=$({ ERROR=$(~/code/sh/x.sh 2>&1 1>&$TMP_FD); } {TMP_FD}>&1),这也允许通过$?查看状态。 - kdubs
@kdubs 我尝试了你的解决方案,但是错误始终为空。即使命令执行失败。我有什么遗漏吗? $ STDOUT="$({ STDERR="$(ls -l /tmp/non-existing-file 2>&1 1>&$out)"; } {out}>&1)"; echo "stdout = [$STDOUT], stderr = [$STDERR]"; stdout = [], stderr = []我有什么遗漏吗? - undefined
@kdubs 我认为为了在出现错误的情况下使其正常工作,你需要交换2>&1和1>&TMP_FD所以它看起来是这样的: STDOUT="$({ STDERR="$(ls -l /tmp/non-existing-file 1>&$out 2>&1)"; } {out}>&1)"; echo "stdout = [$STDOUT], stderr = [$STDERR]"; - undefined
我的东西乱了。我被误导了,因为变量是从之前的尝试中设置的。你的排序似乎只写到标准输出。 - undefined

4

稍微修改一下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一起使用。


编辑: capture 函数的另一种版本,将捕获的 STDERR 输出存储到用户指定的变量中(而不是依赖于全局的 $captured),该版本灵感来源于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

在执行的命令中,看起来$?的值在过程中丢失了。有可能保留它吗? - undefined

4

如果您想捕获 stderrstdoutexitcode,您可以使用以下代码:

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

这个脚本捕获stderrstdout以及exitcode

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

首先,我们使用printf '\0%s\0%d\0'捕获stdout以及exitcode。它们由\0(也称为'null byte')分隔。
接着,我们通过执行1>&2printf重定向到stderr,然后我们使用2>&1将所有内容重定向回stdout。因此,stdout看起来像这样:
"<stderr>\0<stdout>\0<exitcode>\0"

printf命令用<( ... )括起来可以执行进程替换。进程替换允许使用文件名引用进程的输入或输出。这意味着<( ... )将把(printf '\0%s\0%d\0' "$(some_command_with_err)" "${?}" 1>&2) 2>&1stdout导入到第一个<中的命令组stdin中。
然后,我们可以使用read捕获从命令组的stdin中管道传输的stdout。此命令从文件描述符stdin读取一行并将其分成字段。只有在$IFS中找到的字符才被识别为单词分隔符。 $IFS内部字段分隔符是一个变量,它确定Bash在解释字符字符串时如何识别字段或单词边界。$IFS默认为空格、制表符和换行符,但可以更改,例如,以解析逗号分隔的数据文件。请注意,$*使用$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替换为您的脚本文件,您可以捕获和处理stderrstdout以及exitcode

它能处理超过512字节的数据吗? - Johan Boulé

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接