shell编程
一、正则表达式
1、概述
正则表达式和通配符的区别(正则表达式用来在文件中匹配符合条件的字符串,通配符用来匹配符合条件的文件名)只在Shell当中适用,因为用来在文件当中搜索字符串的命令,如grep、awk、sed等命令可以支持正则表达式,而在系统当中搜索文件的命令,如ls、find、cp这些命令不支持正则表达式,所以只能使用shell自己的通配符来进行匹配了。
2、基础正则表达式
元字符 | 作用 |
---|---|
* | 前一个字符匹配0次或任意多次。 |
. | 匹配除了换行符外任意一个字符。 |
^ | 匹配行首。例如:^hello会匹配以hello开头的行。 |
$ | 匹配行尾。例如:hello&会匹配以hello结尾的行。 |
[] | 匹配中括号中指定的任意一个字符,只匹配一个字符。例如:[aoeiu] 匹配任意一个元音字母,[0-9] 匹配任意一位数字,[a-z][0-9]匹配小写字和一位数字构成的两位字符。 |
[^] | 匹配除中括号的字符以外的任意一个字符。例如:[^0-9] 匹配 任意一位非数字字符,[^a-z] 表示任意一位非小写字母。 |
\ | 转义符。用于取消讲特殊符号的含义取消。 |
\{n\} | 表示其前面的字符恰好出现n次。例如:[0-9]{4} 匹配4位数字,[1][3-8][0-9]{9} 匹配手机号码。 |
\{n,\} | 表示其前面的字符出现不小于n次。例如: [0-9]{2,} 表示两位及以上的数字。 |
\{n,m\} | 表示其前面的字符至少出现n次,最多出现m次。例如: [a-z]{6,8} 匹配6到8位的小写字母。 |
在~/.bashrc文件中建立这个别名:
1 | [root@localhost ~]# vi /root/.bashrc |
1)、练习文件建立
1 | [root@localhost ~]# vi test |
2)、“*”前一个字符匹配0次,或任意多次
1 | [root@localhost ~]# grep "a*" test |
如果这样写正则表达式“aa*”代表这行字符串一定要有一个a,但是后面有没有a都可以。也就是说会匹配至少包含有一个a的行:
1 | [root@localhost ~]# grep "aa*" test |
如果正则表达式是“aaa*”,则会匹配最少包含两个连续a的字符串,如:
1 | [root@localhost ~]# grep "aaa*" test |
当然如果再多写两个a,如“aaaaa”就不能从这篇文档中匹配任何内容了,因为我们这篇文档中a最多的单词“aaaasd”只有四个个连续的a,而“aaaaaa”会匹配最少五个连续的a。
3)、 “.” 匹配除了换行符外任意一个字符
正则表达式“.”只能匹配一个字符,这个字符可以是任意字符,举个例子:
1 | [root@localhost ~]# grep "a..d" test |
4)、 “^”匹配行首,“$”匹配行尾
“^”代表匹配行首,比如“^S”会匹配以大写“S”开头的行:
1 | [root@localhost ~]# grep "^S" test |
“$”代表匹配行尾,如果“d$”会匹配以小写“d”结尾的行:
1 | [root@localhost ~]# grep "d$" test |
而“^$”则会匹配空白行:
1 | [root@localhost ~]# grep "^$" test |
5)、“[]”匹配中括号中指定的任意一个字符,只匹配一个字符
“[]”会匹配中括号中指定任意一个字符,注意只能匹配一个字符。比如[ou]要不会匹配一个o字符,要不会匹配一个u字符:
1 | [root@localhost ~]# grep "b[ou]ne" test |
而“[0-9]”会匹配任意一个数字,如:
1 | [root@localhost ~]# grep "[0-9]" test |
而“[A-Z]”则会匹配一个大写字母,如:
1 | [root@localhost ~]# grep "[A-Z]" test |
如果正则是“^[a-z]”代表匹配用小写字母开头的行:
1 | [root@localhost ~]# grep "[a-z]" test |
6)“[^]” 匹配除中括号的字符以外的任意一个字符
1 | [root@localhost ~]# grep "^[^a-z]" test |
而“^[ ^a-zA-Z]”则会匹配不用字母开头的行:
1 | [root@localhost ~]# grep "^[^a-zA-Z]" test |
7)、“\” 转义符
1 | [root@localhost ~]# grep "\.$" test |
8)、“\{n\}”表示其前面的字符恰好出现n次
1 | [root@localhost ~]# grep "a\{3\}" test |
9)、“\{n,\}”表示其前面的字符出现不小于n次 “\{n,\}”会匹配前面的字符出现最少n次。比如“zo\{3,\}m”这个正则就会匹配用z开头,m结尾,中间最少有三个o的字符串。那么“^[0-9] \{3,\}[a-z]”这个正则就能匹配最少用连续三个数字开头的字符串:
1 | [root@localhost ~]# grep "[0-9]\{3,\}[a-z]" test |
10)、“{n,m}”匹配其前面的字符至少出现n次,最多出现m次
1 | [root@localhost ~]# grep "sa\{1,3\}i" test |
3、扩展正则表达式
熟悉正则表达式的童鞋应该很疑惑,在正则表达式中应该还可以支持一些元字符,比如“+”“ ? ”“|”“()”。其实Linux是支持这些元字符的,只是grep命令默认不支持而已。如果要想支持这些元字符,必须使用egrep命令或grep -E选项,所以我们又把这些元字符称作扩展元字符。
如果查询grep的帮助,对egrep的说明就是和grep -E选项一样的命令,所以我们可以把两个命令当做别名来对待。通过表12-2来看看Shell中支持的扩展元字符:
扩展元字符 | 作用 |
---|---|
+ | 前一个字符匹配1次或任意多次。如“go+gle”会匹配“gogle”、“google”或“gooogle”,当然如果“o”有更多个,也能匹配。 |
? | 前一个字符匹配0次或1次。如“colou?r”可以匹配“colour”或“color”。 |
| | 匹配两个或多个分支选择。如“was |
() | 匹配其整体为一个字符,即模式单元。可以理解为由多个单个字符组成的大字符。如“(dog)+”会匹配“dog”、“dogdog”、“dogdogdog”等,因为被()包含的字符会当成一个整体。但“hello (world |
二、字符截取和替换命令
1、cut列提取命令
1 | [root@localhost ~]# cut [选项] 文件名 |
cut命令的默认分隔符是制表符,也就是“tab”键,不过对空格符可是支持的不怎么好啊。我们先建立一个测试文件,然后看看cut命令的作用吧:
1 | [root@localhost ~]# vi test |
1 | [root@localhost ~]# cut -f 2 test |
那如果想要提取多列呢?只要列号直接用“,”分开,命令如下:
1 | [root@localhost ~]# cut -f 2,3 test |
cut可以按照字符进行提取,需要注意“8-”代表的是提取所有行的第十个字符开始到行尾,而“10- 20”代表提取所有行的第十个字符到第二十个字符,而“-8”代表提取所有行从行首到第八个字符:
1 | [root@localhost ~]# cut -c 6- student.txt |
1 | [root@localhost ~]# cut -d ":" -f 1,3 /etc/passwd |
如果我想用cut命令截取df命令的第一列和第三列,就会出现这样的情况:
1 | [root@localhost ~]# df -h | cut -d " " -f 1,3 |
2、awk编程
1)、概述
2)、 printf格式化输出
1 | [root@localhost ~]# printf ‘输出类型输出格式’ 输出内容 |
为了演示printf命令,我们需要修改下刚刚cut命令使用的test文件,文件内容如下:
1 | ID Name PHP Linux MySQL Average |
我们使用printf命令输出下这个文件的内容
1 | [root@localhost ~]# printf '%s' $(cat test) |
这就是printf命令,如果不指定输出格式,则会把所有输出内容连在一起输出。其实文本的输出本身就是这样的,cat等文本输出命令之所以可以按照格式漂亮的输出,那是因为cat命令已经设定了输出格式。那么为了用printf输出合理的格式,应该这样做:
1 | [root@localhost ~]# printf '%s\t %s\t %s\t %s\t %s\t %s\t \n' $(cat test) |
如果不想把成绩当成字符串输出,而是按照整型和浮点型输出,则要这样:
1 | [root@localhost ~]# printf '%i\t %s\t %i\t %i\t %i\t %8.2f\t \n' $(cat test | grep -v Name) |
3)、awk基本使用
1 | [root@localhost ~]# awk '条件1{动作1} 条件2{动作2}...' 文件名 |
我们这里先来学习awk基本用法,也就是只看看格式化输出动作是干什么的。那看看这个例子吧:
1 | [root@localhost ~]# awk '{printf $2 "\t" $6 "\n"}' student.txt |
比如刚刚截取df命令的结果时,cut命令已经力不从心了,我们来看看awk命令:
1 | [root@localhost ~]# df -h | awk '{print $1 "\t" $3}' |
4)、 awk的条件
条件的类型 | 条件 | 说明 |
---|---|---|
awk保留字 | BEGIN | 在awk程序一开始时,尚未读取任何数据之前执行。BEGIN后的动作只在程序开始时执行一次 |
- | END | 在awk程序处理完所有数据,即将结束时执行。END后的动作只在程序结束时执行一次 |
关系运算符 | > | 大于 |
- | < | 小于 |
- | >= | 大于等于 |
- | <= | 小于等于 |
- | == | 等于。用于判断两个值是否相等,如果是给变量赋值,请使用“=”号 |
- | != | 不等于 |
- | A~B | 判断字符串A中是否包含能匹配B 表达式的子字符串 |
- | A!~B | 判断字符串A中是否不包含能匹配B表达式的子字符串 |
正则表达式 | /正则/ | 如果在“//”中可以写入字符,也可以支持正则表达式 |
- BEGIN
BEGIN是awk的保留字,是一种特殊的条件类型。BEGIN的执行时机是“在awk程序一开始时,尚未读取任何数据之前执行”。一旦BEGIN后的动作执行一次,当awk开始从文件中读入数据,BEGIN的条件就不再成立,所以BEGIN定义的动作只能被执行一次。例如:
1 | [root@localhost ~]# awk 'BEGIN{printf "This is a transcript \n" } {printf $2 "\t" $6 "\n"}' test |
- END
END也是awk保留字,不过刚好和BEGIN相反。END是在awk程序处理完所有数据,即将结束时执行。END后的动作只在程序结束时执行一次。例如:
1 | [root@localhost ~]# awk 'END{printf "The End \n" } {printf $2 "\t" $6 "\n"}' test |
- 关系运算符
举几个例子看看关系运算符。假设我想看看平均成绩大于等于87分的学员是谁,就可以这样输入命令:
1 | 例子1: |
加入了条件之后,只有条件成立动作才会执行,如果条件不满足,则动作则不运行。通过这个实验,大家可以发现,虽然awk是列提取命令,但是也要按行来读入的。这个命令的执行过程是这样的:
- 如果有BEGIN条件,则先执行BEGIN定义的动作
- 如果没有BEGIN条件,则读入第一行,把第一行的数据依次赋予$0、$1、$2等变量。其中$0代表此行的整体数据,$1代表第一字段,$2代表第二字段。
- 依据条件类型判断动作是否执行。如果条件符合,则执行动作,否则读入下一行数据。如果没有条件,则每行都执行动作。
- 读入下一行数据,重复执行以上步骤。
再举个例子,如果我想看看Sc用户的平均成绩呢:
1 | 例子2: |
这里要注意在awk中,使用“//”包含的字符串,awk命令才会查找。也就是说字符串必须用“//”包含,awk命令才能正确识别。
- 正则表达式
如果要想让awk识别字符串,必须使用“//”包含,例如:
1 | 例子1: |
当使用df命令查看分区使用情况是,如果我只想查看真正的系统分区的使用状况,而不想查看光盘和临时分区的使用状况,则可以:
1 | 例子2: |
5)、 awk内置变量
awk内置变量 | 作用 |
---|---|
$0 | 代表目前awk所读入的整行数据。我们已知awk是一行一行读入数据的,$0就代表当前读入行的整行数据。 |
$n | 代表目前读入行的第n个字段。 |
NF | 当前行拥有的字段(列)总数。 |
NR | 当前awk所处理的行,是总数据的第几行。 |
FS | 用户定义分隔符。awk的默认分隔符是任何空格,如果想要使用其他分隔符(如“:”),就需要FS变量定义。 |
ARGC | 命令行参数个数。 |
ARGV | 命令行参数数组。 |
FNR | 当前文件中的当前记录数(对输入文件起始为1)。 |
OFMT | 数值的输出格式(默认为%.6g)。 |
OFS | 输出字段的分隔符(默认为空格)。 |
ORS | 输出记录分隔符(默认为换行符)。 |
RS | 输入记录分隔符(默认为换行符)。 |
1 | [root@localhost ~]# cat /etc/passwd | grep "/bin/bash" | awk '{FS=":"} {printf $1 "\t" $3 "\n"}' |
这里“:”分隔符生效了,可是第一行却没有起作用,原来我们忘记了“BEGIN”条件,那么再来试试:
1 | [root@localhost ~]# cat /etc/passwd | grep "/bin/bash" | awk 'BEGIN {FS=":"} {printf $1 "\t" $3 "\n"}' |
1 | [root@localhost ~]# cat /etc/passwd | grep "/bin/bash" | awk 'BEGIN {FS=":"} {printf $1 "\t" $3 "\t 行号:" NR "\t 字段数:" NF "\n"}' |
如果我只想看看sshd这个伪用户的相关信息,则可以这样使用:
1 | [root@localhost ~]# cat /etc/passwd | awk 'BEGIN {FS=":"} $1=="sshd" {printf $1 "\t" $3 "\t 行号:"NR "\t 字段数:"F " \n"}' |
6)、 awk流程控制
我们先来看看该如何在awk中定义变量与调用变量的值。假设我想统计PHP成绩的总分,那么就应该这样:
1 | [root@localhost ~]# awk 'NR==2{php1=$3} NR==3{php2=$3} NR==4{php3=$3;totle=php1+php2+php3;print "totle php is " totle}' test |
我们解释下这个命令。“NR==2{php1=$3}”(条件是NR==2,动作是php1=$3)这句话是指如果输入数据是第二行(第一行是标题行),就把第二行的第三字段的值赋予变量“php1”。“NR==3{php2=$3}”这句话是指如果输入数据是第三行,就把第三行的第三字段的值赋予变量“php2”。“NR==4{php3=$3;totle=php1+php2+php3;print “totle php is “ totle}”(“NR==4”是条件,后面{}中的都是动作)这句话是指如果输入数据是第四行,就把第四行的第三字段的值赋予变量“php3”;然后定义变量totle的值是“php1+php2+php3”;然后输出“totle php is”关键字,后面加变量totle的值。
在awk编程中,因为命令语句非常长,在输入格式时需要注意以下内容:
- 多个条件{动作}可以用空格分割,也可以用回车分割。
- 在一个动作中,如果需要执行多个命令,需要用“;”分割,或用回车分割。
- 在awk中,变量的赋值与调用都不需要加入“$”符。
- 条件中判断两个值是否相同,请使用“==”,以便和变量赋值进行区分。
再看看该如何实现流程控制,假设如果Linux成绩大于90,就是一个好男人(学PHP的表示压力很大!):
1 | [root@localhost ~]# awk '{if (NR>=2) {if ($3>90) printf $2 " is a good man!\n"}}' test |
其实在awk中if判断语句,完全可以直接利用awk自带的条件来取代,刚刚的脚本可以改写成这样:
1 | [root@localhost ~]# awk ' NR>=2 {test=$3} test>90 {printf $2 " is a good man!\n"}' test |
7)、 awk函数
awk编程也允许在编程时使用函数,在本小节我们讲讲awk的自定义函数。awk函数的定义方法如下:
1 | function 函数名(参数列表){函数体} |
函数:
1 | [root@localhost ~]# awk 'function test(a,b) { printf a "\t" b "\n" } |
8)、 awk中调用脚本
对于小的单行程序来说,将脚本作为命令行自变量传递给awk是非常简单的,而对于多行程序就比较难处理。当程序是多行的时候,使用外部脚本是很适合的。首先在外部文件中写好脚本,然后可以使用awk的-f选项, 使其读入脚本并且执行。
例如,我们可以先编写一个awk脚本:
1 | [root@localhost ~]# vi pass.awk |
然后可以使用“-f”选项来调用这个脚本:
1 | [root@localhost ~]# awk -f pass.awk /etc/passwd |
3、sed命令
sed主要是用来将数据进行选取、替换、删除、新增的命令,我们看看命令的语法:
1 | [root@localhost ~]# sed [选项] ‘[动作]’ 文件名 |
对sed命令大家要注意,sed所做的修改并不会直接改变文件的内容(如果是用管道符接收的命令的输出,这种情况连文件都没有),而是把修改结果只显示到屏幕上,除非使用“-i”选项才会直接修改文件。
- 行数据操作
闲话少叙,直奔主题,我们举几个例子来看看sed命令到底是干嘛的。假设我想查看下student.txt的第二行,那么就可以利用“p”动作了:
1 | [root@localhost ~]# sed '2p' test |
“p”命令确实输出了第二行数据,但是sed命令还会把所有数据都输出一次,这时就会看到这个比较奇怪的结果。那如果我想指定输出某行数据,就需要“-n”选项的帮助了:
1 | [root@localhost ~]# sed -n '2p' test |
再来看看如何删除文件的数据:
1 | [root@localhost ~]# sed '2,4d' test |
再来看看如何追加和插入行数据:
1 | [root@localhost ~]# sed '2a hello' test |
”会在指定行后面追加入数据,如果想要在指定行前面插入数据,则需要使用“i”动作:
1 | [root@localhost ~]# sed '2i hello world' test |
如果是想追加或插入多行数据,除最后一行外,每行的末尾都要加入“\”代表数据未完结。再来看看“-n”选项的作用:
1 | [root@localhost ~]# sed -n '2i hello world' test |
-n”只查看sed命令操作的数据,而不是查看所有数据。
再来看看如何实现行数据替换
1 | [root@localhost ~]# cat test | sed '2c No such perso' |
sed命令默认情况是不会修改文件内容的,如果我确定需要让sed命令直接处理文件的内容,可以使用“-i”选项。不过这样非常容易误操作,在操作系统文件时请小心谨慎。可以使用这样的命令:
1 | [root@localhost ~]# sed -i '2c No such person' test |
字符串替换
“c”动作是进行整行替换的,如果仅仅想替换行中的部分数据,就要使用“s”动作了。s动作的格式是:
1 | [root@localhost ~]# sed ‘s/旧字串/新字串/g’文件名 |
替换的格式和vim非常类似
1 | [root@localhost ~]# sed '3s/74/99/g' test |
1 | [root@localhost ~]# sed '4s/^/#/g' test |
1 | [root@localhost ~]# sed -e 's/test1//g ; s/test3//g' test #同时把“test1”和“test3”替换为空 |
“-e”选项可以同时执行多个sed动作,当然如果只是执行一个动作也可以使用“-e”选项,但是这时没有什么意义。还要注意,多个动作之间要用“;”号或回车分割,例如上一个命令也可以这样写:
1 | [root@localhost ~]# sed -e 's/Liming//g |
三、字符处理命令
1、排序命令sort
1 | [root@localhost ~]# sort [选项] 文件名 |
sort命令默认是用每行开头第一个字符来进行排序的,比如:
1 | [root@localhost ~]# sort /etc/passwd |
如果想要反向排序,请使用“-r”选项:
1 | [root@localhost ~]# sort -r /etc/passwd |
如果想要指定排序的字段,需要使用“-t”选项指定分隔符,并使用“-k”选项指定字段号。加入我想要按照UID字段排序/etc/passwd文件:
1 | [root@localhost ~]# sort -t ":" -k 3,3 /etc/passwd |
看起来好像很美,可是如果仔细看看,怎么daemon用户的UID是2,反而排在了下面?这是因为sort默认是按照字符排序,前面用户的UID的第一个字符都是1,所以这么排序。要想按照数字排序,请使用“-n”选项:
1 | [root@localhost ~]# sort -n -t ":" -k 3,3 /etc/passwd |
当然“-k”选项可以直接使用“-k 3”,代表从第三字段到行尾都排序(第一个字符先排序,如果一致,第二个字符再排序,知道行尾)。
2、uniq
uniq命令是用来取消重复行的命令,其实和“sort -u”选项是一样的。命令格式如下:
1 | [root@localhost ~]# uniq [选项] 文件名 |
3、统计命令wc
1 | [root@localhost ~]# wc [选项] 文件名 |
四、条件判断
1、按照文件类型进行判断
测试选项 | 作用 |
---|---|
-b 文件 | 判断该文件是否存在,并且是否为块设备文件(是块设备文件为真) |
-c文件 | 判断该文件是否存在,并且是否为字符设备文件(是字符设备文件为真) |
-d 文件 | 判断该文件是否存在,并且是否为目录文件(是目录为真) |
-e 文件 | 判断该文件是否存在(存在为真) |
-f 文件 | 判断该文件是否存在,并且是否为普通文件(是普通文件为真) |
-L 文件 | 判断该文件是否存在,并且是否为符号链接文件(是符号链接文件为真) |
-p 文件 | 判断该文件是否存在,并且是否为管道文件(是管道文件为真) |
-s 文件 | 判断该文件是否存在,并且是否为非空(非空为真) |
-S 文件 | 判断该文件是否存在,并且是否为套接字文件(是套接字文件为真) |
1 | [root@localhost ~]# [ -e /root/sh/ ] |
还记得多命令顺序执行的“&&”和“||”吗?我们可以再判断一下/root/sh/是否是目录:
1 | [root@localhost ~]# [ -d /root/sh ] && echo "yes" || echo "no" |
2、按照文件权限进行判断
测试选项 | 作用 |
---|---|
-r 文件 | 判断该文件是否存在,并且是否该文件拥有读权限(有读权限为真) |
-w文件 | 判断该文件是否存在,并且是否该文件拥有写权限(有写权限为真) |
-x 文件 | 判断该文件是否存在,并且是否该文件拥有执行权限(有执行权限为真) |
-u 文件 | 判断该文件是否存在,并且是否该文件拥有SUID权 限( 有SUID权限为真) |
-g 文件 | 判断该文件是否存在,并且是否该文件拥有SGID权 限( 有SGID权限为真) |
-k 文件 | 判断该文件是否存在,并且是否该文件拥有SBit权 限( 有SBit权限为真) |
比如:
1 | [root@localhost ~]# ll test |
3、两个文件之间进行比较
测试选项 | 作用 |
---|---|
文件1 -nt 文件2 | 判断文件1的修改时间是否比文件2的新(如果新则为真) |
文件1 -ot 文件2 | 判断文件1的修改时间是否比文件2的旧(如果旧则为真) |
文件1 -ef 文件2 | 判断文件1是否和文件2的Inode号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法 |
到底该如何判断两个文件是否是硬链接呢?这时test就派上用场了:
1 | [root@localhost ~]# ln /root/test /tmp/ts |
4、两个整数之间比较
测试选项 | 作用 |
---|---|
整数1 -eq 整数2 | 判断整数1是否和整数2相等(相等为真) |
整数1 -ne 整数2 | 判断整数1是否和整数2不相等(不相等位置) |
整数1 -gt 整数2 | 判断整数1是否大于整数2(大于为真) |
整数1 -lt 整数2 | 判断整数1是否小于整数2(小于位置) |
整数1 -ge 整数2 | 判断整数1是否大于等于整数2(大于等于为真) |
整数1 -le 整数2 | 判断整数1是否小于等于整数2(小于等于为真) |
举个例子:
1 | [root@localhost ~]# [ 23 -ge 22 ] && echo "yes" || echo "no" |
5、字符串的判断
测试选项 | 作用 |
---|---|
-z 字符串 | 判断字符串是否为空(为空返回真) |
-n 字符串 | 判断字符串是否为非空(非空返回真) |
字串1 ==字串2 | 判断字符串1是否和字符串2相等(相等返回真) |
字串1 != 字串2 | 判断字符串1是否和字符串2不相等(不相等返回真) |
举个例子:
1 | [root@localhost ~]# name=sc |
再来看看如何判断两个字符串相等:
1 | [root@localhost ~]# aa=11 |
6、多重条件判断
测试选项 | 作用 |
---|---|
判断1 -a 判断2 | 逻辑与,判断1和判断2都成立,最终的结果才为真 |
判断1 -o 判断2 | 逻辑或,判断1和判断2有一个成立,最终的结果就为真 |
!判断 | 逻辑非,使原始的判断式取反 |
举个例子:
1 | [root@localhost ~]# aa=11 |
要想让刚刚的判断式返回真,需要给变量aa重新赋个大于23的值:
1 | [root@localhost ~]# aa=24 |
再来看看逻辑非是什么样子的:
1 | [root@localhost ~]# [ ! -n "$aa" ] && echo "yes" || echo "no" no |
注意:“!”和“-n”之间必须加入空格,否则会报错的
五、流程控制
1、if条件判断
1)、单分支if条件语句
单分支条件语句最为简单,就是只有一个判断条件,如果符合条件则执行某个程序,否则什么事情都不做。语法如下:
1 | if [ 条件判断式 ];then |
单分支条件语句需要注意几个点:
- if语句使用fi结尾,和一般语言使用大括号结尾不同
- [ 条件判断式 ]就是使用test命令判断,所以中括号和条件判断式之间必须有空格
- then后面跟 符合条件之后执行的程序,可以放在[]之后,用“;”分割。也可以换行写入,就不需要“;”了,比如单分支if语句还可以这样写:
1 | if [ 条件判断式 ] |
1 | !/bin/bash |
2)、双分支if条件语句
1 | if [ 条件判断式 ] |
例子1:
我们写一个数据备份的例子,来看看双分支if条件语句。
1 | 例子1:备份mysql数据库 |
例子2:
再举个例子,在工作当中,服务器上的服务经常会宕机。如果我们对服务器监控不好,就会造成服务器中服务宕机了,而管理员却不知道的情况,这时我们可以写一个脚本来监听本机的服务,如果服务停止或宕机了,可以自动重启这些服务。我们拿apache服务来举例:
1 | 例子2: |
3)、多分支if条件语句
1 | if [ 条件判断式1 ] |
那我们再写一个例子,用if多分支条件语句来判断一下用户输入的是一个文件,还是一个目录:
1 | 例子:判断用户输入的是什么文件 |
2、多分支case条件语句
case语句和if…elif…else语句一样都是多分支条件语句,不过和if多分支条件语句不同的是,case语句只能判断一种条件关系,而if语句可以判断多种条件关系。case语句语法如下:
1 | case $变量名 in |
这个语句需要注意以下内容:
- case语句,会取出变量中的值,然后与语句体中的值逐一比较。如果数值符合,则执行对应的程序,如果数值不符,则依次比较下一个值。如果所有的值都不符合,则执行“)”(“”代表所有其他值)中的程序。
- case语句以“case”开头,以“esac”结尾。
每一个分支程序之后要通过“;;”双分号结尾,代表该程序段结束。
我们写一个判断是“yes/no”的例子:
1 | !/bin/bash#判断用户输入 |
3、for循环
for循环是固定循环,也就是在循环时已经知道需要进行几次的循环,有时也把for循环称为计数循环。for的语法有如下两种:
1 | 语法一: |
这种语法中for循环的次数,取决于in后面值的个数(空格分隔),有几个值就循环几次,并且每次循环都把值赋予变量。也就是说,假设in后面有三个值,for会循环三次,第一次循环会把值1赋予变量,第二次循环会把值2赋予变量,以此类推。
1 | 语法二: |
语法二中需要注意:
- 初始值:在循环开始时,需要给某个变量赋予初始值,如i=1;
- 循环控制条件:用于指定变量循环的次数,如i<=100,则只要i的值小于等于100,循环就会继续;
- 变量变化:每次循环之后,变量该如何变化,如i=i+1。代表每次循环之后,变量i的值都加1。
1)、语法一举例:
我们先看看语法一是什么样子的:
1 | 例子:打印时间 |
2)、语法二举例那语法二就和其他语言中的for循环更加类似了,也就是事先决定循环次数的固定循环了。先举个简单的例子:
1 | 例子:从1加到100 |
4、while循环
1 | while [ 条件判断式 ] |
对while循环来讲,只要条件判断式成立,循环就会一直继续,直到条件判断式不成立,循环才会停止。好吧,我们还是写个1加到100的例子吧,这种例子虽然对系统管理帮助不大,但是对理解循环非常有帮助:
1 | 例子:1加到100 |
5、until循环
再来看看until循环,和while循环相反,until循环时只要条件判断式不成立则进行循环,并执行循环程序。一旦循环条件成立,则终止循环。语法如下:
1 | until [ 条件判断式 ] |
还是写从1加到100这个例子,注意和while循环的区别:
1 | 例子:从1加到100 |
6、函数
1 | function 函数名 () { |
那我们写一个函数吧,还记得从1加到100这个循环吗?这次我们用函数来实现它,不过不再是从1加到100了,我们让用户自己来决定加到多少吧:
1 | 例子: |
7、特殊流程控制语句
1)、 exit语句
系统是有exit命令的,用于退出当前用户的登录状态。可是在Shell脚本中,exit语句是用来退出当前脚本的。也就是说,在Shell脚本中,只要碰到了exit语句,后续的程序就不再执行,而直接退出脚本。exit的语法如下:
1 | exit [返回值] |
如果exit命令之后定义了返回值,那么这个脚本执行之后的返回值就是我们自己定义的返回值。可以通过查询$?这个变量,来查看返回值。如果exit之后没有定义返回值,脚本执行之后的返回值是执行exit语句之前,最后执行的一条命令的返回值。
写一个exit的例子:
1 | !/bin/bash |
这个脚本中,大家需要思考,如果我输入的不是数字,那么“echo “The number is: $num””这个脚本是否会执行?当然不会,因为如果输入的不是数字,“[ -n “$y” ] && echo “Error! Please input a number!” && exit 18”这句脚本会执行,exit一旦执行脚本就会终止。
2)、 break语句
再来看看特殊流程控制语句break的作用,当程序执行到break语句时,会结束整个当前循环。而continue语句也是结束循环的语句,不过continue语句单次当前循环,而下次循环会继续。画个示意图解释下break语句,如图所示:
举个例子:
1 | !/bin/bash |
在这个脚本中,因为一旦变量i的值等于4,整个循环都会跳出,所以应该只能循环三次
3)、 continue语句
再来看看continue语句,continue也是结束流程控制的语句。如果在循环中,continue语句只会结束单次当前循环,也画个示意图来说明下continue语句:
举个例子:
1 | !/bin/bash |
continue只会退出单次循环,所以并不影响后续的循环,所以只会少4的输出。这个例子和break的例子做个比较,应该可以更清楚的说明break和continue的区别。