Bash功能装饰器


10

在python中,我们可以使用针对函数自动应用和执行的代码来修饰函数。

bash有任何类似的功能吗?

在我目前正在使用的脚本中,我有一些样板可以测试所需的参数,如果不存在则退出,如果指定了调试标志,则会显示一些消息。

不幸的是,我必须将此代码重新插入每个函数中,如果要更改它,则必须修改每个函数。

有没有办法从每个函数中删除此代码并将其应用于所有函数,类似于python中的装饰器?


为了验证函数参数,您也许可以使用我最近整理的这个脚本,至少作为起点。
dimo414

Answers:


12

如果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必要吗?否则不会宣布吗?
mikeserv 2014年

@mikeserv,eval "_inner_$(typeset -f x)"创建_inner_x作为原始的精确副本x(同functions[_inner_x]=$functions[x]zsh)。
斯特凡Chazelas

我明白了-但是为什么您根本需要两个呢?
mikeserv 2014年

您需要不同的上下文,否则您将无法捕捉内部return
斯特凡Chazelas

1
我不在那儿跟着你。我的回答是试图作为一个紧密的地图我是这么理解的Python装饰是
斯特凡Chazelas

5

我之前已经多次讨论过以下方法的工作方式和原因,因此不再赘述。就个人而言,我自己在该主题上的最爱在这里这里

如果您对阅读不感兴趣,但仍感到好奇,请了解在函数运行之前对函数输入附加的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函数。


与using相比,文件描述符具有额外的复杂性有什么好处eval
斯特凡Chazelas

@StephaneChazelas从我的角度来看,没有增加任何复杂性。实际上,我反过来认为。而且,引用要容易得多,并且.dot可以与文件和流一起使用,因此不会遇到原本可能遇到的同一种参数列表问题。尽管如此,这可能仍然是一个优先考虑的问题。我当然认为这更干净-特别是在您逃避评估时-那是我所坐的噩梦。
mikeserv 2014年

@StephaneChazelas不过有一个优势-这是一个非常好的优势。此方法不需要初始评估和第二次评估。Heredocument是根据输入进行评估的,但是您无需.dot准备就可以使用,除非您已经做好准备并且可以使用。这使您有更多的自由来测试其评估。并且它提供了输入状态的灵活性(可以通过其他方式处理),但是从这个角度来看,它的危险性要远小于实际情况eval
mikeserv

2

我认为一种打印有关功能的信息的方法是

测试所需的参数,如果不存在则退出,并显示一些消息

是要更改内置的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[@]}数组计算缩进量。

这种方式有一些缺陷,例如,无法将选项传递给printlike echo,例如-n-e,并且如果函数返回1,则不进行修饰。另外,对于传递到print超过终端宽度的参数,这些参数将被包裹在屏幕上,但不会看到包裹行的缩进。

使用这些装饰器的好方法是将它们放置在单独的文件中,并在每个新脚本中将其用作源文件source ~/script/hand_made_bash_functions.sh

我认为将函数装饰器合并到bash中的最佳方法是在每个函数的主体中编写装饰器。我认为用bash在函数内部编写函数要容易得多,因为它可以选择全局设置所有变量,这与标准的面向对象语言不同。这就好像在bash中在代码周围放置标签一样。至少那对调试脚本有帮助。



0

对我来说,这就像在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

为什么要禁用这些ShellCheck警告?它们似乎是正确的(当然,SC2068警告应通过引用进行修复"$@")。
dimo414

0

我在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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.