如何编写优雅易读的Shell脚本:规范与技巧全解析

前言

如何编写优雅易读的Shell脚本:规范与技巧全解析

因为工作有需求,最近又开始整理 shell 脚本了。大部分命令自己平常经常会用到,然而写成脚本时总感觉写得很不美观。并且当我看其他人写的脚本时,总觉得难以读懂。要知道 shell 脚本这种东西不算正式的编程语言,它更像是一个工具,用于把不同的程序混合在一起供我们调用。

很多人在写作时想到哪写到哪,基本就像一段极长的 main 函数,实在难以直视。并且,因为历史缘故,shell 存在很多不同的版本,同时还有很多功能相同的命令供我们选择或舍弃,这导致代码的规范难以达成统一。

因为上述这些原因,我查看了一些与之相关的文档,得知这些问题实际上很多人都曾思考过,并且还形成了一些较为优秀的文章,不过依然显得有些零散。所以我就在此地对这些文章进行了适当的整理,将其作为日后我自己撰写脚本的技术规范。

代码风格规范

开头有“蛇棒”

所谓 shebang 指的是在许多脚本的第一行会出现的以#!开头的注释。它能够指明,在我们没有指定解释器时所使用的默认解释器,通常可能是如下这样的情况:

#!/bin/bash

当然,有很多种解释器。除了 bash 之外,我们能够通过以下命令来查看本机所支持的解释器。

$ cat /etc/shells
/etc/shells 表示有效的登录 shell 。
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
/usr/bin/screen

当直接使用./a.sh 去执行这个脚本时,若没有 shebang,那么它会默认使用 $SHELL 所指定的解释器;若有 shebang,就会用 shebang 指定的解释器。

这种方式是我们推荐的使用方式。

代码有注释

注释是一个常识,这里需要再次强调,在 shell 脚本中这一点尤为重要。因为很多单行的 shell 命令不是很容易理解,没有注释的话在维护时会让人很头疼。

注释的意义在于告诉我们注意事项,同时也在于解释用途,它就像是一个 README。

具体的来说,对于shell脚本,注释一般包括下面几个部分:

参数要规范

这一点很重要。当脚本需要接受参数时,我们要先进行参数是否合乎规范的判断,并且要给出合适的回显,以方便使用者了解参数的使用。

最少,最少,我们至少得判断下参数的个数吧:

