如果使用{fd}
或,则需要bash 4.1 local -n
。
我希望其余的应该在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
请注意此解决方案:
唯一的不良影响是:
- 它需要一个现代的
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变量(或一般而言,环境)从父进程传递到子进程,但反之则不。
如果您进行输出捕获,则此操作通常在子外壳中运行,因此很难传递回变量。
甚至有人告诉你,不可能修复。这是错误的,但这是一个长期以来难以解决的问题。
有几种方法可以最好地解决它,这取决于您的需求。
这是有关操作方法的分步指南。
将变量传递回父母的外壳
有一种方法可以将变量传递回父母的外壳。但是,这是一条危险的路径,因为使用eval
。如果做得不当,您会冒很多邪恶的危险。但是,如果操作正确,只要中没有bug,这是绝对安全的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上下文中安全地重用它。
但这是一个痛苦。
这不仅看起来难看,而且键入起来也很多,所以容易出错。只有一个错误,您注定要失败,对吗?
好吧,我们处于外壳程序级别,因此您可以对其进行改进。只需考虑您想要看到的接口,然后就可以实现它。
增强,外壳如何处理事物
让我们退后一步,考虑一些API,这些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的功能了,因为它很好地包装在函数中。
考虑一个易于重用的界面
第二件事是,我们要变得干燥(不要重复自己)。所以我们绝对不想键入类似
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
这里不仅是多余的,它的错误倾向始终在正确的上下文repeate。如果在脚本中使用它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
我们也可以摆脱它吗?
当然,我们可以!我们在一个外壳中,因此我们需要完成此操作。
如果您看起来更靠近电话,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
运行时执行。
- 如果您添加一个
sleep 1;
before printf
或,则您可以自己查看_passback
。
_xcapture a d; echo
然后分别输出x
或a
先输出。
- 本
_passback x
不应该成为其中的一部分_xcapture
,因为这使得它很难重用那几招。
- 另外,我们在这里(
$(cat)
)有一些未分类的叉子,但是由于此解决方案是!DO NOT USE!
我走的最短路线。
但是,这表明我们可以做到,而无需修改d()
(和没有local -n
)!
请注意,我们根本不需要_xcapture
,因为我们可以在中正确地写上所有内容eval
。
但是,这样做通常不是很容易理解。而且,如果您在几年后回到自己的脚本中,则可能希望能够再次阅读它而没有太多麻烦。
修理比赛
现在,让我们解决竞赛条件。
诀窍可能是等到printf
关闭它的STDOUT之后再输出x
。
有很多方法可以对此进行存档:
- 您不能使用外壳管道,因为管道在不同的进程中运行。
- 一个可以使用临时文件,
- 或类似锁定文件或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。
- 因此,无论花费多长时间,都
printf
不可能在之前发生。_passback
_passback
- 请注意,在
printf
组装完整的命令行之前不会执行该命令,因此我们无法从中printf
独立地看到如何printf
实现的伪像。
因此,首先_passback
执行,然后执行printf
。
这样就解决了竞争问题,牺牲了一个固定文件描述符3。当然,在FD3在您的shellscript中不可用的情况下,您当然可以选择另一个文件描述符。
另请注意,3<&-
它保护FD3传递给该功能。
使它更通用
_capture
d()
从可重用性的角度来看,包含的部分属于,这是不好的。如何解决呢?
好吧,通过引入更多的东西(一个附加的函数,必须返回正确的东西)来以绝望的方式进行操作,该函数以_
附带原始功能的名字命名。
该函数在实函数之后被调用,并且可以扩充事物。这样,就可以将其作为一些注释来阅读,因此非常易读:
_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。或者,您甚至想在变量之间传入和传出多个文件描述符。
我还没有解决方案,但是这是一种捕获多个FD的方法,因此我们也可以以这种方式传递变量。
也不要忘记:
这必须调用shell函数,而不是外部命令。
没有简单的方法可以从外部命令中传递环境变量。(LD_PRELOAD=
尽管应该有可能!)但是,这是完全不同的事情。
最后的话
这不是唯一可能的解决方案。这是解决方案的一个例子。
与往常一样,您可以通过多种方式在Shell中表达事物。因此,随时进行改进并找到更好的东西。
这里介绍的解决方案还远远不够完美:
- 它几乎根本不是睾丸,所以请原谅错别字。
- 有很多改进的余地,请参见上文。
- 它使用了来自modern的许多功能
bash
,因此可能很难移植到其他shell。
- 可能有一些我没想到的怪癖。
但是我认为它很容易使用:
- 仅添加4行“库”。
- 仅添加1行“注释”作为您的shell函数。
- 暂时只牺牲一个文件描述符。
- 而且即使在数年后,每个步骤也应该易于理解。