如何编写健壮的Bash脚本(经验分享)

shell脚本在运行异常时会受到非常大的影响。

本文介绍一些让bash脚本变得健壮的技术。

使用set -u

因为没有对变量初始化而使脚本崩溃过多少次?对于我来说,很多次。
chroot=$1
...
rm -rf $chroot/usr/share/doc
如果上面的代码没有给参数就运行,不会仅仅删除掉chroot中的文档,而是将系统的所有文档都删除。那应该做些什么呢?好在bash提供了set -u,当使用未初始化的变量时,让bash自动退出。

也可以使用可读性更强一点的set -o nounset。

代码如下:

david% bash /tmp/shrink-chroot.sh
$chroot=
david% bash -u /tmp/shrink-chroot.sh
/tmp/shrink-chroot.sh: line 3: $1: unbound variable
david%

使用set -e

写的每一个脚本的开始都应该包含set -e。这告诉bash一但有任何一个语句返回非真的值,则退出bash。使用-e的好处是避免错误滚雪球般的变成严重错误,能尽早的捕获错误。更加可读的版本:set -o errexit

使用-e把从检查错误中解放出来。如果忘记了检查,bash会替做这件事。不过也没有办法使用$?来获取命令执行状态了,因为bash无法获得任何非0的返回值。可以使用另一种结构:

command

if [ "$?"-ne 0]; then echo "command failed"; exit 1; fi

可以替换成:

command || { echo "command failed"; exit 1; }

或者使用:

if ! command; then echo "command failed"; exit 1; fi

如果必须使用返回非0值的命令,或者对返回值并不感兴趣呢?可以使用 command || true ,或者有一段很长的代码,可以暂时关闭错误检查功能,不过我建议谨慎使用。

set +e

command1

command2

set -e

相关文档指出,bash默认返回管道中最后一个命令的值,也许是不想要的那个。比如执行 false | true 将会被认为命令成功执行。如果想让这样的命令被认为是执行失败,可以使用 set -o pipefail

程序防御 - 考虑意料之外的事

的脚本也许会被放到“意外”的账户下运行,像缺少文件或者目录没有被创建等情况。可以做一些预防这些错误事情。比如,当创建一个目录后,如果父目录不存在,mkdir 命令会返回一个错误。如果创建目录时给mkdir命令加上-p选项,它会在创建需要的目录前,把需要的父目录创建出来。另一个例子是rm 命令。如果要删除一个不存在的文件,它会“吐槽”并且的脚本会停止工作。(因为使用了-e选项,对吧?)可以使用-f选项来解决这个问题,在文件不存在的时候让脚本继续工作。

准备好处理文件名中的空格

有些人从在文件名或者命令行参数中使用空格,需要在编写脚本时时刻记得这件事。需要时刻记得用引号包围变量。

if [ $filename = "foo" ];

当$filename变量包含空格时就会挂掉。可以这样解决:

if [ "$filename" = "foo" ];

使用$@变量时,也需要使用引号,因为空格隔开的两个参数会被解释成两个独立的部分。

代码如下:

david% foo() { for i in $@; do echo $i; done }; foo bar "baz quux"
bar
baz
quux
david% foo() { for i in "$@"; do echo $i; done }; foo bar "baz quux"
bar
baz quux

我没有想到任何不能使用"$@"的时候,所以当有疑问的时候,使用引号就没有错误。

如果同时使用find和xargs,应该使用 -print0 来让字符分割文件名,而不是换行符分割。

代码如下:

david% touch "foo bar"
david% find | xargs ls
ls: ./foo: No such file or directory
ls: bar: No such file or directory
david% find -print0 | xargs -0 ls
./foo bar

设置的陷阱

当编写的脚本挂掉后,文件系统处于未知状态。比如锁文件状态、临时文件状态或者更新了一个文件后在更新下一个文件前挂掉。如果能解决这些问题,无论是 删除锁文件,又或者在脚本遇到问题时回滚到已知状态,都是非常棒的。幸运的是,bash提供了一种方法,当bash接收到一个UNIX信号时,运行一个 命令或者一个函数。可以使用trap命令。

trap command signal [signal ...]

可以链接多个信号(列表可以使用kill -l获得),但是为了清理残局,我们只使用其中的三个:INT,TERM和EXIT。可以使用-as来让traps恢复到初始状态。

信号描述
INT
Interrupt - 当有人使用Ctrl-C终止脚本时被触发

TERM
Terminate - 当有人使用kill杀死脚本进程时被触发

EXIT
Exit - 这是一个伪信号,当脚本正常退出或者set -e后因为出错而退出时被触发

当使用锁文件时,可以这样写:

代码如下:

if [ ! -e $lockfile ]; then
touch $lockfile
critical-section
rm $lockfile
else
echo "critical-section is already running"
fi

当最重要的部分(critical-section)正在运行时,如果杀死了脚本进程,会发生什么呢?
锁文件会被扔在那,而且的脚本在它被删除以前再也不会运行了。

