编写更好 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

时间: 2024-08-12 07:37:01

编写更好 Bash 脚本的 8 个建议的相关文章

编写快速安全Bash脚本的建议

昨天我和一些朋友聊起Bash,我意识到:即使我已经使用Bash十多年了,现在还有一些基础的杂项,我理解的并不是很清晰. 像往常一样,我认为我应该写一个博文. 我们会包含: 一些bash基础知识("你怎么写一个for循环") 杂项事宜("总是引用你的bash变量") bash脚本安全提示("总是使用set -u") 如果你编写shell脚本,并且你没有阅读这篇文章中其他任何内容,你应该知道有一个shell脚本校验工具(linter),叫做 shel

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

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

如何用bash-support插件将Vim编辑器打造成编写Bash脚本的IDE

IDE(集成开发环境)就是这样一个软件,它为了最大化程序员生产效率,提供了很多编程所需的设施和组件. IDE 将所有开发工作集中到一个程序中,使得程序员可以编写.修改.编译.部署以及调试程序. 在这篇文章中,我们会介绍如何通过使用 bash-support vim 插件将 Vim 编辑器安装和配置 为一个编写 Bash 脚本的 IDE. 什么是 bash-support.vim 插件? bash-support 是一个高度定制化的 vim 插件,它允许你插入:文件头.补全语句.注释.函数.以及代

如何用 bash-support 插件将 Vim 编辑器打造成编写 Bash 脚本的 IDE

IDE(集成开发环境)就是这样一个软件,它为了最大化程序员生产效率,提供了很多编程所需的设施和组件. IDE 将所有开发工作集中到一个程序中,使得程序员可以编写.修改.编译.部署以及调试程序. 在这篇文章中,我们会介绍如何通过使用 bash-support vim 插件将 Vim 编辑器安装和配置 为一个编写 Bash 脚本的 IDE. 什么是 bash-support.vim 插件? bash-support 是一个高度定制化的 vim 插件,它允许你插入:文件头.补全语句.注释.函数.以及代

java-linux bash脚本编写问题

问题描述 linux bash脚本编写问题 我想要实现linux后台运行jar 于是编写了脚本: exec java -Xms128m -Xmx2048m -jar /var/www/JavaWork/BidCheck.jar 5 >pid.log 这里是保存进程到文件pid.log 让进程id保存在文件,但是运行我这个jar需要参数 /workspace 完整的java执行命令如下:java -Xms128m -Xmx2048m -jar /var/www/JavaWork/BidCheck.

Linux中高效编写Bash脚本的10个技巧

Shell 脚本编程 是你在 Linux 下学习或练习编程的最简单的方式.尤其对 系统管理员要处理着自动化任务,且要开发新的简单的实用程序或工具等(这里只是仅举几例)更是必备技能. 本文中,我们将分享 10 个写出高效可靠的 bash 脚本的实用技巧,它们包括: 1. 脚本中多写注释 这是不仅可应用于 shell 脚本程序中,也可用在其他所有类型的编程中的一种推荐做法.在脚本中作注释能帮你或别人翻阅你的脚本时了解脚本的不同部分所做的工作. 对于刚入门的人来说,注释用 # 号来定义. # TecM

《UNIX/Linux 系统管理技术手册(第四版)》——2.2 bash脚本编程

2.2 bash脚本编程 UNIX/Linux 系统管理技术手册(第四版) bash特别适合编写简单的脚本,用来自动执行那些以往在命令行输入的操作.在命令行用的技巧也能用在bash的脚本里,反之亦然,这让用户在bash上投入的学习时间获得了最大的回报.不过,一旦bash脚本超过了100行,或者需要的特性bash没有,那么就要换到Perl或者Python上了. bash脚本的注释以一个井号(#)开头,并且注释一直延续到行尾.和命令行中一样,可以把逻辑上的一行分成多个物理上的多行来写,每行末尾用反斜

处理Apache日志的Bash脚本

去年一年,我写了将近100篇网络日志. 现在这一年结束了,我要统计"访问量排名",看看哪些文章最受欢迎.(隆重预告:本文结尾处将揭晓前5名.) 以往,我用的是AWStats日志分析软件.它可以生成很详细的报表,但是不太容易定制,得不到某些想要的信息.所以,我就决定自己写一个Bash脚本,统计服务器的日志,顺便温习一下脚本知识. 事实证明,这件事比我预想的难.虽然最终脚本只有20多行,但花了我整整一天,反复查看手册,确认用法和合适的参数.下面就是我的日志分析脚本,虽然它还不是通用的,但是

编写可靠shell脚本的八个建议

这八个建议,来源于键者几年来编写 shell 脚本的一些经验和教训.事实上开始写的时候还不止这几条,后来思索再三,去掉几条无关痛痒的,最后剩下八条.毫不夸张地说,每条都是精挑细选的,虽然有几点算是老生常谈了. 1. 指定bash shell 脚本的第一行,#!之后应该是什么?如果拿这个问题去问别人,不同的人的回答可能各不相同. 我见过/usr/bin/env bash,也见过/bin/bash,还有/usr/bin/bash,还有/bin/sh,还有/usr/bin/env sh.这算是编程界的