编写更好 Bash 脚本的 8 个建议

简介:

在我最开始管理Linux和Unix服务器时,经常遇到其他管理员编写的一大堆临时脚本。时常会因为其中某个脚本突然停止工作而进行故障排查。有时这些脚本编写得规范好理解,其他时候则是杂乱且令人困惑。

虽然排查编写糟糕的脚本很麻烦,但我从中吸取到了教训。即使你认为该脚本只会在今天使用,最好也抱着两年后还将有人去排查的态度编写脚本。因为总会有人查看,甚至很可能是你自己。

在本篇文章中,我想介绍一些优化脚本的建议,不是为了方便你编写脚本,而是方便想要弄清脚本为何不工作的人。

释伴shebang行开头

Shell脚本编写的第一条规则是以释伴shebang行开头。虽然听起来很好笑,但释伴shebang行却很重要,它告诉系统使用哪种二进制作为脚本的解释器。没有释伴shebang行,系统就不知道使用哪种语言解释执行脚本。

一个典型的bash 以释伴shebang行如下所示:


 
 
  1. #!/bin/bash

与本文中其他建议不同,这不仅仅是一条建议,而是一条规定。shell脚本必须以解释器行开始;没有这行,你的脚本最终将不能工作。我发现很多脚本没有这一行,有人认为没有这行脚本就不能工作,但事实并非如此。如果没有指定脚本解释器,有些系统会默认使用/bin/sh目录下的解释器。如果是bourne shell脚本,默认/bin/sh路径没有问题,如果是KSH或者使用特定bash脚本而不是bourne,该脚本可能产生无法预料的结果。

添加脚本描述头

当编写脚本或者其他程序时,我总会在脚本开头描述脚本的用途,同时添加我的名字。如果这些脚本是在工作中编写,我还会加上工作邮箱以及脚本编写日期。

下面是一个有脚本头的例子:


 
 
  1. #!/bin/bash
  2. ### Description: Adds users based on provided CSV file
  3. ### CSV file must use : as separator
  4. ### uid:username:comment:group:addgroups:/home/dir:/usr/shell:passwdage:password
  5. ### Written by: Benjamin Cane - ben@example.com on 03-2012

为什么要添加这些内容?很简单。这里的描述是为了向阅读该脚本的人解释脚本用途并提供他们需要了解的其他信息。添加名字和邮箱,阅读该脚本的人如果有疑问就可以联系上我并提问。添加日期,当他们阅读脚本时,至少知道该脚本是多久之前编写的。日期还能触动你的怀旧之情,当发现自己很久前编写的脚本时,你会问问自己“在编写该脚本时,我是怎么想的?”。

脚本中的描述头可以根据自己的想法随意定制,没有硬性规定哪些是必须的,哪些不需要。通常只要保证信息有效并且放置在脚本开头即可。

缩进代码

代码可读性非常重要,但很多人都会忽略这一点。在深入了解缩进为何很重要前,我们来看一个例子:


 
 
  1. NEW_UID=$(echo $x | cut -d: -f1)
  2. NEW_USER=$(echo $x | cut -d: -f2)
  3. NEW_COMMENT=$(echo $x | cut -d: -f3)
  4. NEW_GROUP=$(echo $x | cut -d: -f4)
  5. NEW_ADDGROUP=$(echo $x | cut -d: -f5)
  6. NEW_HOMEDIR=$(echo $x | cut -d: -f6)
  7. NEW_SHELL=$(echo $x | cut -d: -f7)
  8. NEW_CHAGE=$(echo $x | cut -d: -f8)
  9. NEW_PASS=$(echo $x | cut -d: -f9)
  10. PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
  11. if [ $PASSCHK -ge 1 ]
  12. then
  13. echo "UID: $NEW_UID seems to exist check /etc/passwd"
  14. else
  15. useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
  16. if [ ! -z $NEW_PASS ]
  17. then
  18. echo $NEW_PASS | passwd --stdin $NEW_USER
  19. chage -M $NEW_CHAGE $NEW_USER
  20. chage -d 0 $NEW_USER
  21. fi
  22. fi

上述代码能工作吗?是的,但这段代码写的并不好,如果这是一个500行bash脚本,没有任何缩进,那么理解该脚本的用途将非常困难。下面看一下使用缩进后的同一段代码:


 
 
  1. NEW_UID=$(echo $x | cut -d: -f1)
  2. NEW_USER=$(echo $x | cut -d: -f2)
  3. NEW_COMMENT=$(echo $x | cut -d: -f3)
  4. NEW_GROUP=$(echo $x | cut -d: -f4)
  5. NEW_ADDGROUP=$(echo $x | cut -d: -f5)
  6. NEW_HOMEDIR=$(echo $x | cut -d: -f6)
  7. NEW_SHELL=$(echo $x | cut -d: -f7)
  8. NEW_CHAGE=$(echo $x | cut -d: -f8)
  9. NEW_PASS=$(echo $x | cut -d: -f9)
  10. PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
  11. if [ $PASSCHK -ge 1 ]
  12. then
  13. echo "UID: $NEW_UID seems to exist check /etc/passwd"
  14. else
  15. useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
  16. if [ ! -z $NEW_PASS ]
  17. then
  18. echo $NEW_PASS | passwd --stdin $NEW_USER
  19. chage -M $NEW_CHAGE $NEW_USER
  20. chage -d 0 $NEW_USER
  21. fi
  22. fi