解决方法:

代码如下:

if [ ! -e $lockfile ]; then
trap " rm -f $lockfile; exit" INT TERM EXIT
touch $lockfile
critical-section
rm $lockfile
trap - INT TERM EXIT
else
echo "critical-section is already running"
fi

现在当杀死进程时,锁文件一同被删除。注意在trap命令中明确地退出了脚本,否则脚本会继续执行trap后面的命令。

竟态条件 (wikipedia)

在上面锁文件的例子中,有一个竟态条件是不得不指出的,它存在于判断锁文件和创建锁文件之间。一个可行的解决方法是使用IO重定向和bash的noclobber(wikipedia)模式,重定向到不存在的文件。

可以这么做:

代码如下:

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null;
then
trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
critical-section
rm -f "$lockfile"
trap - INT TERM EXIT
else
echo "Failed to acquire lockfile: $lockfile"
echo "held by $(cat $lockfile)"
fi

更复杂一点儿的问题是要更新一大堆文件,当它们更新过程中出现问题时,是否能让脚本挂得更加优雅一些。想确认那些正确更新了,哪些根本没有变化。比如需要一个添加用户的脚本。

代码如下:

add_to_passwd $user
cp -a /etc/skel /home/$user
chown $user /home/$user -R

当磁盘空间不足或者进程中途被杀死,这个脚本就会出现问题。在这种情况下,也许希望用户账户不存在,而且他的文件也应该被删除。

代码如下:

rollback() {
del_from_passwd $user
if [ -e /home/$user ]; then
rm -rf /home/$user
fi
exit
}

trap rollback INT TERM EXIT
add_to_passwd $user

cp -a /etc/skel /home/$user
chown $user /home/$user -R

trap - INT TERM EXIT

在脚本最后需要使用trap关闭rollback调用,否则当脚本正常退出的时候rollback将会被调用,那么脚本等于什么都没做。

保持原子化

又是需要一次更新目录中的一大堆文件,比如需要将URL重写到另一个网站的域名。
也许会写:

代码如下:

for file in $(find /var/www -type f -name "*.html"); do
perl -pi -e 's/www.example.net/www.example.com/' $file
done

如果修改到一半是脚本出现问题,一部分使用www.example.com,而另一部分使用www.example.net。可以使用备份和trap解决,但在升级过程中的网站URL是不一致的。

解决方法:

将这个改变做成一个原子操作。先对数据做一个副本,在副本中更新URL,再用副本替换掉现在工作的版本。
需要确认副本和工作版本目录在同一个磁盘分区上,这样就可以利用Linux系统的优势,它移动目录仅仅是更新目录指向的inode节点。

代码如下:

cp -a /var/www /var/www-tmp
for file in $(find /var/www-tmp -type -f -name "*.html"); do
perl -pi -e 's/www.example.net/www.example.com/' $file
done
mv /var/www /var/www-old
mv /var/www-tmp /var/www

这意味着如果更新过程出问题,线上系统不会受影响。线上系统受影响的时间降低为两次mv操作的时间,这个时间非常短,因为文件系统仅更新inode而不用真正的复制所有的数据。

缺点:

需要两倍的磁盘空间,而且那些长时间打开文件的进程需要比较长的时间才能升级到新文件版本,建议更新完成后重新启动这些进程。
对于 apache服务器来说这不是问题,因为它每次都重新打开文件。
可以使用lsof命令查看当前正打开的文件。优势是有了一个先前的备份,当需要还原 时,它就派上用场了。

(0)

