shell编程

shell编程

十月 29, 2020

一、正则表达式

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
2
[root@localhost ~]# vi /root/.bashrc
alias grep='grep --color=auto'

1)、练习文件建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# vi test
I am the bone of my sword.

Steel is my body,and fire is my blood.

123I have created over a thousand blades.

Unknown to Death.
Nor known to Life.

Have withstood pain to create many weapons.
456789aaaasd

Yet, those hands will never hold anything.

So as I pray, Unlimited Blade Works.

2)、“*”前一个字符匹配0次,或任意多次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# grep "a*" test
I am the bone of my sword.

Steel is my body,and fire is my blood.

123I have created over a thousand blades.

Unknown to Death.
Nor known to Life.

Have withstood pain to create many weapons.
456789aaaasd

Yet, those hands will never hold anything.

So as I pray, Unlimited Blade Works.

如果这样写正则表达式“aa*”代表这行字符串一定要有一个a,但是后面有没有a都可以。也就是说会匹配至少包含有一个a的行:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# grep "aa*" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Unknown to Death.
Have withstood pain to create many weapons.
456789aaaasd
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.

如果正则表达式是“aaa*”,则会匹配最少包含两个连续a的字符串,如:

1
2
[root@localhost ~]# grep "aaa*" test
456789aaaasd

当然如果再多写两个a,如“aaaaa”就不能从这篇文档中匹配任何内容了,因为我们这篇文档中a最多的单词“aaaasd”只有四个个连续的a,而“aaaaaa”会匹配最少五个连续的a。

3)、 “.” 匹配除了换行符外任意一个字符

正则表达式“.”只能匹配一个字符,这个字符可以是任意字符,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# grep "a..d" test
123I have created over a thousand blades.
456789aaaasd
#“s..d”会匹配在s和d这两个字母之间一定有两个字符的单词

[root@localhost ~]# grep "a.*d" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Have withstood pain to create many weapons.
456789aaaasd
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.

4)、 “^”匹配行首,“$”匹配行尾

“^”代表匹配行首,比如“^S”会匹配以大写“S”开头的行:

1
2
3
[root@localhost ~]# grep "^S" test
Steel is my body,and fire is my blood.
So as I pray, Unlimited Blade Works.

“$”代表匹配行尾,如果“d$”会匹配以小写“d”结尾的行:

1
2
[root@localhost ~]# grep "d$" test
456789aaaasd

而“^$”则会匹配空白行:

1
[root@localhost ~]# grep "^$" test

5)、“[]”匹配中括号中指定的任意一个字符,只匹配一个字符

“[]”会匹配中括号中指定任意一个字符,注意只能匹配一个字符。比如[ou]要不会匹配一个o字符,要不会匹配一个u字符:

1
2
[root@localhost ~]# grep "b[ou]ne" test
I am the bone of my sword.

而“[0-9]”会匹配任意一个数字,如:

1
2
3
[root@localhost ~]# grep "[0-9]" test
123I have created over a thousand blades.
456789aaaasd

而“[A-Z]”则会匹配一个大写字母,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@localhost ~]# grep "[A-Z]" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Unknown to Death.
Nor known to Life.
Have withstood pain to create many weapons.
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.
[root@localhost ~]# grep "[A-Z]" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Unknown to Death.
Nor known to Life.
Have withstood pain to create many weapons.
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.

如果正则是“^[a-z]”代表匹配用小写字母开头的行:

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# grep "[a-z]" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Unknown to Death.
Nor known to Life.
Have withstood pain to create many weapons.
456789aaaasd
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.

6)“[^]” 匹配除中括号的字符以外的任意一个字符

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# grep "^[^a-z]" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Unknown to Death.
Nor known to Life.
Have withstood pain to create many weapons.
456789aaaasd
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.

而“^[ ^a-zA-Z]”则会匹配不用字母开头的行:

1
2
3
[root@localhost ~]# grep "^[^a-zA-Z]" test
123I have created over a thousand blades.
456789aaaasd

7)、“\” 转义符

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# grep "\.$" test
I am the bone of my sword.
Steel is my body,and fire is my blood.
123I have created over a thousand blades.
Unknown to Death.
Nor known to Life.
Have withstood pain to create many weapons.
Yet, those hands will never hold anything.
So as I pray, Unlimited Blade Works.
#输出所有以"."结尾的行

8)、“\{n\}”表示其前面的字符恰好出现n次

1
2
[root@localhost ~]# grep "a\{3\}" test
456789aaaasd

9)、“\{n,\}”表示其前面的字符出现不小于n次 “\{n,\}”会匹配前面的字符出现最少n次。比如“zo\{3,\}m”这个正则就会匹配用z开头,m结尾,中间最少有三个o的字符串。那么“^[0-9] \{3,\}[a-z]”这个正则就能匹配最少用连续三个数字开头的字符串:

1
2
[root@localhost ~]# grep "[0-9]\{3,\}[a-z]" test
456789aaaasd

10)、“{n,m}”匹配其前面的字符至少出现n次,最多出现m次

1
2
3
4
5
[root@localhost ~]# grep "sa\{1,3\}i" test
#匹配在字母s和字母i之间有最少一个a,最多三个a

