学妹面前装 “X” 必备,一篇文章教你学会Shell编程~
Posted 挽周的Java Library
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学妹面前装 “X” 必备,一篇文章教你学会Shell编程~相关的知识,希望对你有一定的参考价值。
好文推荐
前言
-
无法想象没有Shell 的 Linux 会是什么样子。从一开始,Linux就是黑客们的玩具。在Linux的世界里,没有什么是不可控的。如果想要成为一名高级Linux用户,那么Shell编程是必须跨过的一道坎。本章将从正则表达式开始,逐步介绍Shell编程的基本知识。这些内容对于没有任何编程经验的读者可能有点困难,不过想一想将要接触到的激动人心的技术,请打起精神来!
-
Shell 教程 | 菜鸟教程:https://www.runoob.com/linux/linux-shell.html
【正则表达式】
- 正则表达式广泛地应用在各种脚本编程语言中,包括Perl、php、Ruby等。Linux 的各种编程工具也大量采用了正则表达式。可以说,有字符串处理的地方,就有正则表达式的身影。本节简要介绍正则表达式的基本语法。在开始之前,首先关心一下正则表达式的“定义”。
1.什么是正则表达式?
-
正则表达式(regexps)
这个词背后的历史似乎很难考证。一直以来,这个说法被人们广泛应用,没有人关心,或许也没有必要关心它是怎么来的。在很多时候,“正则表达式”又被称做“模式”,所以千万别被这两件不同的外衣搞糊涂了。至少在Linux 中,“模式”和“正则表达式”讲的是同一件事情。 -
那么究竟什么是正则表达式?简单地说,这是一组对正在查找的文本的描述。例如一个生活中的例子,老师对调皮捣蛋的学生说:“把单词表中 a开头、t结尾的单词抄写10遍交给我。”那么对于正在抄写单词的学生而言,“a开头、t结尾的单词”就是“对正在查找的文本的描述”。同样可以告诉Shell,“把当前目录下所有以e结尾的文件名列出来”,这是正则表达式擅长做的事情。
2.不同风格的正则表达式
- 正则表达式是一种概念。在此基础上,人们充分发挥想象力,发明了各种风格的正则表达式。在这个领域,至今没有什么标准可言,不同的软件和编程语言支持不同风格的表达式写法,也难怪刚刚接触正则表达式的用户会因此感到困惑。
- 目前在GNU/Linux中有两套库可用于正则表达式编程:POSIX库和PCRE 库。前者是Linux自带的正则表达式库,后者是Perl 的正则表达式库。从功能上看,PCRE风格的正则表达式更强大一些,但也更难掌握一些。本文选择POSIX风格的正则表达式作为讨论对象,POSIX库不需要额外安装,直接使用即可。在工具方面,本文所有的示例都在egrep工具中测试通过。
egrep工具
- Linux egrep命令用于在文件内查找指定的字符串。
- egrep执行效果与"grep-E"相似,使用的语法及参数可参照grep指令,与grep的不同点在于解读字符串的方法。
- egrep是用extended regular expression语法来解读的,而grep则用basic regular expression语法解读,extended regular expression比basic regular expression的表达更规范。
3.快速上手:在字典中查找单词
- 现在来考虑本节开始的那个例子。老师要求抄写单词表中“a开头、t结尾”的单词,学生现在很想知道他究竟要花多少时间在这个作业上。
/usr/share/dict/words
中包含了多达98568个单词,看起来无论老师所说的“单词表”是什么,Linux给出的估算只多不少。
egrep "^a.*t$" /usr/share/dict/words ##列出文件words中a开头t结尾的所有单词
- 一个个数过来是不可能的了,可以使用wc命令来统计这些单词的数量。
egrep "^a.*t$" words | wc -w
- 也就是说,这位学生最多要抄写1546个单词。看起来他还能抽空赶写一份检讨。
4.字符集和单词
- 在正则表达式中,句点
.
用于匹配除换行符之外的任意一个字符。下面这条正则表达式可以匹配诸如cat、sat、 bat这样的字符串。
.at
.
能够匹配的字符范围是最大的。上面这个正则表达式还能够匹配#at
、~at
,at
这样的字符串。很多时候,需要缩小选择范围使匹配更为精确。为了限定at前的那个字符只能是小写字母,可以这样写正则表达式。
[a-z]at
- 方括号
[
用于指定一个字符集。]
无论中有多少东西,在实际工作时只能匹配其中的一个字符。下面这条表达式用于匹配 a或b或c,而不能匹配 ab、abc、bc 或者3个字母间其他任意的组合。
[abc]
- 使用连字符
-
描述一个范围。下面这条表达式匹配所有的英文字母。
[a-zA-z]
- 数字也可以用范围来指定。
[0-9]
- 有了字符集的概念,匹配特定的单词就灵活多了。但这里面还是有一点问题,使用下面这条命令查找单词表,看看结果是否和预想的一样。
egrep '[a-z]at' /usr/share/dict/words
-
可以看到,这条命令不仅会列出 cat、sat这样的单词,而且列出了zupamate、zumatic这样的词。因为这些单词中包含了mat、cat这些符合正则表达式[a-z]at的字符串,于是也被匹配了(尽管这可能不是用户的初衷)。
-
为了让
[a-z]at
能够严格地匹配一个单词,需要为它加上一对分隔符\\<
和>
。
\\<[a-z]at\\>
- 下面这条命令在单词表中查找所有符合模式
\\<[a-z]atl>
的行。
egrep '\\<[a-z]at\\>' /usr/share/dict/words
-
奇怪的是像
bat's
这样的行也被匹配了。事实上,如果有a#$bat
、bat!!!
、!@#S#@!SR%@!bat#@!$%^
这样的行,它们也同样会被匹配。这就涉及正则表达式中对 “单词” 的定义: -
“单词” 指的是两侧由非单词字符分隔的字符串。非单词字符指的是字母、数字、下划线以外的任何字符。
-
仔细分析一下上面这些例子。第二十九行中 bat 分别由行首和行尾分隔,因此符合单词的定义,可以被匹配。
a#$bat
中的 bat分别由标点$
和行尾分隔,符合单词的定义,可以被匹配;!@#$#@!SR%@!bat#@!$%^
中的bat 分别由标点!和#分隔,同样符合单词的定义。而 Alcatraz中的cat分别被字母1和r分隔,就不符合匹配条件了。
5.字符类
- 除了字符集,POSIX风格的正则表达式还提供了预定义字符类来匹配某些特定的字符。例如,下面的命令列出文件中所有以大写字母开头,以小写t结尾的行。
egrep "^[[:upper:]]t$" /usr/share/dict/words
- 正则表达式
[L:upper:]
就是一个字符类,表示所有的大写字母,等价于[A-Z]。
POSIX正则表达式中的字符类
类 | 匹配字符 |
---|---|
[[:alnum:]] | 文字,数字字符 |
[[:alpha:]] | 字母字符 |
[[:lower:]] | 小写字母 |
[[:upper:]] | 大写字母 |
[[:digit:]] | 小数 |
[[:xdigit:]] | 十六进制数字 |
[[:punct:]] | 标点符号 |
[[:blank:]] | 制表符和空格 |
[[:space:]] | 空格 |
[[:cntrl:]] | 所有控制符 |
[[:pring:]] | 所有可打印的字符 |
[[:graph:]] | 除空格外所有可打印的字符 |
6.位置匹配
- 字符
^
和S
分别用于匹配行首和行尾。下面这条正则表达式用于匹配所有以 a开头、t结尾、a和t之间包含一个小写字母的行。
^a[a-z]t$
A
和$
不必同时使用。下面这条表达式匹配所有以数字开头的行。
^[0-9]
- 可以想像,
$
这样的写法将匹配空行。而$^
则是没有意义的,系统不会对这个表达式报错,但也不会输出任何东西。
7.字符转义
- 大家可能已经有了这样的疑问,既然句点
.
在正则表达式中表示 除换行符之外的任意一个字符,那么如何匹配句点.
本身呢?这就需要用到转义字符\\
。\\
可以取消所有元字符的特殊含义。 - 例如,
\\.
匹配句点.
,\\[
匹配左方括号[
……而为了匹配\\
,就要用\\\\
来指定。 - 例如,下面这条正则表达式匹配www.google.cn。
www\\.google\\.cn
8.重复
- 用户有时候希望某个字符能够不止一次地出现。正则表达式中的星号
*
表示在它前面的模式应该重复0次或者多次。下面这条正则表达式用于匹配所有以a开头、以t结尾的行。
^a.*t$
-
简单地讲解一下这条表达式:
^a
匹配以a
开头的行。.
匹配一个字符(除了换行符),*
指定之前的那个字符(由.
匹配)可以重复0次或多次;t$
匹配以t结尾的行。 -
与此相类似的两个元字符是
+
和?
。+
指定重复1次或更多次;?
指定重复0次或1次。 -
下面这条正则表达式匹配所有在单词hi后面隔了一个或几个字符后出现单词Jerry的行。
\\<hi\\>.+\\<Jerry\\>
使用花括号{ }
可以明确指定模式重复的次数。例如,{3}
表示重复3次,{3,}
表示重复3次或者更多次,{5,12}
则表示重复的次数不少于5次,不多于12次。下面这条正则表达式匹配所有不少于8位的数。
\\<[1-9][0-9]{7,}\\>
- 这条表达式之所以以
[1-9]
开始,是因为没有哪个超过7位的数是以0开头的。相应地,最高位后面的数字应该重复7次或更多次。
用于重复模式的元字符
元字符 | 描述 |
---|---|
* | 重复0次或更多次 |
+ | 重复1次或更多次 |
? | 重复0次或1次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复不少于n次,不多于m次 |
9.子表达式
子表达式
也被称为分组
,这不是什么新的概念。小学生都知道,为了计算1+3的和与4的乘积,必须用括号把1+3括起来。正则表达式也一样。
请看下面这个例子:
- 正则表达式
(or){2,}
匹配所有or重复2次或更多次的行。如果去掉or两边的括号,那么这条正则表达式匹配的将是字母o后面紧跟两个或更多个字母r的行。
egrep "or{2,}" /usr/share/dict/words
10.反义
- 很多时候用户想说的是“除了这个字符,其他什么都可以”,这就需要用到“反义”。下面这条正则表达式匹配除了字母y的任何字符。
[^y]
- 与此相似的是,下面这条正则表达式匹配除了字母a、e、i、o、u的所有字符。
[^aeiou]
- 注意:
^
在表示行首和反义时在位置上的区别。下面的例子匹配所有不以字母y开头的行。
^[^y]
11.分支
- 大家已经看到,正则表达式对用户提交的信息简单地执行 “与” 的组合。举例来说,下面的这条语句匹配所有以字母h开头,“并且” 以字母t结尾的行。
^ht$
- 那么,如何匹配以字母h开头,“或者” 以字母t结尾的行? 分支(以竖线
|
表示)就用来完成 “或” 的组合。下面这条正则表达式用于匹配以字母h开头,“或者” 以字母t结尾的行。
^h|t$
- 再看一个稍微复杂一些的例子。下面这一长串正则表达式可以匹配1~12月的英文写法,包括完整拼写和缩写形式。
Jan(uary| |\\.) |Feb(uary| |\\.)|Mar(ch| |\\.)|Apr(il| |\\.)|May( |\\.)|Jun(e|\\.)|Jul(y| |\\.)|Aug(ust| |\\.)|Sep(tember| |\\.)|Oct(ober| |\\.)|Nov(ember| |\\.)|Dec(ember| |\\.)
- 如果合在一起很难看清楚的话,下面以一月份为例分析这个正则表达式的写法。
Jan(uary| |\\.)
- 紧跟着开头3个字母Jan的是一个子表达式(用括号限定),两个分支元字符
|
分隔了3个字符(串),分别是uary、空格、句点(注意,描述句点需要使用转义符号\\
)。这意味着January或者Jan或者Jan.这样的字符串都能够被匹配。 - 5月份May是比较特殊的一个。由于May的完整写法和缩写形式是一样的,因此只使用一个分支字符匹配空格或者句点。不同的月份之间使用分支字符
I
来分隔。
12. 逆向引用
- 在子表达式(分组)中捕获的内容可以在正则表达式中的其他地方再次使用,用户可以使用反斜杠
\\
加上子表达式的编号来指代该分组匹配到的内容。这样的说法看上去有点不知所措,不妨来看几个例子。
(\\<.*\\>).?( )*\\1
- 上面这行正则表达式匹配所有在某个单词出现后,紧跟着0个或1个标点符号,以及任意个空格之后再次出现这个单词的行。例如,cart cart、long,long ago、ha!ha!……为了便于理解,下面对这个正则表达式断句做些解释。
(\\<.*\\>)
:匹配任意长度的单词。第 1 个子表达式。
.?
:匹配 0 个或 1 个标点符号。由于在句点之前匹配的是单词,因此句点.
在这里只能匹配标点。
( )*
:匹配 0 个或多个空格。第 2 个子表达式。
\\1
:指代第 1 个子表达式匹配到的模式。如果第 1 个子表达式匹配到单词 cart,那么这里也自动成为 cart。
- 当然,用户也可以使用\\2、\\3……来指代编号为2、3……的子表达式匹配到的模式。
- 子表达式的编号规则是:从左至右,第 1 个出现的子表达式编号为1,第2个编号为2……依次类推。
【Shell 脚本编程】
- 博主的建议是,不必陷入编程工具优劣的争论。Vim、Emacs、gedit、kate或其他文本编辑器都是不错的编写Shell 脚本的工具,只要用的顺手就可以。如果找不到足够的理由学习Vim和Emacs,那么就先放在一边吧。工具永远只是工具,使用工具做出些什么才是真正重要和值得去关心的。
第一个程序:Hello World
-
这是最古老、最经典的入门程序,用于在屏幕上打印一行字符串
Hello World!
。借用这个程序,来看一看一个基本的 Shell程序的构成。 -
使用文本编辑器建立一个名为hello的文件。
vim hello
- 包含以下内容:
#! /bin/bash
#Display a line
echo "Hello World"
- 要执行这个Shell 脚本,首先应该要为它加上可执行权限。完成操作后,就可以运行脚本了。
chmod +x hello ##为脚本加上可执行权限
./hello ##执行脚本
- 下面逐行解释这个脚本程序。
#! /bin/bash
- 这一行告诉Shell,运行这个脚本时应该使用哪个Shell程序。本例中使用的是/bin/bash,也就是BASH。一般来说,Shell程序的第一行总是以“#!”开头,指定脚本的运行环境。尽管在当前环境就是BASH SHELL时可以省略这一行,但这并不是一个好习惯。
Display a line
-
以
#
号开头的行是注释,Shell 会直接忽略#
号后面的所有内容。保持写注释的习惯无论对别人(在团队合作时)还是对自己(几个月后回来看这个程序)都是很有好处的。 -
和几乎所有编程语言一样,Shell脚本会忽略空行。用空行分割一个程序中不同的任务代码是一个良好的编程习惯。
echo "Hello"
- echo命令把其参数传递给标准输出,在这里就是显示器。如果参数是一个字符串的话那么应该用双引号把它包含起来。echo命令最后会自动加上一个换行符。
变量和运算符
- 本节介绍变量和运算符的使用。变量是任何一种编程语言所必备的元素,运算符也是。通过将一些信息保存在变量中,可以留作以后使用。通过本节的学习,读者将学会如何操
1.变量的赋值和使用
- 修改我们的hello程序,将一个字符串赋给变量,并在最后将其输出。
# ! /bin/bash
#将一个字符串赋给变量 output
log="monday"
echo "The value of logfile is:"
#美元符号 ($) 用于变量替换
echo $log
- 下面是这个脚本程序的运行结果。
- 在Shell中使用变量不需要事先声明。使用等号
=
将一个变量右边的值赋给这个变量时,直接使用变量名就可以了(注意在这赋值变量时=
左右两边没空格)。
log ="monday"
- 当需要存取变量时,就要使用一个字符来进行变量替换。在BASH中,美元符号
$
用于对一个变量进行解析。Shell在碰到带有$
的变量时会自动将其替换为这个变量的值。 - 例如上面这个脚本的最后一行,echo最终输出的是变量 log 中存放的值。
- 需要指出的是,变量只在其所在的脚本中有效。在上面这个脚本退出后,变量 log 就失效了,此时在 Shell中试图查看log的值将什么也得不到。
- export 让脚本可以影响其子Shell环境。下面这一段命令在子Shell中显示变量的值。
- 使用unset命令可以手动注销一个变量。这个命令的使用就像下面这样简单。
unset count
2.变量替换
- 前面已经提到,美元提示符
$
用于解析变量。如果希望输出这个符号,那么就应该使用转义字符)
,告诉Shell 忽略特殊字符的特殊含义。
注意这两个的区别
- Shell提供了花括号
{}
来限定一个变量的开始和结束。在紧跟变量输出字母后缀时,就必须要使用这个功能。
3.位置变量
- Shell脚本使用位置变量来保存参数。当脚本启动的时候,就必须知道传递给自己的参数是什么。考虑cp命令,这个命令接受两个参数,用于将一个文件复制到另一个地方。传递给脚本文件的参数分别存放在
$
符号带有数字的变量中。简单地说,第一个参数存放在$1
,第二个参数存放在$2
……依次类推。当存取的参数超过10个的时候,就要用花括号把这个数字括起来,例如${13}
、${20}
等。 - 一个比较特殊的位置变量是
$0
,这个变量用来存放脚本自己的名字。有些时候,例如创建日志文件时这个变量非常有用。下面来看一个脚本,用于显示传递给它的参数。
#! /bin/bash
echo "\\$0 = *$0*"
echo "\\$1 = *$1*"
echo "\\$2 = *$2*"
echo "\\$3 = *$3*"
- 下面是这个程序的运行结果。注意:因为没有第3个参数,因此
$3
的值是空的。
- 除了以数字命名的位置变量,Shell 还提供了另外3个位置变量。
如下:
字符 | 说明 |
---|---|
$* | 包含参数列表 |
$@ | 包含参数列表,同上 |
$# | 包含参数的个数 |
- 下面这个脚本listfiles 显示文件的详细信息。尽管还没有学习过for命令,但这里可以先体验一下,这几乎是
$@
最常见的用法。
#! /bin/bash
#显示有多少文件需要列出
echo "$# file(s) to list"
#将参数列表中的值逐一赋给变量file
for file in $@
do
ls -l $file
done
- for 语句每次从参数列表($@)中取出一个参数,放到变量file 中。脚本运行的结果如下:
4.BASH 引号规则
- 尽管还没有正式介绍引号的使用规则,但之前的脚本程序已经大量使用了引号(不过也只是双引号而已)。现在弥补这个空缺还来得及。在 Shell 脚本中可以使用的引号有如下3种。
- 双引号:阻止 Shell对大多数特殊字符(例如#)进行解释。但
$
、` 和 ” 仍然保持其特殊含义。 - 单引号:阻止 Shell 对所有字符进行解释。
- 倒引号:`,这个符号通常位于键盘上Esc键的下方。当用倒引号括起一个 Shell命令时,这个命令将会被执行,执行后的输出结果将作为这个表达式的值。倒引号中的特殊字符一般都被解释。
下面的脚本quote显示这3个引号的不同之处。
[root@localhost ~]# vim quote
#! /bin/bash
log=Saturday
#双引号会对其中的 “$” 字符进行解释
echo "Today is $log"
#单引号不会对特殊字符进行解释
echo 'Today is $log'
#倒引号会运行其中的命令,并把命令输出作为最终结果
echo "Today is 'date'"
- 以下是该脚本的运行结果。注意脚本的最后一行,双引号也会对 ` 做出解释。
5.运算符
- 运算符是类似于
+
、-
这样的符号,用于告诉计算机执行怎样的运算。 - Shell定义了一套运算符,其中的大部分读者应该已经非常熟悉了。和数学中一样,这些运算符具有不同的优先级,优先级高的运算更早被执行。
Shell 中用到的运算符
运算符 | 含义 |
---|---|
-,+ | 单目负,单目正 |
!,~ | 逻辑非,按位取反 |
*,/,% | 乘,除,取模 |
+ | 加,减 |
<<,>> | 按位左移,按位右移 |
<,>,<=,>= | 大于,小于 ,大于等于,小于等于 |
==,!= | 等于,不等于 |
& | 按位与 |
^ | 按位异或 |
| | 按位或 |
&& | 逻辑与 |
|| | 逻辑或 |
=,+=,-=,*=,/=,%=,&=,^= ,|= ,<<=,>>= | 赋值,运算并赋值 |
- 这里无法对其中的每一个运算符做详细解释。Shell完全复制了C语言中的运算符及其优先级规则。在日常使用中,只需要使用其中的一部分就可以了,数学运算并不是Shell 的强项。
- 运算符的优先级并不需要特别地记忆。如果使用的时候搞不清楚,只要简单地使用括号就可以了,就像小学里学习算术时一样。
( 7 + 8 ) / ( 6 − 3 ) (7 + 8) / (6 - 3) (7+8)/(6−3)
- 值得注意的是,在Shell中表示相等时,
==
和=
在大部分情况下不存在差异,大家将会在后文中逐渐熟悉如何进行表达式的判断。
表达式求值
- 之所以单独列出这一节,因为这是让很多初学者感到困惑的地方。Shell中进行表达式求值有和其他编程语言不同的地方。首先来看一个例子,这个例子可以“帮助”读者产生困惑。
- 为什么结果不是2?原因很简单,Shell脚本语言是一种“弱类型”的语言,它并不知道变量num中保存的是一个数值,因此在遇到num=$num+1这个命令时,Shell 只是简单地把Snum和“+1”连在一起作为新的值赋给变量 num(在这方面,其他脚本语言——例如PHP似乎表现得更“聪明”一些)。为了让 Shell得到“正确”的结果,可以试试下面这条命令。
num=$[$num+1]
- $[]这种表示形式告诉 Shell应该对其中的表达式求值。如果上面这条命令不太容易能看清楚的话,那么不妨对比一下下面这两条命令的输出。
-
$[]
的使用方式非常灵活,可以接受不同基数的数字(默认情况下使用十进制)。可以采用[base#]n来表示从二到三十六进制的任何一个n值,例如2#10就表示二进制数10(对应于十进制的2)。 -
下面的几个例子显示了如何在
$[]
中使用不同的基数求值。
- expr命令也可以对表达式执行求值操作,这个命令允许使用的表达式更为复杂,也更为灵活,这里无法介绍 expr的高级用法。下面的例子是用expr计算1+2的值,注意expr会同时把结果输出。
- 注意:在
1
、+和2
之间要有空格,否则expr会简单地将其当做字符串输出。 - 另一种指导Shell进行表达式求值的方法是使用let命令。更准确地说,let命令用于计算整数表达式的值。下面这个例子显示了let命令的用法。
脚本执行命令和控制语句
- 本节将介绍Shell脚本中的执行命令以及控制语句。在正常情况下,Shell 按顺序执行每一条语句,直至碰到文件尾。但在多数情况下,需要根据情况选择相应的语句执行,或者对一段程序循环执行。这些都是通过控制语句实现的。
1.if选择结构
- if命令判断条件是否成立,进而决定是否执行相关的语句。这也许是程序设计中使用频率最高的控制语句了。
最简单的if结构如下:
if test-commands
then
commands
fi
-
上面这段代码首先检查表达式test-commands是否为真。如果是,就执行commands所包含的命令——commands可以是一条,也可以是多条命令。如果test-commands为假,那么直接跳过这段if结构(以fi作为结束标志),继续执行后面的脚本。
-
下面这段程序提示用户输入口令。如果口令正确,就显示一条欢迎信息。
#! /bin/bash
echo "Enter password:"
read password
if ["$password" = "mypassswd"]
then
echo "Welcome!!"
fi
- 注意:这里用于条件测试的语句[ $password = “mypasswd” ],在[、Spassword、=、"mypasswd"和]之间必须存在空格。条件测试语句将在随后介绍,大家暂时只要能“看懂”就可以了。该脚本的运行效果如下:
- if 结构的这种形式在很多时候显得太过“单薄”了,为了方便用户做出“如果……如果……否则……”这样的判断,if结构提供了下面这种形式。
if test-command-1
then
commands-1
elif test-command-2
then
commands-2
elif test-command-3
then
commands-3
...
else
commands
fi
- 上面这段代码依次判断test-command-1、test-command-2、test-command-3……如果上面这些条件都不满足,就执行else语句中的commands。注意这些条件都是“互斥”的。也就是说,Shell依次检查每一个条件,其中任何一个条件一旦匹配,就退出整个if结构。现在修改上面刚才的脚本,根据不同的口令显示不同的欢迎信息。
- 下面显示了这个脚本的运行结果。在输入fjn之后,Shell 发现 if语句的第一个条件成立,于是Shell就执行命令echo “Hello, 方加你”,然后跳出if语句块结束脚本,而不会继续去判断"Spassword" = "mike"这个条件。从这个意义上,if-elif-else语句和连续使用多个if语句是有本质区别的。
2.case多选结构
- Shell中另一种控制结构是case语句。casc用于在一系列模式中匹配某个变量的值,这个结构的基本语法如下:
case word in
pattern-1)
commands-1
;;
pattern-2)
commands-2
;;
...
pattern-N)
commands-N
;;
esac
- 变量word逐一同从pattern-1到pattern-2的模式进行比较,当找到一个匹配的模式后,就执行紧跟在后面的命令commands(可以是多条命令)﹔如果没有找到匹配模式,case语句就什么也不做。
- 命令
;;
只在case结构中出现,Shell一旦遇到这条命令就跳转到case结构的最后。也就是说,如果有多个模式都匹配变量word,那么Shell 只会执行第一条匹配模式所对应的命令。与此类似的是,C语言提供了break语句在switch结构中实现相同的功能,Shell只是继承了这种书写“习惯”。区别在于,程序员可以在C程序的switch结构中省略break语句(用于实现一种几乎不被使用的流程结构),而在Shell的case结构中省略;;
则是不允许的。 - 相比较if语句而言,case语句在诸如
a=b
这样判断上能够提供更简洁、可读性更好的代码结构。在Linux 的服务器启动脚本中,case结构用于判断用户究竟是要启动、停止还是重新启动服务器进程。下面是从 openSUSE 中截取的一段控制SSH 服务器的脚本(/etc/init.d/sshd) 。
- 在这个例子中,如果用户运行命令
/etc/init.d/sshd start
,那么Shell将执行下面这段命令:通过startproc启动SSH守护进程。
echo -n "Starting SSH daemon"
## Start daemon with startproc(8). If this fails
## the echo return value is set appropriate.
startproc -f -p $SSHD_PIDFILE $SSHD_BIN $SSHD_OPTS -o "PidFile=$SSHD_PIDFILE"
#Remember status and be verbose
rc_status -v
- 值得注意的是最后使用的
*)
,星号(*)
用于匹配所有的字符串。在上面的例子中,如果用户输入的参数不是start、stop或是restart 中的任何一个,那么这个参数将匹配*)
,脚本执行下面这行命令,提示用户正确的使用方法。
echo "Usage: $0 {start|stop|restart|}"
- 由于case语句是逐条检索匹配模式,因此
*)
所在的位置很重要。如果上面这段脚本将*)
放在case 结构的开头,那么无论用户输入什么,脚本只会说Usage: $o{start/stopprestart}
这一句话。
条件测试
- 几乎所有初学Shell编程的人都会对这部分内容感到由衷的困惑。Shell和其他编程语言在条件测试上的表现非常不同。读者在C/C++积累的经验甚至可能会帮倒忙。理解本节对顺利进行Shell编程至关重要,因此如果读者是第一次接触的话,请耐心地读完这冗长的一节。
1.if判断的依据
- 和大部分人的经验不同的是,if 语句本身并不执行任何判断。它实际上接受一个程序名作为参数,然后执行这个程序,并依据这个程序的返回值来判断是否执行相应的语句。如果程序的返回值是0,就表示“真”,if语句进入对应的语句块;所有非0的返回值都表示“假”,if语句跳过对应的语句块。下面的这段脚本 testif很好地显示了这一点。
#!/bin/bash
if ./testscript -1
then
echo "test exit -1"
fi
if ./testscript 0
then
echo "test exit 0"
fi
if ./testscript 1
then
echo "test exit 1"
fi
- 脚本的运行结果如下:
- 这段脚本依次测试返回值-1、0和1,最后只有返回值为0所对应的echo语句被执行了。脚本中调用的testscript 接受用户输入的参数,然后简单地把这个参数返回给其父进程。testscript脚本只有两行代码,其中的exit语句用于退出脚本并返回一个值。
#!/bin/bash
exit $@
- 现在大家应该能够大致了解if语句(包括后面将要介绍的 while、until等语句)的运行机制。也就是说,if语句事实上判断的是程序的返回值,返回值0表示真,非0值表示假。
2.test命令和空格的使用
- 既然if语句需要接受一个命令作为参数,那么像
"$password" = "john"
这样的表达式就不能直接放在 if语句的后面。为此需要额外引入一个命令,用于判断表达式的真假。test命令的语法如下:
test expr
- 其中 expr是通过test命令可以理解的选项来构建的。例如下面这条命令用于判断字符串变量password是否等于"john"。
test "$password" = "john"
- 如果两者相等,那么test命令就返回值0;反之则返回1。作为test的同义词,用户也可以使用方括号
[
进行条件测试。后者的语法如下:
[ expr ]
必须提醒读者注意的是,在Shell编程中,空格的使用绝不仅仅是编程风格上的差异。
- 现在来对比下面3条命令:
password="john"
test "$password" = "john"
[ "$password" = "john"]
-
第一条是赋值语句,在password、= 和"john"之间没有任何空格;第2条是条件测试命令,在test、“Spassword”、=和"john"之间都有空格;第3条也是条件测试命令(是test命令的另一种写法),在[、"$password"、=、"john"和]之间都有空格。
-
一些C程序员喜欢在赋值语句中等号“=”的左右两边都加上空格,因为这样看上去会比较清晰,但是在 Shell中这种做法会导致语法错误。
password = "john"
bash: password: 找不到命令
-
同样地,试图去掉条件测试命令中的任何一个空格也是不允许的。去掉“[”后面的空格是语法错误,去掉等号(=)两边的空格会让测试命令永远都返回0(表示真)。
-
之所以会出现这样的情况是因为Shell首先是一个命令解释器,而不是一门编程语言。空格在Shell这个“命令解释器”中用于分隔命令和传递给它的参数(或者用于分隔命令的两个参数)。使用whereis命令查找test和“[”可以看到,这是个存放在/usr/bin目录下的“实实在在”的程序文件。
- 因此在上面的例子中,“Spassword”、=和"john"都是test命令和[命令的参数,参数和命令、参数和参数之间必须要使用空格分隔。而单独的赋值语句password="john"不能掺杂空格的原因也就很明显了。password是变量名,而不是某个可执行程序。
test和[
命令可以对以下3类表达式进行测试;
- 字符串比较
- 文件测试
- 数字比较
字符串比较
- test和[命令的字符串比较主要用于测试字符串是否为空,或者两个字符串是否相等。
用于字符串比较选项
选项 | 描述 |
---|