在Bash中,函数是否可以扩展?

5

如果我在一个文件中定义了一个函数,比如说test1.sh

#!/bin/bash

foo() {
    echo "foo"
}

在第二个脚本test2.sh中,我试图重新定义foo

#!/bin/bash

source /path/to/test1.sh    
...
foo() {
    ...
    echo "bar"
}
foo

有没有一种方法可以将 test2.sh 更改为生成以下内容:
foo
bar

我知道可以使用Bash内置的command命令实现,但我想知道是否可以扩展用户函数?

5个回答

5

我不知道有什么好的方法来做到这一点(但我很乐意被证明是错误的)。

这里有一个丑陋的方法:

# test2.sh
# ..
eval 'foo() {
        '"$(declare -f foo | tail -n+2)"'

        echo bar
      }'

2
有一句广为人知的老话,我刚编出来的:“丑陋和淫秽是不同的。丑陋可以成为艺术,而淫秽只是淫秽。Shell脚本编程是丑陋的,使用eval则是淫秽的。”(仍然喜欢这种巧妙) - drldcsta
这对我没用。declare -f foo | tail -n+3 | head -n-1 有效。 - Oleh Prypin
@Oleh: head -n1 将会获取任何输入的第一行,这里只有一个 foo 的定义,所以我不认为它会产生任何有用的作用。但是你说得对,我应该使用 tail 而不是 head。已经更正了,感谢提醒。(顺便说一句,保留原始函数定义周围的大括号是有意义的。它们不会造成任何损害。) - rici
@rici 好的,如果它们没有危害,那么我同意你目前的解决方案更好。但请注意我 head 调用中的 -。它会丢弃最后一行(括号)。 - Oleh Prypin
@oleh:你说得对,我确实错过了“-”。抱歉。花括号使现有函数体成为一个复合命令,在这种情况下实际上是一个无操作。 - rici
建议使用 grep -v -E -e '\{$' -e '^}' 替代 tailhead,代码如下:'"$(declare -f foo | grep -v -E -e '\{$' -e '^})"' - wviana

2

不可能的。新声明会覆盖先前的函数实例。但是,即使没有这种能力,在您想要禁用函数而不必取消设置它时,还是有帮助的,比如:

foo() {
    :  ## Do nothing.
}

它还可以帮助懒惰初始化:

foo() {
    # Do initializations.
    ...

    # New declaration.
    if <something>; then
        foo() {
            ....
        }
    else
        foo() {
            ....
        }
    fi

    # Call normally.
    foo "$@"
}

如果你足够勇敢和能力强,可以使用eval来优化你的函数,以便不需要额外的if语句就能根据条件执行。


我明白这可能会有所帮助,但我不确定我将使用哪些条件语句。看起来在原始函数中,我必须实现单独的功能。这是你的意图吗? - MrAlias
@MrAlias 在这种情况下,这并不是一种被建议遵循的实践 :) - konsolebox
1
请查看我的log.shexts.sh - konsolebox
1
有趣。谢谢@konsolebox提供的链接。 - MrAlias

2

我不确定为什么要这样做。你可以在函数内部使用函数,所以为什么要重复使用名称呢?你可以像这样在新创建的函数中调用原始源函数:

AirBoxOmega:stack d$ cat source.file
#!/usr/local/bin/bash

foo() {
    echo "foo"
}
AirBoxOmega:stack d$ cat subfoo.sh
#!/usr/local/bin/bash


source /Users/d/stack/source.file
sub_foo() {
  foo
  echo "bar"
}

sub_foo
AirBoxOmega:stack d$ ./subfoo.sh
foo
bar

当然,如果你真的想修改,你可以在新函数内部引用你的函数,调用它,然后做一些其他的事情,像这样:
AirBoxOmega:stack d$ cat source.file
#!/usr/local/bin/bash

foo() {
    echo "foo"
}
AirBoxOmega:stack d$ cat modify.sh
#!/usr/local/bin/bash


foo() {
  source /Users/d/stack/source.file
  foo
  echo "bar"
}

foo
AirBoxOmega:stack d$ ./modify.sh
foo
bar

