Shell(2)脚本编程.md
title: Shell(2)脚本编程date: 2021-06-02 21:36:44.763
updated: 2021-06-04 20:28:04.698
url: /?p=198
categories: Linux
tags: Linux
Shell 脚本编程,重点讲解变量、字符串、数组、数学计算、选择结构、循环结构和函数。
变量
变量是任何一种编程语言都必不可少的组成部分,变量用来存放各种数据。脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。
在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。
这意味着,Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串,这一点和大部分的编程语言不同。例如在C语言或者 C++ 中,变量分为整数、小数、字符串、布尔等多种类型。
当然,如果有必要,你也可以使用 Shell declare 关键字显式定义变量的类型,但在一般情况下没有这个需求,Shell 开发者在编写代码时自行注意值的类型即可。
定义
Shell 支持以下三种定义变量的方式:
1 | variable=value |
variable 是变量名,value 是赋给变量的值。如果 value 不包含任何空白符(例如空格、Tab 缩进等),那么可以不使用引号;如果 value 包含了空白符,那么就必须使用引号包围起来。使用单引号和使用双引号也是有区别的,稍后我们会详细说明。
Shell 变量的命名规范和大部分编程语言都一样:
- 变量名由数字、字母、下划线组成;
- 必须以字母或者下划线开头;
- 不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。
使用变量
使用一个定义过的变量,只要在变量名前面加美元符号$即可,如:
1 | author="lautung" |
变量名外面的花括号{ }是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界
单引号和双引号的区别
以单引号’ ‘包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。这种方式比较适合定义显示纯字符串的情况,即不希望解析变量、命令等的场景。
以双引号” “包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式比较适合字符串中附带有变量和命令并且想将其解析后再输出的变量定义。
如果变量的内容是数字,那么可以不加引号;如果真的需要原样输出就加单引号;其他没有特别要求的字符串等最好都加上双引号,定义变量时加双引号是最常见的使用场景。
将命令的结果赋值给变量
Shell 也支持将命令的执行结果赋值给变量,常见的有以下两种方式:
1 | variable=`command` |
第一种方式把命令用反引号``(位于 Esc 键的下方)包围起来,反引号和单引号非常相似,容易产生混淆,所以不推荐使用这种方式;第二种方式把命令用$()包围起来,区分更加明显,所以推荐使用这种方式。
只读变量 readonly
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。
1 |
|
运行脚本,结果如下:
1 | bash: myUrl: This variable is read only. |
删除变量 unset
使用 unset 命令可以删除变量。语法:
纯文本复制
unset variable_name
变量被删除后不能再次使用;unset 命令不能删除只读变量。
1 |
|
Shell变量的作用域:全局变量、环境变量和局部变量
Shell 变量的作用域(Scope),就是 Shell 变量的有效范围(可以使用的范围)。
Shell 变量的作用域可以分为三种:
- 有的变量只能在函数内部使用,这叫做局部变量(local variable);
- 有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable);
- 而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)。
局部变量
要想变量的作用域仅限于函数内部,可以在定义时加上local
命令,此时该变量就成了局部变量。
1 | lautung@PC:~$ cat test.sh |
全局变量
所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。在 Shell 中定义的变量,默认就是全局变量。
环境变量
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效。如果使用export
命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为“环境变量”。
1 | lautung@PC:~$ echo $$ |
注意了,后面我们在子进程中修改变量a,然后再主进程中输出,发现还是原来的值。
说来惭愧,我现在才知道环境变量原来也是作用域。
在 Shell 子进程中有效,并没有说它在所有的 Shell 进程中都有效;如果你通过终端创建了一个新的 Shell 窗口,那它就不是当前 Shell 的子进程,环境变量对这个新的 Shell 进程仍然是无效的。
Shell位置参数(命令行参数)
运行 Shell 脚本文件时我们可以给它传递一些参数,这些参数在脚本文件内部可以使用$n的形式来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。
同样,在调用函数时也可以传递参数。Shell 函数参数的传递和其它编程语言不同,没有所谓的形参和实参,在定义函数时也不用指明参数的名字和数目。换句话说,定义 Shell 函数时不能带参数,但是在调用函数时却可以传递参数,这些传递进来的参数,在函数内部就也使用$n的形式接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。
这种通过$n的形式来接收的参数,在 Shell 中称为位置参数。
变量的名字必须以字母或者下划线开头,不能以数字开头;但是位置参数却偏偏是数字,这和变量的命名规则是相悖的,所以我们将它们视为“特殊变量”。除了 $n,Shell 中还有
$#、$*、$@、$?、$$
几个特殊参数。
【示例1】给脚本文件传递位置参数
1 | lautung@PC:~$ cat test.sh |
【示例2】给函数传递位置参数
1 | lautung@PC:~$ cat test.sh |
注意事项
如果参数个数太多,达到或者超过了 10 个,那么就得用${n}的形式来接收了,例如 ${10}、${23}。{ }的作用是为了帮助解释器识别参数的边界,这跟使用变量时加{ }是一样的效果。
特殊变量:Shell $#、$*、$@、$?、$$
变量 | 含义 |
---|---|
$0 | 当前脚本的文件名。 |
$n(n≥1) | 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。 |
$# | 传递给脚本或函数的参数个数。 |
$* | 传递给脚本或函数的所有参数。 |
$@ | 传递给脚本或函数的所有参数。当被双引号" " 包含时,$@ 与 $* 稍有不同,我们将在《Shell $*和$@的区别》一节中详细讲解。 |
$? | 上个命令的退出状态,或函数的返回值,我们将在《Shell $?》一节中详细讲解。 |
$$ | 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID。 |
【示例1】给脚本文件传递参数
编写下面的代码,并保存为 test.sh:
1 |
|
运行. test.sh 1 2 3 4 5
,结果:
1 | Process ID: 9 |
【示例2】给函数传递参数
编写下面的代码,并保存为 test.sh
:
1 |
|
运行结果为:
1 | Language: Java |
$*和$@之间的区别
$* 和 $@ 都表示传递给函数或脚本的所有参数。
当 $* 和 $@ 不被双引号" "
包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔。
但是当它们被双引号” “包含时,就会有区别了:
- “$*”会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据。
- “$@”仍然将每个参数都看作一份数据,彼此之间是独立的。
如果使用 echo 直接输出”$*”和”$@”做对比,是看不出区别的;但如果使用 for 循环来逐个输出数据,立即就能看出区别来。
$?:获取函数返回值或者上一个命令的退出状态
$? 是一个特殊变量,用来获取上一个命令的退出状态,或者上一个函数的返回值。
所谓退出状态,就是上一个命令执行后的返回结果。退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1,这和C语言的 main() 函数是类似的。
不过,也有一些命令返回其他值,表示不同类型的错误。
【示例1】$? 获取上一个命令的退出状态
编写下面的代码,并保存为 test.sh:
1 |
|
exit
表示退出当前 Shell 进程,我们必须在新进程中运行 test.sh,否则当前 Shell 会话(终端窗口)会被关闭,我们就无法取得它的退出状态了。
例如,运行 test.sh 时传递参数 100:
1 | cd demo |
输出结果为:0
再如,运行 test.sh 时传递参数 89:
1 | bash ./test.sh 89 #作为一个新进程运行 |
输出结果为:1
【示例2】$? 获取函数的返回值
编写下面的代码,并保存为 test.sh:
1 |
|
运行结果:
73
字符串
字符串(String)就是一系列字符的组合。字符串是 Shell 编程中最常用的数据类型之一(除了数字和字符串,也没有其他类型了)。
字符串可以由单引号’ ‘包围,也可以由双引号” “包围,也可以不用引号。它们之间是有区别的,稍后我们会详解。
字符串举例:
1 | str1=c.biancheng.net |
下面我们说一下三种形式的区别:
- 由单引号
' '
包围的字符串:
任何字符都会原样输出,在其中使用变量是无效的。
字符串中不能出现单引号,即使对单引号进行转义也不行。
2. 由双引号" "
包围的字符串:
如果其中包含了某个变量,那么该变量会被解析(得到该变量的值),而不是原样输出。
字符串中可以出现双引号,只要它被转义了就行。
3. 不被引号包围的字符串
不被引号包围的字符串中出现变量时也会被解析,这一点和双引号” “包围的字符串一样。
字符串中不能出现空格,否则空格后边的字符串会作为其他变量或者命令解析。
获取字符串长度
在 Shell 中获取字符串长度很简单,具体方法如下:
1 | ${#string_name} |
string_name 表示字符串名字。
字符串拼接
在 Shell 中你不需要使用任何运算符,将两个字符串并排放在一起就能实现拼接,非常简单粗暴。请看下面的例子:
1 |
|
运行结果:
1 | Shellhttp://c.biancheng.net/shell/ |
字符串截取
Shell 截取字符串通常有两种方式:从指定位置开始截取和从指定字符(子字符串)开始截取。
指定位置开始截取
这种方式需要两个参数:除了指定起始位置,还需要截取长度,才能最终确定要截取的字符串。
既然需要指定起始位置,那么就涉及到计数方向的问题,到底是从字符串左边开始计数,还是从字符串右边开始计数。答案是 Shell 同时支持两种计数方式。
1.
从字符串左边开始计数
如果想从字符串的左边开始计数,那么截取字符串的具体格式如下:
1 | ${string: start :length} |
2.
从右边开始计数
如果想从字符串的右边开始计数,那么截取字符串的具体格式如下:
1 | ${string: 0-start :length} |
同第 1 种格式相比,第 2 种格式仅仅多了0-,这是固定的写法,专门用来表示从字符串右边开始计数。
这里需要强调两点:
- 从左边开始计数时,起始数字是 0(这符合程序员思维);从右边开始计数时,起始数字是 1(这符合常人思维)。计数方向不同,起始数字也不同。
- 不管从哪边开始计数,截取方向都是从左到右。
指定字符(子字符串)开始截取
这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾。Shell 可以截取指定字符(子字符串)右边的所有字符,也可以截取左边的所有字符。
1.
使用 # 号截取右边字符
使用#号可以截取指定字符(或者子字符串)右边的所有字符,具体格式如下:
1 | ${string#*chars} |
其中,string 表示要截取的字符,chars 是指定的字符(或者子字符串),_是通配符的一种,表示任意长度的字符串。_chars连起来使用的意思是:忽略左边的所有字符,直到遇见 chars(chars 不会被截取)。
2.
使用 % 截取左边字符
使用%号可以截取指定字符(或者子字符串)左边的所有字符,具体格式如下:
1 | ${string%chars*} |
汇总
格式 | 说明 |
---|---|
${string: start :length} | 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。 |
${string: start} | 从 string 字符串的左边第 start 个字符开始截取,直到最后。 |
${string: 0-start :length} | 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。 |
${string: 0-start} | 从 string 字符串的右边第 start 个字符开始截取,直到最后。 |
${string#*chars} | 从 string 字符串第一次出现 _chars 的位置开始,截取 _chars 右边的所有字符。 |
${string##*chars} | 从 string 字符串最后一次出现 _chars 的位置开始,截取 _chars 右边的所有字符。 |
${string%*chars} | 从 string 字符串第一次出现 _chars 的位置开始,截取 _chars 左边的所有字符。 |
${string%%*chars} | 从 string 字符串最后一次出现 _chars 的位置开始,截取 _chars 左边的所有字符。 |
数组
和其他编程语言一样,Shell 也支持数组。数组(Array)是若干数据的集合,其中的每一份数据都称为元素(Element)。
Shell 并且没有限制数组的大小,理论上可以存放无限量的数据。和 C++、Java、C# 等类似,Shell 数组元素的下标也是从 0 开始计数。
获取数组中的元素要使用下标[ ],下标可以是一个整数,也可以是一个结果为整数的表达式;当然,下标必须大于等于 0。
遗憾的是,常用的 Bash Shell 只支持一维数组,不支持多维数组。
数组的定义
在 Shell 中,用括号( )来表示数组,数组元素之间用空格来分隔。由此,定义数组的一般形式为:
1 | array_name=(ele1 ele2 ele3 ... elen) |
注意,赋值号=两边不能有空格,必须紧挨着数组名和数组元素。
下面是一个定义数组的实例:
1 | nums=(29 100 13 8 91 44) |
Shell 是弱类型的,它并不要求所有数组元素的类型必须相同,例如:
1 | arr=(20 56 "lautung") |
第三个元素就是一个“异类”,前面两个元素都是整数,而第三个元素是字符串。
Shell 数组的长度不是固定的,定义之后还可以增加元素。例如,对于上面的 nums 数组,它的长度是 6,使用下面的代码会在最后增加一个元素,使其长度扩展到 7:
1 | nums[6]=88 |
此外,你也无需逐个元素地给数组赋值,下面的代码就是只给特定元素赋值:
1 | ages=([3]=24 [5]=19 [10]=12) |
以上代码就只给第 3、5、10 个元素赋值,所以数组长度是 3。
获取数组元素
获取数组元素的值,一般使用下面的格式:
1 | ${array_name[index]} |
其中,array_name 是数组名,index 是下标。例如:
1 | n=${nums[2]} |
表示获取 nums 数组的第二个元素,然后赋值给变量 n。再如:
1 | echo ${nums[3]} |
表示输出 nums 数组的第 3 个元素。
使用@或*可以获取数组中的所有元素,例如:
1 | ${nums[*]} |
两者都可以得到 nums 数组的所有元素。
【示例】
1 |
|
运行结果:
1 | 29 100 13 8 91 44 |
获取数组长度
所谓数组长度,就是数组元素的个数。
利用@或*,可以将数组扩展成列表,然后使用#来获取数组元素的个数,格式如下:
1 | ${#array_name[@]} |
其中 array_name 表示数组名。两种形式是等价的,选择其一即可。
如果某个元素是字符串,还可以通过指定下标的方式获得该元素的长度,如下所示:
1 | ${#arr[2]} |
获取 arr 数组的第 2 个元素(假设它是字符串)的长度。
回忆字符串长度的获取
回想一下 Shell 是如何获取字符串长度的呢?其实和获取数组长度如出一辙,它的格式如下:
1 | ${#string_name} |
string_name 是字符串名。
【示例】
1 |
|
运行结果:
1 | lautung@PC:~$ sudo bash test.sh |
数组拼接
所谓 Shell 数组拼接(数组合并),就是将两个数组连接成一个数组。
拼接数组的思路是:先利用@或*,将数组扩展成列表,然后再合并到一起。具体格式如下:
1 | array_new=(${array1[@]} ${array2[@]}) |
两种方式是等价的,选择其一即可。其中,array1 和 array2 是需要拼接的数组,array_new 是拼接后形成的新数组。
下面是完整的演示代码:
1 |
|
运行结果:
1 | lautung@PC:~$ sudo bash test.sh |
删除数组元素
在 Shell 中,使用 unset 关键字来删除数组元素,具体格式如下:
1 | unset array_name[index] |
其中,array_name 表示数组名,index 表示数组下标。
如果不写下标,而是写成下面的形式:
1 | unset array_name |
那么就是删除整个数组,所有元素都会消失。
下面我们通过具体的代码来演示:
1 | #!/bin/bash |
运行结果:
1 3 lautung
注意最后的空行,它表示什么也没输出,因为数组被删除了,所以输出为空。
关联数组(下标是字符串的数组)
现在最新的 Bash Shell 已经支持关联数组了。关联数组使用字符串作为下标,而不是整数,这样可以做到见名知意。
关联数组也称为“键值对(key-value)”数组,键(key)也即字符串形式的数组下标,值(value)也即元素值。
例如,我们可以创建一个叫做 color 的关联数组,并用颜色名字作为下标。
1 | declare -A color |
也可以在定义的同时赋值:
1 | declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff") |
不同于普通数组,关联数组必须使用带有-A选项的 declare 命令创建。关于 declare 命令后续再说。
访问关联数组元素
访问关联数组元素的方式几乎与普通数组相同,具体形式为:
1 | array_name["index"] |
例如:
1 | color["white"]="#ffffff" |
加上$()即可获取数组元素的值:
1 | $(array_name["index"]) |
例如:
1 | echo $(color["white"]) |
获取所有元素的下标和值
使用下面的形式可以获得关联数组的所有元素值:
1 | ${array_name[@]} |
使用下面的形式可以获取关联数组的所有下标值:
1 | ${!array_name[@]} |
获取关联数组长度
使用下面的形式可以获得关联数组的长度:
1 | ${#array_name[*]} |
关联数组实例演示:
1 |
|
运行结果:
1 | #ff0000 |
内建命令(内置命令)
所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。
例如,用于进入或者切换目录的 cd 命令,虽然我们一直在使用它,但如果不加以注意很难意识到它与普通命令的性质是不一样的:该命令并不是某个外部文件,只要在 Shell 中你就一定可以运行这个命令。
可以使用 type 来确定一个命令是否是内建命令:
1 | lautung@PC:~$ type cd |
由此可见,cd 是一个 Shell 内建命令,而 ifconfig 是一个外部文件,它的位置是/usr/sbin/ifconfig
。
还记得系统变量 $PATH 吗?$PATH 变量包含的目录中几乎聚集了系统中绝大多数的可执行命令,它们都是外部命令。
通常来说,内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。
下表列出了 Bash Shell 中直接可用的内建命令。
命令 | 说明 |
---|---|
: | 扩展参数列表,执行重定向操作 |
. | 读取并执行指定文件中的命令(在当前 shell 环境中) |
alias | 为指定命令定义一个别名 |
bg | 将作业以后台模式运行 |
bind | 将键盘序列绑定到一个 readline 函数或宏 |
break | 退出 for、while、select 或 until 循环 |
builtin | 执行指定的 shell 内建命令 |
caller | 返回活动子函数调用的上下文 |
cd | 将当前目录切换为指定的目录 |
command | 执行指定的命令,无需进行通常的 shell 查找 |
compgen | 为指定单词生成可能的补全匹配 |
complete | 显示指定的单词是如何补全的 |
compopt | 修改指定单词的补全选项 |
continue | 继续执行 for、while、select 或 until 循环的下一次迭代 |
declare | 声明一个变量或变量类型。 |
dirs | 显示当前存储目录的列表 |
disown | 从进程作业表中刪除指定的作业 |
echo | 将指定字符串输出到 STDOUT |
enable | 启用或禁用指定的内建shell命令 |
eval | 将指定的参数拼接成一个命令,然后执行该命令 |
exec | 用指定命令替换 shell 进程 |
exit | 强制 shell 以指定的退出状态码退出 |
export | 设置子 shell 进程可用的变量 |
fc | 从历史记录中选择命令列表 |
fg | 将作业以前台模式运行 |
getopts | 分析指定的位置参数 |
hash | 查找并记住指定命令的全路径名 |
help | 显示帮助文件 |
history | 显示命令历史记录 |
jobs | 列出活动作业 |
kill | 向指定的进程 ID(PID) 发送一个系统信号 |
let | 计算一个数学表达式中的每个参数 |
local | 在函数中创建一个作用域受限的变量 |
logout | 退出登录 shell |
mapfile | 从 STDIN 读取数据行,并将其加入索引数组 |
popd | 从目录栈中删除记录 |
printf | 使用格式化字符串显示文本 |
pushd | 向目录栈添加一个目录 |
pwd | 显示当前工作目录的路径名 |
read | 从 STDIN 读取一行数据并将其赋给一个变量 |
readarray | 从 STDIN 读取数据行并将其放入索引数组 |
readonly | 从 STDIN 读取一行数据并将其赋给一个不可修改的变量 |
return | 强制函数以某个值退出,这个值可以被调用脚本提取 |
set | 设置并显示环境变量的值和 shell 属性 |
shift | 将位置参数依次向下降一个位置 |
shopt | 打开/关闭控制 shell 可选行为的变量值 |
source | 读取并执行指定文件中的命令(在当前 shell 环境中) |
suspend | 暂停 Shell 的执行,直到收到一个 SIGCONT 信号 |
test | 基于指定条件返回退出状态码 0 或 1 |
times | 显示累计的用户和系统时间 |
trap | 如果收到了指定的系统信号,执行指定的命令 |
type | 显示指定的单词如果作为命令将会如何被解释 |
typeset | 声明一个变量或变量类型。 |
ulimit | 为系统用户设置指定的资源的上限 |
umask | 为新建的文件和目录设置默认权限 |
unalias | 刪除指定的别名 |
unset | 刪除指定的环境变量或 shell 属性 |
wait | 等待指定的进程完成,并返回退出状态码 |
alias:给命令创建别名
alisa 用来给命令创建一个别名。若直接输入该命令且不带任何参数,则列出当前 Shell 进程中使用了哪些别名。现在你应该能理解类似ll
这样的命令为什么与ls -l
的效果是一样的吧。
查看已经起别名的命令:
1 | lautung@PC:~$ alias |
使用 alias 命令自定义别名
使用 alias 命令自定义别名的语法格式为:
1 | alias new_name='command' |
比如,一般的关机命令是shutdown-h now,写起来比较长,这时可以重新定义一个关机命令,以后就方便多了。
1 | alias myShutdown='shutdown -h now' |
再如,通过 date 命令可以获得当前的 UNIX 时间戳,具体写法为date +%s,如果你嫌弃它太长或者不容易记住,那可以给它定义一个别名。
1 | alias timestamp='date +%s' |
我们使用date +%s计算脚本的运行时间,现在学了 alias,就可以简化代码了。
1 |
|
别名只是临时的 :在代码中使用 alias 命令定义的别名只能在当前 Shell 进程中使用,在子进程和其它进程中都不能使用。当前 Shell 进程结束后,别名也随之消失。
要想让别名对所有的 Shell 进程都有效,就得把别名写入 Shell 配置文件。Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,将别名放在配置文件中,那么每次启动进程都会定义这个别名。
使用 unalias 命令删除别名
使用 unalias 内建命令可以删除当前 Shell 进程中的别名。unalias 有两种使用方法:
- 第一种用法是在命令后跟上某个命令的别名,用于删除指定的别名。
- 第二种用法是在命令后接-a参数,删除当前 Shell 进程中所有的别名。
同样,这两种方法都是在当前 Shell 进程中生效的。要想永久删除配置文件中定义的别名,只能进入该文件手动删除。
1 | # 删除 ll 别名 |
echo命令:输出字符串
echo 是一个 Shell 内建命令,用来在终端输出字符串,并在最后默认加上换行符。请看下面的例子:
1 |
|
运行结果:
1 | 读者,你好! |
不换行
echo 命令输出结束后默认会换行,如果不希望换行,可以加上-n参数,如下所示:
1 |
|
运行结果:
1 | Tom is 20 years old, 175cm in height and 62kg in weight. |
输出转义字符
默认情况下,echo 不会解析以反斜杠\开头的转义字符。比如,\n表示换行,echo 默认会将它作为普通字符对待。请看下面的例子:
1 | [root@localhost ~]# echo "hello \nworld" |
我们可以添加-e
参数来让 echo 命令解析转义字符。例如:
1 | [root@localhost ~]# echo -e "hello \nworld" |
\c 转义字符
有了-e参数,我们也可以使用转义字符\c来强制 echo 命令不换行了。请看下面的例子:
1 |
|
运行结果:
1 | Tom is 20 years old, 175cm in height and 62kg in weight. |
read命令:读取从键盘输入的数据
read 是 Shell 内置命令,用来从标准输入中读取数据并赋值给变量。如果没有进行重定向,默认就是从键盘读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据。
后续我们会在《Linux Shell重定向》一节中深入讲解重定向的概念,不了解的读者可以不用理会,暂时就认为:read 命令就是从键盘读取数据。
read 命令的用法为:
1 | read [-options] [variables] |
options
表示选项,如下表所示;variables
表示用来存储数据的变量,可以有一个,也可以有多个。
options
和variables
都是可选的,如果没有提供变量名,那么读取的数据将存放到环境变量 REPLY 中。
选项 | 说明 |
---|---|
-a array | 把读取的数据赋值给数组 array,从下标 0 开始。 |
-d delimiter | 用字符串 delimiter 指定读取结束的位置,而不是一个换行符(读取到的数据不包括 delimiter)。 |
-e | 在获取用户输入的时候,对功能键进行编码转换,不会直接显式功能键对应的字符。 |
-n num | 读取 num 个字符,而不是整行字符。 |
-p prompt | 显示提示信息,提示内容为 prompt。 |
-r | 原样读取(Raw mode),不把反斜杠字符解释为转义字符。 |
-s | 静默模式(Silent mode),不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这是很有必要的。 |
-t seconds | 设置超时时间,单位为秒。如果用户没有在指定时间内输入完成,那么 read 将会返回一个非 0 的退出状态,表示读取失败。 |
-u fd | 使用文件描述符 fd 作为输入源,而不是标准输入,类似于重定向。 |
【实例1】使用 read 命令给多个变量赋值。
1 |
|
运行结果:
1 | lautung@PC:~$ sudo bash test2.sh |
注意,必须在一行内输入所有的值,不能换行,否则只能给第一个变量赋值,后续变量都会赋值失败。
本例还使用了-p
选项,该选项会用一段文本来提示用户输入。
【示例2】只读取一个字符。
1 | #!/bin/bash |
运行结果:
1 | lautung@PC:~$ sudo bash test3.sh |
-n 1
表示只读取一个字符。运行脚本后,只要用户输入一个字符,立即读取结束,不用等待用户按下回车键。
printf "\n"
语句用来达到换行的效果,否则 echo 的输出结果会和用户输入的内容位于同一行,不容易区分。
【实例3】在指定时间内输入密码。
1 |
|
这段代码中,我们使用&&
组合了多个命令,这些命令会依次执行,并且从整体上作为 if 语句的判断条件,只要其中一个命令执行失败(退出状态为非 0 值),整个判断条件就失败了,后续的命令也就没有必要执行了。
如果两次输入密码相同,运行结果为:
Enter password in 20 seconds(once) >
Enter password in 20 seconds(again)>
Valid password
如果两次输入密码不同,运行结果为:
Enter password in 20 seconds(once) >
Enter password in 20 seconds(again)>
Invalid password
如果第一次输入超时,运行结果为:
Enter password in 20 seconds(once) > Invalid password
如果第二次输入超时,运行结果为:
Enter password in 20 seconds(once) >
Enter password in 20 seconds(again)> Invalid password
exit命令:退出当前进程
exit 是一个 Shell 内置命令,用来退出当前 Shell 进程,并返回一个退出状态;使用$?可以接收这个退出状态。
exit 命令可以接受一个整数值作为参数,代表退出状态。如果不指定,默认状态值是 0。
exit 退出状态只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。
Shell 进程执行出错时,可以根据退出状态来判断具体出现了什么错误,比如打开一个文件时,我们可以指定 1 表示文件不存在,2 表示文件没有读取权限,3 表示文件类型不对。
编写下面的脚本,并命名为 test.sh:
1 |
|
运行该脚本
1 | $ bash ./test.sh |
可以看到,”after exit
“并没有输出,这说明遇到 exit 命令后,test.sh 执行就结束了。
注意,exit 表示退出当前 Shell 进程,我们必须在新进程中运行 test.sh,否则当前 Shell 会话(终端窗口)会被关闭,我们就无法看到输出结果了。
我们可以紧接着使用$?
来获取 test.sh 的退出状态:
1 | $ echo $? |
declare和typeset命令:设置变量属性
declare 和 typeset 都是 Shell 内建命令,它们的用法相同,都用来设置变量的属性。不过 typeset 已经被弃用了,建议使用 declare 代替。
declare 命令的用法如下所示:
1 | declare [+/-] [aAfFgilprtux] [变量名=变量值] |
其中,-
表示设置属性,+
表示取消属性,aAfFgilprtux
都是具体的选项,它们的含义如下表所示:
选项 | 含义 |
---|---|
-f [name] | 列出之前由用户在脚本中定义的函数名称和函数体。 |
-F [name] | 仅列出自定义函数名称。 |
-g name | 在 Shell 函数内部创建全局变量。 |
-p [name] | 显示指定变量的属性和值。 |
-a name | 声明变量为普通数组。 |
-A name | 声明变量为关联数组(支持索引下标为字符串)。 |
-i name | 将变量定义为整数型。 |
-r name[=value] | 将变量定义为只读(不可修改和删除),等价于 readonly name。 |
-x name[=value] | 将变量设置为环境变量,等价于 export name[=value]。 |
【实例1】将变量声明为整数并进行计算。
1 |
|
运行结果:
40
【实例2】将变量定义为只读变量
1 | $ declare -r n=10 |
【实例3】显示变量的属性和值。
1 | $ declare -r n=10 |
数学计算(算术运算,四则运算)
如果要执行算术运算(数学计算),就离不开各种运算符号,和其他编程语言类似,Shell 也有很多算术运算符,下面就给大家介绍一下常见的 Shell 算术运算符,如下表所示。
算术运算符 | 说明/含义 |
---|---|
+、- | 加法(或正号)、减法(或负号) |
*、/、% | 乘法、除法、取余(取模) |
** | 幂运算 |
++、– | 自增和自减,可以放在变量的前面也可以放在变量的后面 |
!、&&、|| | 逻辑非(取反)、逻辑与(and)、逻辑或(or) |
<、<=、>、>= | 比较符号(小于、小于等于、大于、大于等于) |
==、!=、= | 比较符号(相等、不相等;对于字符串,= 也可以表示相当于) |
<<、>> | 向左移位、向右移位 |
~、|、 &、^ | 按位取反、按位或、按位与、按位异或 |
=、+=、-=、*=、/=、%= | 赋值运算符,例如 a+=1 相当于 a=a+1,a-=1 相当于 a=a-1 |
注意,在 Bash Shell 中,如果不特别指明,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。所以,但是,Shell 和其它编程语言不同,Shell 不能直接进行算数运算,必须使用数学计算命令。
数学计算命令
要想让数学计算发挥作用,必须使用数学计算命令,Shell 中常用的数学计算命令如下表所示。
运算操作符/运算命令 | 说明 |
---|---|
(( )) | 用于整数运算,效率很高,推荐使用。 |
let | 用于整数运算,和 (()) 类似。 |
$[] | 用于整数运算,不如 (()) 灵活。 |
expr | 可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。 |
bc | Linux下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。 |
declare -i | 将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用。 |
如果大家时间有限,只学习 (()) 和 bc 即可,不用学习其它的了:(()) 可以用于整数计算,bc 可以小数计算。
(()):对整数进行数学运算
(( ))
只能进行整数运算,不能对小数(浮点数)或者字符串进行运算。
双小括号 (( ))
的语法格式为:
1 | ((表达式)) |
通俗地讲,就是将数学运算表达式放在((和))之间。
表达式可以只有一个,也可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个 (( )) 命令的执行结果。
可以使用$获取 (( ))
命令的结果,这和使用$
获得变量值是类似的。
运算操作符/运算命令 | 说明 |
---|---|
((a=10+66) | |
((b=a-15)) | |
((c=a+b)) | 这种写法可以在计算完成后给变量赋值。以 ((b=a-15)) 为例,即将 a-15 的运算结果赋值给变量 c。 |
注意,使用变量时不用加$
前缀,(( )) 会自动解析变量名。 |
| a=$((10+66)
b=$((a-15))
c=$((a+b)) | 可以在 (( )) 前面加上$
符号获取 (( )) 命令的执行结果,也即获取整个表达式的值。以 c=$((a+b)) 为例,即将 a+b 这个表达式的运算结果赋值给变量 c。
注意,类似 c=((a+b)) 这样的写法是错误的,不加$
就不能取得表达式的结果。 |
| ((a>7 && b==c)) | (( )) 也可以进行逻辑运算,在 if 语句中常会使用逻辑运算。 |
| echo $((a+10)) | 需要立即输出表达式的运算结果时,可以在 (( )) 前面加$
符号。 |
| ((a=3+5, b=a+10)) | 对多个表达式同时进行计算。 |
在 (( )) 中使用变量无需加上$前缀,(( )) 会自动解析变量名,这使得代码更加简洁,也符合程序员的书写习惯。
let命令:对整数进行数学运算
let 命令和双小括号 (( ))
的用法是类似的,它们都是用来对整数进行运算,读者已经学习了《Shell (())》,再学习 let 命令就相当简单了。
注意:和双小括号 (( )) 一样,let 命令也只能进行整数运算,不能对小数(浮点数)或者字符串进行运算。
Shell let 命令的语法格式为:
1 | let 表达式 |
它们都等价于((表达式))
。
当表达式中含有 Shell 特殊字符(例如 |)时,需要用双引号” “或者单引号’ ‘将表达式包围起来。
和 (( )) 类似,let 命令也支持一次性计算多个表达式,并且以最后一个表达式的值作为整个 let 命令的执行结果。但是,对于多个表达式之间的分隔符,let 和 (( )) 是有区别的:
- let 命令以空格来分隔多个表达式;
- (( )) 以逗号
,
来分隔多个表达式。
另外还要注意,对于类似let x+y
这样的写法,Shell 虽然计算了 x+y
的值,但却将结果丢弃;若不想这样,可以使用let sum=x+y
将 x+y 的结果保存在变量 sum 中。
这种情况下 (( )) 显然更加灵活,可以使用$((x+y))来获取 x+y 的结果。请看下面的例子:
1 | $ a=10 b=20 |
【实例1】给变量 i 加 8:
1 | $ i=2 |
let i+=8
等同于((i+=8))
,但后者效率更高。
【实例2】let 后面可以跟多个表达式。
1 | $ a=10 b=35 |
$[]:对整数进行数学运算
和 (())、let 命令类似,$[] 也只能进行整数运算。
Shell $[] 的用法如下:$[表达式]
$[] 会对表达式进行计算,并取得计算结果。如果表达式中包含了变量,那么你可以加$,也可以不加。
Shell $[] 举例:
1 | $ echo $[3*5] #直接输出结算结果 |
需要注意的是,不能单独使用 $[],必须能够接收 $[] 的计算结果。例如,下面的用法是错误的:
1 | $ $[3+4] |
expr命令:对整数进行运算
expr 是 evaluate expressions 的缩写,译为“表达式求值”。Shell expr 是一个功能强大,并且比较复杂的命令,它除了可以实现整数计算,还可以结合一些选项对字符串进行处理,例如计算字符串长度、字符串比较、字符串匹配、字符串提取等。
本节只讲解 expr 在整数计算方面的应用,并不涉及字符串处理,有兴趣的读者请自行研究。
Shell expr 对于整数计算的用法为:
1 | expr 表达式 |
expr 对表达式
的格式有几点特殊的要求:
- 出现在
表达式
中的运算符、数字、变量和小括号的左右两边至少要有一个空格,否则会报错。 - 有些特殊符号必须用反斜杠
\
进行转义(屏蔽其特殊含义),比如乘号*
和小括号()
,如果不用\
转义,那么 Shell 会把它们误解为正则表达式中的符号(*
对应通配符,()
对应分组)。 - 使用变量时要加
$
前缀。
【实例1】expr 整数计算简单举例:
1 | ~$ expr 2 +3 #错误:加号和 3 之前没有空格 |
以上是直接使用 expr 命令,计算结果会直接输出,如果你希望将计算结果赋值给变量,那么需要将整个表达式用反引号````(位于 Tab 键的上方)包围起来,请看下面的例子。
【实例2】将 expr 的计算结果赋值给变量:
1 | ~$ m=5 |
你看,使用 expr 进行数学计算是多么的麻烦呀,需要注意各种细节,我奉劝大家还是省省心,老老实实用 (())、let 或者 $[] 吧。
bc命令:一款数学计算器
Bash Shell 内置了对整数运算的支持,但是并不支持浮点运算,而 Linux bc 命令可以很方便的进行浮点运算,当然整数运算也不再话下。
bc 甚至可以称得上是一种编程语言了,它支持变量、数组、输入输出、分支结构、循环结构、函数等基本的编程元素,所以 Linux 手册中是这样来描述 bc 的:
1 | An arbitrary precision calculator language |
翻译过来就是“一个任意精度的计算器语言”。
在终端输入bc
命令,然后回车即可进入 bc 进行交互式的数学计算。在 Shell 编程中,我们也可以通过管道和输入重定向来使用 bc。
1.
从终端进入 bc
bc 命令还有一些选项,可能你会用到,请看下表。
选项 | 说明 |
---|---|
-h | –help |
-v | –version |
-l | –mathlib |
-i | –interactive |
-w | –warn |
-s | –standard |
-q | –quiet |
例如你不想输入 bc 命令后显示一堆没用的信息,那么可以输入bc -q:
2.
在交互式环境下使用 bc
使用 bc 进行数学计算是非常容易的,像平常一样输入数学表达式,然后按下回车键就可以看到结果,请看下图。
值得一提的是,我们定义了一个变量 n,然后在计算中也使用了 n,可见 bc 是支持变量的。
bc还支持函数,暂时不写了…个人用不上。
选择结构
Shell 也支持选择结构,并且有两种形式,分别是 if else
语句和 case in
语句。
if else语句
if 语句
最简单的用法就是只使用 if 语句,它的语法格式为:
1 | if condition |
condition
是判断条件,如果 condition 成立(返回“真”),那么 then 后边的语句将会被执行;如果 condition 不成立(返回“假”),那么不会执行任何语句。从本质上讲,if 检测的是命令的退出状态。注意,最后必须以fi
来闭合,fi 就是 if 倒过来拼写。也正是有了 fi 来结尾,所以即使有多条语句也不需要用{ }
包围起来。
也可以如下形式编写:
1 | if condition; then |
请注意 condition 后边的分号;,当 if 和 then 位于同一行的时候,这个分号是必须的,否则会有语法错误。
【实例1】
1 |
|
运行结果:
1 | lautung@PC:~$ sudo sh test.sh |
if else 语句
如果有两个分支,就可以使用 if else 语句,它的格式为:
1 | if condition |
如果 condition 成立,那么 then 后边的 statement1 语句将会被执行;否则,执行 else 后边的 statement2 语句。
举个例子:
1 |
|
运行结果:
10↙
20↙
a 和 b 不相等,输入错误
从运行结果可以看出,a 和 b 不相等,判断条件不成立,所以执行了 else 后边的语句。
if elif else 语句
Shell 支持任意数目的分支,当分支比较多时,可以使用 if elif else 结构,它的格式为:
1 | if condition1 |
注意,if 和 elif 后边都得跟着 then。
整条语句的执行逻辑为:
- 如果 condition1 成立,那么就执行 if 后边的 statement1;如果 condition1 不成立,那么继续执行 elif,判断 condition2。
- 如果 condition2 成立,那么就执行 statement2;如果 condition2 不成立,那么继续执行后边的 elif,判断 condition3。
- 如果 condition3 成立,那么就执行 statement3;如果 condition3 不成立,那么继续执行后边的 elif。
- 如果所有的 if 和 elif 判断都不成立,就进入最后的 else,执行 statementn。
举个例子,输入年龄,输出对应的人生阶段:
1 |
|
运行结果1:
19
成年
运行结果2:
100
老年
再举一个例子,输入一个整数,输出该整数对应的星期几的英文表示:
1 |
|
运行结果1:
Input integer number: 4
Thursday
运行结果2:
Input integer number: 9
error
Shell退出状态
每一条 Shell 命令,不管是 Bash 内置命令(例如 cd、echo),还是外部的 Linux 命令(例如 ls、awk),还是自定义的 Shell 函数,当它退出(运行结束)时,都会返回一个比较小的整数值给调用(使用)它的程序,这就是命令的退出状态(exit statu)。
很多 Linux 命令其实就是一个C语言程序,熟悉C语言的读者都知道,main() 函数的最后都有一个return 0,如果程序想在中间退出,还可以使用exit 0,这其实就是C语言程序的退出状态。当有其它程序调用这个程序时,就可以捕获这个退出状态。
if 语句的判断条件,从本质上讲,判断的就是命令的退出状态。
按照惯例来说,退出状态为 0 表示“成功”;也就是说,程序执行完成并且没有遇到任何问题。除 0 以外的其它任何退出状态都为“失败”。
之所以说这是“惯例”而非“规定”,是因为也会有例外,比如 diff 命令用来比较两个文件的不同,对于“没有差别”的文件返回 0,对于“找到差别”的文件返回 1,对无效文件名返回 2。
有编程经验的读者请注意,Shell 的这个部分与你所熟悉的其它编程语言正好相反:在C语言、C++、Java、Python 中,0 表示“假”,其它值表示“真”。
在 Shell 中,有多种方式取得命令的退出状态,其中 $? 是最常见的一种。上节《if else》中使用了 (()) 进行数学计算,我们不妨来看一下它的退出状态。请看下面的代码:
1 |
|
运行结果1:
26
26
退出状态:0
运行结果2:
17
39
退出状态:1
退出状态和逻辑运算符的组合
Shell if 语句的一个神奇之处是允许我们使用逻辑运算符将多个退出状态组合起来,这样就可以一次判断多个条件了。
运算符 | 使用格式 | 说明 |
---|---|---|
&& | expression1 && expression2 | 逻辑与运算符,当 expression1 和 expression2 同时成立时,整个表达式才成立。 |
如果检测到 expression1 的退出状态为 0,就不会再检测 expression2 了,因为不管 expression2 的退出状态是什么,整个表达式必然都是不成立的,检测了也是多此一举。 |
| || | expression1 || expression2 | 逻辑或运算符,expression1 和 expression2 两个表达式中只要有一个成立,整个表达式就成立。
如果检测到 expression1 的退出状态为 1,就不会再检测 expression2 了,因为不管 expression2 的退出状态是什么,整个表达式必然都是成立的,检测了也是多此一举。 |
| ! | !expression | 逻辑非运算符,相当于“取反”的效果。如果 expression 成立,那么整个表达式就不成立;如果 expression 不成立,那么整个表达式就成立。 |
test命令(Shell [])
test 是 Shell 内置命令,用来检测某个条件是否成立。test 通常和 if 语句一起使用,并且大部分 if 语句都依赖 test。
test 命令有很多选项,可以进行数值、字符串和文件三个方面的检测。
Shell test 命令的用法为:
1 | test expression |
当 test 判断 expression 成立时,退出状态为 0,否则为非 0 值。
test 命令也可以简写为[]
,它的用法为:
1 | [ expression ] |
注意[]和expression之间的空格,这两个空格是必须的,否则会导致语法错误。[]的写法更加简洁,比 test 使用频率高。
test 和 [] 是等价的,后续我们会交替使用 test 和 [],以让读者尽快熟悉。
在《if else》中,我们使用 (()) 进行数值比较,这节我们就来看一下如何使用 test 命令进行数值比较。
1 |
|
其中,-le
选项表示小于等于,-ge
选项表示大于等于,&&
是逻辑与运算符。
学习 test 命令,重点是学习它的各种选项,下面我们就逐一讲解。
与文件检测相关的 test 选项
文件类型判断 |
---|
选 项 |
-b filename |
-c filename |
-d filename |
-e filename |
-f filename |
-L filename |
-p filename |
-s filename |
-S filename |
文件权限判断 |
选 项 |
-r filename |
-w filename |
-x filename |
-u filename |
-g filename |
-k filename |
文件比较 |
选 项 |
filename1 -nt filename2 |
filename -ot filename2 |
filename1 -ef filename2 |
Shell test 文件检测举例:
1 |
|
在 Shell 脚本文件所在的目录新建一个文本文件并命名为 urls.txt,然后运行 Shell 脚本,运行结果为:
urls.txt↙
lautung.com↙
写入成功
与数值比较相关的 test 选项
选 项 | 作 用 |
---|---|
num1 -eq num2 | 判断 num1 是否和 num2 相等。 |
num1 -ne num2 | 判断 num1 是否和 num2 不相等。 |
num1 -gt num2 | 判断 num1 是否大于 num2 。 |
num1 -lt num2 | 判断 num1 是否小于 num2。 |
num1 -ge num2 | 判断 num1 是否大于等于 num2。 |
num1 -le num2 | 判断 num1 是否小于等于 num2。 |
注意,test 只能用来比较整数,小数相关的比较还得依赖 bc 命令。
Shell test 数值比较举例:
1 |
|
运行结果1:
10 10
两个数相等
运行结果2:
10 20
两个数不相等
与字符串判断相关的 test 选项
选 项 | 作 用 |
---|---|
-z str | 判断字符串 str 是否为空。 |
-n str | 判断宇符串 str 是否为非空。 |
str1 = str2 | |
str1 == str2 | = 和== 是等价的,都用来判断 str1 是否和 str2 相等。 |
str1 != str2 | 判断 str1 是否和 str2 不相等。 |
str1 \> str2 | 判断 str1 是否大于 str2。\> 是> 的转义字符,这样写是为了防止> 被误认为成重定向运算符。 |
str1 \< str2 | 判断 str1 是否小于 str2。同样,\< 也是转义字符。 |
有C语言、C++、Python、Java 等编程经验的读者请注意,==、>、< 在大部分编程语言中都用来比较数字,而在 Shell 中,它们只能用来比较字符串,不能比较数字,这是非常奇葩的,大家要习惯。
其次,不管是比较数字还是字符串,Shell 都不支持 >= 和 <= 运算符,切记。
Shell test 字符串比较举例:
1 |
|
运行结果:
lautung111
lautung222
两个字符串不相等
细心的读者可能已经注意到,变量 $str1 和 $str2 都被双引号包围起来,这样做是为了防止 $str1 或者 $str2 是空字符串时出现错误,本文的后续部分将为你分析具体原因。
与逻辑运算相关的 test 选项
选 项 | 作 用 |
---|---|
expression1 -a expression | 逻辑与,表达式 expression1 和 expression2 都成立,最终的结果才是成立的。 |
expression1 -o expression2 | 逻辑或,表达式 expression1 和 expression2 有一个成立,最终的结果就成立。 |
!expression | 逻辑非,对 expression 进行取反。 |
改写上面的代码,使用逻辑运算选项:
1 |
|
前面的代码我们使用两个[]
命令,并使用||
运算符将它们连接起来,这里我们改成-o
选项,只使用一个[]
命令就可以了。
在 test 中使用变量建议用双引号包围起来
test 和 [] 都是命令,一个命令本质上对应一个程序或者一个函数。即使是一个程序,它也有入口函数,例如C语言程序的入口函数是 main(),运行C语言程序就从 main() 函数开始,所以也可以将一个程序等效为一个函数,这样我们就不用再区分函数和程序了,直接将一个命令和一个函数对应起来即可。
有了以上认知,就很容易看透命令的本质了:使用一个命令其实就是调用一个函数,命令后面附带的选项和参数最终都会作为实参传递给函数。
假设 test 命令对应的函数是 func(),使用test -z $str1
命令时,会先将变量 $str1 替换成字符串:
- 如果 $str1 是一个正常的字符串,比如 abc123,那么替换后的效果就是
test -z abc123
,调用 func() 函数的形式就是func("-z abc123")
。test 命令后面附带的所有选项和参数会被看成一个整体,并作为实参传递进函数。 - 如果 $str1 是一个空字符串,那么替换后的效果就是test -z,调用 func() 函数的形式就是func(“-z “),这就比较奇怪了,因为-z选项没有和参数成对出现,func() 在分析时就会出错。
如果我们给 $str1 变量加上双引号,当 $str1 是空字符串时,test -z "$str1"
就会被替换为test -z ""
,调用 func() 函数的形式就是func(“-z ""“),很显然,-z
选项后面跟的是一个空字符串("
表示转义字符),这样 func() 在分析时就不会出错了。
所以,当你在 test 命令中使用变量时,我强烈建议将变量用双引号""
包围起来,这样能避免变量为空值时导致的很多奇葩问题。
总结
test 命令比较奇葩,>、<、== 只能用来比较字符串,不能用来比较数字,比较数字需要使用 -eq、-gt 等选项;不管是比较字符串还是数字,test 都不支持 >= 和 <=。有经验的程序员需要慢慢习惯 test 命令的这些奇葩用法。
对于整型数字的比较,我建议大家使用 (()),这在《Shell if else》中已经进行了演示。(()) 支持各种运算符,写法也符合数学规则,用起来更加方便,何乐而不为呢?
几乎完全兼容 test ,并且比 test 更加强大,比 test 更加灵活的是[[ ]]
;[[ ]]
不是命令,而是 Shell 关键字,下节《Shell [[]]》我们将会讲解。
[[]]详解:检测某个条件是否成立
[[ ]]
是 Shell 内置关键字,它和 test 命令类似,也用来检测某个条件是否成立。
test 能做到的,[[ ]] 也能做到,而且 [[ ]] 做的更好;test 做不到的,[[ ]] 还能做到。可以认为 [[ ]] 是 test 的升级版,对细节进行了优化,并且扩展了一些功能。
[[ ]] 的用法为:
1 | [[ expression ]] |
当 [[ ]] 判断 expression 成立时,退出状态为 0,否则为非 0 值。注意[[ ]]
和expression
之间的空格,这两个空格是必须的,否则会导致语法错误。
[[ ]] 不需要注意某些细枝末节
[[ ]] 是 Shell 内置关键字,不是命令,在使用时没有给函数传递参数的过程,所以 test 命令的某些注意事项在 [[ ]] 中就不存在了,具体包括:
- 不需要把变量名用双引号
""
包围起来,即使变量是空值,也不会出错。 - 不需要、也不能对 >、< 进行转义,转义后会出错。
请看下面的演示代码:
1 |
|
[[ ]] 支持逻辑运算符
对多个表达式进行逻辑运算时,可以使用逻辑运算符将多个 test 命令连接起来,例如:
1 | [ -z "$str1" ] || [ -z "$str2" ] |
你也可以借助选项把多个表达式写在一个 test 命令中,例如:
1 | [ -z "$str1" -o -z "$str2" ] |
但是,这两种写法都有点“别扭”,完美的写法是在一个命令中使用逻辑运算符将多个表达式连接起来。我们的这个愿望在 [[ ]] 中实现了,[[ ]] 支持 &&、|| 和 ! 三种逻辑运算符。
使用 [[ ]] 对上面的语句进行改进:
1 | [[ -z $str1 || -z $str2 ]] |
这种写法就比较简洁漂亮了。
**注意,[[ ]] 剔除了 test 命令的-o和-a选项,你只能使用 || 和 &&**。这意味着,你不能写成下面的形式:
1 | [[ -z $str1 -o -z $str2 ]] |
当然,使用逻辑运算符将多个 [[ ]] 连接起来依然是可以的,因为这是 Shell 本身提供的功能,跟 [[ ]] 或者 test 没有关系,如下所示:
1 | [[ -z $str1 ]] || [[ -z $str2 ]] |
test 或 [] | [[ ]] |
---|---|
[ -z “$str1” ] || [ -z “$str2” ] | √ |
[ -z “$str1” -o -z “$str2” ] | √ |
[ -z $str1 || -z $str2 ] | × |
[[ ]] 支持正则表达式
在 Shell [[ ]] 中,可以使用=~
来检测字符串是否符合某个正则表达式,它的用法为:
1 | [[ str =~ regex ]] |
str 表示字符串,regex 表示正则表达式。
下面的代码检测一个字符串是否是手机号:
1 |
|
对^1[0-9]{10}$
的说明:
^
匹配字符串的开头(一个位置);[0-9]{10}
匹配连续的十个数字;$
匹配字符串的末尾(一个位置)。
总结
有了 [[ ]],你还有什么理由使用 test 或者 [ ],[[ ]] 完全可以替代之,而且更加方便,更加强大。
但是 [[ ]] 对数字的比较仍然不友好,所以我建议,以后大家使用 if 判断条件时,用 (()) 来处理整型数字,用 [[ ]] 来处理字符串或者文件。
case in语句
case in 的用法,它的基本格式如下:
1 | case expression in |
case、in 和 esac 都是 Shell 关键字,expression 表示表达式,pattern 表示匹配模式。
- expression 既可以是一个变量、一个数字、一个字符串,还可以是一个数学计算表达式,或者是命令的执行结果,只要能够得到 expression 的值就可以。
- pattern 可以是一个数字、一个字符串,甚至是一个简单的正则表达式。
case 会将 expression 的值与 pattern1、pattern2、pattern3 逐个进行匹配:
- 如果 expression 和某个模式(比如 pattern2)匹配成功,就会执行这模式(比如 pattern2)后面对应的所有语句(该语句可以有一条,也可以有多条),直到遇见双分号;;才停止;然后整个 case 语句就执行完了,程序会跳出整个 case 语句,执行 esac 后面的其它语句。
- 如果 expression 没有匹配到任何一个模式,那么就执行_)后面的语句(_表示其它所有值),直到遇见双分号;;或者esac才结束。*)相当于多个 if 分支语句中最后的 else 部分。
;;
和*)
就相当于其它编程语言中的 break 和 default。
对*)的几点说明:
- Shell case in 语句中的_)用来“托底”,万一 expression 没有匹配到任何一个模式,_)部分可以做一些“善后”工作,或者给用户一些提示。
- 可以没有*)部分。如果 expression 没有匹配到任何一个模式,那么就不执行任何操作。
除最后一个分支外(这个分支可以是普通分支,也可以是*)
分支),其它的每个分支都必须以;;
结尾,;;
代表一个分支的结束,不写的话会有语法错误。最后一个分支可以写;;
,也可以不写,因为无论如何,执行到 esac 都会结束整个 case in 语句。
上面的代码是 case in 最常见的用法,即 expression 部分是一个变量,pattern 部分是一个数字或者表达式。
case in 和正则表达式
case in 的 pattern 部分支持简单的正则表达式,具体来说,可以使用以下几种格式:
格式 | 说明 |
---|---|
* | 表示任意字符串。 |
[abc] | 表示 a、b、c 三个字符中的任意一个。比如,[15ZH] 表示 1、5、Z、H 四个字符中的任意一个。 |
[m-n] | 表示从 m 到 n 的任意一个字符。比如,[0-9] 表示任意一个数字,[0-9a-zA-Z] 表示字母或数字。 |
| | 表示多重选择,类似逻辑运算中的或运算。比如,abc | xyz 表示匹配字符串 “abc” 或者 “xyz”。 |
如果不加以说明,Shell 的值都是字符串,expression 和 pattern 也是按照字符串的方式来匹配的;本节第一段代码看起来是判断数字是否相等,其实是判断字符串是否相等。
最后一个分支*)
并不是什么语法规定,它只是一个正则表达式,*
表示任意字符串,所以不管 expression 的值是什么,*)
总能匹配成功。
下面的例子演示了如何在 case in 中使用正则表达式:
1 |
|
运行结果1:
Input integer number: S
letter
运行结果2:
Input integer number: ,
Punctuation
循环结构
while循环详解
while 循环是 Shell 脚本中最简单的一种循环,当条件满足时,while 重复地执行一组语句,当条件不满足时,就退出 while 循环。
Shell while 循环的用法如下:
1 | while condition |
condition表示判断条件,statements表示要执行的语句(可以只有一条,也可以有多条),do和done都是 Shell 中的关键字。
while 循环的执行流程为:
- 先对 condition 进行判断,如果该条件成立,就进入循环,执行 while 循环体中的语句,也就是 do 和 done 之间的语句。这样就完成了一次循环。
- 每一次执行到 done 的时候都会重新判断 condition 是否成立,如果成立,就进入下一次循环,继续执行 do 和 done 之间的语句,如果不成立,就结束整个 while 循环,执行 done 后面的其它 Shell 代码。
- 如果一开始 condition 就不成立,那么程序就不会进入循环体,do 和 done 之间的语句就没有执行的机会。
注意,在 while 循环体中必须有相应的语句使得 condition 越来越趋近于“不成立”,只有这样才能最终退出循环,否则 while 就成了死循环,会一直执行下去,永无休止。
while 语句和 if else 语句中的 condition 用法都是一样的,你可以使用 test 或 [] 命令,也可以使用 (()) 或 [[]]。
until循环
unti 循环和 while 循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立,就终止循环。
until 的使用场景很少,一般使用 while 即可。
Shell until 循环的用法如下:
1 | until condition |
condition表示判断条件,statements表示要执行的语句(可以只有一条,也可以有多条),do和done都是 Shell 中的关键字。
until 循环的执行流程为:
- 先对 condition 进行判断,如果该条件不成立,就进入循环,执行 until 循环体中的语句(do 和 done 之间的语句),这样就完成了一次循环。
- 每一次执行到 done 的时候都会重新判断 condition 是否成立,如果不成立,就进入下一次循环,继续执行循环体中的语句,如果成立,就结束整个 until 循环,执行 done 后面的其它 Shell 代码。
- 如果一开始 condition 就成立,那么程序就不会进入循环体,do 和 done 之间的语句就没有执行的机会。
注意,在 until 循环体中必须有相应的语句使得 condition 越来越趋近于“成立”,只有这样才能最终退出循环,否则 until 就成了死循环,会一直执行下去,永无休止。
for循环
for 循环的用法如下:
1 | for((exp1; exp2; exp3)) |
几点说明:
- exp1、exp2、exp3 是三个表达式,其中 exp2 是判断条件,for 循环根据 exp2 的结果来决定是否继续下一次循环;
- statements 是循环体语句,可以有一条,也可以有多条;
- do 和 done 是 Shell 中的关键字。
for in 循环
for in 循环的用法如下:
1 | for variable in value_list |
variable 表示变量,value_list 表示取值列表,in 是 Shell 中的关键字。
对 value_list 的说明
取值列表 value_list 的形式有多种,你可以直接给出具体的值,也可以给出一个范围,还可以使用命令产生的结果,甚至使用通配符:
- 直接给出具体的值
可以在 in 关键字后面直接给出具体的值,多个值之间以空格分隔,比如1 2 3 4 5
、"abc" "390" "tom"
等。
2. 给出一个取值范围
给出一个取值范围的具体格式为:```bash
{start..end}
1 |
|
variable 表示变量,value_list 表示取值列表,in 是 Shell 中的关键字。
我们先来看一个 select in 循环的例子:
1 |
|
运行结果:
1 | What is your favourite OS? |
#?
用来提示用户输入菜单编号;^D
表示按下 Ctrl+D 组合键,它的作用是结束 select in 循环。
运行到 select 语句后,取值列表 value_list 中的内容会以菜单的形式显示出来,用户输入菜单编号,就表示选中了某个值,这个值就会赋给变量 variable,然后再执行循环体中的 statements(do 和 done 之间的部分)。
每次循环时 select 都会要求用户输入菜单编号,并使用环境变量 PS3 的值作为提示符,PS3 的默认值为#?
,修改 PS3 的值就可以修改提示符。
如果用户输入的菜单编号不在范围之内,例如上面我们输入的 9,那么就会给 variable 赋一个空值;如果用户输入一个空值(什么也不输入,直接回车),会重新显示一遍菜单。
注意,select 是无限循环(死循环),输入空值,或者输入的值无效,都不会结束循环,只有遇到 break 语句,或者按下 Ctrl+D 组合键才能结束循环。
完整示例
完整实例
select in 通常和 case in 一起使用,在用户输入不同的编号时可以做出不同的反应。
修改上面的代码,加入 case in 语句:
1 |
|
用户只有输入正确的编号才会结束循环,如果输入错误,会要求重新输入。
运行结果1,输入正确选项:
1 | What is your favourite OS? |
运行结果2,输入错误选项:
1 | What is your favourite OS? |
运行结果3,输入空值:
1 | What is your favourite OS? |
break和continue跳出循环
使用 while、until、for、select 循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用 break 或者 continue 关键字。
在C语言、C++、C#、Python、Java 等大部分编程语言中,break 和 continue 只能跳出当前层次的循环,内层循环中的 break 和 continue 对外层循环不起作用;但是 Shell 中的 break 和 continue 却能够跳出多层循环,也就是说,内层循环中的 break 和 continue 能够跳出外层循环。
在实际开发中,break 和 continue 一般只用来跳出当前层次的循环,很少有需要跳出多层循环的情况。
break 关键字
Shell break 关键字的用法为:
1 | break n |
n 表示跳出循环的层数,如果省略 n,则表示跳出当前的整个循环。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。
continue 关键字
Shell continue 关键字的用法为:
continue n
n 表示循环的层数:
- 如果省略 n,则表示 continue 只对当前层次的循环语句有效,遇到 continue 会跳过本次循环,忽略本次循环的剩余代码,直接进入下一次循环。
- 如果带上 n,比如 n 的值为 2,那么 continue 对内层和外层循环语句都有效,不但内层会跳过本次循环,外层也会跳过本次循环,其效果相当于内层循环和外层循环同时执行了不带 n 的 continue。这么说可能有点难以理解,稍后我们通过代码来演示。
continue 关键字也通常和 if 语句一起使用,即满足条件时便跳出循环。
函数
Shell 函数的本质是一段可以重复使用的脚本代码,这段代码被提前编写好了,放在了指定的位置,使用时直接调取即可。
Shell 中的函数和C++、Java、Python、C# 等其它编程语言中的函数类似,只是在语法细节有所差别。
函数定义
Shell 函数定义的语法格式如下:
1 | function name() { |
对各个部分的说明:
- function是 Shell 中的关键字,专门用来定义函数;
- name是函数名;
- statements是函数要执行的代码,也就是一组语句;
- return value表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写。
由{ }
包围的部分称为函数体,调用一个函数,实际上就是执行函数体中的代码。
如果你嫌麻烦,函数定义时也可以不写 function 关键字:
1 | name() { |
如果写了 function 关键字,也可以省略函数名后面的小括号:
1 | function name { |
我建议使用标准的写法,这样能够做到“见名知意”,一看就懂。
函数调用
调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:
1 | name |
如果传递参数,那么多个参数之间以空格分隔:
1 | name param1 param2 param3 |
不管是哪种形式,函数名字后面都不需要带括号。
和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。
Shell 也不限制定义和调用的顺序,你可以将定义放在调用的前面,也可以反过来,将定义放在调用的后面。
函数参数
函数参数是 Shell 位置参数的一种,在函数内部可以使用$n来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。
除了$n,还有另外三个比较重要的变量:
- $#可以获取传递的参数的个数;
- $@或者$*可以一次性获取所有的参数
具体情况参考特殊变量。
函数返回值
Shell 中的返回值表示的是函数的退出状态:返回值为 0 表示函数执行成功了,返回值为非 0 表示函数执行失败(出错)了。if、while、for 等语句都是根据函数的退出状态来判断条件是否成立。
Shell 函数的返回值只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。
如果函数体中没有 return 语句,那么使用默认的退出状态,也就是最后一条命令的退出状态。如果这就是你想要的,那么更加严谨的写法为:
1 | return $? |
$?
是一个特殊变量,用来获取上一个命令的退出状态,或者上一个函数的返回值。
如何得到函数的处理结果?
有人可能会疑惑,既然 return 表示退出状态,那么该如何得到函数的处理结果呢?比如,我定义了一个函数,计算从 m 加到 n 的和,最终得到的结果该如何返回呢?
这个问题有两种解决方案:
- 一种是借助全局变量,将得到的结果赋值给全局变量;
- 一种是在函数内部使用 echo、printf 命令将结果输出,在函数外部使用
$()
或者````捕获结果。