[root@localhost ~]# grep "sa\{2,3\}i" test
#匹配在字母s和字母i之间有最少两个a,最多三个a

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
2
3
4
5
[root@localhost ~]# cut [选项] 文件名
选项:
-f 列号:提取第几列
-d 分隔符:按照指定分隔符分割列
-c 字符范围:不依赖分隔符来区分列,而是通过字符范围(行首为0)来进行字段提取。“n-”表示从第n个字符到行尾;“n-m”从第n个字符到第m 个字符;“-m”表示从第1个字符到第m个字符。

cut命令的默认分隔符是制表符,也就是“tab”键,不过对空格符可是支持的不怎么好啊。我们先建立一个测试文件,然后看看cut命令的作用吧:

1
2
3
4
5
[root@localhost ~]# vi test
ID Name gender Mark
1 test1 M 86
2 test2 M 90
3 test3 M 83
1
2
3
4
5
6
[root@localhost ~]# cut -f 2 test
Name
test1
test2
test3
#提取第二列内容

那如果想要提取多列呢?只要列号直接用“,”分开,命令如下:

1
2
3
4
5
[root@localhost ~]# cut -f 2,3 test
Name gender
test1 M
test2 M
test3 M

cut可以按照字符进行提取,需要注意“8-”代表的是提取所有行的第十个字符开始到行尾,而“10- 20”代表提取所有行的第十个字符到第二十个字符,而“-8”代表提取所有行从行首到第八个字符:

1
2
3
4
5
6
[root@localhost ~]# cut -c 6- student.txt
me gender Mark
t1 M 86
t2 M 90
t3 M 83
#提取第6个字符开始到行尾
1
2
[root@localhost ~]# cut -d ":" -f 1,3 /etc/passwd      
#以“:”作为分隔符,提取/etc/passwd文件的第一列和第三列

如果我想用cut命令截取df命令的第一列和第三列,就会出现这样的情况:

1
2
3
4
5
[root@localhost ~]# df -h | cut -d " " -f 1,3
Filesystem
/dev/sda2
tmpfs
/dev/sda1

2、awk编程

1)、概述

2)、 printf格式化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# printf ‘输出类型输出格式’ 输出内容
输出类型:
%ns:输出字符串。n是数字指代输出几个字符
%ni:输出整数。n是数字指代输出几个数字
%m.nf:输出浮点数。m和n是数字,指代输出的整数位数和小数位数。如%8.2f代表共输出8位数,其中2位是小数,6位是整数。

输出格式:
\a: 输出警告声音
\b: 输出退格键,也就是Backspace键
\f: 清除屏幕
\n: 换行
\r: 回车,也就是Enter键
\t: 水平输出退格键,也就是Tab键
\v: 垂直输出退格键,也就是Tab键

为了演示printf命令,我们需要修改下刚刚cut命令使用的test文件,文件内容如下:

1
2
3
4
ID      Name    PHP     Linux   MySQL   Average
1 test1 82 86 86 87.66
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66

我们使用printf命令输出下这个文件的内容

1
2
[root@localhost ~]# printf '%s' $(cat test)
IDNamePHPLinuxMySQLAverage1test182868687.662test274908785.663test399839391.66[root@localhost ~]#

这就是printf命令,如果不指定输出格式,则会把所有输出内容连在一起输出。其实文本的输出本身就是这样的,cat等文本输出命令之所以可以按照格式漂亮的输出,那是因为cat命令已经设定了输出格式。那么为了用printf输出合理的格式,应该这样做:

1
2
3
4
5
[root@localhost ~]# printf '%s\t %s\t %s\t %s\t %s\t %s\t \n' $(cat test)
ID Name PHP Linux MySQL Average
1 test1 82 86 86 87.66
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66

如果不想把成绩当成字符串输出,而是按照整型和浮点型输出,则要这样:

1
2
3
4
[root@localhost ~]# printf '%i\t %s\t %i\t %i\t %i\t %8.2f\t \n' $(cat test | grep -v Name)
1 test1 82 86 86 87.66
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66

3)、awk基本使用

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# awk '条件1{动作1} 条件2{动作2}...' 文件名

条件(Pattern):
一般使用关系表达式作为条件。这些关系表达式非常多,具体参考表12-3所示,例如:
x > 10判断变量x是否大于10
x == y判断变量x是否等于变量y
A ~ B 判断字符串A中是否包含能匹配B 表达式的子字符串
A!~ B判断字符串A中是否不包含能匹配B表达式的子字符串

动作(Action):
格式化输出
流程控制语句

我们这里先来学习awk基本用法,也就是只看看格式化输出动作是干什么的。那看看这个例子吧:

