如果您使用 {fd}
或 local -n
,则需要 bash 4.1。
其余部分应该可以在 bash 3.x 中工作。由于 printf %q
可能是 bash 4 的功能,我不完全确定。
概要
您的示例可以修改如下以达到所需效果:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "
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
没有bug,就是完全安全的。
_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级别,所以你可以改进它。只需要思考你想要看到的界面,然后你就可以实现它。
增强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 的全部功能用于它,因为它已经很好地包装在一个函数中了。
考虑一个易于重用的接口
第二点是,我们希望保持DRY(不要重复自己)。
因此,我们绝对不想打一些像这样的东西:
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”内部,我们处于一个子shell中,因此我们可以做任何想做的事情,而不必担心对父shell造成任何不良影响。
太好了,让我们再添加一个包装器,现在直接在eval
中:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; }
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
的一部分,
因为这会使重用该命令变得困难。
- 此处还有一些不必要的fork(
$(cat)
),
但由于这种解决方案是!DO NOT USE!
,所以我选择了最短的路线。
但是,这表明我们可以做到这一点,而不需要修改d()
(也不需要使用local -n
)!
请注意,我们不一定需要_xcapture
,
因为我们可以直接在eval
中编写所有内容。
但是,这样做通常并不容易阅读。
如果您几年后回到脚本,请确保不会有太多麻烦。
解决竞态条件
现在让我们解决竞态条件。
技巧是等待printf
关闭其STDOUT,然后输出x
。
有很多方法可以实现这一点:
- 不能使用shell管道,因为管道在不同的进程中运行。
- 可以使用临时文件、锁文件或fifo之类的东西。这允许等待锁或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
之后,
当子 shell 关闭 STDOUT 时。
- 因此,无论
_passback
花费多长时间,
printf
都不能在 _passback
之前发生。
- 请注意,
printf
命令在完整的命令行组装之前不会执行,
因此,无论如何实现 printf
,我们都无法看到来自 printf
的伪影。
因此,首先执行 _passback
,然后执行 printf
。
这样解决了竞争问题,牺牲了一个固定的文件描述符 3。
当然,在您的 shell 脚本中,如果 FD3 不空闲,则可以选择另一个文件描述符。
请注意 3<&-
,它保护 FD3 不被传递给函数。
使其更通用
_capture
包含属于 d()
的部分,从可重用性的角度来看很糟糕。如何解决?
好吧,以一种绝望的方式引入一个附加功能,
一个额外的函数,必须返回正确的内容,命名为原始函数加上 _
。
此函数在实际函数之后调用,并可以增加内容。
这样,这可以被视为一些注释,因此非常易读:
_passback() { while [ 0 -lt $# ]; do printf '
_capture() { { printf "
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +
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
返回的内容。所以你可能也需要这个。
但是它需要进行一些大的调整:
_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 "$@")"; }
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
打印
23 42 69 FAIL
仍有很多改进的空间
使用passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
可以消除_passback()
使用capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
可以消除_capture()
该解决方案通过在内部使用文件描述符(此处为3)来污染它。如果您恰好传递FD,则需要牢记这一点。
请注意,bash
4.1及以上版本具有{fd}
以使用某些未使用的FD。
(也许我会在回头时添加一个解决方案。)
请注意,这就是我之所以喜欢将其放入类似于_capture
的单独函数中的原因,因为将所有内容塞入一行中是可能的,但会使其越来越难以阅读和理解。
也许您还想捕获调用函数的STDERR,或者甚至想从变量中传递进出更多的文件描述符。
我还没有解决方案,不过这里有一种方法可以捕获多个FD,因此我们也可以通过这种方式将变量传回。
还要记住:
这必须调用一个shell函数,而不是外部命令。
没有简单的方法将环境变量从外部命令传递出去。
(使用LD_PRELOAD=
应该是可能的!)
但这完全是另一回事。
最后的话
这不是唯一的解决方案。它是一个解决方案的例子。
在shell中,通常有很多表达方式。
所以请放心改进并找到更好的东西。
这里提出的解决方案相当远离完美:
- 几乎没有测试,所以请原谅打字错误。
- 仍有很多改进的空间,请参见上文。
- 它使用了许多现代
bash
的功能,因此可能难以移植到其他shell。
- 还可能有一些我没想到的怪癖。
然而,我认为它很容易使用:
- 只需添加4行“库”。
- 只需要为您的shell函数添加1行“注释”。
- 暂时牺牲一个文件描述符。
- 每个步骤应该易于理解,甚至数年后也是如此。