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

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

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`

但这是丑陋和不必要的。

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

我希望还有别的办法。

有人有更好的主意吗?

252208 次浏览

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

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

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

ERROR=$(</tmp/Error)

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

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

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

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

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

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

#!/bin/bash


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


ERROR=$(useless)
echo $ERROR

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

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

如果您想绕过临时文件的使用,可以使用进程替换。我还没把它弄好。这是我的第一次尝试:

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

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

进程替换为我购买了与不再在STDERR中的数据流工作的能力,不幸的是,我似乎无法以我想要的方式操纵它。

我是这样做的:

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

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

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

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

在zsh:

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

对于防止错误你的命令:

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
}

精益生产中的启发:

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

捕获并打印标准码

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

分解

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

如果你想捕获并打印,请使用tee来创建副本。在这种情况下,tee的输出将被$()捕获,而不是进入控制台,但(tee的)stderr仍然会进入控制台,因此我们使用它作为tee的第二个输出,通过特殊文件/dev/fd/2,因为tee需要一个文件路径而不是fd数。

注意:这是一个可怕的多重定向在单行和顺序问题。$()在管道的末尾抓取tee的标准输出,并且在我们为./useless.sh交换标准输入和标准输出后,管道本身将./useless.sh的标准输出路由到tee的标准输入。

使用./useless.sh的stdout

OP说他仍然想使用(不仅仅是打印)标准输出,比如./useless.sh | sed 's/Output/Useless/'

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

然而,如果你想捕获标准输出和标准输出,那么我认为你必须返回临时文件,因为$()一次只会做一个,并且它会生成一个子外壳,从中你不能返回变量。

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

注意,命令的STDOUT管道(这里是ls)是在最里面的{ }中完成的。如果您正在执行一个简单的命令(例如,不是管道),您可以删除这些内部大括号。

你不能在命令外部进行管道操作,因为管道会在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中不起作用。


感谢这个答案提供的一般思想。

为了方便读者,这是食谱

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

如果你想将一些commandstderr捕获到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;

包装成一个易于重用的__abc0函数(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>&1重定向stderr到捕获$(..)的输出
    • 1>&3stdout从捕获$(..)的输出重定向到保存在文件描述符3中的“外部”stdout。注意,stderr仍然指向FD 1之前指向的地方:指向捕获$(..)的输出
    • 3>&-然后关闭文件描述符3,因为它不再需要了,这样command就不会突然出现一些未知的打开的文件描述符。注意,外壳仍然打开fd3,但command将看不到它。
    • 后者很重要,因为像lvm这样的一些程序会抱怨意外的文件描述符。而lvmstderr抱怨——这正是我们要捕获的!
    • 李< / ul > < / >

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

    注意,这牺牲了文件描述符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个参数不能来自第三方。始终以“静态”方式明确地给出它们。

    所以no-no-no catch-var-from-fd-by-fd $var $fda $fdb $command,永远不要这样做!

    如果你碰巧传递了一个变量变量名,至少这样做: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command < / p >

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

    注:

    • catch-var-from-fd-by-fd var 2 3 cmd..catch-stderr var cmd..相同
    • shift || return只是防止在你忘记给出正确的参数数量时出现错误的一种方法。也许终止shell是另一种方法(但这使得从命令行测试变得困难)。
    • 例程是这样写的,更容易理解。你可以重写这个函数,这样它就不需要exec了,但这样它就变得非常丑陋了。
    • 这个例程也可以针对非-bash重写,这样就不需要local -n了。然而,你不能使用局部变量,这是非常丑陋的!
    • 还要注意,__abc0是以安全的方式使用的。通常eval被认为是危险的。然而,在这种情况下,它并不比使用"$@"(执行任意命令)更邪恶。但是,请确保使用此处所示的准确和正确的引用(否则它将成为非常非常危险)。

一个简单的解决方案

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

会产生:

This Is Output
-
This Is Error

汤姆·黑尔的回答上迭代一点,我发现可以将重定向瑜伽包装到一个函数中,以便于重用。例如:

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

改进YellowApple的回答:

这是一个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命令的选项。

我将使用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

我认为你想要捕获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'捕获stdoutexitcode。它们由\0又名“零字节”分开。

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

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

printf命令括在<( ... )中执行进程替换。进程替换允许使用文件名引用进程的输入或输出。这意味着<( ... )将使用第一个<将__abc4的stdout管道到命令组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