是否可以在不使用临时文件的情况下以不同的变量存储或捕获stdout和stderr ?现在,我这样做是为了out
在err
运行时输入stdout和stderr some_command
,但是我想避免使用temp文件。
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file
是否可以在不使用临时文件的情况下以不同的变量存储或捕获stdout和stderr ?现在,我这样做是为了out
在err
运行时输入stdout和stderr some_command
,但是我想避免使用temp文件。
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file
ksh -c 'function f { echo out; echo err >&2; }; x=${ { y=$(f); } 2>&1;}; typeset -p x y'
Answers:
好的,它有点难看,但这是一个解决方案:
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) )"
其中(echo std; echo err >&2)
需要通过实际的命令来代替。通过省略换行()和stderr到的行,将stdout的输出保存到数组$t_std
行中。-t
$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)
除了其值$?
使我们进行了最后修改:
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
喘鸣使用上Debian测试GNU bash
,版本4.2.37(1)-release(1486-PC-Linux的GNU) 。
eval "$( eval "$@" 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"; exit $t_ret
typeset -p t_out
而typeset -p t_err
可能是混合的,撕心裂肺的输出没用。
>>()
而不是> >()
。前者在巴什(Bash)不行。在Zsh中,它可以正确解析出进程替换部分,但有时会发出错误的输出。不知道为什么,但> >()
似乎工作可靠。我仍然没有完全相信。typeset -p
绝对不是原子的,不是吗?
这是为了将stdout和stderr捕获到不同的变量中。 如果您只想按原样赶上
stderr
,stdout
那么有一个更好,更短的解决方案。
bash
解决方案此版本确实使用子外壳,并且不带tempfile
s运行。(对于tempfile
没有子shell的版本,请参阅我的其他答案。)
: 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()
基本上eval
用来设置两个变量。这类似于https://stackoverflow.com/a/18086548
考虑调用catch out err dummy 1 2a 3b
:
让我们暂时跳过eval "$({
和__2="$(
。稍后我会谈到。
__1="$("$("${@:3}")"; } 2>&1;
执行dummy 1 2a 3b
并将其stdout
变成__1
供以后使用。如此__1
成为2a
。它还重定向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
调用的内容)捕获到variable中__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
。
请注意,作为一种优化,我们也可以将这2个代码写成printf
一个像printf '%s=%q\n( exit %q )
“ $ __ 2”,“ $ ret”`之类的代码。
那么到目前为止我们有什么呢?
我们已经写信给stderr:
out=2a
err=3b
( exit 1 )
其中out
是从$1
,2a
是从stdout
的dummy
,err
是从$2
,3b
是从stderr
的dummy
,并且1
是从返回代码dummy
。
请注意,%q
采用格式时要printf
注意引用,以使shell在使用时可以看到正确的(单个)参数eval
。 2a
而且3b
非常简单,因此可以照原样复制它们。
现在到外面eval "$({ ... } 2>&1 )";
:
这将执行以上所有操作,输出2个变量和exit
,并捕获它(因此2>&1
),然后使用将其解析到当前shell中eval
。
这样,将设置两个变量,并返回代码。
问:它使用eval
哪个是邪恶的。这样安全吗?
printf %q
没有错误,就应该安全。但是,您始终必须非常小心,只需考虑一下ShellShock。问:虫子?
除以下情况外,没有已知的错误:
捕获大输出需要大内存和CPU,因为所有内容都变成变量,并且需要由Shell进行反向解析。因此,请明智地使用它。
照常$(echo $'\n\n\n\n')
吞下所有换行符,而不仅仅是最后一个。这是POSIX要求。如果需要使LF不受损害,则只需在输出中添加一些尾随字符,然后像下面的配方中一样将其删除(请参见尾部x
,该尾部允许读取指向以结尾的文件的软链接$'\n'
):
target="$(readlink -e "$file")x"
target="${target%x}"
Shell变量不能携带字节NUL($'\0'
)。如果它们恰好发生在stdout
或中,它们将被忽略stderr
。
给定的命令在子子外壳中运行。因此,它无权访问$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()
。并添加了部分工作原理。
catch
对于重定向数据流的一个或者管道命令?尝试捕获两个输出(其中一个是空的)似乎是有问题的(因为命令本身还是将其重定向)。但是,即使在某些情况下注定其中一个变量为空,它也可以使任何命令一遍又一遍地使用相同的模式(特别是如果该命令是外部提供的,并且您不知道它是否重定向)。
function echo_to_file { echo -n "$1" >"$2" ; }
,然后catch
与该函数一起使用。可以正常工作。但是,仍然可以拥有它catch
本身会很好。(可以执行类似的“技巧”来在命令中添加管道。)
catch
在这里不需要它!直接在外壳程序中捕获单个变量:stdout + stderr : var="$(command 2>&1)"; echo "command gives $? and outputs '$var'"
; 捕获stderr并重定向stdout :(var="$(command 2>&1 >FILE)"
不>FILE 2>&1
,这会将stderr重定向到FILE
!);仅限stdout:var="$(command)"; echo "command gives $? and has stdout '$var'"
和,stderr
或其他FD看到另一个答案
catch
,最终的printf语句不应该printf 'return %q\n' "$ret" >&2
吗?想要函数catch
返回cmd
的退出代码,而不是退出程序。
此命令在当前正在运行的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)
将所有内容包装在execute调用中并回显它:
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|"
declare
,当整个输出超过1008个字节时,原子不会写(Ubuntu 16.04,bash 4.3.46(1))。在两个setval
stdout和stderr调用之间存在隐式同步(for stderrcat
中的setval
for无法在setval
for stdout关闭stderr之前完成)。但是,没有同步setval retval
,因此它可以介于两者之间。在这种情况下,retval
被其他两个变量之一吞没。因此,该retval
案例无法可靠运行。
capturable(){...}
(setval书面)和capture(){ eval "$( $@ 2> >(capturable stderr) > >(capturable stdout); )"; test -z "$stderr" }
。capture make ... && echo "$stdout" || less <<<"$stderr"
页面stderr或如果没有则输出标准输出。这对您有用吗,如果可以,对您有帮助吗?
乔纳森有答案。供参考,这是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环境中执行。开头的空格很重要({
保留字)。
内部命令组的Stderr重定向到stdout(以便应用于内部替换)。接下来,将的stdoutout
分配给y
,并通过捕获重定向的stderr x
,而不会通常丢失y
命令替换子外壳程序。
在其他shell中是不可能的,因为捕获输出的所有构造都需要将生产者放入子shell,在这种情况下,该子shell将包括分配。
更新: mksh现在也支持。
${ ... }
它不是子外壳,其余部分易于解释。整洁的技巧,只要您有ksh
必要使用。
从技术上讲,命名管道不是临时文件,此处没有人提及它们。它们在文件系统中不存储任何内容,您可以在连接它们后立即将它们删除(这样就永远不会看到它们):
#!/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
您可以通过这种方式具有多个后台进程,并在方便的时间异步收集其stdout和stderr等。
如果只需要一个进程使用此代码,则最好也使用3和4之类的硬编码fd数字,而不要使用{fdout}/{fderr}
语法(为您找到一个免费的fd)。
我认为在说“你做不到”之前,人们至少应该亲身尝试一下……
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
和的虚拟脚本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}
。
上面的解决方案依赖于一个NUL
字节用于之间的分隔符stderr
和stdout
,因此它不会如果由于任何原因的工作stderr
包含其他NUL
字节。
尽管永远不会发生,但是可以通过将两个可能的NUL
字节从中删除stdout
,stderr
然后再将两个输出都传递给read
(清除),从而使脚本完全牢不可破-NUL
字节总会丢失,因为无法将它们存储到shell变量中:
{
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
然后,安迪提交了以下“建议的编辑”以捕获退出代码:
简单干净的解决方案,节省退出价值
我们可以在的末尾添加
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
in中的可能字节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
启动:我们再有some_command
'Sstdout
的描述符1some_command
的stderr
对描述符2和some_command
重定向到请求3的退出码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
find /proc
以非超级用户身份尝试命令。以前的版本效果很好,因为您正在使用printf的“ buffer” stdout参数,从而确保在命令完成并且流式传输并刷新了100%的stderr之后才输出stdout。但是,最后一个版本没有使用printf来抛光其中一个流,而只是退出代码。Stderr和stdout是交错的,并且stderr仅包含一次冲洗的值。如果您修复了问题,那么对它的解释将不胜感激,因为在引入FD 4之后我迷路了
capture.sh
用第三个版本修补后,您的计算机上的输出是什么?
stderr
,对不对?
exit "${CAPTURED_EXIT}"
在圆括号内执行操作而不退出全局范围,那么退出状态应该代表字符串的最后一部分,就像我在上一个示例中尝试做的那样。另一个问题是,由于您最里面的输出printf
会立即附加到stderr
of的后面some_command
,因此我们无法再清理NUL
in中可能的字节stderr
,因为在这些字节中也有我们的 NUL
定界符。我会在接下来的几天考虑一下。
为了读者的利益,这里是使用tempfile
s的解决方案。
问题是不使用tempfile
s。但是,这可能是由于/tmp/
外壳死了而对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
为stdout
和创建两个stderr
。但是,它几乎立即删除了这些内容,因此它们仅存在很短的时间。
catch_1()
卡子stdout
(FD 1)代入变量并移动stderr
到stdout
,使得下一个(“左”)catch_1
可以赶上。
处理catch
从右到左完成,因此左catch_1
执行最后并catchs stderr
。
可能发生的最坏情况是,某些临时文件显示在上/tmp/
,但在这种情况下它们始终为空。(它们在填充之前已被移除。)。通常这不是问题,因为在Linux下,tmpfs每GB主内存支持大约128K文件。
给定的命令也可以访问和更改所有本地shell变量。因此,您可以调用具有副作用的shell函数!
这只会拨叉两次tempfile
。
错误:
如果tempfile
失败,将丢失良好的错误处理。
这会照常\n
移除外壳。请参阅中的评论catch_read()
。
您不能使用文件描述符66
将数据传递到命令。如果需要,请使用另一个描述符进行重定向,例如42
(请注意,非常老的外壳程序最多只能提供9个FD)。
这不能处理NUL字节($'\0'
)中stdout
和stderr
。(NUL仅被忽略。对于read
变体,NUL后面的所有内容均被忽略。)
仅供参考:
不喜欢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 -
}
那... 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"
如果命令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调用。
out=$(some_command)
和err=$(some_command 2>&1 1>/dev/null)
?
stdout
且stderr
没有副作用的用例-即使命令在正常情况下是确定性的,错误也不是正常情况。这种方法也很可能容易出现比赛条件。
这是一个较简单的变体,它不是OP想要的,但不同于其他任何选项。您可以通过重新排列文件描述符来获得所需的任何内容。
测试命令:
%> cat xx.sh
#!/bin/bash
echo stdout
>&2 echo stderr
它本身会:
%> ./xx.sh
stdout
stderr
现在,打印stdout,将stderr捕获到变量,并将stdout记录到文件
%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out
stdout
%> echo
$err
stderr
或将stdout记录并捕获stderr到变量中:
export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr
你明白了。
一种变通办法是比较棘手的,但比该页面上的某些建议更直观,它是标记输出流,合并它们,然后基于这些标记进行拆分。例如,我们可以用“ STDOUT”前缀标记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的格式受限制,则可以拿出一个与其允许的内容不冲突的标签。
sed
解释的风险someCmd
?潜在的有害代码执行?
sed
将仅解释字符串参数,即s/^/STDOUT/g
和s/^STDOUT//g
。由于这些是固定的,已知的字符串,因此没有注入/不需要的执行向量。的stdout和stderrsomeCmd
将流过;的stdin和stdout sed
;它们将被编辑但不会执行。对的呼叫也是如此grep
。
someCmd
永远不会包含以“ sentinel” text开头的行STDOUT
。如果这不成立,我们可以选择其他哨兵。但是,如果输出是任意的(例如,用户定义的),则无法使用此方法,因为无法从数据中区分任何前哨文本。
警告:还没有(工作?)!
以下似乎是导致它不创建任何临时文件而仅在POSIX sh上运行的可能原因;它需要base64,但是由于编码/解码的效率可能不高,因此还使用“更大”的内存。
然而,主要的问题是,一切似乎都风趣。尝试使用以下exe文件:
exe(){cat /usr/share/hunspell/de_DE.dic cat /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的破折号上)
这是@madmurphy解决方案的一种变体,应适用于任意大的stdout / stderr流,维护出口返回值,并处理流中的null(将它们转换为换行符)
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,因此它在流关闭时结束,并返回非零值。由于这是预期的行为,因此我们添加了|| :
“或true”的含义,以便该行始终求值为true(0)echo -n "${buf}"
是内置命令,因此不限于参数长度限制实时输出并写入文件:
#!/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}"