1
2
[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
2
3
4
5
6
7
8
9
[root@localhost ~]# awk 'BEGIN{printf "This is a transcript \n" }  {printf $2 "\t" $6 "\n"}' test  
This is a transcript
Name Average
test1 87.66
test2 85.66
test3 91.66
#awk命令只要检测不到完整的单引号不会执行,所以这个命令的换行不用加入“\”,就是一行命令
#这里定义了两个动作
#第一个动作使用BEGIN条件,所以会在读入文件数据前打印“这是一张成绩单”(只会执行一次)
  • END

END也是awk保留字,不过刚好和BEGIN相反。END是在awk程序处理完所有数据,即将结束时执行。END后的动作只在程序结束时执行一次。例如:

1
2
3
4
5
6
7
[root@localhost ~]# awk 'END{printf "The End \n" }  {printf $2 "\t" $6 "\n"}' test  
Name Average
test1 87.66
test2 85.66
test3 91.66
The End
#在输出结尾输入“The End”,这并不是文档本身的内容,而且只会执行一次
  • 关系运算符

举几个例子看看关系运算符。假设我想看看平均成绩大于等于87分的学员是谁,就可以这样输入命令:

1
2
3
4
5
6
例子1:
[root@localhost ~]# cat test | grep -v Name | awk '$6 >= 87 {printf $2 "\n" }'
test1
test3
#使用cat输出文件内容,用grep取反包含“Name”的行
#判断第六字段(平均成绩)大于等于87分的行,如果判断式成立,则打第六列(学员名)

加入了条件之后,只有条件成立动作才会执行,如果条件不满足,则动作则不运行。通过这个实验,大家可以发现,虽然awk是列提取命令,但是也要按行来读入的。这个命令的执行过程是这样的:

  1. 如果有BEGIN条件,则先执行BEGIN定义的动作
  2. 如果没有BEGIN条件,则读入第一行,把第一行的数据依次赋予$0、$1、$2等变量。其中$0代表此行的整体数据,$1代表第一字段,$2代表第二字段。
  3. 依据条件类型判断动作是否执行。如果条件符合,则执行动作,否则读入下一行数据。如果没有条件,则每行都执行动作。
  4. 读入下一行数据,重复执行以上步骤。

再举个例子,如果我想看看Sc用户的平均成绩呢:

1
2
3
例子2:
[root@localhost ~]# awk '$2 ~ /2/ {printf $6 "\n"}' test
#如果第二字段中输入包含有“2”字符,则打印第六字段数据85.66

这里要注意在awk中,使用“//”包含的字符串,awk命令才会查找。也就是说字符串必须用“//”包含,awk命令才能正确识别。

  • 正则表达式

如果要想让awk识别字符串,必须使用“//”包含,例如:

1
2
3
4
例子1:
[root@localhost ~]# awk '/test1/ {print}' test
1 test1 82 86 86 87.66
#打印test1的成绩

当使用df命令查看分区使用情况是,如果我只想查看真正的系统分区的使用状况,而不想查看光盘和临时分区的使用状况,则可以:

1
2
3
4
5
例子2:
[root@localhost ~]# df -h | awk '/sda[0-9]/ {printf $1 "\t" $5 "\n"} '
/dev/sda2 16%
/dev/sda1 13%
#查询包含有sda数字的行,并打印第一字段和第五字段

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
2
3
4
5
6
7
8
[root@localhost ~]# cat /etc/passwd | grep "/bin/bash" | awk '{FS=":"} {printf $1 "\t" $3 "\n"}'
root:x:0:0:root:/root:/bin/bash
zer0 500
0ne 501
user1 502
user2 503
user3 504
#查询可以登录的用户的用户名和UID

这里“:”分隔符生效了,可是第一行却没有起作用,原来我们忘记了“BEGIN”条件,那么再来试试:

1
2
3
4
5
6
7
[root@localhost ~]# cat /etc/passwd | grep "/bin/bash" | awk 'BEGIN {FS=":"} {printf $1 "\t" $3 "\n"}' 
root 0
zer0 500
0ne 501
user1 502
user2 503
user3 504
1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# cat /etc/passwd | grep "/bin/bash" | awk 'BEGIN {FS=":"} {printf $1 "\t" $3 "\t 行号:" NR "\t 字段数:" NF "\n"}'
root 0 行号:1 字段数:7
zer0 500 行号:2 字段数:7
0ne 501 行号:3 字段数:7
user1 502 行号:4 字段数:7
user2 503 行号:5 字段数:7
user3 504 行号:6 字段数:7
#解释下awk命令
#开始执行{分隔符是“:”} {输出第一字段和第三字段输出行号(NR值)字段数(NF值)}
root 0 行号:1 字段数:7
user1 501 行号:2 字段数:7

如果我只想看看sshd这个伪用户的相关信息,则可以这样使用:

1
2
3
[root@localhost ~]# cat /etc/passwd | awk 'BEGIN {FS=":"} $1=="sshd" {printf $1 "\t" $3 "\t 行号:"NR "\t 字段数:"F "  \n"}'   
sshd 74 行号:28 字段数: 7
#可以看到sshd伪用户的UID是74,是/etc/passwd文件的第28行,此行有7个字段

6)、 awk流程控制

我们先来看看该如何在awk中定义变量与调用变量的值。假设我想统计PHP成绩的总分,那么就应该这样:

1
2
[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  
#统计PHP成绩的总分

我们解释下这个命令。“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
2
3
[root@localhost ~]# awk '{if (NR>=2) {if ($3>90) printf $2 " is a good man!\n"}}' test 
#程序中有两个if判断,第一个判断行号大于2,第二个判断PHP成绩大于90分
test3 is a good man!

其实在awk中if判断语句,完全可以直接利用awk自带的条件来取代,刚刚的脚本可以改写成这样:

1
2
3
4
[root@localhost ~]# awk ' NR>=2 {test=$3} test>90 {printf $2 " is a good man!\n"}' test 
#先判断行号如果大于2,就把第三个字段赋予变量test
#在判断如果test的值大于90分,就打印
test3 is a good man!

7)、 awk函数

awk编程也允许在编程时使用函数,在本小节我们讲讲awk的自定义函数。awk函数的定义方法如下:

