shell脚本编程基础

脚本就是多个命令的集合。

使用多个命令

在“理解 shell”一节中说过,可以用 ; 将多个命令分开,这样就能依次执行多个命令。

$ date; who
Tue 02 Mar 2021 02:32:14 PM CST
pi       pts/7        2021-03-02 14:27 (172.28.20.54)

这样也算是简单的脚本,但每次运行之前,你都必须在命令提示符下输入整个命令,这很麻烦。可以将这些命令组合成一个简单的文本文件,这样就不需要在命令行中手动输入了。

创建shell脚本文件

用文本编辑器创建一个文件,文件后缀可以 .sh 或留空。

文件第一行指定要使用的 shell,格式为:

#!/bin/bash

在通常的 shell 脚本中,井号#用作注释行。shell 并不会处理 shell 脚本中的注释行。然而,shell 脚本文件的第一行是个例外,# 后面的惊叹号会告诉 shell 用哪个 shell 来运行脚本。

在指定了shell之后,就可以在文件的每一行中输入命令,然后加一个回车符:

#!/bin/bash
# This script displays the date and who's logged on
date
who

这就是脚本的所有内容了。可以根据需要,使用分号将两个命令放在一行上,但在shell脚本中,你可以在独立的行中书写命令。shell会按根据命令在文件中出现的顺序进行处理。

将这个脚本保存在名为test1的文件中,然后输入文件名:

l$ test1
-bash: test1: command not found

这是因为当前目录不是 shell 的搜索目录。如第6章所述,shell会通过PATH 环境变量来查找命令,所以我们需要将当前目录添加到 PATH,或者输入完整路径:

$ ./test1
-bash: ./test1: Permission denied

但还有一个问题,我们没有执行文件的权限。用 ls 查看一下:

$ ls -l test1
-rw-r--r-- 1 pi sudo 72 Mar  2 14:33 test1

下一步是通过chmod 命令(参见第7章)赋予文件属主执行文件的权限:

$ chmod u+x test1
$ ./test1
Tue 02 Mar 2021 02:43:43 PM CST
pi       pts/7        2021-03-02 14:27 (172.28.20.54)
pi       pts/10       2021-03-02 14:37 (172.28.20.54)

可以看出运行成功了。

或者如果不想赋予执行权限,可以用 /bin/bash 文件 来执行:

$ /bin/bash test1
Tue 02 Mar 2021 02:43:43 PM CST
pi       pts/7        2021-03-02 14:27 (172.28.20.54)
pi       pts/10       2021-03-02 14:37 (172.28.20.54)

显示消息

下面我们来试着显示 “Hello, world!”。可以通过echo 命令来实现这一点。如果在echo 命令后面加上了一个字符串,该命令就能显示出这个文本字符串。

$ echo Hello, world!
Hello, world!

默认情况下,不需要使用引号将要显示的文本字符串划定出来。但有时在字符串中出现引号的话就比较麻烦了。

$ echo Let's see if this'll work
Lets see if thisll work

echo 命令可用单引号或双引号来划定文本字符串,上面那句话相当于 Lets see if thisll work 拼接在一起。要显示引号,有两种方法,一种是用另外一种来将字符串划定起来,或者用转义字符:

$ echo "This is a test to see if you're paying attention"
This is a test to see if you're paying attention
$ echo Todd says \"scripting is easy\".
Todd says "scripting is easy".

echo 默认会在字符串后面加上回车,如果不想要回车,可以加 -n 参数:

$ echo 1; echo 2
1
2
$ echo -n 1; echo 2
12

使用变量

变量允许你临时性地将信息存储在shell脚本中,以便和脚本中的其他命令一起使用。

环境变量

第6章介绍了Linux系统的环境变量。也可以在脚本中访问这些值。shell维护着一组环境变量,用来记录特定的系统信息,可以用 set 命令来显示一份完整的当前环境变量列表。

在脚本中,你可以在环境变量名称之前加上美元符($ )来使用这些环境变量。下面的脚本演示了这种用法:

#!/bin/bash
# display user information from the system.
echo "User info for userid: $USER"
echo UID: $UID
echo HOME: $HOME

脚本输出如下:

User info for userid: pi
UID: 1000
HOME: /home/pi

echo 命令中的环境变量会在脚本运行时替换成当前值。另外,在第一个字符串中可以将 $USER 系统变量放置到双引号中,而 shell依然能够知道我们的意图。如果只是想输出 $,需要加上反斜线 \$

你可能还见过通过 ${variable} 形式引用的变量。变量名两侧额外的花括号通常用来帮助识别美元符后的变量名。

用户变量

shell脚本还允许在脚本中定义和使用自己的变量。定义变量允许临时存储数据并在整个脚本中使用,从而使shell脚本看起来更像一个真正的计算机程序。

