十二 bash 的简单面向对象编程

十二 bash 的简单面向对象编程

【动机】

众所周知, linux shell 编程, 非面向对象, 故以下代码无法运行:

        Person(){
            local name age career
            ctor(){
                name=$1
                age=$2
                career=$3
            }
            say_hello(){
                aop tip center "hello, my name is $name, I am $age years old, I am a $career."
                info "these are other parameters:<$1>,<$2>,<$3>..."
            }
        }
       clear
       local p=$(new Person "John williams" 69 "guitar artist")
       p.say_hello one two three

如何让它正常运行?

【意图】

利用上一篇动态函数的科学实现提到的 command-not-found.sh 模块, 把 new 对象, 以及点操作符等面向对象的特有调用重定向为 linux shell 可执行的操作.

【预备函数】

在 data.sh 中, 新建函数 ensure_par:

:<<COMMENT 
    确保给定的路径(可能是目录或文件, 不论存在与否)的父目录存在.
    参数:
        $1: 给定的路径
    返回: 无
    注意: 根目录 / 的父目录, 仍然等于 / . 
COMMENT
ensure_par(){
    local par=$(dirname "$1")
    [ ! -d "$par" ] && mkdir -p "$par" || :
}

测试略.

【实现】

新建模块文件 oop.sh, 写入以下代码:

#!/bin/bash


:<<SUMMARY
    本模块主要实现: 使用启用或停用的方式, 提供基本的面向对象的支持(不包括继承, 多态等特征)
    注意:  
        1.  由于 command_not_found_handle 运行于独立的执行环境, 在引发错误的 shell 中
            定义的变量可读不可写
        2.  基于上述原因
            1)  如果类中将字段直接定义为指向对象外部变量的引用(或者保存该变量的名称), 可能会导致读写数据失败. 
            2)  字段更新比较艰难(目前需要借助文件)
            3)  调用导致属性改变的实例方法后, 正确的属性值只保留在文件中, 下次的实例方法调用, 会
                先从文件中读取正确值. 但 echo \$obj 的结果可能不是最新值. 作为补救, 模块中默认实现了
                toString 和 sync_data 方法, 可返回其最新值, 同时, 类定义中还可以重写这些方法. 推荐
                重写 toString 即可, sync_data 最好不要动.
    bash 文档相关说明:
    https://www.gnu.org/software/bash/manual/bash.html
    it is invoked in a separate execution environment with the original command 
    and the original command's arguments as its arguments
    函数:
        enable_oop              <false | anyelse_include_empty>
SUMMARY

###################################################################################################
:<<COMMENT
    启用或停用面向对象编程. 
    $1: 可用值为任意字符(串), $1 == false, 表示停用; 其他(包括空)表示启用 
    用法: enable_oop  <false | anyelse_include_empty>
    注意:   
        1. 对每个类的要求: 
            1). 在类定义的末端, 添加:
                _class_transform_function  --fields "... ..." "$@"
                其中, --fields "... ..." 配置需要持久化到对象中的字段名, 例如 --fields "name age career" , 
                注意表示 0 个字段应该最少包含一个空格, 例如 --fields " ", 而不能是 --fields ""
        2. 将 _create_obj / _perform_method / _class_transform_function / _write_object / _read_object 定义为内部函数, 
            目的保证默认状态(未启动 oop 时), 这些函数不可用.
