shell 【bash】脚本模块化实践

起因

众所周知bash是个相当自由的语言,一个函数echo字符串直接就给你运行了,如果时网络上那分分钟就给你注入了。不过还好,没人用bash写网络应用。最近搭建openvpn,自己写了个脚本,但是bug不断。于是想分几个模块写,于是去网上找bash如何导入其他脚本。结果发现bash导入其他脚本非常简单粗暴。所有脚本的变量函数都混在一起,感觉非常不爽。于是就自己写了个导入逻辑。

遇到的问题

期间试过好几种方法都效果不好。
最开是想到的是用 export ,主脚本当作库脚本的子shell。这样就可以屏蔽库脚本的内部变量和函数,起到封装模块的目的。然而这个只有一层依赖还好,如果有多层,库函数的库函数也会暴露给主脚本。另外还有个问题就是如果export 的函数用到了没export的变量就会有问题。后来我用 unset特殊前缀的变量和函数的方法也是这个问题放弃了。期间我用生成脚本代码,或是用内置命令exec效果都不好。搞不好还会死循环,因为导入库后要调用自身。另外提一句,如果是一个没有状态的脚本,可以用一个简单的方法导入,类似./lib.sh fx avg./lib.sh var的方式调用。

代码

最后用的是重命名的方法,把没export的变量加个前缀。
实现了三个方法,

  • visit 用来访问私有的方法,根据调用的位置(上下文)加上前缀,确保访问到变量
  • import 用来导入库脚本,并且根据导入前后全局变量的差异,判断哪些未export的变量要加前缀。
  • private 这个方法并不实现主要逻辑,只是为了方便库函数访问自身的私有变量,假设有一个私有变量a,如果运行了eval "$(private)",就可以接用$a获取到变量a的值,否则只能用$(visit a)来获取到变量a的值。

visit

根据调用者来访问对应的变量或函数,由于import会重命名函数和变量,所以需要用visit函数动态调用。对于变量当然也可以这样调用,但是还是推荐用private函数。这样可以和原来代码无缝衔接 ,只需要在函数开始时加上一句eval "$(private)"

visit() {
    
    
    if [ -z "$1" ]; then
        exit 1
    fi
    local m=($(caller))
    m="m$(ls -i ${
      
      m[1]})"
    m=($m)
    m="${m[0]}_$1"
    local n=$1
    shift
    if declare -p $n >/dev/null 2>&1; then
        eval "echo \$$n"
        return 0
    elif declare -p $m >/dev/null 2>&1; then
        eval "echo \$$m"
        return 0
    elif declare -f $n >/dev/null 2>&1; then
        eval "$n $*"
        return 0
    elif declare -f $m >/dev/null 2>&1; then
        eval "$m $*"
        return 0
    fi
    return 1
}

import

这里每个脚本私有变量和函数的前缀是根据inode 索引生成的。

import() {
    
    
    local f=
    local env1=
    local env2=
    local met=
    local line=
    for f in "$@"; do
        if [ -f "$f" ]; then
            env1="$(declare -p | awk '{
      
      if($2=="--")print $3}' | sed -r 's/^([^=]+)=.*$/\1/g')"
            source "$f"
            env2="$(declare -p | awk '{
      
      if($2=="--")print $3}' | sed -r 's/^([^=]+)=.*$/\1/g')"
            #根据inode id生成前缀
            met=("m$(ls -i $f)")
            met=($met)
            # 重命名变量
            for line in $(echo -e "$env1\n$env2" | sort | uniq -u); do
                # echo "unset $line;${met[0]}_$(declare -p $line | awk '{if($2=="--")print $3}')"
                eval "unset $line;${met[0]}_$(declare -p $line | awk '{
      
      if($2=="--")print $3}')"
            done
            # 重命名函数
            for f in $(declare -F | awk -v OFS=' ' '{
     
     if($2=="-f")print $3}'); do
                # echo "unset -f $f;"$'\n'"${met[0]}_$(declare -f $f)"
                eval "unset -f $f;"$'\n'"${met[0]}_$(declare -f $f)"
            done
        else
            echo "Error: Cannot find library at: $f"
            exit 1
        fi
    done
}

private

注意 这个函数只能在函数内运行,用法: eval "$(private)"

private() {
    
    
    local m=($(caller))
    m="m$(ls -i ${
      
      m[1]})"
    m=($m)
    m="${m[0]}"
    declare -p | awk '{if($3~/^'$m'/){gsub(/'$m'_/,"local ",$3);print $3}}'
}

用法

把上面函数复制到/etc/profile 文件中,再加上

export -f visit
export -f import
export -f private

示例

a.sh

#!/bin/bash

import ./b.sh 

fx

echo $gg

b.sh

#!/bin/bash

fx() {
    
    
    eval "$(private)"
    echo "公有函数"
    echo "访问私有变量p $p"
    visit f
}
f() {
    
    
    echo '私有函数'
}
p='???'
export -f fx
export gg='全局变量'

注意事项

如果在函数内部定义了变量或函数,这些变量将不会重命名,应为在函数第一次运行之前并不会把这些变量和函数加载到内存,因此import 函数检测不到。

后记

至于为什么 visit fx 不优化成 fx。主要是bash 语法太自由了。不太好分析,再说过早的优化是一切罪恶的开始 :)

后后记

刚发布文章,思路就来了,用shopt -s expand_aliases开启alias。这样就彻底用不到visit函数了,真正实现无缝转换。

猜你喜欢

转载自blog.csdn.net/qq_45256489/article/details/123433201
今日推荐