缩进后,很明显第二个if语句内嵌在第一个if语句内,但如果看未缩进的代码,第一眼肯定发现不了。

缩进方式取决于你自己,是使用两个空格、四个空格,还是就使用一个制表符,这都不重要。重要的是代码每次以相同的方式一致缩进。

增加间距

缩进可以增加代码的可理解性,而间距可以增加代码的可读性。通常,我喜欢根据代码的用途来间隔代码,这是个人偏好,其意义在于使代码更加可读并易于理解。

下面是上述代码添加行间距后的例子:


 
 
  1. NEW_UID=$(echo $x | cut -d: -f1)
  2. NEW_USER=$(echo $x | cut -d: -f2)
  3. NEW_COMMENT=$(echo $x | cut -d: -f3)
  4. NEW_GROUP=$(echo $x | cut -d: -f4)
  5. NEW_ADDGROUP=$(echo $x | cut -d: -f5)
  6. NEW_HOMEDIR=$(echo $x | cut -d: -f6)
  7. NEW_SHELL=$(echo $x | cut -d: -f7)
  8. NEW_CHAGE=$(echo $x | cut -d: -f8)
  9. NEW_PASS=$(echo $x | cut -d: -f9)
  10. PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
  11. if [ $PASSCHK -ge 1 ]
  12. then
  13. echo "UID: $NEW_UID seems to exist check /etc/passwd"
  14. else
  15. useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
  16. if [ ! -z $NEW_PASS ]
  17. then
  18. echo $NEW_PASS | passwd --stdin $NEW_USER
  19. chage -M $NEW_CHAGE $NEW_USER
  20. chage -d 0 $NEW_USER
  21. fi
  22. fi

如你所见,行间距虽不易觉察,但每一处整洁都让以后的代码排错更简单。

注释代码

描述头适合于添加脚本函数描述,而代码注释适合于解释代码本身的用途。下面仍是上述相同的代码片段,但这次我将添加代码注释,解释代码的用途:


 
 
  1. ### Parse $x (the csv data) and put the individual fields into variables
  2. NEW_UID=$(echo $x | cut -d: -f1)
  3. NEW_USER=$(echo $x | cut -d: -f2)
  4. NEW_COMMENT=$(echo $x | cut -d: -f3)
  5. NEW_GROUP=$(echo $x | cut -d: -f4)
  6. NEW_ADDGROUP=$(echo $x | cut -d: -f5)
  7. NEW_HOMEDIR=$(echo $x | cut -d: -f6)
  8. NEW_SHELL=$(echo $x | cut -d: -f7)
  9. NEW_CHAGE=$(echo $x | cut -d: -f8)
  10. NEW_PASS=$(echo $x | cut -d: -f9)
  11. ### Check if the new userid already exists in /etc/passwd
  12. PASSCHK=$(grep -c ":$NEW_UID:" /etc/passwd)
  13. if [ $PASSCHK -ge 1 ]
  14. then
  15. ### If it does, skip
  16. echo "UID: $NEW_UID seems to exist check /etc/passwd"
  17. else
  18. ### If not add the user
  19. useradd -u $NEW_UID -c "$NEW_COMMENT" -md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
  20. ### Check if new_pass is empty or not
  21. if [ ! -z $NEW_PASS ]
  22. then
  23. ### If not empty set the password and pass expiry
  24. echo $NEW_PASS | passwd --stdin $NEW_USER
  25. chage -M $NEW_CHAGE $NEW_USER
  26. chage -d 0 $NEW_USER
  27. fi
  28. fi

如果你恰好要阅读这段bash代码,却又不知道这段代码的用途,至少可以通过查看注释充分掌握代码的实现目标。在代码中添加注释对其他人非常有帮助,甚至对你自己也有帮助。我曾发现在浏览自己一个月前编写的脚本时不知道脚本的用途。如果注释添加合理,可以在日后节省你和他人的很多时间。

创建描述性的变量名

描述性变量名非常直观,但我发现自己一直都使用通用变量名。通常这些都是临时变量,从不在该代码块之外使用,但即使是临时变量,解释清楚它们的含义也很有用。

下面例子中的变量名大部分是描述性的:


 
 
  1. for x in `cat $1`
  2. do
  3. NEW_UID=$(echo $x | cut -d: -f1)
  4. NEW_USER=$(echo $x | cut -d: -f2)

可能赋给$NEW_UID和$NEW_USER的值不是很明显,$1的值代表什么以及$x的取值是什么都不够清楚。更具描述性的修改代码如下:


 
 
  1. INPUT_FILE=$1
  2. for CSV_LINE in `cat $INPUT_FILE`
  3. do
  4. NEW_UID=$(echo $CSV_LINE | cut -d: -f1)
  5. NEW_USER=$(echo $CSV_LINE | cut -d: -f2)

