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


190

假设我有一个类似以下的脚本:

无用的

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

我还有另一个shell脚本:

AlsoUseless.sh

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

我想从useless.sh中捕获“ This Is Error”或任何其他stderr到变量中。我们称它为ERROR。

请注意,我正在使用stdout做某事。我想继续使用stdout,因此在这种情况下,将stderr重定向到stdout并没有帮助。

所以,基本上,我想做

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

但这显然行不通。

我也知道我能做

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

但这是丑陋且不必要的。

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

我希望有另一种方式。

有人有更好的主意吗?


4
到底要使用stdout做什么?您是否只想在控制台上查看它?还是您正在捕获/重定向其输出?如果只是控制台,则将stdout重定向到控制台,将stderr重定向到stdout进行捕获:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Answers:


93

捕获错误文件将变得更加整洁:

ERROR=$(</tmp/Error)

Shell可以识别出这一点,而不必运行' cat'来获取数据。

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

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

请注意,需要使用分号(当然,在经典的shell中-Bourne,Korn-也可能在Bash中)。' {}'通过附带的命令进行I / O重定向。如所写,它也会捕获错误sed

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


1
我曾经希望有一些我不知道的非常疯狂的把戏,但看起来就是这样。谢谢。
psycotica0

9
如果您不需要标准输出,可以将其重定向到/dev/null而不是outfile(如果您像我一样,则通过Google找到了这个问题,并且与OP没有相同的要求)
Mark Eirich

2
有关没有临时文件的答案,请参见此处
汤姆·黑尔

1
是一种无需将其重定向到文件的方法;它起着与交换stdoutstderr来回。但是要提防,就像这里所说的那样:在bash中,最好不要假设文件描述符3未使用。”
Golar Ramblar

70

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

5
使用'exec'设置和关闭文件描述符是一种很好的技术。如果脚本此后立即退出,则实际上不需要关闭。
乔纳森·勒夫勒

3
如何捕获变量stderrstdout变量?
Gingi 2012年

优秀。这可以帮助我实现一个dry_run函数,该函数可以在回显其参数和运行它们之间可靠地进行选择,而不管是否正在将命令dry-ran传递给其他文件。
Mihai Danila 2013年

1
@ t00bs:read不接受来自管道的输入。您可以使用其他技术来实现您要演示的内容。
暂停,直到另行通知。

2
可能会更简单,包括:error = $(./useless.sh | sed's / Output / Useless /'2>&1 1>&3)
乔斯林

69

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

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

8
这就是我在示例中包括管道的原因。我仍然想要标准输出,并且希望它做其他事情,去其他地方。
psycotica0 2009年

对于仅将输出发送到stderr的命令,捕获它的简单方法是,例如PY_VERSION="$(python --version 2>&1)"
John Mark

10

这个问题有很多重复项,其中许多重复使用场景都比较简单,您不想同时捕获stderrstdout以及退出代码。

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

适用于常见情况,在这种情况下,您期望成功时输出正确,而失败时则输出标准错误消息。

注意,shell的控制语句已经$?在引擎盖下进行了检查;所以任何看起来像

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

只是笨拙,单调的说法

if cmd; then ...

这对我有用:my_service_status = $(service my_service status 2>&1)谢谢!
JRichardsz

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

2
command这是一个不好的选择,因为实际上有一个内置名称。可能会yourCommand这样,更明确些。
Charles Duffy

5

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

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

如果你想赶上stderr一些commandvar你可以做

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

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

请注意,这会牺牲文件描述符3。如果您碰巧需要该文件描述符,请随时更改数字。但请注意,某些shell(从1980年代开始)可能将其理解99>&19后跟的参数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>&-";
}

安全说明:的前3个参数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认为是危险的。但是,在这种情况下,它没有比使用"$@"(执行任意命令)更邪恶。但是,请务必使用此处显示的正确和正确的引用(否则它会非常非常危险)。

4

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的传递是在最里面完成的{ }。如果执行的是简单命令(例如,不是管道),则可以删除这些内部括号。

