所谓没有if else就不算编程,哈,接下来我们正式进入Bash的编程世界。
简单命令
简单命令(Bash手册关键字Simple Commands)是Bash脚本最基本的语句,我们在上一节中使用的echo XXX,就是一个简单命令。不过如果我们仔细去剖析“简单命令”的内部,你可能会注意到其实它并不简单。
基本结构
简单命令的基本结构为
[变量赋值1 变量赋值2...] 命令字 [参数1 参数2...]
其中的变量赋值和参数部分为可选,所以我们用中括号包起来。举个栗子
[ken ~]$A=3
[ken ~]$B=4
[ken ~]$C=5 D=6 echo $A, $B, $C, $D
3, 4, ,
我们首先给变量A和B赋值,之后是一个简单命令——先给C和D赋值,再执行一个稍长点的echo语句。其中echo是命令字,也是所谓第零个参数,$A, $B, $C和$D分别是简单命令的第一、第二、第三和第四个参数,参数与参数之间用空格分隔。打印结果可能并非如你所想,因为C和D的打印值为空。你可能想问,我们为什么要给C和D赋值呢?唔,这个问题其实并不太容易回答,因为它涉及到环境的概念。简单来讲,就是这里的变量C和D会作为echo命令执行时的环境而暂时存在,并非在当前的Bash中真正定义了这两个变量。如果你一时觉得不好理解,没有关系,等到后面"Bash环境"一节,我们再来详细探讨它。
printf
接下来让我们再举一个栗子,printf也是Bash的内置命令之一,使用方式跟C语言的printf类似
printf "This is digit %d, which is %s" 3 three
This is a digit 3, which is three
套用上面简单命令的结构,printf是命令字,"This is digit %d, it is %s"是第一个参数(格式化字符串,如果你用过C语言或Python的话应该不陌生),3和three分别是第二和第三个参数。
命令的返回状态
讲到这里,还必须提一下命令执行的返回状态(Bash手册关键字return status,也叫exit status)。之所以说是返回状态而不是返回值,是因为Bash脚本中其实并没有严格意义上的返回值这一说,而只有返回状态。任何一个命令在执行完之后,都有一个表示成功与否的返回状态,它是一个大于等于0的整数,0表示成功,大于0表示失败。这个返回状态可以通过一个特别的Bash变量?来查看(这类特别的变量在Bash手册中称作special parameter)。
[ken@Desktop]$ls a.txt
a.txt
[ken@Desktop]$echo $?
0
[ken@Desktop]$rm a.txt
[ken@Desktop]$echo $?
0
[ken@Desktop]$ls a.txt
ls: a.txt: No such file or directory
[ken@Desktop]$echo $?
1
[ken@Desktop]$rm a.txt
rm: a.txt: No such file or directory
[ken@Desktop]$echo $?
1
上面例子中我们假设Desktop目录下有一个叫做a.txt的文件。我们先查看它,之后用rm命令删除掉,再一次查看和删除它。每一个命令执行过后,我们都用echo $?检查上一步的命令返回状态。不难注意到,当a.txt被删除后,ls命令的返回状态为1失败,删除rm同样也返回了失败。
知道了命令返回状态,我们继续看一下简单命令的组合——(命令)列表
(命令)列表
(命令)列表(Lists)简称为列表,它是一个或多个简单的命令组合,在简单命令之间用分号;、&&或||分隔,按照从左到右的顺序执行。下面让我们一个个来看下
分号
分号";"的作用仅仅是,单纯的分隔命令而已,比如
echo hello; echo world
hello
world
&&和||
它们的作用更微妙一些,非常类似于C语言中逻辑运算符&&(and)和||(or)。如下面例子所示,假如当前目录下有一个文件叫a.txt
[ken@Desktop]$ls a.txt && echo "a.txt exists" && rm a.txt
a.txt
a.txt exists
[ken@Desktop]$ls a.txt && echo "a.txt exists"
ls: a.txt: No such file or directory
[ken@Desktop]$ls a.txt || echo "a.txt does not exist"
ls: a.txt: No such file or directory
a.txt does not exist
从上面的示例中不难看出,&&的作用是,如果上一步命令返回状态为0(成功)则继续执行后面的命令,否则终止执行;||的作用刚好反过来,如果上一步命令返回状态为非0(失败),则继续执行下去。这种用法有时又叫做“短路”,因为后一步命令是否会执行,依赖于前一步命令的执行结果。我们再举一个更绕一点的例子,假设一开始non_exist文件不存在
[ken ~]$ls non_exist || touch non_exist && echo file non_exist created
ls: 无法访问 'non_exist': 没有那个文件或目录
file non_exist created
[ken ~]$ls non_exist || touch non_exist && echo file non_exist created
non_exist
file non_exist created
因为non_exist文件不存在,ls命令返回了失败。紧接着是||,所以会继续执行第二步——创建该文件。再接下来是&&,因为上一步创建命令成功,所以继续执行并打印file non_exist created。
第二个命令,这时候文件已存在,所以ls命令会返回成功。紧接着是||,因为上一步成功了所以这一步不会执行。再后面的&&还会执行吗?从上面例子的打印结果中可以知道,这一步打印仍然会被执行。因此,&&和||的“短路”效应只会对紧挨着的下一个命令生效,后续命令仍然会(根据返回状态)继续执行下去。
(命令)列表的返回状态
列表的返回状态(return status),是其中(被运行的)最后一条命令的返回状态,比如说
[ken ~]$pwd; cd not_exist_dir; echo Yes
/home/ken
-bash: cd: not_exist_dir: No such file or directory
Yes
[ken ~]$echo $?
0
上面列表的三条简单命令中,第一句为打印当前所在目录,第二句为切换当前目录cd命令。尽管中间的那一条cd命令失败了,整个列表的最后返回状态依然为0(成功)。
再举一个有点“脑抽筋”的例子
[ken ~]$rm non_exist_file 2>/dev/null; echo $?
1
[ken ~]$echo $?
0
列表的第一句(简单)命令尝试删除一个不存在的文件(2>/dev/null意思是不要在屏幕上打印错误,是后面才会介绍的Shell重定向功能),第二句查看上一句命令的返回状态,结果为1(失败)。接下来我们查看整个列表的命令返回状态,发现为0(成功),这是因为列表的最后一句命令为“打印上一步的命令返回状态”,这一句命令确实成功打印了,返回状态为0^_^
条件结构与条件表达式
条件结构
基本结构(A)
if 列表; then
列表
else
列表
fi
基本结构(B)
if 列表; then
列表
elif 列表...
列表
elif 列表...
列表
fi
列表就是我们上面刚刚讨论过的命令列表,不过我们现在把注意力放在if 列表这里。if成立的条件是它后面列表的返回成功,也就是返回状态为0。举个不太现实的例子示范一下
if ls file_a &>/dev/null; then
echo file_a exists
elif ls file_b &>/dev/null; then
echo file_b exists
else
echo none exists
fi
none exists
这里我们再次使用了重定向,避免ls命令把错误信息打印到屏幕上,读者可暂时忽略。因为file_a和file_b两个文件都不存在,所以最后打印了none exists。不知道有没有觉得这样子去做判断看起来很别扭,或者说很丑陋?事实上Bash脚本中很少会把列表直接当作if的条件,更多的时候我们使用条件表达式
条件表达式
条件表达式(Bash手册关键字CONDITIONAL EXPRESSIONS)的三种使用形式为
后面两个之所以放一起,是因为它们是Bash内置命令,并且[]完全可以看成test的别名。这里我们只详细介绍第一种使用方式
[[ expression ]],它是Bash的关键字(Reserved words),使用方式更自然,且更不容易出错。感兴趣的读者可以参考这篇文章shell中(),[]和[[]]的区别。条件表达式的主要使用场景就是与if结构搭配,它把条件的真假转换为了命令的返回状态。具体而言,若条件为真则命令返回状态为0(成功),为假则返回非0(失败),由此一来我们就可以非常方便而自然地编写if语句了。光说不练非君子,让我们用条件表达式改造一下前面判断文件是否存在的例子
if [[ -e file_a ]]; then
echo file_a exists
elif [[ -e file_b ]]; then
echo file_b exists
else
echo none exists
fi
这里-e的意思是假如文件存在则为真,否则为假。其他的判断条件有
-v varname
True if the shell variable varname is set (has been assigned a value).
-z string
True if the length of string is zero.
-v可用于检查某个变量是否有过赋值,-z判断字符串长度是否为0。变量赋值过与否,与长度是否为0是两码事,读者可以自行验证哈。条件表达式提供了其他非常多的选项,读者可以在Bash手册中搜素关键字CONDITIONAL EXPRESSIONS进行查阅。注意,在上面的例子中[[的后边,以及]]的前边都有一个空格,这个空格一方面是为了可读性和美观,另一方面是,唔,因为必须这么做,否则Bash会认不出来这是个条件表达式[[ ]]符号,而会理解为[[-e命令或者file_a]]变量之类的东西。另外,这个例子里其实漏掉了一种情况,就是假如file_a和file_b同时都存在,我们只会打印出file_a exists而不会打印file_b exists,所以接下来我们用逻辑操作符与&&和或||,把上面的例子改写为更“高端”的样子
if [[ -e file_a && -e file_b ]]; then
echo both exist
elif [[ -e file_a ]]; then
echo file_a exists
elif [[ -e file_b ]]; then
echo file_b exists
else
echo none exists
fi
除了&&和||,还有一个非!,不过注意在使用!时务必在后面也保留有空格,所以要写! -e而不是!-e,否则bash会报错说不认识!-e是啥(我知道你在想什么,Bash有时确实有点笨,哈)。
字符串比较
接下来演示下字符串比较(按照字典序)
a=aaa
b=bbb
if [[ $a > "$b" ]]; then
echo $a is after $b
elif [[ $a == "$b" ]]; then
echo $a is the same as $b
else
echo $a is before $b
fi
aaa is before bbb
输出结果复合预期。稍微提一句,在条件表达式中==与=等同,可以互相替换。上面在比较a和b的变量值时,特意加了个双引号把$b包起来。为什么要这么做呢?其实可不必,但是最好养成加的习惯,因为万一==右边的内容包含某些特殊字符的话,比较结果可能出乎你的意料,比如
if [[ "name" == * ]];then
echo match
fi
match
这个例子的比较结果为真,因为Bash中有一些特殊字符(*, ?和[]),如果不用双引号包起来,会被理解为模式匹配。
模式匹配
Bash支持下面三种通配符
- *匹配任意数量的字符
- ?匹配任意一个字符
- [...] 匹配字符集合中的任意一个字符
话不多说,上示例。
name=a.txt
if [[ $name == ?.txt ]];then
echo $name is a TXT file
fi
a.txt is a TXT file
if [[ $name == a.* ]];then
echo $name 去掉后缀名为a
fi
a.txt 去掉后缀名为a
?和*比较简单直观,至于[]会稍微复杂点。
if [[ $name == [abc].* ]];then
echo $name starts with a, b or c
fi
a.txt starts with a, b or c
[]支持范围,比如数字[1-5],字母[h-k],所以上面的例子还可以再次改写为if [[ $name == [a-c].* ]]。
[]还支持取反的符号!和^,它让Bash匹配除[]中字符集合之外的任意一个字符,比如我们再次改写下上例为
if [[ $name == [!d-z].* ]];then
echo $name starts with a, b or c
fi
a.txt starts with a, b or c
[]还支持字符类(class),形式为[[:class:]],比如小写字母集合可写为[[:alpha:]],数字集合可写为[[:digit:]],空白字符(如空格,Tab)可写为[[:space:]],所以上例我们再改写一次如下
if [[ $name == [[:lower:]].txt ]];then
echo $name starts with a lowercase letter
fi
a.txt starts with a lowercase letter
假如我们希望多次匹配一个字符集合,该怎么写呢?比如需要匹配后缀名为三个英文字母的情况,是不是应该写为if [[ $name == *.[a-z][a-z][a-z] ]],这样看起来很挫呢。没错,所以一般对于复杂的匹配我们会考虑正则表达式。
正则表达式匹配
正则表达式我相信很多同学都有过接触,不了解的也没关系,因为严格来说它并不属于Bash的内容。关于正则表达式的规则不再赘述,大家可以自行搜索文档,这里推荐下菜鸟教程 正则表达式 - 语法。
Bash的条件表达式提供了一个二元运算符=~,专门用于正则匹配,上面匹配三个英文字母的例子可以写为
if [[ $name =~ \.[a-z]{3}$ ]];then
echo $name has three-letter suffix
fi
a.txt has three-letter suffix
至于正则匹配中的捕获,保存在Bash变量BASH_REMATCH中,更多内容可以参考Bash手册中的Compound Commands部分
数字大小比较
最后我们看下关于数字的条件表达式例子。假如我们希望比较两个数字的大小,不能直接用==和!=号,而是要用-eq(等于, equal), -ne(不等于), -gt(大于,greater than), -ge(猜下是什么意思), -lt(less than,小于), -le(猜)
if [[ 1 -lt 2 ]]; then
echo 我发现,原来1小于2
fi
我发现,原来1小于2
于是我们知道,Bash在内部对于字符串和整数是完全区别对待的。

浙公网安备 33010602011771号