if [[ $# != 2 ]];then
    echo "Parameter incorrect."
    exit 1
fi

变量和魔数

通常我们会在开头定义一些重要的环境变量,以确保这些变量的存在。

source /etc/profile
export PATH=”/usr/local/bin:/usr/bin:/bin:/usr/local/apps/bin 路径存在

这种定义方式有一个较为常见的用途。其中最典型的应用情形是,倘若我们本地安装了众多的 java 版本,那么我们或许就需要指定某一个 java 来使用。在这种情况下,我们会在脚本的开头重新对 JAVA_HOME 以及 PATH 变量进行定义,以实现对相关情况的控制。并且,一段优秀的代码通常不会在代码里包含很多硬性编码的“魔数”。如果一定要有这种情况,通常会在开头以变量的形式进行定义,接着在调用的时候直接调用这个变量,如此一来便于日后进行修改。

缩进有规矩

对于 shell 脚本而言,缩进是一个较为突出的问题。许多需要进行缩进的地方,像 if 语句、for 语句等,其长度并不长。正因如此,很多人都懒得去进行缩进操作。并且,还有很多人不习惯使用函数,这就使得缩进功能在一定程度上被弱化了。

正确的缩进很重要。尤其是在写函数时。如果不注意缩进,我们在阅读时很容易把函数体与直接执行的命令弄混。

常见的缩进方法主要有两种,一种是“soft tab”,另一种是“hard tab”。

命名有标准

所谓命名规范,基本包含下面这几点:

编码要统一

写脚本时尽量采用 UTF-8 编码,这样可以支持中文等一些特殊字符。虽然能够书写中文,但在写注释和打 log 时还是尽量使用英文,因为很多机器并未直接支持中文,若使用中文可能会出现乱码。这里需要特别注意的一点是,若我们在 windows 环境下使用 utf-8 编码来编写 shell 脚本,那就必须留意这个 utf-8 是否带有 BOM。通常情况下,windows 会通过在文件开头添加三个 EF BB BF 字节来判断 utf-8 格式,然而在 Linux 中默认是没有 BOM 的。如果我们在 Windows 下写脚本,那么一定要注意把编码改成 Utf-8 无 BOM。一般用 notepad++之类的编辑器可以进行修改。不然的话,在 Linux 下运行时就会识别到开头的三个字符,进而会报一些无法识别命令的错误。当然,对于跨平台写脚本,还有一个比较常见的问题,那就是换行符不同。windows 默认的是回车符(\r),而 unix 下是换行符(\n)。不过存在两个小工具,能够非常便捷地解决这个问题,分别是 dos2unix 和 unix2dos。

权限记得加

这一点虽小,然而我个人时常会将其忘记。若未添加执行权限,便无法直接进行执行,着实令人讨厌。

日志和回显

日志的重要性显而易见。它可以方便我们回头进行纠错。在大型项目中,它具有非常重要的作用。

如果这个脚本是供用户直接在命令行使用的,那么我们最好能够在执行时实时回显执行过程,这样方便用户掌控。

有时为提升用户体验,我们会在回显里添加诸如颜色、闪烁等一些特效,具体可参考 ANSI/VT100 Control sequences 这篇文章的介绍。

密码要移除

不要在脚本中直接写入密码,不要将密码以明文形式存放在脚本中,不要把密码以固定方式嵌入到脚本里。

重要的事情要多说几遍,尤其是当脚本被托管在类似 Github 这样的平台中时。 重要的事情需重复多次,尤其是在脚本处于类似 Github 这类平台托管的情况下。 重要的事情得再三强调,特别是当脚本是托管在类似 Github 这种平台之中的时候。

太长要分行

调用某些程序时,参数可能会很长。在这种情况下,为了确保有较好的阅读体验,我们可以借助反斜杠进行分行。

./configure \
–prefix=/usr \
–sbin-path=/usr/sbin/nginx \
- 相关配置的路径是 /etc/nginx/nginx.conf 。

注意在反斜杠前有个空格。

编码细节规范

代码有效率

使用命令时需了解其具体做法,尤其是数据处理量较大时,要时刻思考该命令是否会对效率产生影响。

比如下面的两个sed命令:

sed -n '1p' file
sed -n '1p;1q' file

他们的作用相同,都是获取文件的首行。然而,第一条命令会读取整个文件,第二条命令则仅读取第一行。若文件规模较大,仅这一条命令的差异就会导致极大的效率差别。

这里只是用来举个例子,而这个例子正确的使用方式应当是运用 head -n1 file 这个命令。

勤用双引号

几乎所有大佬都建议,在使用“$”获取变量时,最好加上双引号。

在很多情况下不加上双引号会造成很大的麻烦。为什么呢?例如:

#!/bin/sh
#已知当前文件夹有一个a.sh的文件
var="*.sh"
echo $var
echo "$var"

他的运行结果如下:

a.sh
*.sh

为啥会这样呢?其实可以解释为他执行了下面的命令:

echo *.sh
echo "*.sh"

在很多情形下,当把变量当作参数时,必须留意上述这点,要仔细去感受其中的差别。上述仅仅是一个极为微小的例子,在实际应用中,因为这个细节而引发的问题实在是太多了……

巧用main函数

我们知道,像 java 和 C 这样的编译型语言都有一个函数入口。这种结构使得代码的可读性很强,我们能够清楚地知道哪些是直接执行的,哪些是函数。然而,脚本与之不同,脚本属于解释性语言,它是从第一行直接执行到最后一行的。如果在这个过程中,命令与函数混杂在一起,那么就会非常难以阅读。

朋友们都知道,用 Python 编写的脚本,若合乎标准,大体上至少是这样的:

#!/usr/bin/env python

def func1():
    pass
def func2():
    pass
if __name__=='__main__':
    func1()
    func2()

他使用了一种很精妙的方式达成了我们所习惯的 main 函数,这样使得代码的可读性变得更强了。

在shell中,我们也有类似的小技巧:

#!/usr/bin/env bash

func1(){
    #do sth
}
func2(){
    #do sth
}
main(){
    func1
    func2
}
main "$@"

我们能够运用这种写法,进而达成类似的 main 函数,让脚本的结构化程度得以提升。

考虑作用域

shell中默认的变量作用域都是全局的,比如下面的脚本:

#!/usr/bin/env bash

var=1
func(){
    var=2
}
func
echo $var

他输出的结果是 2 而非 1,这显然与我们的编码习惯不符,很容易引发一些问题。

因此,我们最好使用 local readonly 这类命令,而不是直接使用全局变量。其次,我们可以使用 declare 来声明变量。这些方式都比使用全局方式定义更优。

函数返回值

使用函数时需注意,shell 中函数的返回值仅能为整数。大概是因为通常一个函数的返回值用于表示该函数的运行状态,所以一般只需 0 或 1 就足够了,故而设计成这样。然而,如果非要传递字符串,可通过以下变通方法:

func(){
    echo "2333"
}
res=$(func)
echo "This is from $res."

这样,利用 echo 或者 print 等方式就能够达成传递一些额外参数的目的。

间接引用值

什么叫间接引用?比如下面这个场景:

VAR1="2323232"
VAR2="VAR1"

我们拥有一个变量叫做 VAR1,同时还有一个变量叫做 VAR2,VAR2 的值是 VAR1 的名字。现在我们希望借助 VAR2 去获取 VAR1 的值,那么此时该怎么做呢?

比较土鳖的方法是这样:

eval echo \$$VAR2

意思是构造了一个字符串 echo XXX,其中的 XXX 为 VAR2 的值 VAR1,接着通过 eval 进行强制解析,从而实现了变相取值。

并且,事实上我们本身就不推荐使用 eval 这个命令。

比较舒服的写法是下面这样:

echo ${!VAR1}

通过在变量名前加一个!就可以做到简单的间接引用了。

需要注意的是,通过上述方法,我们仅能实现取值这一操作,而无法实现赋值。若要实现赋值,就必须老老实实地使用 eval 来进行处理。

VAR1=VAR2
eval $VAR1=233
echo $VAR2

巧用heredocs

所谓heredocs,也可以算是一种多行输入的方法,即在”

你可能想看:
« 上一篇
下一篇 »

相关推荐

FreeBasic:强大免费开源的64位basic编译器及应用优势

2025年05月10日

82阅读

脊柱侧弯矫正方法大盘点:运动康复、支具矫正等多种方式

2025年05月09日

88阅读

如何在winForm标题栏去掉标题文字且任务栏图标正常显示

2025年05月09日

86阅读

十一黄金周促销广告词大全,国庆购物狂欢等你来

2025年05月09日

78阅读

关闭快速编辑模式及插入模式,设置控制台指定文字颜色举例

2025年05月09日

63阅读

黄金搭档低糖高钙饮品包装及成分功效全解析,适合人群要知晓

2025年05月09日

91阅读