您不能在命令外部进行管道传递,因为管道传递在bash和中创建了一个子zsh外壳,并且当前外壳无法使用该子外壳中的变量分配。

重击

在中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

1
tmp在这种情况下,我假设@ThiagoConrado只是一个变量,该变量存储您知道未使用的文件描述符。例如,如果tmp=3then1>&$tmp变为1>&3且命令将与之前说明的相同(它将stdout1)存储在文件描述符中3,则stderr2)将转到stdout并存储在error变量中,最后将内容流式传输到文件描述符中3返回到文件描述符1,即stdout,因为如果我理解正确的话,{tmp}>&1它将变为3>&1
卢卡斯·巴斯奎罗托

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-

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


@ShadowWizard我这一点毫无疑问。在法语中,冒号通常以空格开头。我错误地用英语答案应用了同样的规则。经过检查这个,我知道我不会再犯这样的错误。
斯蒂芬

@Stephan欢呼声,这也已经讨论了这里。:)
暗影巫师为您耳边

1
有比使用更加安全的方法eval。例如,printf -v "$1" '%s' "$(<tmpFile)"如果您的TMPDIR变量已设置为恶意值(或目标变量名称包含此类值),则不必冒着运行任意代码的风险。
Charles Duffy

1
同样,rm -- "$tmpFile"它比还要健壮rm $tmpFile
Charles Duffy

2

这是一个有趣的问题,我希望有一个优雅的解决方案。可悲的是,我最终得到了一个与莱夫勒先生类似的解决方案,但是我要补充一点,您可以从Bash函数内部调用无用的方法来提高可读性:

#!/ bin / bash

功能没用{
    /tmp/useless.sh | sed's / Output / Useless /'
}

ERROR = $(无用)
回声$ ERROR

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


1

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

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

然后,只要我们的MESSAGE不是一个空字符串,我们就将其传递给其他内容。这将让我们知道我们的format_logs.py是否由于某种python异常而失败。


1

捕获并打印标准错误

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

分解

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

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

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

使用./useless.sh的stdout

OP表示,他仍然想使用(而不仅仅是打印)stdout,例如./useless.sh | sed 's/Output/Useless/'

没问题,只是在交换stdout和stderr之前先做。我建议将其移动到函数或文件(also-useless.sh)中,并在上一行中代替./useless.sh进行调用。

但是,如果您想捕获stdout和stderr,那么我认为您必须回退到临时文件,因为一次$()只能执行一个操作,并且它使您无法返回变量。


1

迭代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。


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

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

进程替换使我拥有了使用STDERR中不再存在的数据流的能力,但是不幸的是,我似乎无法以自己想要的方式进行操作。


1
如果您这样做了./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" ),则会看到的输出ERROR。问题在于,进程替换是在子外壳程序中运行的,因此在子外壳程序中设置的值不会影响父外壳程序。
Jonathan Leffler 2014年

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

3
这看起来是个好主意,但是在Mac OSX 10.8.5上,它可以打印a=> b=>stderr
Heath Borders

3
我同意@HeathBorders; 这不会产生所示的输出。这里的问题是,它a是在子外壳中评估和分配的,并且在子外壳中的分配不会影响父外壳。(在Ubuntu 14.04 LTS和Mac OS X 10.10.1。上进行了测试)
Jonathan Leffler 2014年

Windows GitBash中也是如此。因此,它不起作用。(GNU bash, version 4.4.12(1)-release (x86_64-pc-msys)
Kirby

SLE 11.4两者都不起作用,并产生@JonathanLeffler
smarber

尽管此代码可以回答问题,但提供有关此代码为何和/或如何回答问题的其他上下文,可以提高其长期价值。
β.εηοιτ.βε


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
}

精益生产启发


惯用的解决方案是将作业分配到if。让我发布一个单独的解决方案。
Tripleee '18

0

一个简单的解决方案

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

将产生:

This Is Output
-
This Is Error

0

改善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命令的返回选择。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.