1
function 函数名(参数列表){函数体}

函数:

1
2
3
4
5
6
7
8
[root@localhost ~]# awk 'function test(a,b) { printf a "\t" b "\n" } 
#定义函数test,包含两个参数,函数体的内容是输出这两个参数的值
{ test($2,$6) } ' test
#调用函数test,并向两个参数传递值。
Name Average
test1 87.66
test2 85.66
test3 91.66

8)、 awk中调用脚本

对于小的单行程序来说,将脚本作为命令行自变量传递给awk是非常简单的,而对于多行程序就比较难处理。当程序是多行的时候,使用外部脚本是很适合的。首先在外部文件中写好脚本,然后可以使用awk的-f选项, 使其读入脚本并且执行。

例如,我们可以先编写一个awk脚本:

1
2
3
[root@localhost ~]# vi pass.awk
BEGIN {FS=":"}
{ print $1 "\t" $3}

然后可以使用“-f”选项来调用这个脚本:

1
2
3
4
5
[root@localhost ~]# awk -f pass.awk /etc/passwd 
root 0
bin 1
daemon 2
...省略部分输出...

3、sed命令

sed主要是用来将数据进行选取、替换、删除、新增的命令,我们看看命令的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# sed [选项] ‘[动作]’  文件名

选项:
-n:一般sed命令会把所有数据都输出到屏幕,如果加入此选择,则只会把经过sed命令处理的行输出到屏幕。
-e:允许对输入数据应用多条sed命令编辑。
-f 脚本文件名:从sed脚本中读入sed操作。和awk命令的-f非常类似。
-r:在sed中支持扩展正则表达式。
-i:用sed的修改结果直接修改读取数据的文件,而不是由屏幕输出

动作:
a \:追加,在当前行后添加一行或多行。添加多行时,除最后一行外,每行末尾需要用“\”代表数据未完结。
c \:行替换,用c后面的字 符串替换原数据行,替换多行时,除最后一行外,每行末尾需用“\”代表数据未完结。
i \:插入,在当期行前插入一行或多行。插入多行时,除最后一行外,每行末尾需要用“\”代表数据未完结。
d:删除,删除指定的行。
p:打印,输出指定的行。
s:字串替换,用一个字符串替换另外一个字符串。格式为“行范围s/ 旧字串/新字串/g”(和vim中的替换格式类似)。

对sed命令大家要注意,sed所做的修改并不会直接改变文件的内容(如果是用管道符接收的命令的输出,这种情况连文件都没有),而是把修改结果只显示到屏幕上,除非使用“-i”选项才会直接修改文件。

  • 行数据操作

闲话少叙,直奔主题,我们举几个例子来看看sed命令到底是干嘛的。假设我想查看下student.txt的第二行,那么就可以利用“p”动作了:

1
2
3
4
5
6
[root@localhost ~]# sed '2p' test
ID Name PHP Linux MySQL Average
1 test1 82 86 86 87.66
1 test1 82 86 86 87.66
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66

“p”命令确实输出了第二行数据,但是sed命令还会把所有数据都输出一次,这时就会看到这个比较奇怪的结果。那如果我想指定输出某行数据,就需要“-n”选项的帮助了:

1
2
[root@localhost ~]# sed -n '2p' test
1 test1 82 86 86 87.66

再来看看如何删除文件的数据:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# sed '2,4d' test
ID Name PHP Linux MySQL Average

#删除第二行到第四行的数据

[root@localhost ~]# cat test
ID Name PHP Linux MySQL Average
1 test1 82 86 86 87.66
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66
#但是文件本身并没有修改

再来看看如何追加和插入行数据:

1
2
3
4
5
6
7
[root@localhost ~]# sed '2a hello' test
ID Name PHP Linux MySQL Average
1 test1 82 86 86 87.66
hello
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66
#在第二行后加入hello

”会在指定行后面追加入数据,如果想要在指定行前面插入数据,则需要使用“i”动作:

1
2
3
4
5
6
7
[root@localhost ~]# sed '2i hello world' test
ID Name PHP Linux MySQL Average
hello world
1 test1 82 86 86 87.66
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66
#在第二行前插入两行数据

如果是想追加或插入多行数据,除最后一行外,每行的末尾都要加入“\”代表数据未完结。再来看看“-n”选项的作用:

1
2
3
[root@localhost ~]# sed -n '2i hello world' test
hello world
#只查看sed命令操作的数据

-n”只查看sed命令操作的数据,而不是查看所有数据。

再来看看如何实现行数据替换

1
2
3
4
5
[root@localhost ~]# cat test | sed '2c No such perso'
ID Name PHP Linux MySQL Average
No such perso
2 test2 74 90 87 85.66
3 test3 99 83 93 91.66

sed命令默认情况是不会修改文件内容的,如果我确定需要让sed命令直接处理文件的内容,可以使用“-i”选项。不过这样非常容易误操作,在操作系统文件时请小心谨慎。可以使用这样的命令:

1
[root@localhost ~]# sed -i '2c No such person' test
  • 字符串替换

    “c”动作是进行整行替换的,如果仅仅想替换行中的部分数据,就要使用“s”动作了。s动作的格式是:

1
[root@localhost ~]# sed ‘s/旧字串/新字串/g’文件名

替换的格式和vim非常类似

