编写可靠 Bash 脚本的一些技巧

开发 前端
写过很多 bash 脚本的人都知道,bash 的坑不是一般的多。其实 bash 本身并不是一个很严谨的语言,但是很多时候也不得不用。以下总结了一些编写可靠的 bash 脚本的小 tips。

 [[321413]]

写过很多 bash 脚本的人都知道,bash 的坑不是一般的多。其实 bash 本身并不是一个很严谨的语言,但是很多时候也不得不用。以下总结了一些编写可靠的 bash 脚本的小 tips。

0. set -x -e -u -o pipefail

在写脚本时,在一开始(Shebang 之后)加上下面这一句,或者它的缩略版,能避免很多问题,更重要的是能让很多隐藏的问题暴露出来:

 

  1. set -xeuo pipefail 

下面说明每个参数的作用,以及一些例外的处理方式 :

-x :在执行每一个命令之前把经过变量展开之后的命令打印出来。

这个对于 debug 脚本、输出 Log 时非常有用。正式运行的脚本也可以不加。

-e :遇到一个命令失败(返回码非零)时,立即退出。

bash 跟其它的脚本语言最大的不同点之一,应该就是遇到异常时继续运行下一条命令。这在很多时候会遇到意想不到的问题。加上 -e ,会让 bash 在遇到一个命令失败时,立即退出。

如果有时确实需要忽略个别命令的返回码,可以用 || true 。如:

 

  1. some_cmd || true        # 即使some_cmd失败了,仍然会继续运行some_cmd || RET=$?      # 或者可以这样来收集some_cmd的返回码,供后面的逻辑判断使用 

但是在管道串起多条命令的情况下,只有最后一条命令失败时才会退出。如果想让管道中任意一条命令失败就退出,就要用后面提到的-o pipefail 了。

加-e 有时候可能会不太方便,动不动就退出。但还是应该坚持所谓的fail-fast 原则,也就是有异常时停止正常运行,而不是继续尝试运行可能存在缺陷的过程。如果有命令可以明确忽略异常,那可以用上面提到的 || true 等方式明确地忽略之。

-u :试图使用未定义的变量,就立即退出。

如果在 bash 里使用一个未定义的变量,默认是会展开成一个空串。有时这种行为会导致问题,比如:

 

  1. rm -rf $MYDIR/data 

如果 MYDIR 变量因为某种原因没有赋值,这条命令就会变成 rm -rf /data 。这就比较搞笑了。。使用 -u 可以避免这种情况。

但有时候在已经设置了-u 后,某些地方还是希望能把未定义变量展开为空串,可以这样写:

 

  1. ${SOME_VAR:-}#  bash变量展开语法,可以参考:https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html 

-o pipefail :只要管道中的一个子命令失败,整个管道命令就失败。

pipefail 与-e 结合使用的话,就可以做到管道中的一个子命令失败,就退出脚本。

1. 防止重叠运行

在一些场景中,我们通常不希望一个脚本有多个实例在同时运行。比如用 crontab 周期性运行脚本时,有时不希望上一个轮次还没运行完,下一个轮次就开始运行了。这时可以用 flock 命令来解决。flock 通过文件锁的方式来保证独占运行,并且还有一个好处是进程退出时,文件锁也会自动释放,不需要额外处理。

用法 1:假设你的入口脚本是 myscript.sh,可以新建一个脚本,通过 flock 来运行它:

 

  1. # flock --wait 超时时间   -e 锁文件   -c "要执行的命令" 
  2. # 例如: 
  3. flock  --wait 5  -e "lock_myscript"  -c "bash myscript.sh" 

用法 2:也可以在原有脚本里使用 flock。可以把文件打开为一个文件描述符,然后使用 flock 对它上锁(flock 可以接受文件描述符参数)。

 

  1. exec 123<>lock_myscript   # 把lock_myscript打开为文件描述符123 
  2. flock  --wait 5  123 || { echo 'cannot get lock, exit'; exit 1; } 

2. 意外退出时杀掉所有子进程

我们的脚本通常会启动好多子脚本和子进程,当父脚本意外退出时,子进程其实并不会退出,而是继续运行着。如果脚本是周期性运行的,有可能发生一些意想不到的问题。

在 stackoverflow 上找到的一个方法,原理就是利用 trap 命令在脚本退出时 kill 掉它整个进程组。把下面的代码加在脚本开头区,实测管用:

 

  1. trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT 

不过如果父进程是用 SIGKILL (kill -9) 杀掉的,就不行了。因为 SIGKILL 时,进程是没有机会运行任何代码的。

3. timeout 限制运行时间

有时候需要对命令设置一个超时时间。这时可以使用 timeout 命令,用法很简单:

 

  1. timeout 600s  some_command arg1 arg2 

命令在超时时间内运行结束时,返回码为 0,否则会返回一个非零返回码。

timeout 在超时时默认会发送 TERM 信号,也可以用 -s 参数让它发送其它信号。

4. 连续管道时,考虑使用 tee 将中间结果落盘,以便查问题

有时候我们会用到把好多条命令用管道串在一起的情况。如 cmd1 | cmd2 | cmd3 | ...这样会让问题变得难以排查,因为中间数据我们都看不到。

如果改成这样的格式:

 

  1. cmd1 > out1.dat 
  2. cat out1 | cmd2 > out2.dat 
  3. cat out2 | cmd3 > out3.dat 

性能又不太好,因为这样 cmd1, cmd2, cmd3 是串行运行的,这时可以用 tee 命令:

 

  1. cmd1 | tee out1.dat | cmd2 | tee out2.dat | cmd3 > out3.dat 

 

 

责任编辑:武晓燕 来源: 腾讯技术工程
相关推荐

2020-04-14 09:22:47

bash脚本技巧

2023-02-10 09:46:04

bash脚本变量

2017-06-19 15:46:08

LinuxBash脚本技巧

2017-08-15 11:32:21

LinuxBash脚本技巧

2019-12-12 10:23:34

Linux 代码 开发

2011-06-01 16:50:21

JAVA

2012-05-21 10:13:05

XCode调试技巧

2013-03-29 13:17:53

XCode调试技巧iOS开发

2011-10-26 20:55:43

ssh 安全

2011-07-12 09:47:53

WebService

2011-05-23 18:06:24

站内优化SEO

2021-10-12 23:10:58

UnsafeJavaJDK

2020-09-21 06:58:56

TS 代码建议

2022-04-14 10:22:44

故事卡业务

2022-12-02 14:58:27

JavaScript技巧编程

2017-04-13 10:51:17

Bash建议

2009-11-26 10:32:57

PHP代码优化

2022-05-30 10:31:34

Bash脚本Linux

2022-02-17 13:58:38

Linux技巧文件

2017-05-10 15:30:30

skynet崩溃程序
点赞
收藏

51CTO技术栈公众号