Published on

Shell 编程最佳实践

Authors

每一个学习 bash shell 编程人都领略过 bash 各种诡异的语法。通常写 shell 脚本都是为了快递完成一些系统管理方面的任务,这往往导致写出了很多难以维护的东西。但是如果注意总结,形成一定的规范,也可以把shell脚本写的非常优雅。这是我使用 bash 编程一段时间的总结。

关于数组使用的陷阱

关于数组的其中一个陷阱。数组的索引可以从 0 开始索引,也可以从任意整数位置开始索引。所以如果按照C语言访问数组的方式,可能会出现部分元素无法访问的情况。另外,bash shell 只支持一维数组。

首先,运行下面的代码:

array_words=("This" "is" "a" "array" "test")
declare -r array_length=${#array_words[@]}
for (( i=0; i<${array_length}; i++))
do
    echo ${array_words[$i]}
done

输出结果:

This
is
a
array
test

此时是可以正常输出的。

如果我们声明一个不连续的数组:

city[0]=Nanjing
city[1]=Bejing
city[3]=Tianjin
declare -r array_length=${#city[@]}
for (( i=0; i<${array_length}; i++))
do
    echo ${city[$i]}
done

来看一下执行输出的结果:

Nanjing
Bejing

可以看到,city[3] 并没有输出,这是 bash 数组输出的一个陷阱。

如何保证,任何时候都能遍历到数组的每一个元素呢?可以用下面的这种遍历方法:

for item in ${city[@]}
do
    echo ${item}
done

${city[@]} 的作用是取出数组中的每一个元素,生成列表。

更好地使用全局变量、局部变量

默认 bash 里定义的变量是全局的。有一条放之四海皆准的规范,尽量避免使用全局变量。除非是些全局用的常量,定义这种变量时,用 readonly 对变量进行修饰。在函数内部使用局部变量,用 local 修饰变量。为什么要显式地去声明一个变量是局部变量?注意一个天大的误会,不像 C 语言,在一个函数里的变量就是局部变量,在 bash 里,函数里的变量如果没有用 local 修饰,那么它是一个全局变量!或许这是认为 bash 比较怪异的一个原因。如果要在一个函数内定义一个只读的全局变量,使用 declare -r var_name 或者 local -r var_name,在函数内通过 declarelocal 声明的变量都是局部变量,-r 参数使变量只读。declarelocal 作用非常类似,不过 local 只能在函数内使用,declare 在函数内外都可以使用。在函数内用 declare 声明的是局部变量,在函数外使用 declare 的变量自然是全局变量。

关于编码的风格

在 bash 里这样定义和调用函数

func_name() {
    echo "do something"
}
func_name

在函数中 1,1, 2 等等就是第一个参数,第二个参数等。

其它的小tips:

  • 一个脚本要有一个 main 函数,直接写 main 进行调用,或者 main "$@" 把命令行参数传给它
  • 使用 $( ... ) 代替丑恶的反引号 ``
  • 大多数情况下都使用双引号,除非有强有力的原因
  • 简单的条件判断不需要用 if,用 &&|| 即可
  • then, do 等等,放到 if, for 等关键字的同一行
  • 使用 [[ ... ]] 而不是使用 [ ... ] 或者 test 进行判断
  • 变量一般用小写,因为系统变量一般使用大写
  • shell 中使用 $name 之类的语法是,最好加上双引号,"$name",否则,有时候会出现意想不到的错误
  • expr 命令中运算符要左右加空格,不然会被认为是字符串 。例如 expr 1 + 1,而不能写成expr 1+1,这样会输出1+1
  • (没有写完,随时补充)

其它的小细节

及早退出

脚本的开头,#!/bin/bash 语句之后,加上这几行

set -o errexit
set -o pipefail

第一行语句的作用是,在脚本执行过程中,如果有错误,就退出脚本,不再继续下去。如果一条语句执行完,返回值不是 0,就是错误。

第二行语句的作用是,如果脚本中有一行命令是由一个或多个管道连起来的多个命令,如果其中有一条或者多条命令出现错误(返回非 0 值),这一整行命令返回的结果就是最后那条返回失败结果的命令的返回值。

没有必要使用cat命令

例如:

$ cat /etc/passwd | grep root

正确的方法应该是:

$ grep root /etc/passwd

更多:

grep -C 5 foo file  # 显示file文件里匹配foo字串那行以及上下5行
grep -B 5 foo file  # 显示foo及前5行
grep -A 5 foo file  # 显示foo及后5行

尽量避免臃肿的命令

尝试去从一个大的文件中筛选某条信息。接下来可能写一大堆命令来实现这一功能。可是,尽管你将得到正确的结果,你写的命令却不够好,且晦涩难懂。因此,我们应该尽量避免这种情况发生。下面这个例子就是代码优化的好例子。

下面的命令不好:

$ grep 502 /etc/passwd | cut -d: -f1

这条命令也不够好:

$ grep 502 /etc/passwd | awk -F":" '{print $1}'

这才是一条好的命令:

$ awk -F":" '$3==502{print $1}' /etc/passwd

正如以上示例,用一条简单的awk命令就可以完成检索任务。