COMMENT
enable_oop(){
    #     将对象写入文件
    #     --obj-name: 对象名称, 必须. new 对象时, 无对象名, 应该设置为空格而非空字符 " "
    #     --class-name: 类名称, obj-name 非空时, 该选项被忽略
    #     --field-names: 字段名称列表组成的字符串, 以空格相隔, 必须. 如果无字段(属性), 则为空字符而非空字符 " "
    #     --var-name: 将写入文件的 json 字符串, 也保存为该名称的变量. 可选
    #     用法: _write_object --obj-name "..." --class-name "..." --field-names "..." --file-name "..."
    #     注意:
    #         1. 各个字段的值, 在调用端已经配置好.
    _write_object(){

        local obj_name class_name field_names  var_name # file_name
        eval $PASX
        params  obj-name class-name field-names var_name  # file-name

        # 1. 确定 class 和 id
        local class id
        if [ -z "$obj_name" ]; then
            # 新建对象
            class=$class_name
            id=$(random_string)
        else
            # 已有对象, 分已有记录和没有记录两种情况
            class=$(echox "${!obj_name}" | jq -r ".class")
            id=$(echox "${!obj_name}" | jq -r ".id")
        fi

        # 准备 json 记录
        local v_n json="{\"class\":\"$class\", \"id\":\"$id\", \"data\":{"
        for v_n in $field_names; do
            json+="\"$v_n\":\"${!v_n}\","
        done
        # 如果对象没有字段, 则当前 json 字串的末尾不会是逗号
        [ "${json: -1}" == ',' ] && json=${json%?} # -1 前的空格必不可少
        json+="}}"

        # 如果存在旧记录且不等于新纪录, 则替换之; 不存在旧记录则新建一条记录; 其他情况, 忽略操作 
        local old_rec=$(grep "$id" $OBJS_FILENAME)
        if [ "$old_rec" ]; then
            [ "$old_rec" != "$json" ] && sed -i "s/^.*$id.*$/$json/" $OBJS_FILENAME
        else
            # 不存在旧记录, 则直接添加
            echox "$json" >> $OBJS_FILENAME
        fi

        # 赋值, 供调用端使用
        if check_var_name "$var_name"; then
            local -n ref_obj=$var_name
            ref_obj="$json"
        fi
    }

    #     从文件读取对象
    #     $1: 对象名称, 必须
    #     用法: read_objec <object_name> 
    _read_object(){
        if [ "$1" ]; then
            local -n ref_obj=$1
            local id=$(echox "$ref_obj" | jq -r ".id")
            grep "$id" $OBJS_FILENAME
        fi
    }

    # new 对象使用的函数体. 其中的 $1 $2 ... 编号,  是针对 command_not_found_handle 函数的参数
    # 例如, 创建对象语句是 new Person bill 28 US 则: 
    #   $1: new
    #   $2: 类名称即 Person (使用 function 定义)
    #   $3 及以后: 构造函数需要的所有参数
    _create_obj(){
        if declare -f $2 &>/dev/null; then
            func=$2   
            shift 2
            $func "$@"
        else
            log_error "$2 class undefined"
            return 120
        fi
    }

    # 执行类或对象方法使用的函数体. 其中的 $1 $2 ... 编号,  是针对 command_not_found_handle 函数的参数
    # 例如, 
    #   1. 执行静态方法(其实是以静态的方式执行实例方法, 因为我们目前无法有效区分静态与非静态)的语句: 
    #       Person.say hello world, 则
    #       $1="Person.say"
    #       $2 及以后为方法需要的所有参数
    #       而 类名称 inst=Person ; 类方法 meth="say" 
    #   2. 执行实例方法的语句 john.say hello world 则 
    #       $1="john.say",
    #       $2 及以后为方法需要的所有参数
    #       而 对象实例 inst=john ; 对象方法 meth="say" 
    #   以上两种调用情况, inst 和 meth 均从 $1 解析得到
    _perform_method(){
        local inst=${1%.*}
        local meth=${1#*.}
        shift   
        if declare -f $inst &>/dev/null; then
            # 调用静态方法
            $inst --method $meth  "$@"
        elif declare -p $inst &>/dev/null; then
            # 调用实例方法
            func=$(echo "${!inst}" | jq -r ".class"  2>/dev/null)
            if [ "$func" ] && declare -f $func &>/dev/null; then
                $func --method $meth --instance $inst "$@"
            else
                local msg
                [ -z "$func" ] && msg="The class is not identified internally within the variable" \
                     || msg="The class $func is undefined"
                log_error $msg
                return 125
            fi
        else
            log_error "$inst is not a class, or a variable."
            return 121
        fi
    }

    # 将对象的方法调用, 转化为函数调用
    # --fields: 私有变量名称列表
    # 其他: 传递给类的方法的所有参数, 可能是:
    #       1) "$@"                                     (new 实例)
    #       2) --method $meth "$@"                      (静态方法调用)          
    #       3) --method $meth --instance $prefix "$@"   (实例方法调用)
    # 其中 "$@" 是传递给方法的所有参数
    # 用法: _class_transform_function  --fields "... ..." "$@"
    # 仅用于添加到类定义的最后一行, 作为视角方法到实际的内部函数之间的转移
    _class_transform_function(){

        local fields instance method
        eval $PASX
        params "fields" "instance" "method" 


        if [ "$instance" -a "$method" ]; then
            if declare -f $method &>/dev/null; then
                # 调用已定义的实例方法
                # instance 变量保存的是对象名
                local -n ref_inst=$instance
                # 实例函数
                # 1. 从文件中获取最新值, 
                ref_inst=$(_read_object $instance) # $OBJS_FILENAME

                # 2. 字段赋值
                local fd
                for fd in $fields; do
                    local -n ref_v=$fd
                    ref_v=$(echo "$ref_inst" | jq -r ".data.$fd")
                done

                # 3. 调用. 在方法的实际参数前有 --fields ".." --instance "..." --method "..." 共六个参数
                shift 6
                $method "$@"

                # 4. 上述调用, 可能已经修改字段, 所以需要更新 instance 携带的字段
                # 通过引用修改的结果, 仅在 command_not_found_handle 中能看到
                # 通过 --var-name $instance 修改无效
                _write_object --obj-name $instance --field-names "$fields"  # --var-name $instance --file-name $OBJS_FILENAME
            elif s_ct_w $method  toString sync_data; then
                # 未定义的方法中, 需要单独处理 toString sync_data, 作为其默认实现
                # _read_object $instance | jq -C --indent 4
                _read_object $instance
            else
                # 其他未定义, 则记录错误
                log_error "method $method undefined."
                return 123
            fi
        elif [ -z "$instance" -a  -z "$method" ]; then
            if declare -f ctor &>/dev/null; then
                # 调用构造器
                shift 2         #
                ctor "$@"
                local self
                _write_object  --class-name ${FUNCNAME[1]} --field-names "$fields" --var-name self # --file-name $OBJS_FILENAME
                echox $self
            else
                # 未定义构造器, 记录错误
                log_error "contructor of ${FUNCNAME[1]} class not found."
                return 122
            fi
        elif [ -z "$instance" -a "$method" ]; then
            # 静态函数. 在方法的实际参数前有 --fields ".."  --method "..." 共四个参数
            shift 4 
            $method "$@"
        fi
    }

    local -r REG_NEW='^new$'
    local -r REG_METH='^[_a-zA-Z][_0-9a-zA-z]*\.[_a-zA-Z][_0-9a-zA-z]+$'
    ! declare -p OBJS_FILENAME &>/null && declare  -gr OBJS_FILENAME='/oop-cache/objects.rec'
    if [ "$1" == false ]; then
        unregister_command_not_found_handler $REG_NEW $REG_METH
        unset -f  _class_transform_function _perform_method _create_obj _read_object _write_object
        rm -f $OBJS_FILENAME
    else
        # 将 new 对象和执行类/对象方法的 command_not_found_handler 注册到
        # command_not_found_handle 中
        register_command_not_found_handler   \
            "$REG_NEW '$(f_body _create_obj)'" \
            "$REG_METH '$(f_body _perform_method)'"
       ensure_par $OBJS_FILENAME
        touch $OBJS_FILENAME
    fi
}

该模块只包含一个函数(其余均为嵌套函数) enable_oop, 用于启动或停用面向对象编程. 基本思路是, 根据参数是否是 false, 停用或启用面向对象. 注意 echo $obj 的结果可能不是最新值, 但并不影响任何实例方法调用结果的正确性.

【测试1】

现在试试”动机”里的代码:

    Person(){
        local name age career
        ctor(){
            name=$1
            age=$2
            career=$3
        }
        say_hello(){
            aop tip center "hello, my name is $name, I am $age years old, I am a $career."
            info "these are other parameters:<$1>,<$2>,<$3>..."
        }

        set_age(){
            age=$1
        }

        to_string(){
            echox "name:$name, age:$age, carrer:$career. class:Person"
        }

        get_age(){
            echox $age
        }

        # 必须添加的一行
        _class_transform_function  --fields "name age career" "$@"
    }

    clear
    # 启用面向对象
    enable_oop
    local p=$(new Person "John williams" 153 "guitar artist")

    aop info right "1. call method as static..."
    Person.say_hello here there where

    aop info right "2. modify age to 92, then call say_hello..."
    p.set_age 92
    p.say_hello one two three

    aop info right "3. echo object may be incorrect..."
    echo "\$p is incorrect :<$p>"

    aop info right "4. using toString or sync_data makes correct:"
    p.toString
    p.sync_data

    aop info right "5. now diable oop..."
    enable_oop  false

    pushee
    set +e     
    local p2=$(new Person "Blim" 72 "basketball player")
    p2.say_hello first second third
    popee

运行结果如下:

可见, 面向对象的启用和停用函数 enable_oop 已起作用.

【测试2】

主要针对修改属性(字段)的操作, 静态方式调用方法, 以及使用错误的对象引用调用方法的测试.

    way4(){
        # --method
        # 
        Person(){
            local name age career
            ctor(){
                name=$1
                age=$2
                career=$3
            }
            say_hello(){
                aop tip center "hello, my name is $name, I am $age years old, I am a $career."
                info "these are other parameters:<$1>,<$2>,<$3>..."
            }

            # 注意涉及修改字段的方法, 调用后应立即调用 ud_var, 以便字段数据同步
            set_age(){
                age=$1
            }
            set_name(){
                name=$1
            }
            set_career(){
                career=$1
            }

            another_way(){
                info "another_way is called. parameter information :"
                local idx
                for idx in $(seq 1 $#); do
                    menu "[$idx]=<${!idx}>"
                done
            }

            example_named_parameter(){
                local alive slap swim grap 
                eval $PASX
                local all_pos=$(params)
                params "'alive a' yes no" \
                    " slap china" \
                    "'---swim s' here there" \
                    "'grap ---g' 234 889"
                echox "alive=<$alive>, slap=<$slap>, swim=<$swim>, grap=<$grap>" 
                echox "all postion parameters:"
                info -e $all_pos

                tip "instance fields:"
                info "name:<$name>,age:<$age>,career:<$career>"
                
            }

            # 重写默认的 toString 方法
            toString(){
                echox "class:Person; name=$name, age=$age, career=$career"
            }

            # 每个类定义的最后, 均需要这一行. 其中 --fields 参数, 根据实际情况修改
           _class_transform_function  --fields "name age career" "$@"
        }

        aop caption center "1. 启用面向对象, 并创建对象"
        enable_oop
        local pkm=$(new Person "Jhon Willimas" 69 "guitar artist")

        aop caption center "2.调用只读方法"
        pkm.say_hello "one   two " three "four   five    six.... " 
        pkm.another_way "hello world" 234 998 -15.4 " you  win " "etc ... ...     "
        pkm.example_named_parameter one -s two -g 123 -a 'hello world' "查找构造函数的IL代码。"  \
                     "The Dow Jones Industrial Average closed down 2.5%" ------------swim "  Despite    widespread market      expectations that AP   "
        
        aop caption center "3. 调用写方法"
        pkm.set_age 77
        pkm.set_name Blim
        pkm.set_career "common    worker"

        pkm.say_hello "update  successfully ! " "  you    win  " "  they   lose    " 

        aop caption center  "4. 如果上述 2 的方法, 以静态方式调用, 则实例字段全部为空"
        Person.example_named_parameter one -s two -g 123 -----a 'hello world' -alive '  welcome to our company   !' "查找构造函数的IL代码。"  \
                "The Dow Jones Industrial Average closed down 2.5%" ------------swim "  Despite    widespread market      expectations that AP   "

        aop caption center "5. 重写的 toString 方法"
        notice "override toString:<$(pkm.toString)>"

        pushee
        set +e
        aop caption center "6. 手工触发错误: 未定义方法 / 对象错误(未定义类型) / 对象错误(非对象), 请查看 error.log"
        pkm.no_such_method

        local mkt="{\"class\":\"slim\"}"
        mkt.say_hello "this is " a " null reference"

        local pay="hello"
        pay.say_hello "this is" a "object"


        aop caption center "7. 停用面向对象, 再尝试创建对象, 调用方法, 则恢复原来的报错"      
        enable_oop false
        local ppk=$(new Person "oli gan" 22 "basketball player")
        ppk.say_hello
        pkm.say_hello

        popee
    }
    clear
    way4

运行结果如下:

注意:

  1. 重写的 toString 方法已经生效; 如果在类定义中注释掉该方法, 默认的返回 json 字符串的默认方法将浮出水面.
  2. enable_oop false 后, error.log 写入以下日志(注意应事先保证 set +e, 否则从第二个起的错误将没有机会展示):

【测试3】

示范在对象中如何嵌套数组和其他对象, 以及修改这些数组和对象失效的问题.

    way14(){
        Person(){
            local name age
            ctor(){
                name=$1
                age=$2
            }
            update(){
                name="$name ==> $1"
                age="$age ==> $2"
            }
            _class_transform_function  --fields "name age" "$@"
        }
        Class(){
            local n_students n_head_teacher
            ctor(){
                n_students=$1
                n_head_teacher=$2
            }

            # $1: key
            # $2: student object name
            # 注意: 在 command_not_found 函数内部修改外部变量(arr_stu)无效
            add_student(){
                 local -n ref_stus=$n_students
                 ref_stus+=([$1]=$2)
                # eval "$n_students+=([$1]=$2)"
                #describe_array arr_stu  --item-is-name 
            }
            get_students_name(){
                echox $n_students
            }

            get_head_teacher_name(){
                echox $n_head_teacher
            }

            _class_transform_function  --fields "n_students n_head_teacher" "$@"
        }
        enable_oop 
        local stu1=$(new Person John 15)
        local stu2=$(new Person Mary 16)
        local stu3=$(new Person Luice 17)

        local -A arr_stu=([one]=stu1 [two]=stu2 [three]=stu3)

        aop caption right "1. 学生数组的原始数据"
        describe_array arr_stu --item-is-name 

        aop caption right "2. [two]修改后的学生数组, 注意必须调用 sync_data 已同步修改"
        stu2.update "Soloman  Bill" 23
        stu2=$(stu2.sync_data)
        describe_array arr_stu --item-is-name 

        aop caption right "3. 用学生数组名,  及新建的 Person 作为 teacher, 初始化 Class 实例"
        local teacher=$(new Person "Mr. Qi" 46)
        local fasts=$(new Class arr_stu teacher)
        fasts.toString

        aop caption right "4. 通过对象方法修改 arr_stu 无效; 必须通过其自身"
        local new_stu=$(new Person "Bill Willimas" 48)
        fasts.add_student five new_stu
        arr_stu+=([success]=new_stu)

        local arr_stu_name=$(fasts.get_students_name)
        describe_array $arr_stu_name --item-is-name
        local tea_name=$(fasts.get_head_teacher_name)
        tip "head teacher is : <${!tea_name}>"








        enable_oop false
    }
    clear
    way14

运行结果:

【未完】

很明显, 这里的面向对象, 仅仅是简单实现. 因为面向对象的太多特点, 例如继承, 多态, 虚函数, 接口等等, 鉴于时间关系, 暂时无法实现. 有兴趣的朋友, 欢迎对此进行扩展! 当然, 如果某一天 bash 新版本在语法层面自己实现了面向对象, 这里做的所有工作就多余了呵.

该对象模块 oop.sh, 在脚本的瘦身以及编译脚本为可执行体中, 有具体应用.

谢谢观看!