Shell 编程实践
Contents
对于习惯了使用高级语言编程的程序员来说,shell 脚本的语法实在是有点原始。但是,shell 脚本开箱即用的特点使得它尤其适合在 Linux 服务器上运行。而例如 Python、NodeJS等语言都需要宿主上安装运行环境才可以使用。
最近出于工作的需要,写了不少 shell 脚本,这里简单记录下编写 shell 过程中的一些实践。
一. 模块化
许多 shell 脚本从头到尾都是命令的堆积,一个脚本上千行,一个命令做了N件事情,到处定义和引用全局变量。
其实 shell 脚本也需要按照功能划分模块,良好的模块拆分带来更好的代码可读性和可维护性,同时也方便测试。
1.1 定义main函数
一个shell脚本,可以像 Python 一样定义一个__main__
函数
!/usr/bin/env bash |
这样可以方便的找到程序的入口和退出点。
1.2 功能函数化
正如一个类只负责一件事,在 shell 中一个 function 也应该只做一件事,并且我们要尽可能的将事情的步骤分解为function。
在写 shell function 时,和其他语言不一样,shell 中的变量声明默认是全剧作用域的。所以在定义函数内变量时总是要指定local
关键字,如果该变量是只读(readonly)的常量,还可以加上-r
。
test1() { |
shell 的函数定义不能指定入参名称和个数,也不能定义函数的返回值,参数及返回值的取值是按照下表约定的。
$参数 | 说明 |
---|---|
$0 |
当前脚本的名称 |
$1 |
命令的第一个参数 |
$2 |
命令的第二个参数 |
… | 以此类推 |
$# |
参数的个数 |
$* |
单字符串表示的所有参数 |
$@ |
多字符串表示的所有参数 |
$? |
返回值,0 正常退出 |
函数的返回值有4种方式返回。
- 在函数内将返回结果赋值给全局变量。
- 在函数内将结果通过
echo
输出到标准输出,由于标准输出是会被shell子进程继承的,所以这种方式支持子进程向父进程返回结果。 - 在函数内
return 0/1
,通过$?
可以获取return的值。 - 通过写result文件返回结果,这种思路和赋值给全局变量一样,不过写文件很方便传递多行字符串结果。
1.3 脚本引用
多个脚本之间复用的函数和公共参数可以抽为单独的文件,通过source utils.sh
来引入。不建议使用sh
和 exec
的方式调用别的shell脚本,除非你明确知道你在做什么。
二. 错误处理
脚本中如果有运行失败的命令,shell 会默认继续执行,这个默认特性会给错误处理造成麻烦。
例如有时候,我们需要区分系统错误和业务错误:当碰到系统出错,脚本需要停止继续执行;当遇到业务错误,在处理完错误后,脚本可能需要继续执行。下面有几种方式可以控制 shell 的错误处理逻辑。
2.1 使用短路逻辑运算符
command || exit 1 |
等价于
command |
如果command
正常退出,返回0,||
运算符右半部分被短路,脚本继续执行。
如果command
异常退出,返回非0, 运算符右半部分执行,脚本exit 1
。
2.2 使用 set -e
set -e
等价于-o errexit
,表示打开 shell 脚本的错误退出选项,当脚本运行出现非零返回值时,不再继续运行下面的命令。
set +e
表示关闭返回值判断选项,总是继续运行出错命令后面的命令。
配合这两个条命令,可以灵活的控制 shell 脚本处理业务错误和系统错误。
2.3 使用 trap
捕获信号量
shell 脚本在运行期间会产生三种“伪信号量”,通过 trap ‘command’ signal
可以捕获这些信号。
信号名 | 何时产生 |
---|---|
EXIT | 从一个函数中退出或整个脚本执行完毕 |
ERR | 当一条命令返回非零状态时(代表命令执行不成功) |
DEBUG | 脚本中每一条命令执行之前 |
其中监听 ERR
可以在使得我们在 shell 出错后,有机会做一些比如清理工作现场、发送错误、报警日志到IM等工作。
三. 调试
shell 脚本没法直接调试,只能依靠print大法(有个开源工具bashdb,可以单步调试shell,对于大型的shell脚本可能有帮助),因此一个清晰的日志可以帮助排查很多问题。
3.1 log 日志
- 按照日期时间组织log文件
- 标准输出到terminal时,使用color
- 使用 tee 在标准输出的同时重定向到文件
LOG_LEVEL_ALL=-1 |
$0
代表当前shell脚本的名字
$LINENO
代表shell脚本的当前行号
$FUNCNAME[]
代表当前命令所属的函数名,通过遍历$FUNCNAME[]
数组可以知道当前的函数调用栈。
3.2 set -x
set -x
进入跟踪方式,显示所执行的每一条命令。配合trap command DEBUG
可以在显示执行命令前打印一些有用的帮助信息。
通过设置export PS4='+{$LINENO:${FUNCNAME[0]}} '
还可以改变set -x
模式下的输出格式,可以在每一条实际执行的命令前面显示其行号以及所属的函数名。
四. 一些有用的code snipper
4.1 获取当前工作目录
function get_working_dir(){ |
4.2 json处理
简单的 json 的处理,可以直接使用awk,sed、grep。如果有Python
环境,还可以用Python来帮助解析。
repo_id=$(echo $repo_info_response | python -c "import sys, json; print(json.load(sys.stdin)['id'])") |
不过最终还是推荐直接使用 jq 。jq提供了一套完整的命令行环境下的json解析功能。使用jq时,如果有语法不熟悉的,可以用这个网站测试—— https://jqplay.org
- json decode
# 取 { "id": "value"} 中的id |
- json 数组遍历
for row in $(echo "${response}" | ${JQ} -r '.[] | @base64'); do |
注意这里 base64 是用来将json数组中的某一个多行元素转化为一行输出,方便 for 循环处理。
- json encode
message_json="$(${JQ} -nc --arg str "$message_text" '{"text": $str}')" |
上面的代码将$message_text encode输出到{"text":"message_text"}
中。
五. 参考链接
http://www.ruanyifeng.com/blog/2017/11/bash-set.html
https://www.ibm.com/developerworks/cn/linux/l-cn-shell-debug/index.html
Author: deskid
Link: https://deskid.github.io/2019/08/27/shell-in-action/
License: 知识共享署名-非商业性使用 4.0 国际许可协议