Bash 函数装饰器

Bash 函数装饰器

在Python中,我们可以用自动应用和执行函数的代码来装饰函数。

bash中有类似的功能吗?

在我当前正在处理的脚本中,我有一些样板文件可以测试所需的参数,如果它们不存在则退出 - 如果指定了调试标志则显示一些消息。

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

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

答案1

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'

答案2

我之前已经多次讨论过以下方法的工作方式和原因,所以我不会再这样做了。就我个人而言,我自己最喜欢的主题是这里这里

如果您对阅读不感兴趣,但仍然好奇,只需了解附加到函数输入的此处文档是针对 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

请注意 shell 跟踪的顺序 - 不仅在未设置fn时调用时失败,而且_if_unset它从一开始就不会运行。这是在使用此处文档扩展时要理解的最重要的因素 - 它们必须始终首先出现,因为它们<<input毕竟是。

该错误的/dev/fd/4原因是父 shell 在将输入传递给函数之前正在评估该输入。这是测试必要环境的最简单、最有效的方法。

无论如何,故障很容易修复。

_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)这一事实,那么这一切都非常简单,实际上,并且几乎完全可移植。

您可以轻松扩展此行为并为您的函数配置不同的状态。

更多的?

顺便说一句,这仅仅触及表面。我经常使用这些技术将可随时声明的小辅助函数嵌入到主函数的输入中 - 例如,根据$@需要添加其他位置数组。事实上 - 正如我所相信的,无论如何,高阶炮弹所做的事情一定与此非常接近。您可以看到它们很容易以编程方式命名。

我还喜欢声明一个生成器函数,它接受有限类型的参数,然后沿着 lambda 或内联函数的方式定义一次性或范围有限的燃烧器函数,它本身unset -f就是通过。你可以传递 shell 函数。

答案3

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

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

是改变bash内置return和/或exit在每个脚本的开头(或在某个文件中,您每次在执行程序之前获取该文件)。所以你输入

   #!/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 时,才会执行这种方式的语句(至少这就是我在脚本中使用 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 to echo,例如-nor -e,而且如果函数返回 1,则它不会被修饰。对于传递到print超过终端宽度的参数,这些参数将在屏幕上换行,人们不会看到换行的缩进。

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

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

答案4

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

相关内容