用户变量可以是任何由字母、数字或下划线组成的文本字符串,长度不超过20个,区分大小写。

shell 脚本会自动决定变量值的数据类型。在脚本的整个生命周期里,shell 脚本中定义的变量会一直保持着它们的值,但在 shell 脚本结束时会被删除掉。

与系统变量类似,用户变量可通过美元符引用。

#!/bin/bash
# testing variables
days=10
guest="Katie"
echo "$guest checked in $days days ago"
days=5
guest="Jessica"
echo "$guest checked in $days days ago"
Katie checked in 10 days ago
Jessica checked in 5 days ago

变量每次被引用时,都会输出当前赋给它的值。重要的是要记住,引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用美元符。

命令替换

shell 脚本中最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。有两种方法可以将命令输出赋给变量:

  • 用反引号字符 ` 将命令括起来
  • $() 格式将命令括起来

比如我们可以先获取 date 命令的输出,然后创建一个以当前时间命名的文件,这种在创建 log 文件时十分有用。

#!/bin/bash
# create a log file
today=$(date +%y%m%d)
touch $today

命令替换会创建一个子shell来运行对应的命令。子shell(subshell)是由运行该脚本的shell所创建出来的一个独立的子shell(child shell)。正因如此,由该子shell所执行命令是无法使用脚本中所创建的变量的。 在命令行提示符下使用路径./ 运行命令的话,也会创建出子shell;要是运行命令的时候不加入路径,就不会创建子shell。如果你使用的是内建的shell命令,并不会涉及子shell。

重定向输入和输出

有些时候你想要保存某个命令的输出而不仅仅只是让它显示在显示器上。bash shell提供了几个操作符,可以将命令的输出重定向到另一个位置(比如文件)。重定向可以用于输入,也可以用于输出,可以将文件重定向到命令输入。

输出重定向

最基本的重定向将命令的输出发送到一个文件中。bash shell用大于号 > 来完成这项功能:

$ date > test
$ cat test
Tue 02 Mar 2021 03:31:56 PM CST
$ who > test
$ cat test
pi       pts/7        2021-03-02 14:27 (172.28.20.54)
pi       pts/10       2021-03-02 14:37 (172.28.20.54)

> 会覆盖文件原有内容,如果想要将命令的输出追加到已有文件中,可以用双大于号 >> 来追加数据

$ date >> test
$ who >> test
$ cat test
Tue 02 Mar 2021 03:39:50 PM CST
pi       pts/7        2021-03-02 14:27 (172.28.20.54)
pi       pts/10       2021-03-02 14:37 (172.28.20.54)

输入重定向

输入重定向将文件的内容重定向到命令,输入重定向符号是小于号 <

比如,wc 命令可以统计文本进行计数,默认情况下,它会输出3个值:行数、词数、字节数。

$ wc < test
  3  17 140

还有另外一种输入重定向的方法,称为 内联输入重定向 (inline input redirection)。这种方法无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据就可以了。

内联输入重定向符号是远小于号 <<,除了这个符号,你必须指定一个文本标记来划分输入数据的开始和结尾。

那和直接在命令行中输入有什么不同呢?emm……举个例子:

$ wc << EOF
> test string 1
> test string 2
> test string 3
> EOF
 3  9 42

管道

有时需要将一个命令的输出作为另一个命令的输入。这可以用重定向来实现,只是有些笨拙:

$ ls > file.list
$ sort < file.list
file.list
test
test1
test2
test3
test4

我们用不着将命令输出重定向到文件中,可以将其直接重定向到另一个命令。这个过程叫作管道连接 (piping)。管道符号是 |,放在命令之间,将前面命令的输出重定向到后面命令中。

不要以为由管道串起的两个命令会依次执行。Linux系统实际上会同时运行这两个命令,在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。

$ ls | sort
file.list
test
test1
test2
test3
test4

到目前为止,管道最流行的用法之一是将命令产生的大量输出通过管道传送给 more 命令。这对 ls 命令来说尤为常见:

$ ls -l /etc | more

ls -l 命令产生了目录中所有文件的长列表。对包含大量文件的目录来说,这个列表会相当长。通过将输出管道连接到 more 命令,可以强制输出在一屏数据显示后停下来。

执行数学运算

shell 脚本的数学运算特别麻烦,它需要借助一些命令,并且这些命令用起来也很麻烦。

expr 命令

最开始,Bourne shell提供了一个特别的命令 expr 用来处理数学表达式:

$ expr 1 + 5
6

如果你漏了加号两边的空格,你会得到错误的结果:

$ expr 1+5
1+5

expr 命令能够识别少数的数学和字符串操作符,如下:

操作符

描述

ARG1 | ARG2

如果ARG1 既不是null也不是零值,返回ARG1 ;否则返回ARG2

ARG1 & ARG2

如果没有参数是null或零值,返回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

返回ARG1ARG2 的算术运算和

ARG1 - ARG2

返回ARG1ARG2 的算术运算差

ARG1 * ARG2

返回ARG1ARG2 的算术乘积

ARG1 / ARG2

返回ARG1ARG2 除的算术商

ARG1 % ARG2

返回ARG1ARG2 除的算术余数

STRING : REGEXP

如果REGEXP 匹配到了STRING 中的某个模式,返回该模式匹配

match STRING REGEXP

如果REGEXP 匹配到了STRING 中的某个模式,返回该模式匹配

substr STRING POS LENGTH

返回起始位置为POS (从1 开始计数)、长度为LENGTH 个字符的子字符串

index STRING CHARS

返回在STRING 中找到CHARS 字符串的位置;否则,返回0

length STRING

返回字符串STRING 的数值长度

+ TOKEN

TOKEN 解释成字符串,即使是个关键字

(EXPRESSION)

返回EXPRESSION 的值

然后 expr 用起来很麻烦,比如星号在 shell 中另用含义,需要用转义符号:

$ expr 5 \* 2
10

幸好在 bash shell 有一个针对处理数学运算符的改进,见下一节。

使用方括号

在bash中,可以用美元符和方括号 $[ operation ] 将数学表达式围起来。在使用方括号来计算公式时,不用担心shell会误解乘号或其他符号。shell知道它不是通配符,因为它在方括号内。

$ echo $[5 * 2]
10
$ cat test6
#!/bin/bash
var1=100
var2=45
var3=$[$var1 / $var2]
echo The final result is $var3
$ /bin/bash test6
The final result is 2

bash shell 数学运算符只支持整数运算。

浮点解决方案

常见的方案是用内建的bash计算器,叫作 bc,bc 可以识别:

  • 数字(整数和浮点数)
  • 变量(简单变量和数组)
  • 注释(以#或C语言中的/* */ 开始的行)
  • 表达式
  • 编程语句(例如if-then 语句)
  • 函数

可以在 shell 提示符下通过 bc 命令访问 bash 计算器:

$ bc
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
1+1
2

使用方法和 Python 的交互式输入一样,输入表达式后回车就能得到结果。要退出bash计算器,你必须输入quit 。

浮点运算是由内建变量scale 控制的。必须将这个值设置为你希望在计算结果中保留的小数位数,否则无法得到期望的结果。

$ bc -q
3.44/5
0
scale=4
3.44/5
.6880

除了普通数字,bash计算器还能支持变量,但在bash计算器中创建的变量只在bash计算器中有效,不能在shell脚本中使用。bc 中的变量不需要加 $,直接用就行:

$ bc -q
var1=10
var1*4
40

在脚本中使用 bc,可以用管道:

$ cat test7
#!/bin/bash
var1=$(echo "1*2" | bc)
echo $var1
$ /bin/bash test7
2

你可能会想用输入重定向 bc < file,但 bc 不支持这种方式。我们需要用内联输入重定向:

$ cat test8
#!/bin/bash
cat file
echo
var1=$(cat file)
var2=$(bc << EOF
$var1
EOF
)
echo $var2
$ /bin/bash test8
1*2
2

退出脚本

迄今为止所有的示例脚本中,我们都是突然停下来的。运行完最后一条命令时,脚本就结束了。

我们可以使用退出状态码 (exit status)告诉 shell 它已经运行完毕,退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。

查看退出状态码

Linux提供了一个专门的变量 $? 来保存上个已执行命令的退出状态码。

$ date
Tue Mar  2 17:02:40 CST 2021
$ echo $?
0

按照惯例,一个成功结束的命令的退出状态码是0 。如果一个命令结束时有错误,退出状态码就是一个正数值。

$ todd
Command 'todd' not found
$ echo $?
127

Linux错误退出状态码没有什么标准可循,但有一些可用的参考,如下表所示:

状态码 描述
0 命令成功结束
1 一般性未知错误
2 不适合的shell命令
126 命令不可执行
127 没找到命令
128 无效的退出参数
128+x 与Linux信号x相关的严重错误
130 通过Ctrl+C终止的命令
255 正常范围之外的退出状态码

exit 命令

默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出。如果你需要返回自己的退出状态码,exit 命令可以指定一个退出状态码。

$ cat test13
#!/bin/bash
# testing the exit status
var1=10
var2=30
var3=$[$var1 + $var2]
echo The answer is $var3
exit 5
$ /bin/bash test8
The answer is 40
$ echo $?
5

状态码最大只能是255,如果超出这个范围,则会取模到 0~255