Kris.Chi
Kris.Chi
Aug 26, 2018 · 8 min read

Best Practice of Shell

Shell 脚本,Make构建工具是 Linux/Unix 中使用率最高,也是最能提高工作效率,体现 Linux/Unix 优势的两个方面,最近一直在和 Shell 打交道,看了一些文章资料,总结了一下 Shell 脚本编写的最佳 实践, 记录一些 Shell Tips 。

Magic $

Shell脚本中的变量只能包含数字、字母、下划线,尤其不能包含美元符号$,是因为 $ 符号在Shell脚本 中有着非常重要的特殊含义。

  1. $<数字>: $1 $2 … $n 等,代表向脚本传递的参数,例如 test.sh param1 param2, 在 test.sh 中 就可以使用 $1 $2 分别指代第一个参数 param1, 第二个参数 $2。当然,$0 就代表当前脚本名。
  2. $$: 当前脚本的pid,一般用来处理多个脚本实例问题。
  3. “$” 和 “$@” : 这两种表达都指代所有向脚本传递的参数,只是在脚本中如果加了双引号,”$@” 会 将所有参数分开输出“$1”, “$2” … “$n” ,方便For-loop,而 ”$” 将所有参数作为一个整体输出 “$1 $2 … $n”
  4. $?: 代表上一个脚本的exit code. 0 代表正常退出, 非0 代表非正常退出。 如果任务被一个信号杀掉, 返回值为 128 加上信号的值. 例如: 标准kill信号值是 15, 那么返回值就是 143。
  5. $#: 代表向脚本传递的参数个

set/shopt 开关

Shell 脚本中经常能看到在开始设定 set -o, set -x 等,这是 Linux 系统脚本处理过程中的一些开关( 常用的包括禁止EOF,即Ctrl+D 退出shell脚本,禁止重定向文件被重写, 变量未定义则退出等,详细的开关设置查看这里),set -o 表示查看当前开关状态,也可以后接开关名称使用,set -o <开关选项>,用 set +o <开关选项> 关闭开关。

当然,也可直接用 set -x , set -u 等快捷方式直接操作系统开关。

shopt 和 set 是一个意思,只是有些不同版本的shell推荐使用shopt。

  1. set -x 或 set -o xtrace: 打开所有debug/echo 信息,这点在脚本调试时非常好用。
#!/bin/bash
echo "Hello $USER,"
set -x
echo "Today is $(date %Y-%m-%d)"
set +x

返回:

Hello chenhao,
++example_script.sh:4:: date +%Y-%m-%d
+example_script.sh:4:: echo 'Today is 2009-08-31'
Today is 2009-08-31
+example_script.sh:5:: set +x
  1. set -u 或 set-o nounset : 如果变量未设置,则程序直接退出,这在某些危险操作时十分关键。
#!/bin/bash
set -u
chroot=$1
...
rm -rf $chroot/usr/share/doc
...

如果上述操作没有开启 set -u , 刚好又忘了传递参数给 chroot,则脚本执行时会删掉整个 /usr/share/doc 目录, 如果开启了 set -u ,脚本在发现 chroot 变量没有设置时会自动退出。

  1. set -e 或 set -o errexit: 开启脚本中任何一条语句返回non-true则退出,这样可以防止错误扩大,导致不可设想 的后果。

当然,如果在全局开启的情况下,某些你根本不在乎错对的语句,你可以使用 set +e 关闭。

set +e
command1
command2
set -e
  1. set -o noclobber: 不允许重定向文件覆盖,即下面的操作会返回错误。
set -o noclobber
touch $exist_file
echo “some text” > $exist_file

学会使用陷阱,traps

在我刚开始工作的时候,看老外写的shell脚本,到处都是traps, 国内的脚本几乎不用这个非常实用的工具。简单来说, traps就是通过捕获一些系统信号,实现一些动作。例如收到程序Kill信号(kill -9),清理脚本产生的文件。

系统信号有很多,kill -l 可以列出所有支持的信号类型。一般来说,我们只关心几个常用信号,例如:

  1. INT (value: 2): 系统中断信号,ctrl-c触发的。
  2. TERM (value: 15): 系统终止信号,往往是通过 kill -15 触发的。
  3. KILL (value: 9): 进程被杀信号,通过 kill -9 触发。
  4. EXIT: 程序结束信号,这个分几种情况,要么是脚本走到最后一行正常退出,要么在set +e 情况下出错退出,都会触发EXIT信号。

举一个简单的例子,如果脚本在执行过程中,需要利用 Linux 的文件锁 flock, 例如:

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

如果上述脚本在critical-section中途退出,无论是人为的ctrl-c或者其他程序杀掉它。$lockfile 就会保持锁定状态(或者已创建),则下次脚本再 执行时,会永远不能执行critical-section。这时候,就需要traps来捕捉一系列信号:

注意traps需要包裹起来已确定捕捉范围

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

这样,无论程序如何退出或被杀掉,$lockfile总是会被释放,逻辑正确。

同样的,下面的例子展示了如何可靠的添加一个用户。

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

始终使用引号操作文件名和目录

很容易理解,如果路径或者文件名的变量中包含空格,则会导致错误。

if [ $filename = "foo" ];

如果$filename出现空格,脚本会出错,但是如果加上引号,则不会。

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

记得上面提到的$@, 如果加了引号,则可以用For-loop来处理所有传入参数。

foo() { for i in "$@"; do printf "%s\n" "$i"; done }

判断是否存在某个命令

这里非常tricky, 很多人会直接使用 which 或者更粗暴的查找/usr/local/bin这类目录,但是往往 可能会出现连which命令,/usr/local/bin/目录都不存在的情况,这时候,只能使用shell中非常底层 的命令来判断,例如:

** type **

type $1 >/dev/null 2>&1 || { echo >&2 "command $1 not found"; exit 1; }

** hash **

hash $1 2>/dev/null || { echo >&2 "command $1 not found"; exit 1; }

这在Docker中有很大用处,往往Docker Image 为了压缩空间,会去除which, find这类第三方工具, 用上面的脚本,可以很容易的判断命令存在与否。

保持原子性

意思就是,尽量使脚本语句为一个原子操作,出错整条语句退出,而不会继续“滚雪球”。 这里要提醒的是,command1 && command2代表command1 执行成功时才执行command2, 而 command1 ; command2,代表无论command1执行成功与否,都执行command2。

总的来说,shell脚本编写格式要求非常严格(试想下if, while语句),并且缺乏好用的编辑调试 工具(一般来说一个趁手的Vim + set -x 就足够基本调试),但是Shell脚本的Linux平台通用性和高效率,以及 丰富的第三方工具(Sed, xargs)都让Shell脚本编写在很长时间内都是熟练掌握Linux的标志。

这里放几个wiki和资料,会随时更新:

Fixing Unix/Linux/POSIX Filenames:Control Characters (such as Newline), Leading Dashes, and Other Problems

Sending signal to Processes

Setting shell options

    Kris.Chi

    Written by

    Kris.Chi

    读书笔记、工作感悟

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade