本文开始正式介绍 shell 脚本的编写方法以及 bash 的语法。
定义
元字符
用来分隔词 (token) 的单个字符,包括:
|
|
token
是指被 shell 看成一个单一单元的字符序列
bash 中包含三种基本的 token:保留关键字
,操作符
,单词
。
保留关键字
是指在 shell 中有明确含义的词语,通常用来表达程序控制结构。包括:
|
|
操作符
由一个或多个元字符
组成,其中控制操作符
包括:
|
|
余下的 shell 输入都可以视为普通的单词
(word)。
shell 脚本是指包含若干 shell 命令的文本文件,标准的 bash 脚本的第一行形如 #!/bin/bash
,其中顶格写的字符 #! 向操作系统申明此文件是一个脚本,紧随其后的 /bin/bash
是此脚本程序的解释器,解释器可以带一个选项 (选项一般是为了对一些情况做特殊处理,比如 -x
表示开启 bash 的调试模式)。
除首行外,其余行中以符号 #
开头的单词及本行中此单词之后的字符将作为注释,被解析器所忽略。
语法
相比于其他更正式的语言,bash 的语法较为简单。大多数使用 bash 的人员,一般都先拥有其他语言的语法基础,在接触 bash 的语法之后,会自然的将原有语法习惯套用到 bash 中来。事实上,bash 的语法灵活多变,许多看起来像是固定格式的地方,实际上并不是。这让一些初学者觉得 bash 语法混乱不堪,复杂难记。这和 bash 的目的和使用者使用 bash 的目的有很大的关系,bash 本身是为了提供一个接口,来支持用户通过命令与操作系统进行交互。用户使用 bash,一般是为了完成某种系统管理的任务,而不是为了做一款独立的软件。这些,都使人难以像学习其他编程语言那样对 bash 认真对待。其实,只要系统学习一遍 bash 语法以及一条命令的执行流程,就可以说掌握了 bash 脚本编程的绝大多数内容。
bash 语法只包括六种:简单命令
、管道命令
、序列命令
、复合命令
、协进程命令
(bash 版本 4.0 及以上) 和函数定义
。
简单命令
shell 简单命令 (Simple Commands
) 包括命令名称,可选数目的参数和重定向(redirection
)。我们在 Linux 基础命令介绍系列里所使用的绝大多数命令都是简单命令。另外,在命令名称前也可以有若干个变量赋值语句(如上一篇所述,这些变量赋值将作为命令的临时环境变量被使用,后面有例子)。简单命令以上述控制操作符
为结尾。
shell 命令执行后均有返回值
(会赋给特殊变量 $?
),是范围为 0-255 的数字。返回值为 0,表示命令执行成功;非 0,表示命令执行失败。(可以使用命令 echo $?
来查看前一条命令是否执行成功)
管道命令
管道命令 (pipeline
) 是指被 |
或 |&
分隔的一到多个命令。格式为:
|
|
其中保留关键字 time
作用于管道命令表示当命令执行完成后输出消耗时间 (包括用户态和内核态占用时间),选项 -p
可以指定时间格式。
默认情况下,管道命令的返回值是最后一个命令的返回值,为 0,表示 true
,非 0,则表示 false
;当保留关键字 !
作用于管道命令时,会对管道命令的返回值进行取反。
之前我们介绍过管道的基本用法,表示将 command1
的标准输出通过管道连接至 command2
的标准输入,这个连接要先于命令的其他重定向操作 (试对比 >/dev/null 2>&1
和 2>&1 >/dev/null
的区别)。如果使用 |&
,则表示将 command1
的标准输出和标准错误都连接至管道。
管道两侧的命令均在子 shell(subshell
) 中执行,这里需要注意:在子 shell 中对变量进行赋值时,父 shell 是不可见的。
|
|
序列命令
序列命令 (list
) 是指被控制操作符;,&,&&
或 ||
分隔的一到多个管道命令,以;
、&
或 <newline>
为结束。
在这些控制操作符中,&&
和 ||
有相同的优先级,然后是 ;
和 &
(也是相同的优先级)。
如果命令以 &
为结尾,此命令会在一个子 shell 中后台执行,当前 shell 不会等待此命令执行结束,并且不论它是否执行成功,其返回值均为 0。
以符号 ;
分隔的命令按顺序执行 (和换行符的作用几乎相同),shell 等待每个命令执行完成,它们的返回值是最后一个命令的返回值。
以符号 &&
和 ||
连接的两个命令存在逻辑关系。
command1 && command2
:先执行 command1,当且仅当 command1 的返回值为 0,才执行 command2。
command1 || command2
:先执行 command1,当且仅当 command1 的返回值非 0,才执行 command2。
脚本举例:
|
|
执行结果 (在脚本所在目录直接执行./test.sh):
|
|
注意例子中序列命令的写法,其中 IFS=’:’只临时对内置命令 read 起作用 (作为单词分隔符来分隔 read 的输入),read 命令结束后,IFS 又恢复到原来的值:$’\d\n’。
&& 和 || 在这里类似于分支语句,read 命令执行成功则执行输出数组的第五个元素,否则执行输出 “赋值失败”。
复合命令
1、(list)
list 将在 subshell 中执行 (注意赋值语句和内置命令修改 shell 状态不能影响当父 shell),返回值是 list 的返回值。
此复合命令前如果使用扩展符 $,shell 称之为命令替换 (另一种写法为 \list`)。shell 会把命令的输出作为命令替换 ` 扩展之后的结果使用。
命令替换可以嵌套。
2、{list;}
list 将在当前 shell 环境中执行,必须以换行或分号为结尾 (即使只有一个命令)。注意不同于 shell 元字符:(和),{和} 是 shell 的保留关键字,因为保留关键字不能分隔单词,所以它们和 list 之间必须有空白字符或其他 shell 元字符。
3、((expression))
expression 是数学表达式 (类似 C 语言的数学表达式),如果表达式的值非 0,则此复合命令的返回值为 0;如果表达式的值为 0,则此复合命令的返回值为 1。
此种复合命令和使用内置命令 let “expression” 是一样的。
数学表达式中支持如下操作符,操作符的优先级,结合性,计算方法都和 C 语言一致 (按优先级从上到下递减排列):
|
|
在数学表达式中,可以使用变量作为操作数,变量扩展要先于表达式的求值。变量还可以省略扩展符号 $,如果变量的值为空或非数字和运算符的其他字符串,将使用 0 代替它的值做数学运算。
以 0 开头的数字将被解释为八进制数,以 0x 或 0X 开头的数字将被解释为十六进制数。其他情况下,数字的格式可以是 [base#]n。可选的 base# 表示后面的数字 n 是以 base(范围是 2-64) 为基的数字,如 2#11011 表示 11011 是一个二进制数字,命令 ((2#11011)) 的作用会使二进制数转化为十进制数。如果 base# 省略,则表示数字以 10 为基。
复合命令 ((expression)) 并不会输出表达式的结果,如果需要得到结果,需使用扩展符 $ 表示数学扩展(另一种写法为 $[expression])。数学扩展也可以嵌套。
括号 () 可以改变表达式的优先级。
脚本举例:
|
|
执行结果:
|
|
4、[[expression]]
此处的 expression 是条件表达式 (并非数学扩展中的条件表达式)。此种命令的返回值取决于条件表达式的结果,结果为 true,则返回值为 0,结果为 false,则返回值为 1。
条件表达式除可以用在复合命令中外,还可以用于内置命令 test 和 [,由于 test、[[、]]、[和] 是内置命令或保留关键字,所以同保留关键字 {和} 一样,它们与表达式之间都要有空格或其他 shell 元字符。
条件表达式的格式包括:
|
|
[[expr]] 中比较两个字符串时还可以用操作符 =~,符号右边的 string2 可以被视为是正则表达式匹配 string1,如果匹配,返回真,否则返回假。
5、if list; then list; [elif list; then list;] … [ else list; ] fi
条件分支命令。首先判断 if 后面的 list 的返回值,如果为 0,则执行 then 后面的 list;如果非 0,则继续判断 elif 后面的 list 的返回值,如果为 0,则……,若返回值均非 0,则最终执行 else 后的 list。fi 是条件分支的结束词。
注意这里的 list 均是命令,由于要判断返回值,通常使用上述条件表达式来进行判断
形如:
|
|
甚至,许多人认为这样就是 if 语句的固定格式,其实 if 后面可以是任何 shell 命令,只要能够判断此命令的返回值。如:
|
|
脚本举例:
|
|
6、for name [[in [ word …] ];]do list;done
7、for ((expr1;expr2;expr3));do list;done
bash 中的 for 循环语句支持如上两种格式,在第一种格式中,先将 in 后面的 word 进行扩展,然后将得到的单词列表逐一赋值给变量 name,每一次赋值都执行一次 do 后面的 list,直到列表为空。如果 in word 被省略,则将位置变量逐一赋值给 name 并执行 list。第二种格式中,双圆括号内都是数学表达式,先计算 expr1,然后反复计算 expr2,直到其值为 0。每一次计算 expr2 得到非 0 值,执行 do 后面的 list 和第三个表达式 expr3。如果任何一个表达式省略,则表示其值为 1。for 语句的返回值是执行最后一个 list 的返回值。
脚本举例:
|
|
执行:
|
|
8、while list-1; do list-2; done
9、until list-1; do list-2; done
while 命令会重复执行 list-2,只要 list-1 的返回值为 0;until 命令会重复执行 list-2,只要 list-1 的返回值为非 0。while 和 until 命令的返回值是最后一次执行 list-2 的返回值。
break 和 continue 两个内置命令可以用于 for、while、until 循环中,分别表示跳出循环和停止本次循环开始下一次循环。
10、case word in [[(] pattern [ | pattern]…) list ;;] … esac
case 命令会将 word 扩展后的值和 in 后面的多个不同的 pattern 进行匹配 (通配符匹配),如果匹配成功则执行相应的 list。
list 后使用操作符;; 时,表示如果执行了本次的 list,那么将不再进行下一次的匹配,case 命令结束;
使用操作符;&,则表示执行完本次 list 后,再执行紧随其后的下一个 list(不判断是否匹配);
使用操作符;;&,则表示继续下一次的匹配,如果匹配成功,那么执行相应的 list。
case 命令的返回值是执行最后一个命令的返回值,当匹配均没有成功时,返回值为 0。
脚本举例:
|
|
执行结果:
|
|
11、select name [in word] ; do list ; done
select 命令适用于交互式菜单选择场景。word 的扩展结果组成一系列可选项供用户选择,用户通过键入提示字符中可选项前的数字来选择特定项目,然后执行 list,完成后继续下一轮选择,需要使用内置命令 break 来跳出循环。
脚本举例:
|
|
执行结果:
|
|
协进程命令
协进程命令是指由保留关键字 coproc 执行的命令 (bash4.0 版本以上),其命令格式为:
|
|
命令 command 在子 shell 中异步执行,就像被控制操作符 & 作用而放到了后台执行,同时建立起一个双向管道,连接该命令和当前 shell。
执行此命令,即创建了一个协进程,如果 NAME 省略 (command 为简单命令时必须省略,此时使用默认名 COPROC),则称为匿名协进程,否则称为命名协进程。
此命令执行时,command 的标准输出和标准输入通过双向管道分别连接到当前 shell 的两个文件描述符,然后文件描述符又分别赋值给了数组元素 NAME[0] 和 NAME[1]。此双向管道的建立要早于命令 command 的其他重定向操作。被连接的文件描述符可以当成变量来使用。子 shell 的 pid 可以通过变量 NAME_PID 来获得。
关于协进程的例子,我们在下一篇给出。
函数定义
bash 函数定义的格式有两种:
|
|
这样定义了名为 name 的函数,使用保留关键字 function 定义函数时,括号可以省略。函数的代码块可以是任意一个上述的复合命令 (compound-command)。
脚本举例:
|
|
执行结果:
|
|
这些就是bash的所有命令语法。bash中任何复杂难懂的语句都是这些命令的变化组合。