0%

高级 Bash 脚本编程指南(01):为什么使用 shell 编程

毫无疑问,UNIX/Linux 下最重要的软件之一就是 shell,目前最流行的 shell 被称为 Bash(Bourne Again Shell)。作为系统和用户之间的交互接口,shell 几乎是你在 UNIX 工作平台上最亲密的朋友。学好 shell 是学习 Linux/UNIX 的开始。

想真正学习脚本编程的唯一途径就是编写脚本。

shell 是什么

shell 是一种命令解释器,是介于操作系统 kernel 和用户之间的一个接口层。一个 shell 程序被称为一个脚本,它可以调用所有 Unix 命令、实用程序以及工具软件。除此之外,shell 还提供内建命令,比如 test 与循环结构等,可以让脚本更加灵活强大。

为什么使用 shell 编程

1
2
3
没有程序语言是完美的,甚至没有一个唯一的最好的语言。只有在特定环境下适合的语言。

-- Herbert Mayer

只要你想熟悉 Linux 系统管理,学习掌握 shell 脚本编程是必不可少的。Linux 系统本身也存在大量 shell 脚本,比如系统启动时会执行 /etc/rc.d/ 目录下的脚本来配置系统环境。

shell 脚本的语法简单直观,编写脚本就像是在命令行中把一些相关命令、工具连接起来,而你只需要遵循很少一部分规则即可。shell 脚本遵循典型的 UNIX 哲学:将大的、复杂的工程划分成简单的子任务,再将这些子任务连接起来。

使用 shell 脚本来构建一个复杂应用的原型是可行的。在使用高级编程语言编写最终代码之前,使用 shell 脚本可以快速测试方案的可行性,提前发现重大缺陷。

那什么时候不应该使用 shell 脚本呢?对于以下场景,不推荐使用 shell 脚本,而应该使用高级编程语言:

  • 资源密集型的任务,尤其在需要考虑运行效率
  • 要处理大量数学运算,尤其是浮点运算、高精度运算、复数运算等
  • 跨平台移植需求
  • 必须使用结构化编程的复杂应用
  • 影响系统全局的关键应用
  • 对于安全有高要求的任务
  • 项目包含有连锁依赖关系的组件
  • 需要大量的文件操作
  • 需要使用多维数组、链表、树等数据结构
  • 需要提供或操作图形化界面 GUI
  • 需要直接操作系统硬件或外部设备
  • 需要 I/O 或 socket 编程接口
  • 需要使用库或者旧代码接口
  • 私有的、非开源应用(shell 脚本直接将源代码公开)

Bash 是 Bourne-Again shell 的缩写,它是对 Bourne shell(称为 sh)的改进。现在 Bash 已经成为了绝大多数 Unix-Like 操作系统中 shell 事实上的标准了。

带着一个Sha-Bang 出发

最简单的 shell 脚本其实就是将一堆系统命令存放在一个文件中,它至少有一个好处:减少重复输入这一系列命令。如下 cleanup.sh 就是一个简单的脚本文件。

1
2
3
4
5
6
cd /var/log

cat /dev/null > messages
cat /dev/null > wtmp

echo "Log files cleanup"

根据惯例,用户编写的 Bash 脚本应该以 .sh 作为文件扩展名,而一些系统脚本,例如 /etc/rc.d 中的脚本,则通常不遵循这种命名规范。

Linux 系统下的脚本文件一般以 #!/bin/bash 作为起始行,这其实是告诉操作系统以指定的解释器程序 /bin/bash 来执行该文件:

  • #! 读作 Sha-Bang 或者 She-Bang,是一个两个字节的魔数字,是 Unix-Like 系统下指定解释器程序的一种特殊语法
  • #! 后面紧跟着的是解释器程序的路径名,操作系统会以该路径指定的程序来执行该文件
  • 指定的程序可以是某种 shell,也可以是其他任意程序,例如 Python 等等
  • #!*nix 操作系统提供的通用特性,用来为脚本文件指定解释器程序

如下 cat.me 文件指定 cat 程序来执行这个输入文件的内容,其最终结果就是输出整个文件的内容:

1
2
3
#!/usr/bin/cat

this is output from cat
1
2
3
4
5
# chmod +x cat.me
#./cat.me
#!/usr/bin/cat

this is output from cat

对于 bash 脚本,我们就是通过 #!/bin/bash 来指定脚本文件的解释器程序。bash 执行脚本文件时,首先解释第一行 #!/bin/bash,由于它是以 # 开头,bash 会认为该行是注释行而直接忽略该行。

需要注意,如果使用的是 #!/bin/sh,则调用的是系统的默认 shell 解释器,它并不一定总是 bash

1
2
# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Mar 23 2022 /bin/sh -> dash

所以如下是一个改进后的 cleaup.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

LOG_DIR=/var/log

cd $LOG_DIR

cat /dev/null > messages
cat /dev/null > wtmp

echo "Logs clean up"

# status code is omitted, the exit status is that of the last command executed.
exit

使用 #! 来显式指定解释器程序总是一种好的编程习惯,如果脚本的起始行没有使用 #!,此时到底使用哪个解释器是比较复杂的,这就可能会导致一些莫名奇妙的问题。另外,如果没有正确指定解释器的路径,会出现如下错误:

1
./cleanup.sh: /bash: bad interpreter: No such file or directory

所以有时候也会使用 #!/bin/env bash 来避免硬编码 bash 解释器的路径。

调用一个脚本

编写完脚本之后,可以使用 bash scriptname 来执行该脚本。更方便的方法是使用 chmod a+x 命令给脚本文件增加可执行权限,然后再通过 ./scriptname 的方式来执行它。

当脚本文件测试成功后,可以将其移动到 PATH 环境变量包含的目录中,例如 /usr/local/bin 目录,这样就可以直接在命令行中以 scriptname 的方式执行脚本了,无需指定脚本的路径。

一个更复杂的例子

最后再来看一个更复杂的 shell 脚本,后续会详细介绍该脚本所涉及的知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
# cleanup

LOG_DIR=/var/log
ROOT_UID=0
LINES=50

E_XCD=86
E_NOTROOT=87

if [ "$UID" -ne "$ROOT_UID" ]; then
echo "Must be root to run the script"
exit $E_NOTROOT
fi


if [ -n "$1" ]; then
lines=$1
else
lines=$LINES
fi

cd $LOG_DIR || {
echo "Can't change to to dir" >&2
exit $E_XCD
}

tail -n $lines messages > messages.tmp
mv messages.tmp messages

cat /dev/null > wtmp

echo "Log files clean up"
exit 0