相关推荐

  • 如何编写健壮的Bash脚本(经验分享)

    shell脚本在运行异常时会受到非常大的影响. 本文介绍一些让bash脚本变得健壮的技术. 使用set -u 因为没有对变量初始化而使脚本崩溃过多少次?对于我来说,很多次.chroot=$1...rm -rf $chroot/usr/share/doc如果上面的代码没有给参数就运行,不会仅仅删除掉chroot中的文档,而是将系统的所有文档都删除.那应该做些什么呢?好在bash提供了set -u,当使用未初始化的变量时,让bash自动退出. 也可以使用可读性更强一点的set -o nounset.

  • nodejs编写bash脚本的终极方案分享

    目录 前言 zx库 $`command` cd() fetch() question() sleep() nothrow() chalk fs os $.shell $.quote 传递环境变量 传递数组 总结 前言 最近在学习bash脚本语法,但是如果对bash语法不是熟手的话,感觉非常容易出错,比如说:显示未定义的变量shell中变量没有定义,仍然是可以使用的,但是它的结果可能不是你所预期的.举个例子: #!/bin/bash # 这里是判断变量var是否等于字符串abc,但是var这个变量

  • Jar包一键重启的Shell脚本及新服务器部署的一些经验分享

    前言 最近公司为客户重新部署了一套新环境,由我来完成了基础环境的配置,配置过程中总结了一些经验,分享给各位园友 使用 curl 命令检查网络 拿到新服务器后,首先检查服务器网络是否通畅.我们常用的 ping 命令使用的是 ICMP 协议,大部分服务器都设置了域名出入站规则,即使某些地址可以 ping 通,也存在服务器无法访问的情况.这时可以使用 curl host:port 命令来测试该服务器能否正常发送 http 请求到外部服务器 安装 JDK 新服务器一般没有 JDK ,可以使用 java

  • php-fpm优化总结经验分享

    目录 Nginx 与 php-fpm 运行流程 Nginx 与 php-fpm 通信机制 php-fpm 进程管理 php-fpm 优化 php.ini 优化 php-fpm.conf 优化 如何避免程序 hang 死 Nginx 与 php-fpm 运行流程 Nginx 查看 nginx.conf 配置文件 加载 nginx 的 fast-cgi 模块 php-fpm 监听 127.0.0.1:9000 php-fpm 接收到请求,启用 worker 进程处理请求 php-fpm 处理完请求,

  • Java异常区分和处理的一些经验分享

    异常处理的一些经验总结 这篇文章主要是对Java异常选择和使用中的一些误区的总结和归纳,希望各位读者能够熟练掌握异常处理的一些注意点和原则.只有处理好了异常,才能提升开发人员的基本素养,提高系统的健壮性,提升用户体验,提高产品的价值.废话少说,直接看: 误区一.异常的选择 这张图描述了异常的结构,其实我们都知道异常分检测异常和非检测异常,但是在实际中又混淆了这两种异常的应用.由于非检测异常使用方便,很多开发人员就认为检测异常没什么用处.其实异常的应用情景可以概括为以下: 1.调用代码不能继续执行

  • linux Bash脚本判别使用者的身份方法示例

    经常要在bash脚本里面或者直接对脚本本身加上sudo运行命令,但是这引发了一系列的问题. 比如用sudo的时候,脚本里的~或$HOME指代用户文件夹的这个变量,到底是应该指向我真正的用户文件夹如/home/pi呢,还是指向了超级管理员的用户文件夹/root/呢? 实际上它指向了/root/文件夹,这是我们绝对不想要的.但是很多命令如安装个程序,都不得不用sudo,那怎么办? 首先要说下经验:命令行的权限执行,从表现上来看,可以分为以下5种情况: admin-manual: 普通用户手敲命令 s

  • 如何利用Bash脚本监控Linux的内存使用情况

    前言 目前市场上有许多开源监控工具可用于监控 Linux 系统的性能.当系统达到指定的阈值限制时,它可以发送电子邮件警报.它可以监视 CPU 利用率.内存利用率.交换利用率.磁盘空间利用率等所有内容. 如果你只有很少的系统并且想要监视它们,那么编写一个小的 shell 脚本可以使你的任务变得非常简单. 在本教程中,我们添加了两个 shell 脚本来监视 Linux 系统上的内存利用率.当系统达到给定阈值时,它将给特定电子邮件地址发邮件. 方法-1:用 Linux Bash 脚本监视内存利用率并发

  • sql server编写archive通用模板脚本实现自动分批删除数据

    博主做过比较多项目的archive脚本编写,对于这种删除数据的脚本开发,肯定是一开始的话用最简单的一个delete语句,然后由于部分表数据量比较大啊,索引比较多啊,会发现删除数据很慢而且影响系统的正常使用.然后就对delete语句进行按均匀数据量分批delete的改写,这样的话,原来的删除一个表用一个语句,就可能变成几十行,如果archive的表有十几个甚至几十个,那我们的脚本篇幅就非常大了,增加了开发和维护的成本,不利于经验比较少的新入职同事去开发archive脚本,也容易把注意力分散到所谓分

  • IDEA中编写并运行shell脚本的实现

    IEDA中的bashsupport插件支持在IDEA中编写shell脚本文件,有友好的代码格式,支持自动补全,检查错误,并且配置完之后,还可以在IEDA中直接运行shell脚本.下面将一步一步演示插件的安装和配置. 打开IEDA,安装bashsupport插件  安装完之后,保持插件选中并切实enable的状态,如下图所示,然后重启IDEA. 安装git软件. https://www.git-scm.com/download/ 基本上直接安装全部默认就可以,不过要记住自己的安装目录. IDEA配

  • MySQL从库维护经验分享

    前言: MySQL 主从架构应该是最常用的一组架构了.从库会实时同步主库传输来的数据,一般从库可以作为备用节点或作查询使用.其实不只是主库需要多关注,从库有时候也要经常维护,本篇文章将会分享几点从库维护经验,一起来学习吧. 1.主从复制建议采用 GTID 模式 GTID 即全局事务 ID(Global Transaction ID),GTID 实际上是由 server_uuid:transaction_id 组成的.其中 server_uuid 是一个 MySQL 实例的唯一标识, transa

随机推荐