起因
众所周知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函数了,真正实现无缝转换。