1
2
[root@localhost ~]# sed '3s/74/99/g' test
#在第三行中,把74换成99
1
2
[root@localhost ~]# sed '4s/^/#/g' test
#这里使用正则表达式,“^”代表行首
1
[root@localhost ~]# sed -e 's/test1//g ; s/test3//g' test    #同时把“test1”和“test3”替换为空

“-e”选项可以同时执行多个sed动作,当然如果只是执行一个动作也可以使用“-e”选项,但是这时没有什么意义。还要注意,多个动作之间要用“;”号或回车分割,例如上一个命令也可以这样写:

1
2
[root@localhost ~]# sed -e 's/Liming//g  
> s/Tg//g' student.txt

三、字符处理命令

1、排序命令sort

1
2
3
4
5
6
7
8
9
[root@localhost ~]# sort [选项] 文件名
选项:
-f:忽略大小写
-b:忽略每行前面的空白部分
-n:以数值型进行排序,默认使用字符串型排序
-r:反向排序
-u:删除重复行。就是uniq命令
-t:指定分隔符,默认是分隔符是制表符
-k n[,m]:按照指定的字段范围排序。从第n字段开始,m字段结束(默认到行尾)

sort命令默认是用每行开头第一个字符来进行排序的,比如:

1
2
[root@localhost ~]# sort /etc/passwd
#排序用户信息文件

如果想要反向排序,请使用“-r”选项:

1
2
[root@localhost ~]# sort -r /etc/passwd
#反向排序

如果想要指定排序的字段,需要使用“-t”选项指定分隔符,并使用“-k”选项指定字段号。加入我想要按照UID字段排序/etc/passwd文件:

1
2
[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
2
3
[root@localhost ~]# uniq [选项] 文件名
选项:
-i:忽略大小写

3、统计命令wc

1
2
3
4
5
[root@localhost ~]# wc [选项] 文件名
选项:
-l:只统计行数
-w:只统计单词数
-m:只统计字符数

四、条件判断

1、按照文件类型进行判断

测试选项 作用
-b 文件 判断该文件是否存在,并且是否为块设备文件(是块设备文件为真)
-c文件 判断该文件是否存在,并且是否为字符设备文件(是字符设备文件为真)
-d 文件 判断该文件是否存在,并且是否为目录文件(是目录为真)
-e 文件 判断该文件是否存在(存在为真)
-f 文件 判断该文件是否存在,并且是否为普通文件(是普通文件为真)
-L 文件 判断该文件是否存在,并且是否为符号链接文件(是符号链接文件为真)
-p 文件 判断该文件是否存在,并且是否为管道文件(是管道文件为真)
-s 文件 判断该文件是否存在,并且是否为非空(非空为真)
-S 文件 判断该文件是否存在,并且是否为套接字文件(是套接字文件为真)
1
2
3
4
5
6
7
8
[root@localhost ~]# [ -e /root/sh/ ]
[root@localhost ~]# echo $?
0
#判断结果为0,/root/sh/目录是存在的
[root@localhost ~]# [ -e /root/test ]
[root@localhost ~]# echo $?
1
#在/root/下并没有test文件或目录,所以“$?”的返回值为非零

还记得多命令顺序执行的“&&”和“||”吗?我们可以再判断一下/root/sh/是否是目录:

1
2
[root@localhost ~]# [ -d /root/sh ] && echo "yes" || echo "no" 
#第一个判断命令如果正确执行,则打印“yes”,否则打印“no”

2、按照文件权限进行判断

测试选项 作用
-r 文件 判断该文件是否存在,并且是否该文件拥有读权限(有读权限为真)
-w文件 判断该文件是否存在,并且是否该文件拥有写权限(有写权限为真)
-x 文件 判断该文件是否存在,并且是否该文件拥有执行权限(有执行权限为真)
-u 文件 判断该文件是否存在,并且是否该文件拥有SUID权 限( 有SUID权限为真)
-g 文件 判断该文件是否存在,并且是否该文件拥有SGID权 限( 有SGID权限为真)
-k 文件 判断该文件是否存在,并且是否该文件拥有SBit权 限( 有SBit权限为真)

比如:

1
2
3
4
[root@localhost ~]# ll test
-rw-r--r-- . 1 root root 97 6月7 07:34 test
[root@localhost ~]# [ -w test ] && echo "yes" || echo "no" yes
#判断文件是拥有写权限的

3、两个文件之间进行比较

测试选项 作用
文件1 -nt 文件2 判断文件1的修改时间是否比文件2的新(如果新则为真)
文件1 -ot 文件2 判断文件1的修改时间是否比文件2的旧(如果旧则为真)
文件1 -ef 文件2 判断文件1是否和文件2的Inode号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法

到底该如何判断两个文件是否是硬链接呢?这时test就派上用场了:

1
2
3
4
5
[root@localhost ~]# ln /root/test /tmp/ts
#创建个硬链接吧
[root@localhost ~]# [ /root/test -ef /tmp/ts ] && echo "yes" || echo "no
yes
#用test测试下,果然很有用

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
2
3
4
5
6
[root@localhost ~]# [ 23 -ge 22 ] && echo "yes" || echo "no"                  
yes
#判断23是否大于等于22,当然是了
[root@localhost ~]# [ 23 -le 22 ] && echo "yes" || echo "no"
no
#判断23是否小于等于22,当然不是了

5、字符串的判断

测试选项 作用
-z 字符串 判断字符串是否为空(为空返回真)
-n 字符串 判断字符串是否为非空(非空返回真)
字串1 ==字串2 判断字符串1是否和字符串2相等(相等返回真)
字串1 != 字串2 判断字符串1是否和字符串2不相等(不相等返回真)

举个例子:

1
2
3
4
5
[root@localhost ~]# name=sc
#给name变量赋值
[root@localhost ~]# [ -z "$name" ] && echo "yes" || echo "no"
no
#判断name变量是否为空,因为不为空,所以返回no

再来看看如何判断两个字符串相等:

1
2
3
4
5
6
[root@localhost ~]# aa=11 
[root@localhost ~]# bb=22
#给变量aa和变量bb赋值
[root@localhost ~]# [ "$aa" == "bb" ] && echo "yes" || echo "no"
no
#判断两个变量的值是否相等,明显不相等,所以返回no

6、多重条件判断

测试选项 作用
判断1 -a 判断2 逻辑与,判断1和判断2都成立,最终的结果才为真
判断1 -o 判断2 逻辑或,判断1和判断2有一个成立,最终的结果就为真
!判断 逻辑非,使原始的判断式取反

举个例子:

1
2
3
4
5
6
[root@localhost ~]# aa=11 
#给变量aa赋值
[root@localhost ~]# [ -n "$aa" -a "$aa" -gt 23 ] && echo "yes" || echo "no"
no
#判断变量aa是否有值,同时判断变量aa的是否大于23
#因为变量aa的值不大于23,所以虽然第一个判断值为真,返回的结果也是假

要想让刚刚的判断式返回真,需要给变量aa重新赋个大于23的值:

1
2
3
[root@localhost ~]# aa=24 
[root@localhost ~]# [ -n "$aa" -a "$aa" -gt 23 ] && echo "yes" || echo "no"
yes

再来看看逻辑非是什么样子的:

1
2
3
[root@localhost ~]# [ ! -n "$aa" ] && echo "yes" || echo "no" no 
#本来“-n”选项是变量aa不为空,返回值就是真。
#加入!之后,判断值就会取反,所以当变量aa有值时,返回值是假

注意:“!”和“-n”之间必须加入空格,否则会报错的

五、流程控制

1、if条件判断

1)、单分支if条件语句

