Shell 脚本编程(基础篇)

基础篇

一、输出信息

大部分 Shell 命令都会生成自己的输出信息,在脚本运行时打印到终端屏幕上。但是很多时候,仍需要在输出的信息中添加上自己的内容,以提示用户脚本运行时究竟发生着什么,达到更好的交互效果。

echo 命令可以用来打印字符串内容。

1
2
3
$  echo This is a test
This is a test
$

PS:默认是不需要将 echo 命令后面的字符串包含在一对引号中的

echo 可以使用引号作为文本字符串的分隔符。如:

1
2
3
4
$ echo 'Hello
> World'
Hello
World

当输出的文本内容中本来就有引号出现时,如(Let's see if this'll work),可以结合单、双引号的使用,或者使用转义符\)前缀

1
2
3
4
5
6
7
8
9
# 引号作为字符串分隔符而不是文本内容
$ echo Let's see if this'll work
Lets see if thisll work
# 使用 \ 前缀进行转义
$ echo Let\'s see if this\'ll work
Let's see if this'll work
# 使用双引号作为分隔符,单引号作为中间的文本正常输出
$ echo "Let's see if this'll work"
Let's see if this'll work

脚本的执行权限

默认创建的脚本文件没有执行权限,不能直接在命令行中运行。
如创建包含以下内容的脚本文件 info.sh

1
2
3
4
5
6
#!/bin/bash

echo The time and date are:
date
echo "Let's see who's logged into the system:"
who

直接执行上述脚本时会提示 permission denied 错误:

1
2
$  ./info.sh
zsh: permission denied: ./info.sh

需要使用 chomd +x filename 命令为该脚本文件添加执行权限后再运行,效果如下:

1
2
3
4
5
6
7
8
$ chmod +x info.sh
$ ./info.sh
The time and date are:
Wed Oct 31 11:48:07 CST 2018
Let's see who's logged into the system:
starky console Oct 28 12:42
starky ttys000 Oct 29 16:52
starky ttys001 Oct 31 11:23

二、变量

1. 系统环境变量

在 Shell 脚本中可以直接访问系统中的环境变量,以获取相关的系统信息(如计算机名称,当前登录用户的账户名、 UID 和主目录等)。
当前定义的所有环境变量可以通过 set 命令获取。

1
2
3
4
5
6
7
8
9
10
$ set
...
HISTCMD=2217
HISTFILE=/Users/starky/.zsh_history
HISTSIZE=50000
HOME=/Users/starky
HOST=skitars-MacBook-Pro.local
IFS=$' \t\n\C-@'
ITERM_PROFILE=starky
...

当需要在 Shell 脚本中使用具体某个环境变量的值时,可以用环境变量名称加上 $ 前缀表示(如 $HOST)。
编辑如下 sys_info.sh 文件:

1
2
3
4
5
#!/bin/bash

echo User info for userid: $USER
echo UID: $UID
echo HOME: $HOME

运行效果如下:

1
2
3
4
5
$ chmod +x sys_info.sh
$ ./sys_info.sh
User info for userid: starky
UID: 501
HOME: /Users/starky

PS:由于在 Shell 脚本中,$ 作为变量的前缀符,所以当需要在文本输出中显示 $ 时,应使用转义。

1
2
3
4
5
6
# $15 被当成了代入到字符串中的“变量”
$ echo "The cost of the item is $15"
The cost of the item is
# 使用 \ 转义后正常打印 $ 字符
$ echo "The cost of the item is \$15"
The cost of the item is $15

2. 用户自定义变量

Shell 脚本允许用户自行定义和使用变量,这样就可以将脚本中用到的数据临时存储在指定的变量中,使用时再通过 $变量名 的形式获取。

  • 变量赋值:var=value (注意 = 号两边不能有空格,即 var = value 是错误的)
  • 变量使用:$var