从这段重写的代码块中,很容易看出我们是在读取一个输入文件,该文件名是一个CSV文件。同时很容易看出我们从什么地方获取新的UID和新的USER信息来存储在$NEW_UID和$NEW_USER变量中。

上面的例子看上去有点大材小用,但日后会有人感谢你花费额外时间让变量更具描述性。

使用 $(command) 进行命令替换

如果你想创建一个变量,其值是其他指令的输出,在bash中有两种方式实现。第一种是将命令封装在反引号中,如下所示:


 
 
  1. DATE=`date +%F`

第二种是使用一个不同的语法:


 
 
  1. DATE=$(date +%F)

虽然两者都正确,但我个人更喜欢第二种方法。这纯粹是个人偏好,但我通常认为$(command)句法比使用反引号更加明显。假如你在挖掘上百行的bash代码;你会发现随着自己不断阅读,那些反引号有时看起来像是单引号。此外,有时单引号看起来像是反引号。最后,所有的建议都与偏好挂钩。所以使用最适合你的,确保与你所选择使用的方法一致。

在出错退出前描述问题

上述示例可以让代码更加易于阅读和理解,最后一条建议对在排错过程前找到错误点非常有用。在脚本中添加描述性错误信息,可以在前期节省很多排错时间。浏览下面的代码,看看如何能使它更具描述性:


 
 
  1. if [ -d $FILE_PATH ]
  2. then
  3. for FILE in $(ls $FILE_PATH/*)
  4. do
  5. echo "This is a file: $FILE"
  6. done
  7. else
  8. exit 1
  9. fi

该脚本首先检查$FILE_PATH变量的值是否是一个目录,如果不是,脚本将退出,并返回一个错误代码1。虽然使用退出代码能够告诉其他脚本该脚本未成功执行,但却没有给运行该脚本的人做出解释。

我们让代码变得更加友好些:


 
 
  1. if [ -d $FILE_PATH ]
  2. then
  3. for FILE in $(ls $FILE_PATH/*)
  4. do
  5. echo "This is a file: $FILE"
  6. done
  7. else
  8. echo "exiting... provided file path does not exist or is not a directory"
  9. exit 1
  10. fi

如果运行第一个代码片段,你将得到大量输出。如果你得不到输出,你将不得不打开脚本文件查看哪些地方可能出错。但如果你运行第二个代码片段,你立刻就能知道是在脚本指定了无效路径。仅添加一行代码就省去了以后大量的排错时间。

上述例子仅仅是我在编程时尝试使用的技巧。我相信编写整洁可读的bash脚本还有其他很多好建议,如果你有任何建议,随时在评论区回复。很高兴能看到其他人提出来的技巧。


本文来自云栖社区合作伙伴“Linux中国”,原文发布日期:2015-10-17

目录
相关文章
|
6月前
|
监控 安全 Shell
防止员工泄密的措施:在Linux环境下使用Bash脚本实现日志监控
在Linux环境下,为防止员工泄密,本文提出使用Bash脚本进行日志监控。脚本会定期检查系统日志文件,搜索敏感关键词(如"password"、"confidential"、"secret"),并将匹配项记录到临时日志文件。当检测到可疑活动时,脚本通过curl自动将数据POST到公司内部网站进行分析处理,增强信息安全防护。
175 0
|
22天前
|
Devops 关系型数据库 大数据
1000个开源免费的bash脚本合集
【10月更文挑战第4天】
|
6月前
|
存储 Shell Linux
Linux Bash 脚本中的 IFS 是什么?
【4月更文挑战第25天】
123 0
Linux Bash 脚本中的 IFS 是什么?
|
3月前
|
Shell
一个能够生成 Markdown 表格的 Bash 脚本
【8月更文挑战第20天】这是一个使用Bash脚本生成Markdown表格的示例。脚本首先设置表头与内容数据,然后输出Markdown格式的表格。用户可以根据需要自定义表格内容。使用时,只需将脚本保存为文件(如 `generate_table.sh`),赋予执行权限,并运行它,即可在终端看到生成的Markdown表格。
|
3月前
|
Unix Shell Linux
在Linux中,什么是Bash脚本,并且如何使用它。
在Linux中,什么是Bash脚本,并且如何使用它。
|
3月前
|
Shell 开发者
深入理解Bash脚本中的函数
【8月更文挑战第20天】
47 0
|
3月前
|
存储 Shell 数据处理
深入探讨Bash脚本中的数组
【8月更文挑战第20天】
26 0
|
3月前
|
存储 Shell
Bash 脚本中的 `hash` 命令
【8月更文挑战第19天】
27 0
|
5月前
|
Unix Shell Linux
技术经验分享:Bash脚本命令使用详解
技术经验分享:Bash脚本命令使用详解
39 0
|
6月前
|
存储 弹性计算 运维
用bash脚本创建目录
【4月更文挑战第29天】
48 3
下一篇
无影云桌面