单分支条件语句最为简单,就是只有一个判断条件,如果符合条件则执行某个程序,否则什么事情都不做。语法如下:

1
2
3
if [ 条件判断式 ];then
程序
fi

单分支条件语句需要注意几个点:

  • if语句使用fi结尾,和一般语言使用大括号结尾不同
  • [ 条件判断式 ]就是使用test命令判断,所以中括号和条件判断式之间必须有空格
  • then后面跟 符合条件之后执行的程序,可以放在[]之后,用“;”分割。也可以换行写入,就不需要“;”了,比如单分支if语句还可以这样写:
1
2
3
4
if [ 条件判断式 ]      
then
程序
fi
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
#统计根分区使用率
rate=$(df -h | grep "/dev/sda3" | awk '{print $5}' | cut -d "%" -f1)
#把根分区使用率作为变量值赋予变量rate
if [ $rate -ge 80 ]
#判断rate的值如果大于等于80,则执行then程序
then
echo "Warning! /dev/sda3 is full!!"
#打印警告信息。在实际工作中,也可以向管理员发送邮件。
fi

2)、双分支if条件语句

1
2
3
4
5
6
if [ 条件判断式 ]      
then
条件成立时,执行的程序
else
条件不成立时,执行的另一个程序
fi

例子1:

我们写一个数据备份的例子,来看看双分支if条件语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
例子1:备份mysql数据库

#!/bin/bash
#备份mysql数据库。

ntpdate asia.pool.ntp.org &>/dev/null
#同步系统时间
date=$(date +%y%m%d)
#把当前系统时间按照“年月日”格式赋予变量date
size=$(du -sh /var/lib/mysql)
#统计mysql数据库的大小,并把大小赋予size变量

if [ -d /tmp/dbbak ]
#判断备份目录是否存在,是否为目录
then
#如果判断为真,执行以下脚本
echo "Date : $date!" > /tmp/dbbak/dbinfo.txt
#把当前日期写入临时文件
echo "Data size : $size" >> /tmp/dbbak/dbinfo.txt
#把数据库大小写入临时文件
cd /tmp/dbbak
#进入备份目录
tar -zcf mysql-lib-$date.tar.gz /var/lib/mysql dbinfo.txt &>/dev/null
#打包压缩数据库与临时文件,把所有输出丢入垃圾箱(不想看到任何输出)
rm -rf /tmp/dbbak/dbinfo.txt
#删除临时文件
else
mkdir /tmp/dbbak
#如果判断为假,则建立备份目录
echo "Date : $date!" > /tmp/dbbak/dbinfo.txt
echo "Data size : $size" >> /tmp/dbbak/dbinfo.txt
#把日期和数据库大小保存如临时文件
cd /tmp/dbbak
tar -zcf mysql-lib-$date.tar.gz dbinfo.txt /var/lib/mysql &>/dev/null
#压缩备份数据库与临时文件
rm -rf /tmp/dbbak/dbinfo.txt
#删除临时文件
fi

例子2:

