是否可以在不使用临时文件的情况下在不同的变量中存储或捕获标准输出和标准错误?现在我这样做是为了在运行时获取 stdout inout
和 stderr in ,但我想避免使用临时文件。err
some_command
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file
是否可以在不使用临时文件的情况下在不同的变量中存储或捕获标准输出和标准错误?现在我这样做是为了在运行时获取 stdout inout
和 stderr in ,但我想避免使用临时文件。err
some_command
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file
好的,它有点难看,但这里有一个解决方案:
unset t_std t_err
eval "$( (echo std; echo err >&2) \
2> >(readarray -t t_err; typeset -p t_err) \
> >(readarray -t t_std; typeset -p t_std) )"
where(echo std; echo err >&2)
需要替换为实际的命令。stdout的输出$t_std
逐行保存到数组中,省略换行符 (the -t
) 和stderr into $t_err
。
如果你不喜欢数组,你可以做
unset t_std t_err
eval "$( (echo std; echo err >&2 ) \
2> >(t_err=$(cat); typeset -p t_err) \
> >(t_std=$(cat); typeset -p t_std) )"
这几乎模仿了var=$(cmd)
except 的行为,它的值$?
将我们带到最后一个修改:
unset t_std t_err t_ret
eval "$( (echo std; echo err >&2; exit 2 ) \
2> >(t_err=$(cat); typeset -p t_err) \
> >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"
这里$?
被保存到$t_ret
使用GNU 在 Debian wheezy 上测试bash
,版本4.2.37(1)-release (i486-pc-linux-gnu)。
这是为了在不同的变量中捕获标准输出和标准错误。 如果你只想抓住
stderr
,保持stdout
原样,有一个更好更短的解决方案。
bash
解决方案这个版本确实使用了 subshell 并且不带tempfile
s 运行。(对于tempfile
没有子外壳的版本,请参阅我的其他答案。)
: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
{ __1="$("${@:3}")"; } 2>&1;
ret=$?;
printf '%q=%q\n' "$1" "$__1" >&2;
exit $ret
)";
ret="$?";
printf '%s=%q\n' "$2" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}
示例使用:
dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}
catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n data \n\n'
printf 'ret=%q\n' "$?"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"
这打印
ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n data '
因此无需深入思考即可使用。只要放在catch VAR1 VAR2
任何前面,command args..
你就完成了。
有些if cmd args..; then
会变成if catch VAR1 VAR2 cmd args..; then
. 真的没有什么复杂的。
catch
在严格模式下同样适用于我。唯一需要注意的是,上面的示例返回错误代码 3,在严格模式下,它调用 ERR 陷阱。因此,如果您运行一些set -e
预期会返回任意错误代码(不仅仅是 0)的命令,您需要将返回代码捕获到某个变量 && ret=$? || ret=$?
中,如下所示:
dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}
catch stdout stderr dummy 3 $'\ndifficult\n data \n\n\n' $'\nother\n difficult \n data \n\n' && ret=$? || ret=$?
printf 'ret=%q\n' "$ret"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"
问:它是如何工作的?
它只是将此处其他答案的想法包装到一个函数中,以便可以轻松地重复使用它。
catch()
基本上eval
用来设置这两个变量。这类似于https://stackoverflow.com/a/18086548
考虑调用catch out err dummy 1 2a 3b
:
让我们暂时跳过 theeval "$({
和 the __2="$(
。我稍后会谈到这个。
__1="$("$("${@:3}")"; } 2>&1;
执行dummy 1 2a 3b
并将其存储stdout
到__1
以供以后使用。就这样__1
变成了2a
。它还重定向to stderr
,以便外部捕获可以聚集dummy
stdout
stdout
ret=$?;
捕获退出代码,即1
printf '%q=%q\n' "$1" "$__1" >&2;
然后输出out=2a
到stderr
. stderr
在这里使用,因为当前stdout
已经接管了命令的stderr
作用dummy
。
exit $ret
然后将退出代码 ( 1
) 转发到下一阶段。
现在到外部__2="$( ... )"
:
stdout
这将上述(即stderr
调用的)捕获dummy
到变量__2
中。(我们可以在这里重复使用__1
,但我曾经__2
让它不那么混乱。)。所以__2
变成3b
ret="$?";
再次捕获(返回的)返回码1
(来自dummy
)
printf '%s=%q\n' "$2" "$__2" >&2;
然后输出err=3a
到stderr
. stderr
再次使用,因为它已经用于输出另一个变量out=2a
。
printf '( exit %q )' "$ret" >&2;
然后输出代码以设置正确的返回值。我没有找到更好的方法,因为将其分配给变量需要一个变量名,然后不能将其用作catch
.
请注意,作为一种优化,我们也可以将这两个printf
写成一个单独的,比如printf '%s=%q\n( exit %q )
"$__2" "$ret"`。
那么到目前为止我们有什么?
我们已将以下内容写入 stderr:
out=2a
err=3b
( exit 1 )
where out
is from $1
, 2a
is from stdout
of dummy
, err
is from $2
, 3b
is from stderr
of dummy
, the1
来自返回码 from dummy
。
请注意,%q
在格式中printf
注意引用,这样 shell 在涉及到eval
. 2a
并且3b
是如此简单,以至于它们被逐字复制。
现在到外部eval "$({ ... } 2>&1 )";
:
这将执行以上所有输出 2 个变量和 的exit
,捕获它(因此2>&1
)并使用eval
.
这样 2 个变量和返回码都被设置了。
问:它使用eval
哪个是邪恶的。那么它安全吗?
printf %q
没有错误,它应该是安全的。但是你总是要非常小心,想想 ShellShock。问:虫子?
没有明显的错误已知,除了以下:
捕获大输出需要大内存和 CPU,因为所有内容都进入变量并且需要由 shell 进行反向解析。所以明智地使用它。
像往常一样$(echo $'\n\n\n\n')
吞下所有换行符,而不仅仅是最后一个。这是 POSIX 要求。如果您需要让 LF 不受伤害,只需在输出中添加一些尾随字符,然后像下面的配方一样将其删除(查看x
允许读取指向以 a 结尾的文件的软链接的尾随$'\n'
):
target="$(readlink -e "$file")x"
target="${target%x}"
Shell 变量不能携带字节 NUL ( $'\0'
)。stdout
如果它们碰巧出现在or中,它们就会被忽略stderr
。
给定的命令在子子 shell 中运行。所以它无法访问$PPID
,也无法更改 shell 变量。您可以catch
使用 shell 函数,甚至是内置函数,但这些函数将无法更改 shell 变量(因为在其中运行的所有内容$( .. )
都无法做到这一点)。因此,如果您需要在当前 shell 中运行一个函数并捕获它的 stderr/stdout,您需要以通常的方式使用tempfile
s. (有很多方法可以做到这一点,打断外壳通常不会留下碎片,但这很复杂,值得自己回答。)
问:Bash 版本?
printf %q
)问:这看起来仍然很尴尬。
ksh
更干净地完成它。但是我不习惯ksh
,所以我把它留给其他人来创建一个类似的易于重用的配方ksh
。问:那为什么不使用ksh
呢?
bash
解决方案问:脚本可以改进
问:有错别字。 : catch STDOUT STDERR cmd args..
应阅读# catch STDOUT STDERR cmd args..
:
在bash -x
评论被默默吞下时出现。因此,如果您碰巧在函数定义中出现拼写错误,您可以看到解析器在哪里。这是一个古老的调试技巧。但请注意,您可以轻松地在:
.编辑:添加了更多内容;
,以便更轻松地从catch()
. 并添加了它是如何工作的部分。
我认为在说“你不能”做某事之前,人们至少应该亲手尝试一下……</p>
eval
或任何异国情调{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(some_command)" 1>&2) 2>&1)
要求: printf
,read
stdout
and的虚拟脚本stderr
:useless.sh
#!/bin/bash
#
# useless.sh
#
echo "This is stderr" 1>&2
echo "This is stdout"
stdout
和的实际脚本stderr
:capture.sh
#!/bin/bash
#
# capture.sh
#
{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(./useless.sh)" 1>&2) 2>&1)
echo 'Here is the captured stdout:'
echo "${CAPTURED_STDOUT}"
echo
echo 'And here is the captured stderr:'
echo "${CAPTURED_STDERR}"
echo
capture.sh
Here is the captured stdout:
This is stdout
And here is the captured stderr:
This is stderr
命令
(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1
some_command
发送to的标准输出printf '\0%s\0'
,从而创建字符串\0${stdout}\n\0
(其中\0
是一个NUL
字节,\n
是一个换行符);然后将字符串\0${stdout}\n\0
重定向到标准错误,其中some_command
已经存在标准错误,从而组成字符串${stderr}\n\0${stdout}\n\0
,然后将其重定向回标准输出。
之后,命令
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
开始读取字符串${stderr}\n\0${stdout}\n\0
直到第一个NUL
字节,并将内容保存到${CAPTURED_STDERR}
. 然后命令
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
一直读取相同的字符串直到下一个NUL
字节,并将内容保存到${CAPTURED_STDOUT}
.
上面的解决方案依赖于一个字节作为andNUL
之间的分隔符,因此如果出于任何原因包含其他字节,它将不起作用。stderr
stdout
stderr
NUL
尽管这永远不会发生,但可以通过在将两个输出传递给(清理)之前和之前剥离所有可能的NUL
字节来使脚本完全牢不可破——字节无论如何都会丢失,因为不可能将它们存储到 shell 变量中:stdout
stderr
read
NUL
{
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
} < <((printf '\0%s\0' "$((some_command | tr -d '\0') 3>&1- 1>&2- 2>&3- | tr -d '\0')" 1>&2) 2>&1)
要求: printf
, read
,tr
我已经删除了另一个将退出状态传播到当前 shell 的示例,因为正如Andy在评论中指出的那样,它并不像它应该的那样“牢不可破”(因为它没有printf
用来缓冲其中一个流)。作为记录,我在此处粘贴有问题的代码:
保持退出状态(仍然牢不可破)
以下变体还将退出状态传播
some_command
到当前 shell:{ IFS= read -r -d '' CAPTURED_STDOUT; IFS= read -r -d '' CAPTURED_STDERR; (IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}"); } < <((({ { some_command ; echo "${?}" 1>&3; } | tr -d '\0'; printf '\0'; } 2>&1- 1>&4- | tr -d '\0' 1>&4-) 3>&1- | xargs printf '\0%s\0' 1>&4-) 4>&1-)
要求:
printf
,read
,tr
,xargs
然后, Andy提交了以下“建议编辑”以捕获退出代码:
简单而干净的解决方案,节省退出价值
我们可以在末尾添加
stderr
第三条信息,另一个NUL
加上exit
命令的状态。它将在之后stderr
但之前输出stdout
{ IFS= read -r -d '' CAPTURED_STDERR; IFS= read -r -d '' CAPTURED_EXIT; IFS= read -r -d '' CAPTURED_STDOUT; } < <((printf '\0%s\n\0' "$(some_command; printf '\0%d' "${?}" 1>&2)" 1>&2) 2>&1)
他的解决方案似乎有效,但有一个小问题,即退出状态应该作为字符串的最后一个片段,以便我们能够exit "${CAPTURED_EXIT}"
在圆括号内启动而不污染全局范围,就像我试图做的那样删除的例子。另一个问题是,当他最内层的输出printf
立即附加到stderr
of 时some_command
,我们无法再清理 中的可能NUL
字节stderr
,因为其中现在还有我们的 NUL
分隔符。
在考虑了一些最终方法之后,我提出了一个解决方案,用于将退出代码和退出代码printf
缓存为两个 stdout
不同的参数,这样它们就不会干扰。
我做的第一件事是概述一种将退出状态传达给 的第三个参数的方法printf
,这很容易以最简单的形式(即无需清理)完成。
{
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
(IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(some_command)" "${?}" 1>&2) 2>&1)
要求: exit
, printf
,read
但是,当我们尝试引入消毒时,事情变得非常混乱。启动tr
以清理流实际上会覆盖我们之前的退出状态,因此显然唯一的解决方案是在后者丢失之前将后者重定向到单独的描述符,将其保留在那里直到tr
完成它的工作两次,然后将其重定向回它的位置.
在文件描述符之间进行了一些相当杂技的重定向之后,这就是我想出来的。
下面的代码是对我已删除的示例的重写。它还清理NUL
流中可能的字节,以便read
始终正常工作。
{
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
(IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ some_command; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
要求: exit
, printf
, read
,tr
这个解决方案非常强大。退出代码始终保持在不同的描述符中,直到它printf
作为单独的参数直接到达。
我们还可以将上面的代码转换为通用函数。
# SYNTAX:
# catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch() {
{
IFS=$'\n' read -r -d '' "${1}";
IFS=$'\n' read -r -d '' "${2}";
(IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
}
要求: cat
, exit
, printf
, read
,tr
使用该catch
功能,我们可以启动以下代码段,
catch MY_STDOUT MY_STDERR './useless.sh'
echo "The \`./useless.sh\` program exited with code ${?}"
echo
echo 'Here is the captured stdout:'
echo "${MY_STDOUT}"
echo
echo 'And here is the captured stderr:'
echo "${MY_STDERR}"
echo
并得到以下结果:
The `./useless.sh` program exited with code 0
Here is the captured stdout:
This is stderr 1
This is stderr 2
And here is the captured stderr:
This is stdout 1
This is stdout 2
下面是一个快速的模式化:
some_command
启动:然后我们在描述符 1 上有some_command
' ,在描述符 2 上有' ,并且' 退出代码重定向到描述符 3stdout
some_command
stderr
some_command
stdout
被输送到tr
(消毒)stderr
与stdout
(临时使用描述符 4)交换并通过管道传输到tr
(清理)stderr
(现在的描述符 1)交换并通过管道传输到exit $(cat)
stderr
(现在是描述符 3)被重定向到描述符 1,结束扩展为的第二个参数printf
exit $(cat)
由第三个参数捕获printf
printf
被重定向到stdout
已经存在的描述符 2stdout
和输出通过printf
管道传送到read
进程替换(< <()
语法)不是 POSIX 标准(尽管它们事实上是)。在不支持< <()
语法的 shell 中,获得相同结果的唯一方法是通过<<EOF … EOF
语法。不幸的是,这不允许我们使用NUL
字节作为分隔符,因为这些在到达read
. 我们必须使用不同的分隔符。自然选择落在CTRL+Z
字符上(ASCII 字符号 26)。这是一个可破解的版本(输出不得包含该CTRL+Z
字符,否则它们将混合在一起)。
_CTRL_Z_=$'\cZ'
{
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDERR;
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDOUT;
(IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; exit ${_ERRNO_});
} <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(some_command)" "${?}" 1>&2) 2>&1)
EOF
要求: exit
, printf
,read
这是它的牢不可破的版本,直接采用函数形式(如果其中一个stdout
或stderr
包含CTRL+Z
字符,则流将被截断,但永远不会与另一个描述符交换)。
_CTRL_Z_=$'\cZ'
# SYNTAX:
# catch_posix STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch_posix() {
{
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${1}";
IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${2}";
(IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; return ${_ERRNO_});
} <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(((({ ${3}; echo "${?}" 1>&3-; } | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 4>&2- 2>&1- | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
EOF
}
要求: cat
, cut
, exit
, printf
, read
,tr
从技术上讲,命名管道不是临时文件,这里没有人提到它们。它们在文件系统中不存储任何内容,您可以在连接它们后立即删除它们(因此您将永远看不到它们):
#!/bin/bash -e
foo () {
echo stdout1
echo stderr1 >&2
sleep 1
echo stdout2
echo stderr2 >&2
}
rm -f stdout stderr
mkfifo stdout stderr
foo >stdout 2>stderr & # blocks until reader is connected
exec {fdout}<stdout {fderr}<stderr # unblocks `foo &`
rm stdout stderr # filesystem objects are no longer needed
stdout=$(cat <&$fdout)
stderr=$(cat <&$fderr)
echo $stdout
echo $stderr
exec {fdout}<&- {fderr}<&- # free file descriptors, optional
您可以通过这种方式拥有多个后台进程,并在方便的时候异步收集它们的标准输出和标准错误等。
如果你只需要一个进程,你也可以使用硬编码的 fd 数字,比如 3 和 4,而不是{fdout}/{fderr}
语法(它会为你找到一个空闲的 fd)。
此命令在当前运行的 shell 中设置 stdout (stdval) 和 stderr (errval) 值:
eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"
如果此函数已定义:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }
将 execcommand 更改为捕获的命令,可以是“ls”、“cp”、“df”等。
所有这些都是基于我们可以在函数 setval 的帮助下将所有捕获的值转换为文本行的想法,然后 setval 用于捕获此结构中的每个值:
execcommand 2> CaptureErr > CaptureOut
将每个捕获值转换为 setval 调用:
execcommand 2> >(setval errval) > >(setval stdval)
将所有内容包装在执行调用中并回显它:
echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"
您将获得每个 setval 创建的声明调用:
declare -- stdval="I'm std"
declare -- errval="I'm err"
要执行该代码(并获取 vars 集),请使用 eval:
eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"
最后回显设置的变量:
echo "std out is : |$stdval| std err is : |$errval|
也可以包含返回(退出)值。
一个完整的 bash 脚本示例如下所示:
#!/bin/bash --
# The only function to declare:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }
# a dummy function with some example values:
function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; }
# Running a command to capture all values
# change execcommand to dummy or any other command to test.
eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )"
echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"
乔纳森有答案。作为参考,这是 ksh93 技巧。(需要非古代版本)。
function out {
echo stdout
echo stderr >&2
}
x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values
生产
x=stderr
y=stdout
该${ cmds;}
语法只是一个不会创建子shell 的命令替换。这些命令在当前 shell 环境中执行。开头的空格很重要({
是保留字)。
内部命令组的标准错误被重定向到标准输出(以便它适用于内部替换)。接下来,将 的 stdoutout
分配给y
,并由 捕获重定向的 stderr ,而不会像命令替换的子 shellx
一样丢失。y
这在其他 shell 中是不可能的,因为所有捕获输出的结构都需要将生产者放入子 shell 中,在这种情况下,子shell 将包括分配。
更新:现在 mksh 也支持。
为了读者的利益,这里有一个使用tempfile
s 的解决方案。
问题是不要使用tempfile
s。然而,这可能是由于/tmp/
在 shell 死机的情况下对 tempfile 造成了不必要的污染。万一kill -9
有些trap 'rm "$tmpfile1" "$tmpfile2"' 0
不火。
如果您处于可以使用tempfile
但又不想留下碎片的情况下,这里有一个食谱。
再次调用它catch()
(作为我的另一个答案)并具有相同的调用语法:
catch stdout stderr command args..
# Wrappers to avoid polluting the current shell's environment with variables
: catch_read returncode FD variable
catch_read()
{
eval "$3=\"\`cat <&$2\`\"";
# You can use read instead to skip some fork()s.
# However read stops at the first NUL byte,
# also does no \n removal and needs bash 3 or above:
#IFS='' read -ru$2 -d '' "$3";
return $1;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "$1";
"${@:3}" 66<&-;
catch_read $? 66 "$2";
} 2>&1 >"$1" 66<"$1";
}
: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}
它能做什么:
它为和创建两个tempfile
s 。然而,它几乎立即删除了这些,因此它们只存在很短的时间。stdout
stderr
catch_1()
将 (FD 1)捕获stdout
到变量中并移动stderr
到stdout
,以便下一个(“左”)catch_1
可以捕获它。
处理catch
是从右到左完成的,所以左边catch_1
最后执行并捕获stderr
。
可能发生的最糟糕的情况是,一些临时文件出现在 上/tmp/
,但在这种情况下它们总是空的。(它们在填充之前被移除。)。通常这应该不是问题,因为在 Linux 下,tmpfs 支持每 GB 主内存大约 128K 文件。
给定的命令也可以访问和更改所有本地 shell 变量。所以你可以调用一个有副作用的shell函数!
tempfile
这只会为调用分叉两次。
错误:
tempfile
在失败的情况下缺少良好的错误处理。
这通常会\n
移除外壳。见评论catch_read()
。
您不能使用文件描述符66
将数据通过管道传输到您的命令。如果需要,请使用另一个描述符进行重定向,例如42
(请注意,非常旧的 shell 只提供最多 9 个 FD)。
这无法处理和中的 NUL 字节 ( $'\0'
) 。(NUL 只是被忽略。对于变体,NUL 后面的所有内容都被忽略。)stdout
stderr
read
供参考:
不喜欢 eval,所以这里有一个解决方案,它使用一些重定向技巧将程序输出捕获到变量,然后解析该变量以提取不同的组件。-w 标志设置块大小并影响中间格式的 std-out/err 消息的顺序。1以开销为代价提供潜在的高分辨率。
#######
# runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later.
# limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output.
# example:
# var=$(keepBoth ls . notHere)
# echo ls had the exit code "$(extractOne r "$var")"
# echo ls had the stdErr of "$(extractOne e "$var")"
# echo ls had the stdOut of "$(extractOne o "$var")"
keepBoth() {
(
prefix(){
( set -o pipefail
base64 -w 1 - | (
while read c
do echo -E "$1" "$c"
done
)
)
}
( (
"$@" | prefix o >&3
echo ${PIPESTATUS[0]} | prefix r >&3
) 2>&1 | prefix e >&1
) 3>&1
)
}
extractOne() { # extract
echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode -
}
这是一张显示@madmurphy非常简洁的解决方案如何工作的图表。
以及单线的缩进版本:
catch() {
{
IFS=$'\n' read -r -d '' "$out_var";
IFS=$'\n' read -r -d '' "$err_var";
(IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
}\
< <(
(printf '\0%s\0%d\0' \
"$(
(
(
(
{ ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-
) 4>&2- 2>&1- | tr -d '\0' 1>&4-
) 3>&1- | exit "$(cat)"
) 4>&1-
)" "${?}" 1>&2
) 2>&1
)
}
简而言之,我相信答案是“不”。捕获$( ... )
仅将标准输出捕获到变量;没有办法将标准错误捕获到单独的变量中。所以,你所拥有的就是尽可能的整洁。
怎么样... =D
GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
GET_STDERR=""
GET_STDOUT=""
unset t_std t_err
eval "$( (eval $1) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )"
GET_STDERR=$t_err
GET_STDOUT=$t_std
}
get_stderr_stdout "command"
echo "$GET_STDERR"
echo "$GET_STDOUT"
一种解决方法是标记输出流,合并它们,然后根据标记进行拆分,这很hacky,但可能比此页面上的一些建议更直观。例如,我们可以用“STDOUT”前缀标记标准输出:
function someCmd {
echo "I am stdout"
echo "I am stderr" 1>&2
}
ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1)
OUT=$(echo "$ALL" | grep "^STDOUT" | sed -e 's/^STDOUT//g')
ERR=$(echo "$ALL" | grep -v "^STDOUT")
```
如果您知道 stdout 和/或 stderr 是受限制的形式,您可以想出一个与其允许的内容不冲突的标签。
警告:(还没有?)工作!
以下似乎可能导致它在不创建任何临时文件且仅在 POSIX sh 上工作的情况下工作;但是它需要base64,并且由于编码/解码可能不是那么有效并且还使用“更大”的内存。
然而,主要问题是一切似乎都很活泼。尝试使用类似的exe:
exe() { 猫 /usr/share/hunspell/de_DE.dic 猫 /usr/share/hunspell/en_GB.dic >&2 }
你会看到例如base64编码的部分在文件的顶部,部分在最后,未解码的stderr在中间。
好吧,即使下面的想法不能发挥作用(我假设),它也可以作为那些可能错误地认为它可以像这样工作的人的反例。
想法(或反例):
#!/bin/sh
exe()
{
echo out1
echo err1 >&2
echo out2
echo out3
echo err2 >&2
echo out4
echo err3 >&2
echo -n err4 >&2
}
r="$( { exe | base64 -w 0 ; } 2>&1 )"
echo RAW
printf '%s' "$r"
echo RAW
o="$( printf '%s' "$r" | tail -n 1 | base64 -d )"
e="$( printf '%s' "$r" | head -n -1 )"
unset r
echo
echo OUT
printf '%s' "$o"
echo OUT
echo
echo ERR
printf '%s' "$e"
echo ERR
给出(使用 stderr-newline 修复):
$ ./ggg
RAW
err1
err2
err3
err4
b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW
OUT
out1
out2
out3
out4OUT
ERR
err1
err2
err3
err4ERR
(至少在 Debian 的 dash 和 bash 上)
这是@madmurphy 解决方案的一个变体,它应该适用于任意大的 stdout/stderr 流,维护退出返回值,并处理流中的空值(通过将它们转换为换行符)
function buffer_plus_null()
{
local buf
IFS= read -r -d '' buf || :
echo -n "${buf}"
printf '\0'
}
{
IFS= time read -r -d '' CAPTURED_STDOUT;
IFS= time read -r -d '' CAPTURED_STDERR;
(IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr '\0' '\n' | buffer_plus_null; } 2>&1 1>&4 | tr '\0' '\n' | buffer_plus_null 1>&4 ) 3>&1 | xargs printf '%s\0' 1>&4) 4>&1 )
缺点:
read
命令是操作中最昂贵的部分。例如:find /proc
在一台计算机上运行 500 个进程,需要 20 秒(而命令只有 0.5 秒)。第一次阅读需要 10 秒,第二次阅读需要 10 秒,总时间翻了一番。最初的解决方案是printf
缓冲流的参数,但是由于需要将退出代码放在最后,一种解决方案是缓冲 stdout 和 stderr。我试过xargs -0 printf
了,但你很快就开始达到“最大参数长度限制”。所以我决定一个解决方案是编写一个快速缓冲函数:
read
将流存储在变量中read
将在流结束或收到 null 时终止。由于我们已经删除了空值,因此它会在流关闭时结束,并返回非零值。由于这是预期的行为,我们添加|| :
了“或真”的含义,以便该行始终评估为真 (0)echo -n "${buf}"
是内置命令,因此不受参数长度限制如果命令 1) 没有有状态的副作用和 2) 在计算上很便宜,最简单的解决方案就是运行两次。当您还不知道磁盘是否可以工作时,我主要将其用于在引导序列期间运行的代码。就我而言,它很小some_command
,因此运行两次不会影响性能,并且该命令没有副作用。
主要好处是它干净且易于阅读。这里的解决方案非常聪明,但我不想成为必须维护包含更复杂解决方案的脚本的人。如果您的方案适用于此,我建议使用简单的两次运行方法,因为它更清洁且更易于维护。
例子:
output=$(getopt -o '' -l test: -- "$@")
errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
echo "Option Error: $errout"
fi
同样,这只是可以做的,因为 getopt 没有副作用。我知道它是性能安全的,因为我的父代码在整个程序中调用它的次数不到 100 次,并且用户永远不会注意到 100 次 getopt 调用与 200 次 getopt 调用。
这是一个更简单的变体,它不是 OP 想要的,但与其他任何选项都不同。您可以通过重新排列文件描述符来获得所需的任何内容。
测试命令:
%> cat xx.sh
#!/bin/bash
echo stdout
>&2 echo stderr
它本身就是:
%> ./xx.sh
stdout
stderr
现在,打印标准输出,将标准错误捕获到一个变量,并将标准输出记录到一个文件中
%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out
stdout
%> echo
$err
stderr
或将标准输出和捕获标准错误记录到变量中:
export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr
你明白了。
实时输出并写入文件:
#!/usr/bin/env bash
# File where store the output
log_file=/tmp/out.log
# Empty file
echo > ${log_file}
outToLog() {
# File where write (first parameter)
local f="$1"
# Start file output watcher in background
tail -f "${f}" &
# Capture background process PID
local pid=$!
# Write "stdin" to file
cat /dev/stdin >> "${f}"
# Kill background task
kill -9 ${pid}
}
(
# Long execution script example
echo a
sleep 1
echo b >&2
sleep 1
echo c >&2
sleep 1
echo d
) 2>&1 | outToLog "${log_file}"
# File result
echo '==========='
cat "${log_file}"
我已经在这里发布了我对这个问题的解决方案。它确实使用进程替换并且需要 Bash > v4,但也将 stdout、stderr 和返回代码捕获到您在当前范围内命名的变量中:
https://gist.github.com/pmarreck/5eacc6482bc19b55b7c2f48b4f1db4e8
这个练习的重点是让我可以在测试套件中断言这些东西。事实上,我整个下午都在弄清楚这个听起来很简单的事情......我希望这些解决方案中的一个可以帮助其他人!