PS:Shell 脚本中的变量名区分大小写
Shell 脚本会自动判断变量值的数据类型
变量的有效性贯穿脚本的整个生命周期,即脚本执行完毕后变量会自行删除

编辑如下 variables.sh 文件:

1
2
3
4
5
6
7
8
9
#!/bin/bash

days=10
guest="Katie"
echo "$guest checked in $days days ago"

days=5
guest="Jessica"
echo "$guest checked in $days days ago"

运行效果如下:

1
2
3
4
5
$ chmod +x variables.sh
$ ./variables.sh
Katie checked in 10 days ago
Jessica checked in 5 days ago
$ echo $days

可以看到,脚本退出后,脚本中定义的 $days 变量又恢复为未定义的状态。

三、命令替换

Shell 脚本最有用处的特性之一,就是它可以提取某个命令的输出信息,并将其赋值给一个变量
可以通过以下两种方式将命令输出赋值给变量:

  • 反单引号(`)
  • $() 格式

如:

1
2
3
4
5
6
7
8
9
10
11
# 使用 date 命令获取当前的日期和时间
$ date
Thu Nov 1 01:03:02 CST 2018
# 将 date 命令的输出(即当前日期和时间)赋值给 var1 变量
$ var1=`date`
$ echo Today is: $var1
Today is: Thu Nov 1 01:03:15 CST 2018
# 将 date 命令的输出赋值给 var2 变量(使用 $() 格式)
$ var2=$(date)
$ echo Today is: $var2
Today is: Thu Nov 1 01:03:36 CST 2018

示例程序
使用命令替换完成一个脚本(log.sh),该脚本可以创建以当前时间水印为后缀的文本文件,内容为 /usr/bin 目录下的所有文件列表。

1
2
3
4
5
#!/bin/bash

today=$(date +%y%m%d%H%M%S)
ls -al /usr/bin > log.$today
echo The file log.$today has been created, you can check it later.

其中 date +%y%m%d%H%M%S 命令可以输出纯数字格式的日期和时间
运行效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ chmod +x log.sh
$ ./log.sh
The file log.181101012159 has been created, you can check it later.
$ ls log*
log.181101012159 log.sh
$ head log.181101012159
total 103992
drwxr-xr-x 971 root wheel 31072 Oct 13 17:44 .
drwxr-xr-x@ 9 root wheel 288 Sep 21 12:01 ..
-rwxr-xr-x 4 root wheel 925 Aug 18 08:45 2to3-
lrwxr-xr-x 1 root wheel 74 Oct 13 17:44 2to3-2.7 -> ../../System/Library/Frameworks/Python.framework/Versions/2.7/bin/2to3-2.7
-rwxr-xr-x 1 root wheel 55072 Sep 21 12:16 AssetCacheLocatorUtil
-rwxr-xr-x 1 root wheel 53472 Sep 21 12:16 AssetCacheManagerUtil
-rwxr-xr-x 1 root wheel 48256 Sep 21 12:17 AssetCacheTetheratorUtil
-rwxr-xr-x 1 root wheel 18320 Sep 21 12:17 BuildStrings
-rwxr-xr-x 1 root wheel 18288 Sep 21 12:17 CpMac

四、重定向输入和输出

1. 输出重定向

最基本的重定向,就是通过大于号(>),将某个命令的输出内容保存至一个文件中。

格式:command > outputfile

1
2
3
4
5
$ date > current_date.txt
$ ls -l current_date.txt
-rw-r--r-- 1 starky staff 29 Nov 1 01:29 current_date.txt
$ cat current_date.txt
Thu Nov 1 01:29:45 CST 2018

PS:如使用重定向时,指定的文件已存在,则该文件的原始内容会被新内容覆盖
如果只是想在文件末尾追加内容,则可以使用双大于号(>>)

1
2
3
4
$ date >> current_date.txt
$ cat current_date.txt
Thu Nov 1 01:29:45 CST 2018
Thu Nov 1 01:34:03 CST 2018

2. 输入重定向

输入重定向和输出重定向相反。即从文件中读取内容,并将该内容传递给某个命令。

格式:command < inputfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ls -l /Users/starky
total 49864
drwx------@ 3 starky staff 96 Oct 13 18:00 Applications
drwx------+ 23 starky staff 736 Oct 31 19:14 Desktop
drwx------+ 21 starky staff 672 Oct 31 19:15 Documents
drwx------+ 37 starky staff 1184 Oct 30 20:18 Downloads
drwx------+ 72 starky staff 2304 Oct 27 01:38 Library
drwx------+ 6 starky staff 192 Oct 29 10:34 Movies
drwx------+ 3 starky staff 96 Sep 27 13:13 Music
...
$ cat directory.txt
/Users/starky
$ ls -l < directory.txt
total 49864
drwx------@ 3 starky staff 96 Oct 13 18:00 Applications
drwx------+ 23 starky staff 736 Oct 31 19:14 Desktop
drwx------+ 21 starky staff 672 Oct 31 19:15 Documents
drwx------+ 37 starky staff 1184 Oct 30 20:18 Downloads
drwx------+ 72 starky staff 2304 Oct 27 01:38 Library
drwx------+ 6 starky staff 192 Oct 29 10:34 Movies
drwx------+ 3 starky staff 96 Sep 27 13:13 Music
...
3. 管道

有些时候,需要将某个命令的输出内容作为另一个命令的输入。如:

1
2
3
4
5
6
$ ls -al > tmp_file
$ grep vim < tmp_file
drwxr-xr-x 3 starky staff 96 Oct 20 15:45 .vim
-rw------- 1 starky staff 23799 Nov 1 01:40 .viminfo
-rw-r--r-- 1 starky staff 3935 Oct 21 01:31 .vimrc
-rw-r--r-- 1 starky staff 24849808 Oct 25 21:02 vim.tar.gz

上面的命令先将当前目录下的文件列表(ls -al)保存在 tmp_file 中,再使用 grep 命令读取 tmp_file 的内容,筛选文件名中包含 vim 的文件。

其实可以通过管道|)的使用,将前面命令的输出,定向给后面的命令作为输入。

格式:command1 | command2

1
2
3
4
5
ls -al | grep vim
drwxr-xr-x 3 starky staff 96 Oct 20 15:45 .vim
-rw------- 1 starky staff 23799 Nov 1 01:40 .viminfo
-rw-r--r-- 1 starky staff 3935 Oct 21 01:31 .vimrc
-rw-r--r-- 1 starky staff 24849808 Oct 25 21:02 vim.tar.gz

五、数学运算

操作数学运算对于任何编程语言来说,都是一个很重要的特性。但是 Shell 脚本并不能直接完成算术运算的操作,只能通过以下两种方式来实现。

1. expr 命令

Shell 提供了一个特殊的命令(expr)用来处理数学算式,如:

1
2
$ expr 1 + 2
3

PS:注意算式中 + 号两边的空格
expr 命令支持的算术操作符如下:

操作符 含义
ARG1 双管道符 ARG2 如果两个参数值都不为 null 或 0,返回 ARG1,否则返回 ARG2
ARG1 && ARG2 如果两个参数值都不为 null 或 0,返回 ARG1,否则返回 0
ARG1 < ARG2 如果 ARG1 小于 ARG2,返回 1,否则返回 0
ARG1 > ARG2 如果 ARG1 大于 ARG2,返回 1,否则返回 0
ARG1 = ARG2 如果 ARG1 等于 ARG2,返回 1,否则返回 0
ARG1 >= ARG2 如果 ARG1 大于或等于 ARG2,返回 1,否则返回 0
ARG1 <= ARG2 如果 ARG1 小于或等于 ARG2,返回 1,否则返回 0
ARG1 != ARG2 如果 ARG1 不等于 ARG2,返回 1,否则返回 0
ARG1 + ARG2 返回 ARG1 与 ARG2 的数字加和
ARG1 - ARG2 求 ARG1 减去 ARG2 的数字差
ARG1 * ARG2 返回 ARG1 与 ARG2 的数字乘积
ARG1 / ARG2 求 ARG1 除以 ARG2 的数字商(结果为整数)
ARG1 % ARG2 对 ARG1 和 ARG2 进行求余操作

数学运算示例(divide.sh):

1
2
3
4
5
6
7
#!/bin/bash

var1=10
var2=20
var3=`expr $var2 / $var1`

echo $var2 divided by $var1 equals $var3

运行效果:

1
2
3
$ chmod +x divide.sh
$ ./divide.sh
20 divided by 10 equals 2

2. 使用中括号

Bash Shell 中的 expr 命令主要是为了保持和 Bourne Shell 的兼容性,它其实还提供了一种更简单的方式用来处理数学运算。即使用这样的形式:
$[ operation ]

如下面的脚本(compute.sh

1
2
3
4
5
6
7
8
#!/bin/bash

var1=100
var2=50
var3=45

result=$[$var1 * ($var2 - $var3)]
echo The final result is $result

运行效果:

1
2
3
$ chmod +x compute.sh
$ ./compute.sh
The final result is 500

但是在进行除法运算时,上面的方式只支持整数。如两个数相除结果为小数,则该结果会舍去小数部分只保留整数。

1
2
3
4
5
$ var1=10
$ var2=3
$ result=$[$var1 / $var2]
$ echo The result is $result
The result is 3

3. 浮点数运算

有很多种方案可以克服 bash 的整数限制,最常用的一种就是使用系统内置的 bash calculator,即 bc 程序。
bash calculator 其实是一种支持浮点数运算的编程语言,可以识别以下几种类型的数据:

  • 数字(整数和浮点数
  • 变量(简单变量和数组
  • 注释(单行注释 # 和多行注释 /* */
  • 表达式
  • 编程语句(如 if-then 语句)
  • 函数
1
2
3
4
5
6
7
8
9
10
$ bc
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
12 * 5.4
64.8
3.156 * (3 + 5)
25.248
quit

计算结果精确到的小数位数是通过一个内建的 scale 变量定义的,默认的 scale 数值为 0(即默认舍去商的小数位数):

1
2
3
4
5
6
7
$ bc -q
3.44 / 5
0
scale=4
3.44 / 5
.6880
quit

bc 程序也支持自定义变量:

1
2
3
4
5
6
7
8
$ bc -q
var1=10
var1 * 4
40
var2=var1 / 5
print var2
2
quit

4. 在脚本中使用 bc

可以通过管道将数学表达式传递给 bc 程序,再将计算得出的结果通过赋值语句赋值给某个变量:
variable=$(echo "options; expression" | bc)

其中 variable=$(...) 用于提取命令的输出并赋值给一个变量(参考第三章命令替换

如下面的脚本(bc1.sh):

1
2
3
4
5
6
7
#!/bin/bash

var1=100
var2=45
var3=$(echo "scale=4; $var1 / $var2" | bc)

echo The answer for this is $var3

运行效果:

1
2
3
$ chmod +x bc1.sh
$ ./bc1.sh
The answer for this is 2.2222

更复杂的形式

在脚本中使用 bc 还可以通过如下的形式:

1
2
3
4
5
6
variable=$(bc << EOF
options
statements
expressions
EOF
)

示例程序如下(bc2.sh):

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

var1=10.46
var2=43.67
var3=33.2
var4=71

var5=$(bc << EOF
scale = 4
a1=$var1 * $var2
b1=$var3 * $var4
a1 + b1
EOF
)

echo The final answer for this mess is $var5

运行结果:

1
2
3
$ chmod +x bc2.sh
$ ./bc2.sh
The final answer for this mess is 2813.9882

参考资料

Linux Command Line and Shell Scripting Bible 3rd Edition