我遇到了这样的情况,当我想要扩展源文件中某个东西的功能时,该源文件又被其他函数引用。我认为你直接在第二个foo的命名空间中覆盖foo是正确的做法。谢谢! - MrAlias
这种方法可能会出现错误,特别是当源文件中有只读变量时,但我可以接受。=) - MrAlias
我并不认为这是对函数的扩展。你只是使用了另一个名称的函数。而且在 source 之后,你原来的函数被覆盖了。一种方法是使用子shell,但子shell不会影响调用者的环境。 - konsolebox
你可以在source命令的结尾加上2> /dev/null来压制错误。当然,如果这些错误指向将要破坏事情的东西,那就不好了...但如果没有,那没什么大不了的。 @konsolebox - 你指的是我的代码哪个部分?我有两种方法,一种涉及创建第二个不同的函数,另一种涉及在函数内部进行源操作(我觉得这很傻,但似乎达到了效果)。 - drldcsta
@DarrellFx 这就是我的意思:http://pastebin.com/59tzFLZ0。最终函数没有得到扩展自己。扩展实例只能使用一次。你可以像 Ruby 模块一样将功能封装在一个文件中,但这会很丑陋,因为每次调用都要源化一个文件。Base: foo() { . foo.sh; }。Extension: foo() { . foo.sh; . foo_ext.sh; } - konsolebox
关于你的第一个答案,我猜你是建议不要扩展函数,而是使用另一个具有不同名称的函数来调用前一个函数。所以我想这样做没问题。 - konsolebox

1

是的,你可以。

请查看此页面:https://mharrison.org/post/bashfunctionoverride/

    save_function() {
        local ORIG_FUNC=$(declare -f $1)
        local NEWNAME_FUNC="$2${ORIG_FUNC#$1}"
        eval "$NEWNAME_FUNC"
    }

    save_function foo old_foo
    foo() {
        initialization_code()
        old_foo()
        cleanup_code()
    }

0
我写了一个函数来实现这个功能,有点受到Python装饰器的启发。虽然有点啰嗦,但我把它放在我的.bashrc文件中,经常发现它很有用。
受到Python装饰器的启发,这是一个我写的函数,允许包装bash函数、内置命令和可执行文件的调用。我把它放在我的.bashrc文件中。
# bashwrap function: given a function name and code to run before and/or after,
# wrap the existing function with the code that comes before and after.  The
# before and after code is taken literally and eval'd, so it can do things like
# access "$@" and indeed change "$@" by using shift or set or similar.
bashwrap () {
    local command beforecode aftercode type unset_extglob n
    local innerfuncname innerfunccode
    local -n varname

    command="$1"
    beforecode="$2"
    aftercode="$3"

    # Check the current state of extglob: this code needs it to be set,
    # but it should be reset to avoid unexpected changes to the global
    # envirnoment.
    if ! shopt -q extglob; then
        unset_extglob=YesPlease
        shopt -s extglob
    fi

    # Tidy the before and after code: trim whitespace from the start and end,
    # and make sure they end with a single semicolon.
    for varname in beforecode aftercode; do
        varname="${varname##+([$'\n\t '])}"
        varname="${varname%%+([$'\n\t '])}"
        if [[ "$varname" ]]; then
            varname="${varname%%+(;)};"
        fi
    done

    # Now finished with extglob.
    if [[ "$unset_extglob" ]]; then shopt -u extglob; fi

    type="$(type -t "$command")"
    case "$type" in
        alias)
            printf "bashwrap doesn't (yet) know how to handle aliases\n" >&2
            return 69  # EX_UNAVAILABLE
            ;;
        keyword)
            printf 'bashwrap cannot wrap Bash keywords\n' >&2
            return 64  # EX_USAGE
            ;;
        builtin|file)
            eval "$command () { $beforecode command $command \"\$@\"; $aftercode }"
            ;;
        function)
            # Keep generating function names until we get to one that doesn't
            # exist.  This allows a function to be wrapped multiple times; the
            # original function will always have the name
            # _bashwrapped_0_<name>.
            n=0
            innerfuncname="_bashwrapped_${n}_$command"
            while declare -Fp -- "$innerfuncname" &>/dev/null; do
                innerfuncname="_bashwrapped_$((++n))_$command"
            done

            # Define a new function with the new function name and the old function
            # code.
            innerfunccode="$(declare -fp -- "$command")"
            eval "${innerfunccode/#$command /$innerfuncname }"

            # Redefine the existing function to call the new function, in
            # between the wrapper code.
            eval "$command () { $beforecode $innerfuncname \"\$@\"; $aftercode }"
            ;;
        '')
            printf 'Nothing called %q found to wrap\n' "$command" >&2
            return 64  # EX_USAGE
            ;;
        *)
            printf 'Unexpected object type %s\n' "$type" >&2
            return 70  # EX_SOFTWARE
            ;;
    esac
}

原问题中的示例可以这样实现:
$ bashwrap foo '' 'echo bar'
$ foo
foo
bar

在对bashwrap的第二个参数中使用空字符串是为了表示在原始foo函数之前没有要运行的代码。
您可以使用type来查看底层发生了什么:
$ foo () { echo "foo"; }
$ type foo
foo is a function
foo ()
{
    echo "foo"
}
$ bashwrap foo '' 'echo "bar"'
$ type foo
foo is a function
foo ()
{
    _bashwrapped_0_foo "$@";
    echo "bar"
}
$ type _bashwrapped_0_foo
_bashwrapped_0_foo is a function
_bashwrapped_0_foo ()
{
    echo "foo"
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接