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

时间:2009-06-07 16:38:12

标签: bash shell redirect variables stderr

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

useless.sh

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

我还有另一个shell脚本:

alsoUseless.sh

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

我想将“This Is Error”或任何其他来自useless.sh的stderr捕获到变量中。 我们称之为ERROR。

请注意,我正在使用stdout。我想继续使用stdout,所以在这种情况下将stderr重定向到stdout是没有用的。

所以,基本上,我想做

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

但这显然不起作用。

我也知道我能做到

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

但这很丑陋且没必要。

不幸的是,如果没有答案出现在这里,那就是我将要做的事情。

我希望还有另一种方式。

任何人都有更好的想法吗?

19 个答案:

答案 0 :(得分:75)

因此捕获错误文件会更简洁:

ERROR=$(</tmp/Error)

shell识别出这一点,并且不必运行“cat”来获取数据。

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

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

请注意,需要使用分号(在经典的shell中 - Bourne,Korn - 肯定;也可能在Bash中)。 '{}'对所附命令执行I / O重定向。如上所述,它也会从sed捕获错误。

  

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

答案 1 :(得分:63)

alsoUseless.sh

这将允许您通过useless.sh之类的命令管道sed脚本的输出,并将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

答案 2 :(得分:52)

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

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

答案 3 :(得分:6)

这个问题有很多重复,其中许多都有一个稍微简单的使用场景,你不想捕获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 ...

答案 4 :(得分:5)

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

答案 5 :(得分:3)

我是这样做的:

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

使用临时文件。但至少丑陋的东西包含在一个函数中。

答案 6 :(得分:2)

这是一个有趣的问题,我希望有一个优雅的解决方案。遗憾的是,我最终得到了一个类似于Leffler先生的解决方案,但我要补充一点,你可以在Bash函数中调用无用的东西来提高可读性:

#!/bin/bash

function useless {
    /tmp/useless.sh | sed 's/Output/Useless/'
}

ERROR=$(useless)
echo $ERROR

所有其他类型的输出重定向必须由临时文件支持。

答案 7 :(得分:2)

Tom Hale's answer上进行一些迭代,我发现可以将重定向瑜伽包装为一个函数,以便于重用。例如:

#!/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's answer的灵感,同时保留了上述实现的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

答案 8 :(得分:1)

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

答案 9 :(得分:1)

这篇文章帮助我为自己的目的提出了类似的解决方案:

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

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

答案 10 :(得分:1)

YellowApple's answer上进行改进:

这是一个Bash函数,用于将stderr捕获到任何变量中

stderr_capture_example.sh

#!/usr/bin/env bash

