十一 动态函数的科学实现

十一 动态函数的科学实现

【动机】

还记得在日志记录和动态函数一篇中, 曾经提到的动态实现? 基本思路是, 先建立动态日志函数的”根”函数 logx, 再在 command_not_found_handle 函数中, 将不存在的日志函数, 例如 logx_ok, 剥离出 ok 作为参数, 重定向到 logx 处理. 这样的方案看上去挺优雅, 但是至少存在以下缺点:

  1. 整个运行时, command_not_found_handle 只能有一个, 如果将来需要更多的动态函数, 必须手工修改该函数, 具体就是增加 elif 分支. 如此扩展性极差的方案相信大家很难接受;
  2. 随着动态函数的增加, command_not_found_handle 日益臃肿, 维护困难;

如何保证只写一次 command_not_found_handle, 同时又能按需增加动态函数?

【意图】

将 command_not_found_handle 保存在一个专门模块, 并定义注册函数供客户端调用, 在需要增加动态函数时, 调用该函数即可.

【预备函数】

在 data.sh, 新建函数 shield_key.

:<<COMMENT
    将给定原始字符串转换为掩护字符串, 或者反之.
    说明:
        鉴于关联数组的 key, 如果是正则表达式(例如 command_not_found_handle 所需), 可能会包含以下敏感字符:
            [ ] \ 
        原则上方括号无法成为 key 的一部分(虽然实际测试结果为可为), 也无法查找, 也就无法 unset.
        为保险起见, 也为了顺利 unset, 需要先将这些字符转换为特定字符(串), 得到的掩护字符串才能用作 key.
        例如, 用于模块 command_not_found_handle.sh 时,
        1. 在 register / unregister 中, 需要转换正则表达式为可用 key(掩护字符串);
        2. 在 command_not_found_handle 中, 需要将掩护字符串临时还原为原始的正则表达式, 才能重定向函数的调用.
    参数:
        $1: 要掩护或还原的字符串
        $2: true 或者 其他任意字符(串)包括空字符 
    返回:
        $2 为 true, 返回 $1 的掩护字符串; 否则返回 $1 的原始字符串
    使用: shield_key <string> < true | {other} >  
    注意:
        测试函数位于: blogging/unit_test.sh/array_delete_element/way3