再举个例子,在工作当中,服务器上的服务经常会宕机。如果我们对服务器监控不好,就会造成服务器中服务宕机了,而管理员却不知道的情况,这时我们可以写一个脚本来监听本机的服务,如果服务停止或宕机了,可以自动重启这些服务。我们拿apache服务来举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
例子2:
判断apache是否启动,如果没有启动则自动启动

#!/bin/bash
#判断apache是否启动,如果没有启动则自动启动

port=$(nmap -sT 192.168.85.140 | grep tcp | grep http | awk '{print $2}')
#使用nmap命令扫描服务器,并截取apache服务的状态,赋予变量port

if [ "$port" == "open" ]
#如果变量port的值是“open”
then
echo “$(date) httpd is ok!” >> /tmp/autostart-acc.log
#则证明apache正常启动,在正常日志中写入一句话即可
else
/etc/rc.d/init.d/httpd start &>/dev/null
#否则证明apache没有启动,自动启动apache
echo "$(date) restart httpd !!" >> /tmp/autostart-err.log
#并在错误日志中记录自动启动apche的时间
fi

3)、多分支if条件语句

1
2
3
4
5
6
7
8
9
10
if [ 条件判断式1 ]     
then
当条件判断式1成立时,执行程序1
elif [ 条件判断式2 ]
then
当条件判断式2成立时,执行程序2
...省略更多条件...
else
当所有条件都不成立时,最后执行此程序
fi

那我们再写一个例子,用if多分支条件语句来判断一下用户输入的是一个文件,还是一个目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
例子:判断用户输入的是什么文件

#!/bin/bash
#判断用户输入的是什么文件

read -p "Please input a filename: " file
#接收键盘的输入,并赋予变量file

if [ -z "$file" ]
#判断file变量是否为空
then
echo "Error,please input a filename"
#如果为空,执行程序1,也就是输出报错信息
exit 1
#退出程序,并返回值为1(把返回值赋予变量$?)
elif [ ! -e "$file" ]
#判断file的值是否存在
then
echo "Your input is not a file!"
#如果不存在,则执行程序2
exit 2#退出程序,把并定义返回值为2
elif [ -f "$file" ]
#判断file的值是否为普通文件
then
echo "$file is a regulare file!"
#如果是普通文件,则执行程序3
elif [ -d "$file" ]
#判断file的值是否为目录文件
then
echo "$file is a directory!"
#如果是目录文件,则执行程序4
else
echo "$file is an other file!"
#如果以上判断都不是,则执行程序5
fi

2、多分支case条件语句

case语句和if…elif…else语句一样都是多分支条件语句,不过和if多分支条件语句不同的是,case语句只能判断一种条件关系,而if语句可以判断多种条件关系。case语句语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
case $变量名 in     
"值1")
如果变量的值等于值1,则执行程序1
;;
"值2")
如果变量的值等于值2,则执行程序2
::
...省略其他分支...
*)
如果变量的值都不是以上的值,则执行此程序
;;
esac

这个语句需要注意以下内容:

  • case语句,会取出变量中的值,然后与语句体中的值逐一比较。如果数值符合,则执行对应的程序,如果数值不符,则依次比较下一个值。如果所有的值都不符合,则执行“)”(“”代表所有其他值)中的程序。
  • case语句以“case”开头,以“esac”结尾。

每一个分支程序之后要通过“;;”双分号结尾,代表该程序段结束。

我们写一个判断是“yes/no”的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash#判断用户输入

read -p "Please choose yes/no: " -t 30 cho
#在屏幕上输出“请选择yes/no”,然后把用户选择赋予变量cho

case $cho in
#判断变量cho的值
"yes")
#如果是yes
echo "Your choose is yes!"
#执行程序1
;;
"no")
#如果是no
echo "Your choose is no!"
#执行程序2
;;
*)
#如果既不是yes,也不是no
echo "Your choose is error!"
#则执行此程序
;;
esac

3、for循环

for循环是固定循环,也就是在循环时已经知道需要进行几次的循环,有时也把for循环称为计数循环。for的语法有如下两种:

1
2
3
4
5
语法一:
for 变量 in 值1 值2 值3...
do
程序
done

这种语法中for循环的次数,取决于in后面值的个数(空格分隔),有几个值就循环几次,并且每次循环都把值赋予变量。也就是说,假设in后面有三个值,for会循环三次,第一次循环会把值1赋予变量,第二次循环会把值2赋予变量,以此类推。

1
2
3
4
5
语法二:
for (( 初始值;循环控制条件;变量变化))
do
程序
done

语法二中需要注意:

  • 初始值:在循环开始时,需要给某个变量赋予初始值,如i=1;
  • 循环控制条件:用于指定变量循环的次数,如i<=100,则只要i的值小于等于100,循环就会继续;
  • 变量变化:每次循环之后,变量该如何变化,如i=i+1。代表每次循环之后,变量i的值都加1。

1)、语法一举例:

我们先看看语法一是什么样子的:

1
2
3
4
5
6
7
8
例子:打印时间
#!/bin/bash
#打印时间#

for time in morning noon afternoon evening
do
echo "This time is $time!"
done

2)、语法二举例那语法二就和其他语言中的for循环更加类似了,也就是事先决定循环次数的固定循环了。先举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
例子:从1加到100 
#!/bin/bash
#从1加到100

s=0

