判断 Bash 中是否存在一个函数。

247

目前我正在进行一些从bash执行的单元测试。单元测试在bash脚本中初始化、执行和清理。这个脚本通常包含一个init()、execute()和cleanup()函数,但它们不是必需的。我想测试它们是否被定义。

我之前通过grep和sed源代码来实现这个功能,但感觉不太对。有没有更优雅的方法?

编辑:下面的代码片段非常好用:

fn_exists()
{
    LC_ALL=C type $1 | grep -q 'shell function'
}

谢谢。我使用这个来有条件地定义存根函数版本,当加载一个 shell 库时。fn_exists foo || foo() { :; } - Harvey
3
通过使用type -t==,您可以简化grep的命令。 - Roland Weber
1
当地区设置为非英语时无法正常工作。在使用芬兰语区域设置时,“type test_function”会显示“test_function on funktio。”而在使用德语区域设置时,则会显示“ist eine Funktion”。 - Kimmo Lehto
4
对于非英语的区域设置,使用LC_ALL=C来解决问题。 - gaRex
15个回答

265

像这样:[[ $(type -t foo) == function ]] && echo "Foo exists"

内置命令type可以告诉你一个东西是函数、内置函数、外部命令还是未定义的。

更多示例:

$ LC_ALL=C type foo
bash: type: foo: not found