COMMENT
shield_key(){
    local -r lb=LEFT_BR rb=RIGHT_BR rs=RIGHT_SLASH

    local target
    # 使用花括号扩展应该比 sed 效率高.
    if [ "$2" == true ]; then
        target=${1//[/$lb}
        target=${target//]/$rb}
        target=${target//\\/$rs}
    else
        target=${1//$lb/[}
        target=${target//$rb/]}
        target=${target//$rs/\\}
    fi
    echox $target
}

函数的意义件其注释, 目的是为了在 command_not_found.sh 中, 顺利添加删除以及使用.

【实现】

新建文件 command_not_found.sh, 写入 上述的注册函数 register_command_not_found_handler , 取消注册函数 unregister_command_not_found_handler 和核心 command_not_found_handle 函数, 以及用来保存动态函数的关联数组:

#!/bin/bash


:<<SUMMARY
    本模块主要实现: 执行动态函数
    register_command_not_found_handler    \
                "function_pattern1 'statement11; statement12;...'" \
                "function_pattern2 'statement21; statement22;...'"
    command_not_found_handle        <cmd or fun> <arg1> <arg2> ...
SUMMARY


# 用于保存动态函数 pattern 到动态函数执行语句的关联数组
declare -A _CNF_HLS #=()

:<<COMMENT
    将一个或多个具有一定特征(pattern)的"动态函数"注册到命令未发现处理器, 以便动态执行
    每个参数代表一对. 
    first-part: funciton pattern
    second-part: 对应于此"动态函数"的操作, 需要满足 eval 执行格式
    用法: register_command_not_found_handler    \
            "function_pattern1 'statement11; statement12;...'" \
            "function_pattern2 'statement21; statement22;...'"
    示例: blogging/unit_test.sh/multiple_command_not_found_handle
    注意: 函数体应尽量使用 f_body 获取, 非特殊情况下, 不要手工构造函数的 body 字符串
COMMENT
register_command_not_found_handler(){
    local arr pair pattern  key
    for pair in "$@"; do
        readarrayx "$pair" arr  

        # 命令(函数)字符串一定不会包含空格, 只需要消除后面可能的折行即可
        printf -v pattern "%s" ${arr[0]}

        key=$(shield_key "$pattern" true)

        # 函数体可能包含换行, 折行, 需要保留
        _CNF_HLS+=([$key]="${arr[1]}")
    done
}

:<<COMMENT
    取消注册的动态函数
    $@: 要取消注册的 funciton pattern 列表
    注意: 无法 unset {object}.{method} 的对应元素, 因为该 key 包含方括号
COMMENT
unregister_command_not_found_handler(){
    local pattern key
    for pattern in "$@"; do 
        key=$(shield_key "$pattern" true)
        unset  -v _CNF_HLS["$key"]
    done
    #describe_array _CNF_HLS
}

# blogged
:<<COMMENT
    结合 register_command_not_found_handler 函数, 支持添加动态函数, 同时, 
    利用此函数, 可实现简单的面向对象. 
COMMENT
command_not_found_handle() {
    local key pattern
    for key in "${!_CNF_HLS[@]}"; do
        pattern=$(shield_key "$key")
        if [[ "$1" =~ $pattern ]]; then
            eval "${_CNF_HLS[$key]}"
            return $?
        fi
    done

    # 或者其他方式调用默认处理器
    bash -c "command $1"  
    return 127
}

基本原理是, 需要新增动态函数, 随时随地调用注册函数, 将动态函数的特征与其函数体通过关联数组对应起来, 在调用该动态函数时, 由 bash 到 command_not_found_handle 中查找关联数组, 找到则调用; 找不到则按原样报错返回.

【测试】

  1. 比较繁琐的调用方法, 不推荐.
    # 使用 register_command_not_found_handler 一次注册一个或多个"命令无发现处理器"
    way1(){
        # $1: name
        # say_hello_{name} = say_hello name
        say_hello(){
            echox "this is a custom command_not_found_handle. hello <$1>"
        }

        register_command_not_found_handler \
            "^say_hello_
                '
                    say_hello \${1#*say_hello_}
                ' 
            "       \
            "^logx_
                '
                    local level=\${1#*logx_} 
                    shift
                    logx \"\$@\" \$level                    
                '
            "
        describe_array _CNF_HLS

        say_hello_guoshi

        logx_eng "heoo, hello lei,,,, englis, you win"
    }
    way1
<输出>

同时, 文件 eng.log(如果不存在则被创建)末尾添加一行内容:

[2025-05-17 06:42:45] [process id:5695] [way1] heoo, hello lei,,,, englis, you win

可是这样的调用方法非常艰难, 我们需要迅速而准确无误的得到函数体, 于是有了函数 f_body.

2. 先在 system.sh 中创建函数 f_body, 用来获取指定名称的函数的函数体. 注意不包括前两行和末尾一行.

:<<COMMENT
    获取函数体, 以字符串 echox
    $1: 函数名. 未定义的函数, 自动返回空
    用法: f_body <function-name>
    注意: 如果未得到预期的函数体, 请检查引号配置是否恰当
COMMENT
f_body(){
    # local temp=
    # [ "$1" ] && temp=$(declare -f $1 | sed '1d;2d;$d')
    # echox "$temp"

    [ "$1" ] && declare -f $1 | sed '1d;2d;$d'
}

接下来, 注册函数的调用显得很优雅:

    way2(){
        say_hello(){
            echox "this is a custom command_not_found_handle. hello <$1>"
        }   
        call_say_hello(){ 
            say_hello ${1#say_hello_}
            local temp="hello,world"
            tip "here,$temp"
        }   

        local e_cmd1=$(f_body call_say_hello)
        local e_cmd2=$(f_body call_logx)
        local e_cmderr=$(f_body call_sth)

        local tmp
        for tmp in e_cmd1 e_cmd2 e_cmderr; do
            aop info center $tmp
            echo "${!tmp}"
        done

        register_command_not_found_handler "^say_hello_ '$e_cmd1'" "^logx_ '$e_cmd2'" 
        say_hello_guoshiwo
        logx_how "how do you do now?"
    }
    way2

注意 e_cmderr 是有意制造的不存在的函数, 表示它并不会引发错误, 仅仅是得到空函数体. 另外, call_logx 和 logx 函数位于 log.sh 中, 如下:

# $1: content, default to  "content forgotten."
# $2: level, default to info
logx(){
    local dir=./log
    [ "$ROOT_DIR_PATH" ] && dir=$ROOT_DIR_PATH/log
    [ "$__DEBUG__" -a ! -d "$dir" ] && mkdir -p $dir

    if [ -d "$dir" ]; then
        local content=${1:-"content forgotten."}
        local time=$(date +'%Y-%m-%d %H:%M:%S')
        echo -e "[$time] [process id:$$] [${FUNCNAME[2]}] $content" >> $dir/$2.log
    fi    
}

call_logx(){
    local level=${1#*logx_} 
    shift
    logx "$@" $level    
}
<输出>

可见动态函数的注册和反注册均达到预期目的.

【最后】

此处展示的动态函数实现, 乍一看没多大意思. 其实这主要是为了引出下面的 bash 的简单面向对象编程.