215

假设我有一个如下脚本:

没用的.sh

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

我还有另一个 shell 脚本:

也无用.sh

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

我想将 "This Is Error" 或 useless.sh 中的任何其他标准错误捕获到一个变量中。我们称之为错误。

请注意,我正在使用标准输出。我想继续使用标准输出,所以在这种情况下将标准错误重定向到标准输出是没有帮助的。

所以,基本上,我想做

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

但这显然行不通。

我也知道我能做到

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

但这是丑陋和不必要的。

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

我希望有另一种方法。

有人有更好的想法吗?

4

20 回答 20

116

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

ERROR=$(</tmp/Error)

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

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

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

请注意,分号是必需的(在经典的 shell - Bourne、Korn - 当然;也可能在 Bash 中)。' {}' 对封闭的命令进行 I/O 重定向。如所写,它也会从中捕获错误sed

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

于 2009-06-07T16:57:36.297 回答
84

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

ERROR=$(./useless.sh 2>&1 >/dev/null)
于 2009-06-07T16:45:51.913 回答
75

也无用.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
于 2009-06-08T08:11:34.590 回答
15

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

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

适用于常见情况,您希望在成功的情况下得到正确的输出,或者在失败的情况下在 stderr 上显示诊断消息。

请注意,shell 的控制语句已经在后台$?进行了检查;所以任何看起来像的东西

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

只是一种笨拙、不习惯的说法

if cmd; then ...
于 2018-02-11T18:18:34.163 回答
13

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

  • 可以重新用作 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-function(可能需要版本 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 保存标准输出点
  • { 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>&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>&-";
}

安全注意事项:前 3 个参数catch-var-from-fd-by-fd不得来自第 3 方。始终以“静态”方式明确地给出它们。

所以不不不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以安全方式使用 s。通常eval被认为是危险的。但是在这种情况下,它并不比使用"$@"(执行任意命令)更邪恶。但是,请务必使用此处所示的准确和正确的引用(否则它会变得非常非常危险)。
于 2019-06-13T09:36:25.043 回答
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}
于 2013-11-28T15:52:43.027 回答
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 管道是在最里面完成的{ }。如果您正在执行一个简单的命令(例如,不是管道),您可以删除这些内括号。

您不能在命令外部使用管道,因为管道会在bashand中创建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.


感谢这个答案的一般想法。

于 2018-10-01T09:17:59.457 回答
4

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

#!/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
于 2020-01-20T18:47:51.140 回答
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-

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

于 2012-07-25T23:35:26.217 回答
2

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

#!/bin/bash

功能无用{
    /tmp/useless.sh | sed 's/输出/无用/'
}

错误=$(无用)
回声 $ERROR

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

于 2009-06-08T00:36:07.873 回答
2

一个简单的解决方案

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

将产生:

This Is Output
-
This Is Error
于 2019-08-15T07:55:46.933 回答
1

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

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

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

于 2015-08-19T14:34:29.207 回答
1

在 zsh 中:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )
于 2015-11-14T17:04:04.510 回答
1

捕获和打印标准错误

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

分解

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

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

注意:单行中的重定向非常多,而且顺序很重要。 $()正在抓取tee管道末端的标准输出,管道本身将标准输出路由./useless.shtee我们交换标准输入和标准输出后的标准输入./useless.sh

使用 ./useless.sh 的标准输出

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

没问题,在交换标准输出和标准错误之前做。我建议将它移动到一个函数或文件(also-useless.sh)中,并在上面的行中调用它来代替 ./useless.sh。

但是,如果您想捕获标准输出和标准错误,那么我认为您必须依靠临时文件,因为$()一次只会执行一个,并且它会生成一个子shell,您无法从中返回变量。

于 2018-05-22T20:36:39.753 回答
1

改进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命令选择。

于 2020-03-30T17:41:12.400 回答
1

我认为您想要 capture stderr,如果这是您的意图,您可以使用以下代码stdoutexitcode

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

此脚本捕获stderr.stdoutexitcode.

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

首先,我们捕获 thestdoutexitcodeusing printf '\0%s\0%d\0'。它们由\0aka 'null byte'分隔。

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

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

printf命令包含在<( ... )执行进程替换中。进程替换允许使用文件名引用进程的输入或输出。这意味着<( ... )将使用第一个. stdout_(printf '\0%s\0%d\0' "$(some_command_with_err)" "${?}" 1>&2) 2>&1stdin<

然后,我们stdout可以stdin使用read. 此命令从文件描述符中读取一行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'(newline) 定义为分隔符的原因。我们的脚本使用read -r -d '', whereread -r不允许反斜杠转义任何字符,并-d ''继续直到''读取第一个字符,而不是换行符。

最后,替换some_command_with_err为您的脚本文件,您可以捕获和处理stderrstdout以及exitcode您的意愿。

于 2021-11-25T09:37:47.450 回答
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 中的数据流,不幸的是我似乎无法按照我想要的方式操作它。

于 2012-05-31T03:35:10.987 回答
0
$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr
于 2013-10-25T13:07:32.433 回答
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
}

精益制造的启发:

于 2017-12-29T04:18:06.023 回答
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
于 2021-02-18T01:30:31.837 回答