如果您使用 {fd} 或 local -n,这需要 bash 4.1。
我希望其余的应该在 bash 3.x 中工作。由于printf %q,我不完全确定——这可能是 bash 4 的功能。
总结
您的示例可以修改如下以存档所需的效果:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
根据需要打印:
hello
4
注意这个解决方案:
- 也适用于
e=1000。
- 如果您需要
$?,请保留$?
唯一不好的副作用是:
- 它需要一个现代的
bash。
- 它更频繁地分叉。
- 它需要注解(以您的函数命名,并添加
_)
- 它牺牲了文件描述符 3。
- 如果需要,您可以将其更改为另一个 FD。
- 在
_capture 中,只需将所有出现的3 替换为另一个(更高的)数字。
以下内容(很长,对此感到抱歉)希望能解释如何将此配方应用到其他脚本中。
问题
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
输出
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
而想要的输出是
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
问题原因
Shell 变量(或一般而言,环境)从父进程传递到子进程,但反之则不然。
如果您进行输出捕获,这通常在子shell 中运行,因此传递回变量很困难。
有些人甚至告诉你,这是不可能修复的。这是错误的,但这是一个众所周知的难以解决的问题。
有几种方法可以最好地解决它,这取决于您的需求。
这里是一步一步的指导如何做到这一点。
将变量传回父shell
有一种方法可以将变量传递回父 shell。然而,这是一条危险的道路,因为它使用了eval。如果操作不当,您将面临许多邪恶的风险。但如果处理得当,这是绝对安全的,前提是bash 中没有错误。
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
打印
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
请注意,这也适用于危险的事情:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
打印
; /bin/echo *
这是由于 printf '%q',它引用了所有内容,您可以在 shell 上下文中安全地重复使用它。
但这是一种痛苦..
这不仅看起来难看,而且打字很多,所以容易出错。只要一个错误,你就完蛋了,对吧?
嗯,我们处于外壳级别,因此您可以改进它。只要想一个你想看到的接口,然后你就可以实现它。
增强,shell 如何处理事物
让我们退后一步,想想一些 API,它可以让我们轻松表达我们想要做什么。
那么,我们想用d() 函数做什么?
我们希望将输出捕获到一个变量中。
好的,那么让我们为此实现一个 API:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
现在,不用写了
d1=$(d)
我们可以写
capture d1 d
好吧,这看起来我们没有太大变化,因为变量没有从 d 传回父 shell,我们需要输入更多内容。
但是现在我们可以将 shell 的全部功能投入其中,因为它很好地包装在一个函数中。
考虑一个易于重用的界面
第二件事是,我们希望保持干燥(不要重复自己)。
所以我们绝对不想输入类似
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
这里的x 不仅是多余的,而且总是在正确的上下文中重复容易出错。如果你在一个脚本中使用它 1000 次然后添加一个变量会怎样?您绝对不想更改涉及调用 d 的所有 1000 个位置。
所以把x放开,我们可以写:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
输出
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
这看起来已经很不错了。 (但仍有local -n 在其他常见的bash 3.x 中不起作用)
避免更改d()
最后一个解决方案有一些很大的缺陷:
-
d() 需要修改
- 它需要使用
xcapture 的一些内部细节来传递输出。
- 请注意,这会遮蔽(烧毁)一个名为
output 的变量,
所以我们永远不能把这个传回去。
- 需要配合
_passback
我们也可以摆脱这个吗?
当然可以!我们在一个 shell 中,所以我们需要完成这一切。
如果您更接近对eval 的调用,您会看到,我们在此位置拥有 100% 的控制权。 “内部”eval 我们在一个子外壳中,
这样我们就可以做我们想做的一切,而不必担心对父母的外壳做坏事。
是的,很好,所以让我们添加另一个包装器,现在直接在 eval 中:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
打印
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
然而,这也有一些主要缺点:
-
!DO NOT USE! 标记在那里,
因为这里有一个非常糟糕的比赛条件,
你不能轻易看到:
-
>(printf ..) 是后台作业。所以可能还是
在_passback x 运行时执行。
- 如果您在
printf 或_passback 之前添加sleep 1;,您可以自己查看。
_xcapture a d; echo 然后分别输出x 或a。
-
_passback x 不应该是_xcapture 的一部分,
因为这使得重复使用该配方变得困难。
- 此外,我们这里还有一些未定义的分叉(
$(cat)),
但由于这个解决方案是!DO NOT USE!,所以我选择了最短的路线。
但是,这表明我们可以做到,无需修改 d()(也无需修改 local -n)!
请注意,我们根本不需要_xcapture,
因为我们可以在eval 中写下所有内容。
但是这样做通常不是很可读。
如果你几年后回到你的剧本,
您可能希望能够轻松地再次阅读它。
修复比赛
现在让我们修复竞态条件。
诀窍可能是等到printf 关闭它的STDOUT,然后输出x。
有很多方法可以存档:
- 您不能使用外壳管道,因为管道在不同的进程中运行。
- 可以使用临时文件,
- 或类似锁定文件或fifo 的东西。这允许等待锁定或先进先出,
- 或不同的通道,输出信息,然后以正确的顺序组合输出。
遵循最后一条路径可能看起来像(注意它最后是printf,因为这在这里效果更好):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
输出
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
为什么这是正确的?
-
_passback x 直接与 STDOUT 对话。
- 但是,由于需要在内部命令中捕获 STDOUT,
我们首先使用 '3>&1' 将其“保存”到 FD3(当然,您可以使用其他)
然后用
>&3 重复使用它。
-
$("${@:2}" 3<&-; _passback x >&3) 在_passback 之后结束,
当 subshell 关闭 STDOUT 时。
- 所以
printf 不能在_passback 之前发生,
不管_passback 需要多长时间。
- 注意
printf命令在完成前不会执行
命令行已组装,因此我们无法看到来自printf 的人工制品,
独立于printf 的实现方式。
因此首先执行_passback,然后执行printf。
这解决了竞争,牺牲了一个固定的文件描述符 3。
当然,您可以在这种情况下选择另一个文件描述符,
FD3 在你的 shellscript 中不是免费的。
还请注意3<&-,它保护 FD3 被传递给函数。
让它更通用
_capture 包含部分,属于d(),这很糟糕,
从可重用性的角度来看。如何解决?
好吧,通过引入另外一件事来做这种绝望的方式,
一个附加函数,它必须返回正确的东西,
它以带有_的原始函数命名。
这个函数是在真正的函数之后调用的,可以扩充事物。
这样,this 就可以作为一些注解来读取,所以可读性非常好:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
仍然打印
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
允许访问返回码
只有一点点缺失:
v=$(fn) 将 $? 设置为 fn 返回的内容。所以你可能也想要这个。
不过,它需要一些更大的调整:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
打印
23 42 69 FAIL
还有很大的改进空间
-
_passback() 可以用passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; } 消除
-
_capture() 可以用capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; } 消除
-
该解决方案通过在内部使用文件描述符(此处为 3)来污染它。
如果您碰巧通过了 FD,则需要牢记这一点。
注意bash 4.1 及以上有{fd} 使用一些未使用的FD。
(也许我会在这里添加一个解决方案。)
请注意,这就是为什么我习惯将它放在单独的函数中,例如 _capture,因为将所有这些都放在一行中是可能的,但它会变得越来越难以阅读和理解
-
也许您也想捕获被调用函数的 STDERR。
或者您甚至想传入和传出多个文件描述符
从变量到变量。
我还没有解决方案,但是here is a way to catch more than one FD,所以我们也可以通过这种方式传回变量。
也不要忘记:
这必须调用一个shell函数,而不是一个外部命令。
没有简单的方法可以将环境变量从外部命令中传递出去。
(不过,使用LD_PRELOAD= 应该是可能的!)
但这完全不同。
遗言
这不是唯一可能的解决方案。这是解决方案的一个示例。
与往常一样,您可以通过多种方式在 shell 中表达事物。
因此,请随时改进并找到更好的东西。
这里提出的解决方案远非完美:
- 几乎没有经过测试,所以请原谅拼写错误。
- 还有很大的改进空间,见上文。
- 它使用了现代
bash 的许多功能,因此可能很难移植到其他 shell。
- 可能还有一些我没有想到的怪癖。
但是我认为它很容易使用:
- 仅添加 4 行“库”。
- 只需为您的 shell 函数添加 1 行“注释”。
- 暂时只牺牲一个文件描述符。
- 即使多年后,每个步骤也应该很容易理解。