for (( i=1;i<=100;i=i+1 ))
#定义循环100次
do
s=$(( $s+$i ))每次循环给变量s赋值
done

echo "The sum of 1+2+...+100 is : $s"#输出1加到100的和

4、while循环

1
2
3
4
while [ 条件判断式 ]    
do
程序
done

对while循环来讲,只要条件判断式成立,循环就会一直继续,直到条件判断式不成立,循环才会停止。好吧,我们还是写个1加到100的例子吧,这种例子虽然对系统管理帮助不大,但是对理解循环非常有帮助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
例子:1加到100 

#!/bin/bash
#从1加到100

i=1 s=0
#给变量i和变量s赋值

while [ $i -le 100 ]
#如果变量i的值小于等于100,则执行循环
do
s=$(( $s+$i ))
i=$(( $i+1 ))
done

echo "The sum is: $s"

5、until循环

再来看看until循环,和while循环相反,until循环时只要条件判断式不成立则进行循环,并执行循环程序。一旦循环条件成立,则终止循环。语法如下:

1
2
3
4
until [ 条件判断式 ]    
do
程序
done

还是写从1加到100这个例子,注意和while循环的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
例子:从1加到100 

#!/bin/bash#从1加到100

i=1 s=0
#给变量i和变量s赋值
until [ $i -gt 100 ]
#循环直到变量i的值大于100,就停止循环
do
s=$(( $s+$i ))
i=$(( $i+1 ))
done

echo "The sum is: $s"

6、函数

1
2
3
function 函数名 ()  { 
程序
}

那我们写一个函数吧,还记得从1加到100这个循环吗?这次我们用函数来实现它,不过不再是从1加到100了,我们让用户自己来决定加到多少吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
例子:

#!/bin/bash
#接收用户输入的数字,然后从1加到这个数字

function sum () {
#定义函数sum
s=0
for (( i=0;i<=$1;i=i+1 ))
#循环直到i大于$1为止。$1是函数sum的第一个参数#在函数中也可以使用位置参数变量,不过这里的$1指的是函数的第一个参数
do
s=$(( $i+$s ))
done

echo "The sum of 1+2+3...+$1 is : $s"
#输出1加到$1的和
}

read -p "Please input a number: " -t 30 num
#接收用户输入的数字,并把值赋予变量num
y=$(echo $num | sed 's/[0-9]//g')
#把变量num的值替换为空,并赋予变量y

if [ -z "$y" ]
#判断变量y是否为空,以确定变量num中是否为数字
then
sum $num#调用sum函数,并把变量num的值作为第一个参数传递给sum函数
else
echo "Error!! Please input a number!"
#如果变量num的值不是数字,则输出报错信息
fi

7、特殊流程控制语句

1)、 exit语句

系统是有exit命令的,用于退出当前用户的登录状态。可是在Shell脚本中,exit语句是用来退出当前脚本的。也就是说,在Shell脚本中,只要碰到了exit语句,后续的程序就不再执行,而直接退出脚本。exit的语法如下:

1
exit [返回值]

如果exit命令之后定义了返回值,那么这个脚本执行之后的返回值就是我们自己定义的返回值。可以通过查询$?这个变量,来查看返回值。如果exit之后没有定义返回值,脚本执行之后的返回值是执行exit语句之前,最后执行的一条命令的返回值。

写一个exit的例子:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
#演示exit的作用

read -p "Please input a number: " -t 30 num
#接收用户的输入,并把输入赋予变量num
y=$(echo $num | sed 's/[0-9]//g')
#如果变量num的值是数字,则把num的值替换为空,否则不替换
#把替换之后的值赋予变量y
[ -n "$y" ] && echo "Error! Please input a number!" && exit 18
#判断变量y的值如果不为空,输出报错信息,退出脚本,退出返回值为18
echo "The number is: $num"
#如果没有退出加班,则打印变量num中的数字

这个脚本中,大家需要思考,如果我输入的不是数字,那么“echo “The number is: $num””这个脚本是否会执行?当然不会,因为如果输入的不是数字,“[ -n “$y” ] && echo “Error! Please input a number!” && exit 18”这句脚本会执行,exit一旦执行脚本就会终止。

2)、 break语句

再来看看特殊流程控制语句break的作用,当程序执行到break语句时,会结束整个当前循环。而continue语句也是结束循环的语句,不过continue语句单次当前循环,而下次循环会继续。画个示意图解释下break语句,如图所示:

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
#演示break跳出循环

for (( i=1;i<=10;i=i+1 ))
#循环十次
do
if [ "$i" -eq 4 ]
#如果变量i的值等于4
then
break
#退出整个循环
fi
echo $i
#输出变量i的值
done

在这个脚本中,因为一旦变量i的值等于4,整个循环都会跳出,所以应该只能循环三次

3)、 continue语句

再来看看continue语句,continue也是结束流程控制的语句。如果在循环中,continue语句只会结束单次当前循环,也画个示意图来说明下continue语句:

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
#演示continue跳出循环

for (( i=1;i<=10;i=i+1 ))
#循环十次
do
if [ "$i" -eq 4 ]
#如果变量i的值等于4
then
continue
#退出整个循环
fi
echo $i
#输出变量i的值
done

continue只会退出单次循环,所以并不影响后续的循环,所以只会少4的输出。这个例子和break的例子做个比较,应该可以更清楚的说明break和continue的区别。