# Capture stderr from a command to a variable while maintaining stdout
# @Args:
# $1: The variable name to store the stderr output
# $2: Vararg command and arguments
# @Return:
# The Command's Returnn-Code or 2 if missing arguments
function capture_stderr {
  [ $# -lt 2 ] && return 2
  local stderr="$1"
  shift
  {
    printf -v "$stderr" '%s' "$({ "$@" 1>&3; } 2>&1)"
  } 3>&1
}

# Testing with a call to erroring ls
LANG=C capture_stderr my_stderr ls "$0" ''

printf '\nmy_stderr contains:\n%s' "$my_stderr"

测试:

bash stderr_capture_example.sh

输出:

 stderr_capture_example.sh

my_stderr contains:
ls: cannot access '': No such file or directory

此功能可用于捕获dialog命令的返回选择。

答案 11 :(得分:1)

捕获并打印stderr

ERROR=$( ./useless.sh 3>&1 1>&2 2>&3 | tee /dev/fd/2 )

<强>击穿

您可以使用$()来捕获标准输出,但是您想要捕获stderr。所以你交换了stdout和stderr。使用fd 3作为标准交换算法中的临时存储。

如果要捕获和打印,请使用tee制作副本。在这种情况下,tee的输出将由$()捕获而不是转到控制台,但是stderr(tee)仍将转到控制台,因此我们将其用作第二个tee的输出通过特殊文件/dev/fd/2,因为tee需要文件路径而不是fd编号。

注意:在一行中有很多重定向,顺序很重要。 $()在管道末端抓取tee的stdout,并且管道本身将./useless.sh的stdout路由到tee的stdin,之后我们交换stdin和stdout for { {1}}。

使用./useless.sh

OP表示他仍然想使用(不仅仅是打印)标准输出,例如./useless.sh

在交换stdout和stderr之前,没问题。我建议将它移动到一个函数或文件(也是--useless.sh)中,并在上面的行中调用它来代替./useless.sh。

但是,如果你想捕获stdout和stderr,那么我认为你必须依赖临时文件,因为./useless.sh | sed 's/Output/Useless/'一次只能做一个并且它创建一个子shell,你不能从中返回变量。

答案 12 :(得分:1)

一个简单的解决方案

class Browser(QWebView):
    def __init__(self):
        # QWebView
        self.view = QWebView.__init__(self)
        #self.view.setPage(MyBrowser())
        self.setWindowTitle('Loading...')
        self.titleChanged.connect(self.adjustTitle)
        self.showFullScreen()

会产生:

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

答案 13 :(得分:0)

我将使用 find 命令

find / -maxdepth 2 -iname 'tmp' -type d

作为演示的非超级用户。访问 / 目录时,它应该抱怨“权限被拒绝”。

#!/bin/bash

echo "terminal:"
{ err="$(find / -maxdepth 2 -iname 'tmp' -type d 2>&1 1>&3 3>&- | tee /dev/stderr)"; } 3>&1 | tee /dev/fd/4 2>&1; out=$(cat /dev/fd/4)
echo "stdout:" && echo "$out"
echo "stderr:" && echo "$err"

给出输出:

terminal:
find: ‘/root’: Permission denied
/tmp
/var/tmp
find: ‘/lost+found’: Permission denied
stdout:
/tmp
/var/tmp
stderr:
find: ‘/root’: Permission denied
find: ‘/lost+found’: Permission denied

terminal 输出也包含 /dev/stderr 内容,就像您在没有任何脚本的情况下运行该 find 命令一样。 $out/dev/stdout$err/dev/stderr 内容。

使用:

#!/bin/bash

echo "terminal:"
{ err="$(find / -maxdepth 2 -iname 'tmp' -type d 2>&1 1>&3 3>&-)"; } 3>&1 | tee /dev/fd/4; out=$(cat /dev/fd/4)
echo "stdout:" && echo "$out"
echo "stderr:" && echo "$err"

如果您不想在终端输出中看到 /dev/stderr

terminal:
/tmp
/var/tmp
stdout:
/tmp
/var/tmp
stderr:
find: ‘/root’: Permission denied
find: ‘/lost+found’: Permission denied

答案 14 :(得分:0)

为了读者的利益,此食谱在这里

  • 可以再次用作oneliner以将stderr捕获到变量中
  • 仍然可以访问命令的返回代码
  • 牺牲一个临时文件描述符3(您当然可以更改)
  • 并且不会将此临时文件描述符公开给内部命令

如果您想将stderr中的command捕获到var中,就可以

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

之后,您便拥有了一切:

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

如果command很简单(不是a | b之类的东西),您可以将内部的{}放在一边:

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

包装成易于使用的bash函数(local -n可能需要版本3及更高版本):

: 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的变量)
  • 3>&1使用文件描述符3在此处保存标准输出点
  • { command; }(或“ $ @”)然后在输出捕获$(..)中执行命令
  • 请注意,确切的顺序在这里很重要(用错误的方式错误地乱码文件描述符):
    • 2>&1stderr重定向到捕获$(..)的输出
    • 1>&3stdout从输出捕获$(..)重定向回到保存在文件描述符3中的“外部” stdout。请注意,stderr仍然指的是FD 1指向的位置:到捕获$(..)
    • 的输出
    • 3>&-然后关闭文件描述符3,因为不再需要它,这样command不会突然出现一些未知的打开文件描述符。请注意,外壳仍然打开了FD 3,但是command将看不到它。
    • 后者非常重要,因为诸如lvm之类的某些程序会抱怨意外的文件描述符。 lvmstderr抱怨-正是我们要捕获的东西!

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

请注意,这会牺牲文件描述符3。如果您碰巧需要该文件描述符,请随时更改数字。但是请注意,某些外壳程序(从1980年代开始)可能会将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仅是防止难看的错误的一种方法,以防万一您忘记提供正确数量的参数。也许终止外壳是另一种方式(但这使从命令行进行测试变得困难)。
  • 该例程的编写方式使其更易于理解。可以重写该函数,使它不需要exec,但是它真的很难看。
  • 也可以为非bash重写此例程,从而不需要local -n。但是,那么您将无法使用局部变量,并且它变得非常丑陋!
  • 还请注意,eval是安全使用的。通常eval被认为是危险的。但是,在这种情况下,它仅比使用"$@"(执行任意命令)更邪恶。但是,请务必使用此处显示的正确和正确的引用(否则它会变得非常非常危险)。

答案 15 :(得分:0)

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中创建了一个子外壳,因此您不能在命令之外进行管道传输,并且该子外壳中的变量分配对当前外壳不可用。

重击

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


感谢this answer的一般想法。

答案 16 :(得分:0)

防错您的命令:

execute [INVOKING-FUNCTION] [COMMAND]
execute () {
    function="${1}"
    command="${2}"
    error=$(eval "${command}" 2>&1 >"/dev/null")

    if [ ${?} -ne 0 ]; then
        echo "${function}: ${error}"
        exit 1
    fi
}
精益制造中的

灵感

答案 17 :(得分:0)

在zsh中:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )

答案 18 :(得分:0)

如果您想绕过临时文件的使用,您可以使用进程替换。我还没有完全开始工作。这是我的第一次尝试:

$ .useless.sh 2> >( ERROR=$(<) )
-bash: command substitution: line 42: syntax error near unexpected token `)'
-bash: command substitution: line 42: `<)'

然后我试了

$ ./useless.sh 2> >( ERROR=$( cat <() )  )
This Is Output
$ echo $ERROR   # $ERROR is empty

然而

$ ./useless.sh 2> >( cat <() > asdf.txt )
This Is Output
$ cat asdf.txt
This Is Error

因此,进程替换正在做一般正确的事情......不幸的是,每当我用>( )中的ST $()中的STDIN包围,试图将其捕获到变量,我丢失了$()的内容。我认为这是因为$()启动了一个子进程,该进程不再能够访问父进程拥有的/ dev / fd中的文件描述符。

进程替换使我能够使用不再使用STDERR的数据流,遗憾的是我似乎无法以我想要的方式操作它。