$ LC_ALL=C type ls
ls is aliased to `ls --color=auto'

$ which type

$ LC_ALL=C type type
type is a shell builtin

$ LC_ALL=C type -t rvm
function

$ if [ -n "$(LC_ALL=C type -t rvm)" ] && [ "$(LC_ALL=C type -t rvm)" = function ]; then echo rvm is a function; else echo rvm is NOT a function; fi
rvm is a function

145
type -t $function 是解决问题的关键。 - Allan Wind
5
为什么你没把它发布为一个答案呢? :-) - terminus
6
type [-t] 命令可以很好地告诉你一个东西是什么,但是如果要测试某个东西是否是函数,则较慢,因为你必须使用管道到 grep 或使用反引号,这两种方法都会生成一个子进程。 - Lloeki
1
除非我读错了,使用type将不得不执行一个明显的最小访问,以检查是否有匹配的文件。@Lloeki,你是正确的,但这是产生最小输出的选项,你仍然可以使用errorlevel。你可以在没有子进程的情况下获得结果,例如type -t realpath > /dev/shm/tmpfile ; read < /dev/shm/tmpfile(不好的例子)。然而,declare是最好的答案,因为它没有磁盘io。 - Orwellophile
@StevenLu 如果你需要知道foo是一个函数,难道不只能使用declare(而不是使用子进程或上面的Orwellophile示例)吗? - David Winiecki
显示剩余7条评论

112

内置bash命令declare有一个选项-F,它显示所有已定义的函数名称。如果给定名称参数,它将显示其中哪些函数存在,并且如果全部都存在,它会相应地设置状态:

$ fn_exists() { declare -F "$1" > /dev/null; }

$ unset f
$ fn_exists f && echo yes || echo no
no

$ f() { return; }
$ fn_exist f && echo yes || echo no
yes

1
对我来说非常好用。特别是因为我的 shell 没有 -t 标志来指定类型(我在使用 type "$command" 时遇到了很多麻烦)。 - Dennis
3
确实,它在zsh中也可以工作(对于rc脚本很有用),而且不需要使用grep命令来查找类型。 - Lloeki
3
@DennisHodapp 不需要使用 type -t,你可以依赖于退出状态。我长期以来一直使用 type program_name > /dev/null 2>&1 && program_name arguments || echo "error" 来查看是否能够调用某些东西。显然,type -t 和上述方法也允许检测类型,而不仅仅是它是否“可调用”。 - 0xC0000022L
如果 program_name 不是一个函数,那么 @0xC0000022L 会怎样? - David Winiecki
3
我注意到你使用退出状态无法确定program_name是否为函数,但现在我认为当你说“显然,type -t和上述方法也可以检测类型,不仅仅是可调用的”时,你已经解决了这个问题。抱歉。 - David Winiecki
显示剩余4条评论

49

如果声明比测试快10倍,那么这似乎是显而易见的答案。

编辑:下面,对于BASH来说,-f选项是多余的,可以不用。个人而言,我很难记住哪个选项是做什么的,所以我两个选项都使用。-f显示函数,-F显示函数名。

#!/bin/sh

function_exists() {
    declare -f -F $1 > /dev/null
    return $?
}

function_exists function_name && echo Exists || echo No such function

在使用declare时,"-F"选项只返回找到的函数的名称,而不是整个内容。

使用/dev/null不应该会有任何可衡量的性能惩罚,如果你真的这么担心:

fname=`declare -f -F $1`
[ -n "$fname" ]    && echo Declare -f says $fname exists || echo Declare -f says $1 does not exist

或者将它们结合在一起,以满足你自己毫无意义的娱乐需求。它们都有效。

fname=`declare -f -F $1`
errorlevel=$?
(( ! errorlevel )) && echo Errorlevel says $1 exists     || echo Errorlevel says $1 does not exist
[ -n "$fname" ]    && echo Declare -f says $fname exists || echo Declare -f says $1 does not exist

3
“-f”选项是多余的。 - Rajish
4
在zsh中不存在-F选项(这个选项对于移植很有用)。 - Lloeki
3
虽然不是必需的,但非常希望能够确认函数存在,而不是列出其全部内容(这样效率有些低下)。您可以使用 cat "$fn" | wc -c 命令来检查文件是否存在吗?至于zsh,如果“bash”标签没有让您明白,也许问题本身应该有所提示。“确定 bash 中是否存在一个函数”。我还要指出,尽管 zsh 中不存在 -F 选项,但它也不会导致错误,因此同时使用 -f-F 可以使检查在 zshbash 中都成功,否则将无法实现。 - Orwellophile
@Orwellophile 在zsh中,-F用于浮点数。我不明白在bash中使用-F有什么好处?!我认为declare -f在bash中的作用与返回代码相同。 - blueyed
@Orwellophile 我原本认为在zsh中使用-F会有害,但显然并不是这样。 - blueyed
显示剩余3条评论

21

借鉴其他方案和评论,我得出了以下解决方案:

fn_exists() {
  # appended double quote is an ugly trick to make sure we do get a string -- if $1 is not a known command, type does not output anything
  [ `type -t $1`"" == 'function' ]
}

用作...

if ! fn_exists $FN; then
    echo "Hey, $FN does not exist ! Duh."
    exit 2
fi

它检查给定的参数是否是一个函数,并避免重定向和其他过滤操作。


不错,这是我最喜欢的一组!你不想在参数周围加上双引号吗?就像 [ $(type -t "$1")"" == 'function' ] 这样。 - quickshiftin
谢谢@quickshiftin;我不确定我是否需要那些双引号,但你可能是对的,尽管..一个函数甚至可以用需要加引号的名称声明吗? - Grégory Joseph
6
你正在使用Bash,使用[[...]]替代[...]并且去掉引号技巧。此外,反引号会导致进程分叉,从而降低速度。改用declare -f $1 > /dev/null - Lloeki
4
避免空参数错误,减少引号使用,并使用符合 POSIX 标准的等号来定义函数,可以将原始代码安全地简化为: fn_exists() { [ x$(type -t $1) = xfunction ]; } - qneill

12

翻译如下:挖掘一篇旧帖子……但我最近使用过它,并测试了描述的两种替代方案:

test_declare () {
    a () { echo 'a' ;}

    declare -f a > /dev/null
}

test_type () {
    a () { echo 'a' ;}
    type a | grep -q 'is a function'
}

echo 'declare'
time for i in $(seq 1 1000); do test_declare; done
echo 'type'
time for i in $(seq 1 100); do test_type; done
这是生成的内容:
real    0m0.064s
user    0m0.040s
sys     0m0.020s
type

real    0m2.769s
user    0m1.620s
sys     0m1.130s

使用 declare 速度更快!


1
可以不用grep来完成: test_type_nogrep () { a () { echo 'a' ;}; local b=$(type a); c=${b//is a function/}; [ $? = 0 ] && return 1 || return 0; } - qneill
@qneill 我在我的答案中进行了更加详细的测试。 - jarno
1
管道是最慢的元素。这个测试不比较“类型”和“声明”。它比较“类型| grep”和“声明”。这是一个很大的区别。 - kyb

8

测试不同的解决方案:

#!/bin/bash

test_declare () {
    declare -f f > /dev/null
}

test_declare2 () {
    declare -F f > /dev/null
}

test_type () {
    type -t f | grep -q 'function'
}

test_type2 () {
     [[ $(type -t f) = function ]]
}

funcs=(test_declare test_declare2 test_type test_type2)

test () {
    for i in $(seq 1 1000); do $1; done
}

f () {
echo 'This is a test function.'
echo 'This has more than one command.'
return 0
}
post='(f is function)'

for j in 1 2 3; do

    for func in ${funcs[@]}; do
        echo $func $post
        time test $func
        echo exit code $?; echo
    done

    case $j in
    1)  unset -f f
        post='(f unset)'
        ;;
    2)  f='string'
        post='(f is string)'
        ;;
    esac
done

输出结果如下:

test_declare (f 是函数)

实际 0m0.055秒 用户 0m0.041秒 系统 0m0.004秒 退出代码 0

test_declare2 (f 是函数)

实际 0m0.042秒 用户 0m0.022秒 系统 0m0.017秒 退出代码 0

test_type (f 是函数)

实际 0m2.200秒 用户 0m1.619秒 系统 0m1.008秒 退出代码 0

test_type2 (f 是函数)

实际 0m0.746秒 用户 0m0.534秒 系统 0m0.237秒 退出代码 0

test_declare (f 未设置)

实际 0m0.040秒 用户 0m0.029秒 系统 0m0.010秒 退出代码 1

test_declare2 (f 未设置)

实际 0m0.038秒 用户 0m0.038秒 系统 0m0.000秒 退出代码 1

test_type (f 未设置)

实际 0m2.438秒 用户 0m1.678秒 系统 0m1.045秒 退出代码 1

test_type2 (f 未设置)

实际 0m0.805秒 用户 0m0.541秒 系统 0m0.274秒 退出代码 1

test_declare (f 是字符串)

实际 0m0.043秒 用户 0m0.034秒 系统 0m0.007秒 退出代码 1

test_declare2 (f 是字符串)

实际 0m0.039秒 用户 0m0.035秒 系统 0m0.003秒 退出代码 1

test_type (f 是字符串)

实际 0m2.394秒 用户 0m1.679秒 系统 0m1.035秒 退出代码 1

test_type2 (f 是字符串)

实际 0m0.851秒 用户 0m0.554秒 系统 0m0.294秒 退出代码 1

因此,declare -F f似乎是最佳解决方案。

注意:在zsh中,如果f不存在,则declare -F f不会返回非零值,但在bash中会。使用时要小心。另一方面,declare -f f按预期工作,将函数定义附加到stdout上(这可能很烦人...)。 - Manoel Vilela
1
你尝试过 test_type3 () { [[ $(type -t f) = function ]] ; } 吗?定义本地变量的成本很小(虽然不到10%)。 - Oliver

7

这归结于使用“declare”来检查输出或退出代码。

输出样式:

isFunction() { [[ "$(declare -Ff "$1")" ]]; }

使用方法:

isFunction some_name && echo yes || echo no

然而,如果我没记错的话,重定向到null要比输出替换更快(说起来,可怕而过时的`cmd`方法应该被禁止,应改用$(cmd))。因为declare返回true/false,如果找到/未找到,则函数返回函数中最后一个命令的退出代码,所以通常不需要显式返回。而且,检查错误代码比检查字符串值更快(即使是空字符串):
退出状态风格:
isFunction() { declare -Ff "$1" >/dev/null; }

这可能是您所能得到的最简洁和无害的内容了。

3
为达到最大的简洁性,请使用isFunction() { declare -F "$1"; } >&-。该函数用于判断某个输入是否是一个函数,并返回相应的结果。注意,翻译不能改变原文的意思,但需要让内容更加通俗易懂。 - Neil
3
isFunction() { declare -F -- "$@" >/dev/null; } 是我的建议。它可用于名称列表(仅当所有名称都是函数时才成功),对于以“-”开头的名称没有问题,并且在我这里(使用 bash 4.2.25),当输出关闭时,declare 始终会失败,因为在这种情况下无法将名称写入标准输出。 - Tino
请注意,在某些平台上,“echo”命令有时可能会因为“系统中断调用”而失败。此时,“check && echo yes || echo no”如果“check”为真仍然可以输出“no”。 - Tino

4

以下是我在另一个答案中的评论(每次回到这个页面时都会错过)

$ fn_exists() { test x$(type -t $1) = xfunction; }
$ fn_exists func1 && echo yes || echo no
no
$ func1() { echo hi from func1; }
$ func1
hi from func1
$ fn_exists func1 && echo yes || echo no
yes

4

如果定义了函数,调用函数。

已知函数名称。假设函数名为my_function,则使用:

[[ "$(type -t my_function)" == 'function' ]] && my_function;
# or
[[ "$(declare -fF my_function)" ]] && my_function;

函数名称存储在一个变量中。如果我们声明func=my_function,那么我们就可以使用:

[[ "$(type -t $func)" == 'function' ]] && $func;
# or
[[ "$(declare -fF $func)" ]] && $func;
使用 || 替代 && 同样有效
(在编程过程中,这种逻辑反转可能很有用)
[[ "$(type -t my_function)" != 'function' ]] || my_function;
[[ ! "$(declare -fF my_function)" ]] || my_function;

func=my_function
[[ "$(type -t $func)" != 'function' ]] || $func;
[[ ! "$(declare -fF $func)" ]] || $func;

严格模式和前置检查
我们将 set -e 作为严格模式。
在我们的函数中, 我们使用 || return 作为前置条件。
这会强制终止我们的 shell 进程。

# Set a strict mode for script execution. The essence here is "-e"
set -euf +x -o pipefail

function run_if_exists(){
    my_function=$1

    [[ "$(type -t $my_function)" == 'function' ]] || return;

    $my_function
}

run_if_exists  non_existing_function
echo "you will never reach this code"

以上是的等价于。
set -e
function run_if_exists(){
    return 1;
}
run_if_exists

这会杀死你的进程。
在前置条件中使用 || { true; return; } 代替 || return; 可以解决这个问题。

    [[ "$(type -t my_function)" == 'function' ]] || { true; return; }

3
fn_exists()
{
   [[ $(type -t $1) == function ]] && return 0
}

更新

isFunc () 
{ 
    [[ $(type -t $1) == function ]]
}

$ isFunc isFunc
$ echo $?
0
$ isFunc dfgjhgljhk
$ echo $?
1
$ isFunc psgrep && echo yay
yay
$

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