Answers:
如果zsh
具有匿名函数和带有功能代码的特殊关联数组,那将容易得多。随着bash
但是你可以这样做:
decorate() {
eval "
_inner_$(typeset -f "$1")
$1"'() {
echo >&2 "Calling function '"$1"' with $# arguments"
_inner_'"$1"' "$@"
local ret=$?
echo >&2 "Function '"$1"' returned with exit status $ret"
return "$ret"
}'
}
f() {
echo test
return 12
}
decorate f
f a b
哪个会输出:
Calling function f with 2 arguments
test
Function f returned with exit status 12
您不能调用两次装饰来修饰您的函数两次。
与zsh
:
decorate()
functions[$1]='
echo >&2 "Calling function '$1' with $# arguments"
() { '$functions[$1]'; } "$@"
local ret=$?
echo >&2 "function '$1' returned with status $ret"
return $ret'
typeset
必要吗?否则不会宣布吗?
eval "_inner_$(typeset -f x)"
创建_inner_x
作为原始的精确副本x
(同functions[_inner_x]=$functions[x]
在zsh
)。
return
。
我之前已经多次讨论过以下方法的工作方式和原因,因此不再赘述。就个人而言,我自己在该主题上的最爱在这里和这里。
如果您对阅读不感兴趣,但仍感到好奇,请了解在函数运行之前对函数输入附加的here-docs进行了shell扩展评估,并且它们以定义函数时的状态重新生成每次调用该函数。
您只需要一个声明其他函数的函数。
_fn_init() { . /dev/fd/4 ; } 4<<INIT
${1}() { $(shift ; printf %s\\n "$@")
} 4<<-REQ 5<<-\\RESET
: \${_if_unset?shell will ERR and print this to stderr}
: \${common_param="REQ/RESET added to all funcs"}
REQ
_fn_init $(printf "'%s' " "$@")
RESET
INIT
在这里,我呼吁_fn_init
声明一个名为的函数fn
。
set -vx
_fn_init fn \
'echo "this would be command 1"' \
'echo "$common_param"'
#OUTPUT#
+ _fn_init fn 'echo "this would be command 1"' 'echo "$common_param"'
shift ; printf %s\\n "$@"
++ shift
++ printf '%s\n' 'echo "this would be command 1"' 'echo "$common_param"'
printf "'%s' " "$@"
++ printf ''\''%s'\'' ' fn 'echo "this would be command 1"' 'echo "$common_param"'
#ALL OF THE ABOVE OCCURS BEFORE _fn_init RUNS#
#FIRST AND ONLY COMMAND ACTUALLY IN FUNCTION BODY BELOW#
+ . /dev/fd/4
#fn AFTER _fn_init .dot SOURCES IT#
fn() { echo "this would be command 1"
echo "$common_param"
} 4<<-REQ 5<<-\RESET
: ${_if_unset?shell will ERR and print this to stderr}
: ${common_param="REQ/RESET added to all funcs"}
REQ
_fn_init 'fn' \
'echo "this would be command 1"' \
'echo "$common_param"'
RESET
如果要调用此函数,除非_if_unset
设置了环境变量,否则它将消失。
fn
#OUTPUT#
+ fn
/dev/fd/4: line 1: _if_unset: shell will ERR and print this to stderr
请注意外壳跟踪的顺序-不仅会fn
在_if_unset
未设置when时调用失败,而且不会从头开始运行。这是在使用此处文档扩展时要理解的最重要因素-必须始终首先发生它们,因为它们<<input
毕竟如此。
该错误源于/dev/fd/4
父外壳程序在将该输入传递给函数之前正在评估该输入。这是测试必要环境的最简单,最有效的方法。
无论如何,故障很容易纠正。
_if_unset=set fn
#OUTPUT#
+ _if_unset=set
+ fn
+ echo 'this would be command 1'
this would be command 1
+ echo 'REQ/RESET added to all funcs'
REQ/RESET added to all funcs
common_param
对于所声明的每个函数,该变量在输入时均被评估为默认值_fn_init
。但是该值也可以更改为任何其他值,每个类似声明的函数也都可以使用该值。现在,我将删除外壳痕迹-我们不会在这里进入任何未知领域。
set +vx
_fn_init 'fn' \
'echo "Hi! I am the first function."' \
'echo "$common_param"'
_fn_init 'fn2' \
'echo "This is another function."' \
'echo "$common_param"'
_if_unset=set ;
上面我声明了两个函数并设置了_if_unset
。现在,在调用这两个函数之前,我将取消设置,common_param
以便您可以看到它们在我调用它们时将对其进行设置。
unset common_param ; echo
fn ; echo
fn2 ; echo
#OUTPUT#
Hi! I am the first function.
REQ/RESET added to all funcs
This is another function.
REQ/RESET added to all funcs
现在从调用者的范围:
echo $common_param
#OUTPUT#
REQ/RESET added to all funcs
但是现在我希望它完全是另外一回事:
common_param="Our common parameter is now something else entirely."
fn ; echo
fn2 ; echo
#OUTPUT#
Hi! I am the first function.
Our common parameter is now something else entirely.
This is another function.
Our common parameter is now something else entirely.
如果我未设定_if_unset
?
unset _if_unset ; echo
echo "fn:"
fn ; echo
echo "fn2:"
fn2 ; echo
#OUTPUT#
fn:
dash: 1: _if_unset: shell will ERR and print this to stderr
fn2:
dash: 1: _if_unset: shell will ERR and print this to stderr
如果您需要随时重置功能状态,则很容易做到。您只需要做(从函数内部):
. /dev/fd/5
我将用于初始声明该函数的参数保存在5<<\RESET
输入文件描述符中。因此.dot
,在任何时候在shell中进行采购都会重复首先进行设置的过程。如果您愿意忽略POSIX实际上没有指定文件描述符设备节点路径(这对于shell来说是必需的.dot
)的事实,那么这一切都是非常容易,真正并且完全可移植的。
您可以轻松扩展此行为并为功能配置不同的状态。
顺便说一下,这几乎不会刮擦表面。我经常使用这些技术将随时可声明的少量辅助函数嵌入到主函数的输入中-例如,$@
根据需要添加其他位置数组。实际上-正如我所相信的,无论如何,高阶shell都必须做到这一点。您可以看到它们很容易以编程方式命名。
我还想声明一个生成器函数,该函数接受有限类型的参数,然后沿lambda的行定义一个一次性使用或受范围限制的刻录机函数-或一个内联函数- unset -f
当通过。您可以传递一个shell函数。
eval
?
.dot
可以与文件和流一起使用,因此不会遇到原本可能遇到的同一种参数列表问题。尽管如此,这可能仍然是一个优先考虑的问题。我当然认为这更干净-特别是在您逃避评估时-那是我所坐的噩梦。
.dot
准备就可以使用,除非您已经做好准备并且可以使用。这使您有更多的自由来测试其评估。并且它提供了输入状态的灵活性(可以通过其他方式处理),但是从这个角度来看,它的危险性要远小于实际情况eval
。
我认为一种打印有关功能的信息的方法是
测试所需的参数,如果不存在则退出,并显示一些消息
是要更改内置的bash return
和/或exit
在每次执行程序之前每次获取的每个脚本(或某个文件的开头)中的bash 。所以你输入
#!/bin/bash
return () {
if [ -z $1 ] ; then
builtin return
else
if [ $1 -gt 0 ] ; then
echo function ${FUNCNAME[1]} returns status $1
builtin return $1
else
builtin return 0
fi
fi
}
foo () {
[ 1 != 2 ] && return 1
}
foo
如果运行此命令,则将获得:
function foo returns status 1
如果需要,可以使用调试标志轻松地进行更新,如下所示:
#!/bin/bash
VERBOSE=1
return () {
if [ -z $1 ] ; then
builtin return
else
if [ $1 -gt 0 ] ; then
[ ! -z $VERBOSE ] && [ $VERBOSE -gt 0 ] && echo function ${FUNCNAME[1]} returns status $1
builtin return $1
else
builtin return 0
fi
fi
}
仅当设置了VERBOSE变量时,才会执行这种方式语句(至少这是我在脚本中使用详细信息的方式)。它当然不能解决装饰函数的问题,但可以在函数返回非零状态时显示消息。
同样,如果要退出脚本,可以exit
通过替换所有实例来重新定义return
。
编辑:我想在这里添加我用来装饰bash中函数的方式,如果我还有很多它们以及嵌套函数的话。当我编写此脚本时:
#!/bin/bash
outer () { _
inner1 () { _
print "inner 1 command"
}
inner2 () { _
double_inner2 () { _
print "double_inner1 command"
}
double_inner2
print "inner 2 command"
}
inner1
inner2
inner1
print "just command in outer"
}
foo_with_args () { _ $@
print "command in foo with args"
}
echo command in body of script
outer
foo_with_args
对于输出,我可以得到:
command in body of script
outer:
inner1:
inner 1 command
inner2:
double_inner2:
double_inner1 command
inner 2 command
inner1:
inner 1 command
just command in outer
foo_with_args: 1 2 3
command in foo with args
这对于具有功能并想要调试它们的人很有帮助,以查看发生哪个功能错误。它基于三个功能,如下所述:
#!/bin/bash
set_indentation_for_print_function () {
default_number_of_indentation_spaces="4"
# number_of_spaces_of_current_function is set to (max number of inner function - 3) * default_number_of_indentation_spaces
# -3 is because we dont consider main function in FUNCNAME array - which is if your run bash decoration from any script,
# decoration_function "_" itself and set_indentation_for_print_function.
number_of_spaces_of_current_function=`echo ${#FUNCNAME[@]} | awk \
-v default_number_of_indentation_spaces="$default_number_of_indentation_spaces" '
{ print ($1-3)*default_number_of_indentation_spaces}
'`
# actual indent is sum of default_number_of_indentation_spaces + number_of_spaces_of_current_function
let INDENT=$number_of_spaces_of_current_function+$default_number_of_indentation_spaces
}
print () { # print anything inside function with proper indent
set_indentation_for_print_function
awk -v l="${INDENT:=0}" 'BEGIN {for(i=1;i<=l;i++) printf(" ")}' # print INDENT spaces before echo
echo $@
}
_ () { # decorator itself, prints funcname: args
set_indentation_for_print_function
let INDENT=$INDENT-$default_number_of_indentation_spaces # we remove def_number here, because function has to be right from usual print
awk -v l="${INDENT:=0}" 'BEGIN {for(i=1;i<=l;i++) printf(" ")}' # print INDENT spaces before echo
#tput setaf 0 && tput bold # uncomment this for grey color of decorator
[ $INDENT -ne 0 ] && echo "${FUNCNAME[1]}: $@" # here we avoid situation where decorator is used inside the body of script and not in the function
#tput sgr0 # resets grey color
}
我尝试在注释中添加尽可能多的内容,但这也是说明:我将_ ()
函数用作装饰器,将其放置在每个函数的声明之后:foo () { _
。此函数使用适当的缩进打印函数名称,具体取决于函数在其他函数中的深度(作为默认缩进,我使用4个空格)。我通常将其打印为灰色,以使其与通常的打印分开。如果需要用或不使用参数来修饰函数,则可以修改修饰函数中的前行。
为了在函数中打印某些内容,我引入了print ()
函数,该函数以适当的缩进打印传递给它的所有内容。
该函数set_indentation_for_print_function
完全按照其含义进行工作,根据${FUNCNAME[@]}
数组计算缩进量。
这种方式有一些缺陷,例如,无法将选项传递给print
like echo
,例如-n
或-e
,并且如果函数返回1,则不进行修饰。另外,对于传递到print
超过终端宽度的参数,这些参数将被包裹在屏幕上,但不会看到包裹行的缩进。
使用这些装饰器的好方法是将它们放置在单独的文件中,并在每个新脚本中将其用作源文件source ~/script/hand_made_bash_functions.sh
。
我认为将函数装饰器合并到bash中的最佳方法是在每个函数的主体中编写装饰器。我认为用bash在函数内部编写函数要容易得多,因为它可以选择全局设置所有变量,这与标准的面向对象语言不同。这就好像在bash中在代码周围放置标签一样。至少那对调试脚本有帮助。
也许http://sourceforge.net/projects/oobash/项目中的装饰器示例可以为您提供帮助(oobash / docs / examples / decorator.sh)。
对我来说,这就像在bash中实现装饰器模式的最简单方法。
#!/bin/bash
function decorator {
if [ "${FUNCNAME[0]}" != "${FUNCNAME[2]}" ] ; then
echo "Turn stuff on"
#shellcheck disable=2068
${@}
echo "Turn stuff off"
return 0
fi
return 1
}
function highly_decorated {
echo 'Inside highly decorated, calling decorator function'
decorator "${FUNCNAME[0]}" "${@}" && return
echo 'Done calling decorator, do other stuff'
echo 'other stuff'
}
echo 'Running highly decorated'
# shellcheck disable=SC2119
highly_decorated
"$@"
)。
我在Bash中做了很多(也许太多了:))元编程,并且发现装饰器对于动态地重新实现行为是无价的。我的bash缓存库使用修饰以最少的仪式透明地记住Bash函数:
my_expensive_function() {
...
} && bc::cache my_expensive_function
显然bc::cache
,不仅要做装饰,而且底层装饰还bc::copy_function
需要将现有功能复制到新名称,以便可以用装饰器覆盖原始功能。
# Given a name and an existing function, create a new function called name that
# executes the same commands as the initial function.
bc::copy_function() {
local function="${1:?Missing function}"
local new_name="${2:?Missing new function name}"
declare -F "$function" &> /dev/null || {
echo "No such function ${function}" >&2; return 1
}
eval "$(printf "%s()" "$new_name"; declare -f "$function" | tail -n +2)"
}
这是一个time
使用装饰功能的装饰器的简单示例,使用bc::copy_function
:
time_decorator() {
bc::copy_function "$1" "time_dec::${1}" || return
eval "${1}() { time time_dec::${1} "'"\$@"; }'
}
演示:
$ slow() { sleep 2; echo done; }
$ time_decorator slow
$ $ slow
done
real 0m2.003s
user 0m0.000s
sys 0m0.002s