shell 模块化

一直很好奇 shell 脚本如何能够像 C 一样分成多个文件来写, 在模块中定义好各种函数, 在主文件中调用它们. 这些需求是有的, 如在模块中定义 map 函数, 并在主文件中调用它们来调用 map. 或者像我们的服务器部署脚本有大量类似的功能, 找出最新的代码、压缩备份、只保存备份最新的5各、检查路径是否存在等等这些都可以实现在模块中,然后就可以为不同的项目使用.

shell 并不直接支持模块化, 跟很多别的惯用法一样必须自己遵守某些规则来实现. 利用 source 命令或者 . (点号) 命令来执行一个脚本, 可以将执行脚本的代码执行并且导入其全局函数和全局变量到当前环境. 利用 export 命令可以将全局函数和全局变量为子 shell 所见. 所谓子 shell 就是直接或者间接被调用的 shell 脚本, source 的脚本不是子 shell. 如果当前只有一个 main.sh 脚本需要调用 modules/module.sh 的函数, 直接 source 进来便好了, 但是假如另外有一个 sub.sh 被 main.sh 所调用, 同样希望能够调用 module.sh 中的函数呢? 如果再 source 一次就会导致 module.sh 被执行两次, 如果 module.sh 中只包含了一些函数和常量定义那没什么要紧的. 但是如果 module.sh 中包含了一些全局变量并且希望所有这些全局是共享的, 照目前的情况只要 sub.sh 以 source 方式引入了 module.sh , 那么其中的函数和变量就与上层 main.sh 中是两个不同的副本了.

可实现的方式就是让子 shell 继承父 shell 中的导入的函数和变量. 但是单单在父 shell 中导入模块是无法共享的. 首先, 导入的函数对子 shell 是不可见的, 因为没有用 export. 其次, 即便是可见的也无法阻止子 shell 重新导入这个模块. 所以我们在模块第一次被导入时, 定义一个模块变量, 并且再下次导入时检查这个变量, 如果已经定义了就直接返回, 这非常类似于 C 的 #ifndef #define 的机制. 不过我们换成了:

1
2
3
4
if [ -n "$__MODULE_SH__" ]; then
  return
fi
export __MODULE_SH__='module.sh'

如此只要父 shell 引入了这个模块了, MODULE_SH 变量就会为子 shell 可见, 当子模块再次以 source 方式引入这个模块时将直接返回. 为了让父 shell 中引入的模块中的其它函数也为子 shell 所见, 只需要将它们也用 export 导出便可. 这种可见性必须是具有上下层次的, 如果是平级的如 sub1.sh 和 sub2.sh 那么它们还是会分别导入模块.

讨论到这里其实已经明白了 shell 中本身并不支持模块化编程, 用自己的方式设计的也不如 C 原生的那么好用. 但对于 shell 实现比较简单的模块化还是完全可以的.

实际测试的完整代码在这里: shell.zip