【Linux系统管理】10 Shell 编程进阶篇

六 Shell 编程

6.1 正则表达式

6.1.1 概述

还记得我们在上一章说过正则表达式和通配符的区别(正则表达式用来在文件中匹配符合条件的字符串,通配符用来匹配符合条件的文件名)吗?其实这种区别只在Shell当中适用,因为用来在文件当中搜索字符串的命令,如 grep、awk、sed 等命令可以支持正则表达式,而在系统当中搜索文件的命令,如 ls、find、cp这些命令不支持正则表达式,所以只能使用shell自己的通配符来进行匹配了。

Shell 正则表达式主要用于文本过滤。

6.1.2 基础正则表达式

元字符作用
*前一个字符匹配 0 次或任意多次。
.匹配除了换行符外任意一个字符。
^匹配行首。例如:^hello会匹配以hello开头的行。
$匹配行尾。例如: hello&会匹配以hello结尾的行。
[]匹配中括号中指定的任意一个字符,只匹配一个字符。例如:[aoeiu]匹配任意一个元音字母,[0-9]匹配任意一位数字,[a-z][o-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 文件中建立这个别名

~ vim /root/.bashrc
alias grep='grep --color=auto'

1)、练习文件建立

cat > test_rule.txt <<EOF
Mr. Li Ming said
he was the most honest man.
123despise him.
sid

But since Mr. shen Chao came,
he never saaaid those words.
5555nice!

because, actuaaaally,
Mr. Shen Chao is the most honest man

Later,Mr. Li ming soid his not body.
hello world
hello earth

EOF

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

~ grep "a*" test_rule.txt
Mr. Li Ming said
he was the most honest man.
123despise him.

But since Mr. shen Chao came,
he never saaaid those words.
5555nice!

because, actuaaaally,
Mr. Shen Chao is the most honest man

Later,Mr. Li ming soid his not body.

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

#只包含一个a的行
~ grep "a" test_rule.txt #grep "aa*" test_rule.txt

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

~ grep "aaa*" test_rule.txt #grep aa test_rule.txt

如果正则表达式是" aaaaa* ",则会匹配最少包含四个连续 a 的字符串,如:

~ grep "aaaaa*" test_rule.txt #grep aaaa test_rule.txt

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

~ grep "aaaaaa*" test_rule.txt
#无返回值

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

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

grep "s..d" test_rule.txt
Mr. Li Ming said
Later,Mr. Li ming soid his not body.
#“s..d”会匹配在s和d这两个字母之间一定有两个字符的单词grep "s.*d" test_rule.txt
Mr. Li Ming said
he never saaaid those words.
Later,Mr. Li ming soid his not body.
#最后一句话比较有意思,匹配的是“soid his hot bod”

范例:

#匹配全文grep ".*" test_rule.txt

4)、" ^ " 匹配行首," $ "匹配行尾

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

grep "^M" test_rule.txt

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

grep "n$" test_rule.txt

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

grep "^$" test_rule.txt

#匹配不是空白行的行grep -v "^$" test_rule.txt

范例:查找 . 结尾的行

grep "\.$" test_rule.txt

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

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

grep "s[ao]id" test_rule.txt

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

grep "[0-9]" test_rule.txt

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

grep "[A-Z]" test_rule.txt

如果正则是“1”代表匹配用小写字母开头的行:

grep "^[a-z]" test_rule.txt"

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

grep "^[^a-z]" test_rule.txt

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

grep "^[^a-zA-Z]" test_rule.txt

而“[0-9]”则会匹配不用数字开头的行:

grep "^[^0-9]" test_rule.txt

\7) 、“\”转义符

test grep "\.$" test_rule.txt
he was the most honest man.
123despise him.
he never saaaid those words.
Later,Mr. Li ming soid his not body.

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

~ grep "a\{3\}" test_rule.txt
he never saaaid those words.
because, actuaaaally,

上面的两行都包含三个连续的 a,所以都会匹配。但是如果先要只显示三个连续的a,可以这样来写正则:

~ grep "[su]a\{3\}[il]" test_rule.txt

如果正则是“[0-9]{3}”则会匹配包含连续的三个数字的字符串:

~ grep "[0-9]\{3\}" test_rule.txt

虽然“5555”有四个连续的数字,但是包含三个连续的数字,所以也是可以列出的。可是这样不能体现出来“[0-9]{3}”只能匹配三个连续的数字,而不能匹配四个连续的数字。那么正则就应该这样来写“2 \ {3}[a-z]”:

~ grep "^[0-9]\{3\}[a-z]" test_rule.txt
#只匹配用连续三个数字开头的行

9 ) 、“{n,}”表示其前面的字符出现不小于n次

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

~ grep "^[0-9]\{3,\}[a-z]" test_rule.txt

而“[su]a{3,}[il]”正则则会匹配在字母 s 或 u 和 i 或 l 之间,最少出现三个连续的a的字符串:

~ grep "[su]a\{3,\}[il]" test_rule.txt

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

#匹配在字母s和字母i之间有最少一个a,最多三个a
~ grep "sa\{1,3\}i" test_rule.txt

#匹配在字母s和字母i之间有最少两个a,最多三个a
~ grep "sa\{2,3\}i" test_rule.txt

练习:将系统中把所有的普通用户进行删除

~ grep -v "root" /etc/passwd | grep "/bin/bash" | cut -d ":" -f 1 > userdel.txt
~ for i in $(cat userdel.txt) ;do userdel -r $i ;done
~ rm -rf userdel.txt

6.1.3 扩展正则表达式

熟悉正则表达式的童鞋应该很疑惑,在正则表达式中应该还可以支持一些元字符,比如“+”“?”“|”“()”。其实Linux是支持这些元字符的,只是grep命令默认不支持而已。如果要想支持这些元字符,必须使用egrep命令或grep -E选项,所以我们又把这些元字符称作扩展元字符。

如果查询grep 的帮助,对egrep 的说明就是和 grep -E 选项一样的命令,所以我们可以把两个命令当做别名来对待。通过表来看看Shell中支持的扩展元字符:

在其他语言当中,并没有扩展正则表达式的说法。正则表达式使用场景最多的是在Web端,用户输入之后,需要进行一定的粗过滤,正则过滤要比程序过滤所消耗的资源少。正则表达式也会被利用于匹配数据类型。

扩展元字符作用
+前一个字符匹配1次或任意多次。如“go+gle”会匹配“gogle”、“google”或“gooogle”,当然如果“o”有更多个,也能匹配。
?前一个字符匹配0次或1次。如“colou?r”可以匹配“colour”或“color”。
|匹配两个或多个分支选择。如“was|his”会匹配既包含“was”的行,也匹配包含“his”的行。
()匹配其整体为一个字符,即模式单元。可以理解为由多个单个字符组成的大字符。如“(dog)+”会匹配“dog”、 “dogdog”、 “dogdogdog”等,因为被()包含的字符会当成一个整体。但“hello (world|earth)”会匹配“hello world”及“hello earth”。

范例:

~ grep -E "hello (world|earth)" test_rule.txt

6.1.4 正则实例

匹配邮箱

[0-9a-zA-Z_]+@[0-9a-zA-Z_]+(\.[0-9a-zA-Z]+){1,3}

范例:

~ cat > mail.txt <<EOF
123@atguigu.com
sc@sina.com
aaaaa.com
abc@aaaaa
liming@163.com
EOF

~ grep -E "[0-9a-zA-Z_]+@[0-9a-zA-Z_]+(\.[0-9a-zA-Z]+){1,3}" mail.txt

匹配IP地址

[0-9] 个位数
[1-9][0-9] 十位数
1[0-9][0-9] 百位数
2[0-4][0-5] 百位数
25[0-5] 百位数

^(([0-9]\.)|([1-9][0-9]\.)|(1[0-9][0-9]\.)|(2[0-4][0-9]\.)|(25[0-5]\.)){3}(([0-9])|([1-9][0-9])|(1[0-9][0-9])|(2[0-4][0-9])|(25[0-5]))$

这个正则表达式经过测试,确实可以匹配合法的IP地址!

~ cat > ip.txt <<EOF
192.168.1.200
202.106.0.20
300.36.190.5
222222222222
192.168.1.300
202.2.2
EOF

~ grep -E "^(([0-9]\.)|([1-9][0-9]\.)|(1[0-9][0-9]\.)|(2[0-4][0-9]\.)|(25[0-5]\.)){3}(([0-9])|([1-9][0-9])|(1[0-9][0-9])|(2[0-4][0-9])|(25[0-5]))$" ip.txt

img

6.2 字符截取和替换命令

6.2.1 cut 列提取命令

cut 命令无法提取空格作为分隔符特征的截取,需要使用 tr 等命令进行字符串的替换。

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

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

cat > student.txt <<EOF
ID      Name    gender  Mark
1       Liming  M       86
2       Sc      M       90
3       Tg      M       83
EOF
#需要使用制表符Tab键作为分隔符

范例:

~ cut -f 2 student.txt
Name
Liming
Sc
Tg
#提取第二列内容

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

~ cut -f 2,3 student.txt
Name    gender
Liming  M
Sc      M
Tg      M

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

“10-20”代表提取所有行的第十个字符到第二十个字符,而“-8”代衣远取所有行从行首到八个字符:

~ cut -c 8- student.txt
#提取第八个字符开始到行尾,好像很乱啊,那是因为每行的字符个数不相等啊

~ cut -d ":" -f 1,3 /etc/passwd
#以“:”作为分隔符,提取/etc/passwd文件的第一列和第三列

~ grep -v root /etc/passwd | grep "/bin/bash" | cut -d ":" -f 1
#获取系统普通用户

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

~ df -Th | cut -d " " -f 1,3

#需要使用tr将字符串进行替换
~ df -Th | tr -s " " : | cut -d ":" -f 1,3

6.2.2 awk 编程

1)、概述

文本和数据进行处理的编程语言

awk 是一种编程语言,用于在linux/unix下对文本和数据进行处理。数据可以来自标准输入(stdin)、一个或多个文件,或其它命令的输出。它支持用户自定义函数和动态正则表达式等先进功能,是linux/unix下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。awk有很多内建的功能,比如数组、函数等,这是它和C语言的相同之处,灵活性是awk最大的优势。

2)、printf 格式化输出

~ 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键

范例:

~ print
bash: print: command not found

~ printf
printf: usage: printf [-v var] format [arguments]

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

~ vim student.txt
ID      Name    PHP     Linux   MySQL   Average
1       Liming  82      95      86      87.66
2       Sc      74      96      87      85.66
3       Tg      99      83      93      91.66

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

~ printf '%s' $(cat student.txt)
IDNamePHPLinuxMySQLAverage1Liming82958687.662Sc74968785.663Tg99839391.66

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

~ printf '%s\t %s\t %s\t %s\t %s\t %s\t \n' $(cat student.txt)
#注意在printf命令的单引号中,只能识别格式输出符号,而手工输入的空格是无效的
ID       Name    PHP     Linux   MySQL   Average
1        Liming  82      95      86      87.66
2        Sc      74      96      87      85.66
3        Tg      99      83      93      91.66
#当然双引号也可以进行一部分显示,只是不够严谨

printf 需要人手工调正文本的格式的,cat 之所以可以将文本打印出来,是因为 cat 已经通过程序将文本格式调整过了。

因为 awk 只能支持 printf。printf 才是程序最基本的输出格式,绝大部分程序都是这样的。

如果不想把成绩当成字符串输出,而是按照整型和浮点型输出(可用于数值计算),则要这样:

~ printf '%i\t %s\t %i\t %i\t %i\t %8.2f\t \n' $(cat student.txt | grep -v Name)
1        Liming  82      95      86      87.66
2        Sc      74      96      87      85.66
3        Tg      99      83      93      91.66

printf 不支持识别管道符的标准输出

3)、awk 基本使用

[root@localhost ~]# awk '条件1 {动作1} 条件2 {动作2} …' 文件名
条件(Pattern) :
  一般使用关系表达式作为条件。这些关系表达式非常多,具体参考表所示,例如:
  x > 10 判断变量x是否大于10
  x == y 判断变量x是否等于变量y
  A ~ B  判断字符串A中是否包含能匹配B表达式的子字符串
  A !~ B 判断字符串A中是否不包含能匹配B表达式的子字符串
动作(Action) :
  格式化输出(使用 printf 进行输出)
  流程控制语句

我们这里先来学习awk基本用法,也就是只看看格式化输出动作是干什么的。至于条件类型和流程控制语句我们在后面再详细介绍。那看看这个例子吧:

awk '{print $2 "\t" $6 "\n"}' student.txt #使用的print
awk '{printf $2"\t"$6"\n"}' student.txt   #使用的printf
#输出第二列和第六列

img

比如刚刚截取df命令的结果时,cut命令已经力不从心了,我们来看看awk命令:

df -aTh | awk -F" " '{print $1 "\t" $5}'

4)、awk 的条件

条件的类型条件说明
awk 保留字BEGIN在awk程序一开始时,尚未读取任何数据之前执行。BEGIN后的动作只在程序开始时执行一次
END在awk程序处理完所有数据,即将结束时执行。END后的动作只在程序结束时执行一次
关系运算符>大于
<小于
>=大于等于
<=小于等于
==等于。用于判断两个值是否相等,如果是给变量赋值,请使用“=”号
!=不等于
A~B判断字符串A中是否包含能匹配B表达式的子字符串
A!~B判断字符串A中是否不包含能匹配B表达式的子字符串
正则表达式/正则/如果在“//”中可以写入字符,也可以支持正则表达式
  • BEGIN

BEGIN 使用的场景比较多

BEGIN是awk的保留字,是一种特殊的条件类型。BEGIN 的执行时机是“在awk程序一开始时,尚未读取任何数据之前执行”。一旦BEGIN后的动作执行一次,当awk开始从文件中读入数据,BEGIN的条件就不再成立,所以BEGIN定义的动作只能被执行一次。例如:

~ awk 'BEGIN{print "This is a tanscript \n"} {print $2"\t"$6}' student.txt
This is a tanscript

Name    Average
Liming  87.66
Sc      85.66
Tg      91.66
#awk 命令只要检测不到完整的单引号不会执行,所以这个命令的换行不用加入“I”,就是一行命令
#这里定义了两个动作
#第一个动作使用BEGIN条件,所以会在读入文件数据前打印“这是一张成绩单”(只会执行一次)
#第二个动作会打印文件的第二字段和第六字段
  • END

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

~ awk 'END{print "The End \n"} {print $2"\t"$6}' student.txt
Name    Average
Liming  87.66
Sc      85.66
Tg      91.66
The End

#在输出结尾输入“The End”,这并不是文档本身的内容,而且只会执行一次
  • 关系运算符

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

例子1:
grep -v "Name" student.txt | awk '$6 >= 87{print $2 "\n"}'
grep -v "Name" student.txt | awk 'BEGIN{print "The average score high"} $6 >= 87{print $2}'

#使用cat输出文件内容,用grep取反包含“Name”的行
#判断第六字段(平均成绩)大于等于87分的行,如果判断式成立,则打第六列(学员名)

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

虽然 awk 是截列的,但是处理数据的时候还是一行行的进行处理,处理完毕后再来截入。

总结:awk 输出之后表面上看出来输出的是列,但是数据处理的时候也是需要一行行的进行处理,直到将

1)如果有BEGIN条件,则先执行 BEGIN定义的动作

2)如果没有BEGIN条件,则读入第一行整行的数据赋予 $0,把第一行的分开的数据依次赋予 $1、$2、$3等变量。其中$0代表此行的整体数据,$1代表第一字段,$2代表第二字段,以此类推。

2)依据条件类型判断动作是否执行。如果条件符合,则执行动作,否则读入下一行数据。如果没有条件,则每行都执行动作。

3)读入下一行数据,重复执行以上步骤。

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

例子2:
#指定列中的包含
~ awk '$2 ~ /Sc/ {print $6}' student.txt
#如果第二字段中输入包含有“Sc”字符,则打印第六字段数据
85.66

#指定整行的包含
~ awk '/Sc/{print $6}' student.txt
85.66

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

  • 正则表达式

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

例子1:
~ awk '/Liming/{print}' student.txt
1       Liming  82      95      86      87.66
#打印Liming的成绩

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

5)、awk 内置变量

awk内置变量作用
$0代表目前awk所读入的整行数据。我们已知awk是一行一行读入数据的,$0就代表当前读入行的整行数据。
$n代表目前读入行的第n个字段。
NF当前行拥有的字段(列,Field)总数。
NR当前awk 所处理的行(Row),是总数据的第几行。
FS用户定义分隔符。awk 的默认分隔符是任何空格,如果想要使用其他分隔符(如“:”),就需要FS变量定义。
ARGC命令行参数个数。
ARGV命令行参数数组。
FNR当前文件中的当前记录数(对输入文件起始为1)。
OFMT数值的输出格式(默认为%.6g) 。
OFS输出字段的分隔符(默认为空格)。
ORS输出记录分隔符(默认为换行符)。
RS输入记录分隔符(默认为换行符)。

awk 默认是使用空格键或者Tab制表符作为分隔符。而 cut 只能使用 Tab 制表符作为分隔符。

~ cat /etc/passwd | grep "/bin/bash" | awk '{FS=":"} {printf $1"\t"$3"\n"}'
root:x:0:0:root:/root:/bin/bash
zzw     500
mysql   27

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

~ cat /etc/passwd | grep "/bin/bash" | awk 'BEGIN{FS=":"} {printf $1"\t"$3"\n"}'
root    0
zzw     500
mysql   27
cat /etc/passwd | grep "/bin/bash" | awk 'BEGIN{FS=":"} {printf $1 "\t" $3 "\t 行号:" NR "\t 字段数:"NF "\n"}'
#解释下awk命令
#开始执行{分隔符是“:”}{输出第一字段和第三字段输出行号(NR值)字段数(NF值)}
root    0        行号:1  字段数:7
zzw     500      行号:2  字段数:7
mysql   27       行号:3  字段数:7

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

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

6)、流程控制

在 awk 编程中,因为命令语句非常长,在输入格式时需要注意以下内容:

  • 多个条件{动作}可以用空格分割,也可以用回车分割。
  • 在一个动作中,如果需要执行多个命令,需要用“;”分割,或用回车分割。
  • 在 awk 中,变量的赋值与调用都不需要加入“$”符。
  • 条件中判断两个值是否相同,请使用“==”,以便和变量赋值进行区分。

7)、函数

awk 函数的定义方法如下:
  function 函数名(参数列表){
    函数体
  }

8)、调用脚本

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

6.2.3 sed 命令

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

[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”动作了:

~ sed '2p' student.txt
ID      Name    PHP     Linux   MySQL   Average
1       Liming  82      95      86      87.66
1       Liming  82      95      86      87.66
2       Sc      74      96      87      85.66
3       Tg      99      83      93      91.66

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

~ sed -n '2p' student.txt
1       Liming  82      95      86      87.66

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

~ sed '2,4d' student.txt
#删除第二行到第四行的数据
ID      Name    PHP     Linux   MySQL   Average

~ cat student.txt
ID      Name    PHP     Linux   MySQL   Average
1       Liming  82      95      86      87.66
2       Sc      74      96      87      85.66
3       Tg      99      83      93      91.66
#但是文件本身并没有修改

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

~ sed '2a hello' student.txt
ID      Name    PHP     Linux   MySQL   Average
1       Liming  82      95      86      87.66
hello
2       Sc      74      96      87      85.66
3       Tg      99      83      93      91.66

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

~ sed '2i hello \
> world' student.txt
ID      Name    PHP     Linux   MySQL   Average
hello
world
1       Liming  82      95      86      87.66
2       Sc      74      96      87      85.66
3       Tg      99      83      93      91.66
#在第二行前插入两行数据

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

~ sed -n '2i hello \
world' student.txt
hello
world
#只查看sed命令操作的数据

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

再来看看如何实现行数据替换,假设李明老师的成绩太好了,我实在是不想看到他的成绩刺激我,那我就可以这样:

~ cat student.txt | sed '2c No such person'
ID      Name    PHP     Linux   MySQL   Average
No such person
2       Sc      74      96      87      85.66
3       Tg      99      83      93      91.66

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

~ cat student.txt | sed -i.bak '2c No such person'
  • 字符串替换

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

~ sed 's/旧字符串/新字符串/g' 文件名

替换的格式和vim非常类似,假设我觉得我自己的PHP成绩太低了,想作弊给他改高点,就可以这样来做:

~ sed '3s/74/99/g' student.txt
ID      Name    PHP     Linux   MySQL   Average
1       Liming  82      95      86      87.66
2       Sc      99      96      87      85.66
3       Tg      99      83      93      91.66
#在第三行中,把74换成99

这样看起来就比较爽了吧。如果我想把Tg老师的成绩注释掉,让他不再生效(没有成绩了吧?补考去吧? )。可以这样做:

~ sed '4s/^/#/g' student.txt
ID      Name    PHP     Linux   MySQL   Average
1       Liming  82      95      86      87.66
2       Sc      74      96      87      85.66
#3      Tg      99      83      93      91.66
#这里使用正则表达式," ^ " 代表行首

在sed中只能指定行范围,所以很遗憾我在他们两个的中间,不能只把他们两个注释掉,那么我们可以这样:

~ sed -e 's/Liming//g ; s/Tg//g' student.txt
ID      Name    PHP     Linux   MySQL   Average
1               82      95      86      87.66
2       Sc      74      96      87      85.66
3               99      83      93      91.66
#同时把“Liming”和“Tg”替换为空

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

~ sed -e 's/Liming//g
> s/Tg//g' student.txt
ID      Name    PHP     Linux   MySQL   Average
1               82      95      86      87.66
2       Sc      74      96      87      85.66
3               99      83      93      91.66

6.3 字符处理命令

6.3.1 排序命令 sort

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

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

sort /etc/passwd
#排序用户信息文件

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

sort -r /etc/passwd
#反向排序

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

sort -t ":" -k 3,3 /etc/passwd

看起来好像很美,可是如果仔细看看,怎么daemon用户的UID是2,反而排在了下面?这是因为sort默认是按照字符排序,前面用户的UID的第一个字符都是1,所以这么排序。要想按照数字排序,请使用“-n”选项:

sort -n -t ":" -k 3,3 /etc/passwd

当然“-k”选项可以直接使用“-k 3”,代表从第三字段到行尾都排序(第一个字符先排序,如果一致,第二个字符再排序,知道行尾)。

sort -n -t ":" -k 3 /etc/passwd

6.3.2 uniq

uniq命令是用来取消重复行的命令,其实和“sort -u”选项是一样的。命令格式如下:

~ uniq [选项] 文件名
选项:
  -c, --count                在每行开头增加重复次数。
  -d, --repeated             所有邻近的重复行只被打印一次。
  -D                         所有邻近的重复行将全部打印。
  --all-repeated[=METHOD]    类似于 -D,但允许每组之间以空行分割。METHOD取值范围{none(默认),prepend,separate}。
  -f, --skip-fields=N        跳过对前N个列的比较。
  --group[=METHOD]           显示所有行,允许每组之间以空行分割。METHOD取值范围:{separate(默认),prepend,append,both}。
  -i, --ignore-case          忽略大小写的差异。
  -s, --skip-chars=N         跳过对前N个字符的比较。
  -u, --unique               只打印非邻近的重复行。
  -z, --zero-terminated      设置行终止符为NUL(空),而不是换行符。
  -w, --check-chars=N        只对每行前N个字符进行比较。
  --help                     显示帮助信息并退出。
  --version                  显示版本信息并退出。
  1. uniq只检测邻近的行是否重复,sort -u将输入文件先排序然后再处理重复行。
  2. 该命令是GNU coreutils包中的命令,相关的帮助信息请查看man -s 1 uniq,info coreutils ‘uniq invocation’。

6.3.3 统计命令 wc

wc命令 统计指定文件中的字节数、字数、行数,并将统计结果显示输出。利用wc指令我们可以计算文件的Byte数、字数或是列数,若不指定文件名称,或是所给予的文件名为“-”,则wc指令会从标准输入设备读取数据。wc同时也给出所指定文件的总统计数。

[root@localhost ~]# wc [选项] 文件名
选项:
  -l :只统计行数
  -w :只统计单词数
  -m :只统计字符数
  -L :打印最长行的长度

6.4 条件判断

6.4.1 按照文件类型进行判断

条件判断的命令是 test,也可以使用 [] 中括号内填写判断条件,但是在脚本以及在命令执行中一般都是使用 [] 中括号,可以利用 help test 查看条件判断的帮助文档。

根据表,我们先来看看test可以进行哪些文件类型的判断:

测试选项作用
-b 文件判断该文件是否存在,并且是否为块设备文件(是块设备文件为真)
-c 文件判断该文件是否存在,并且是否为字符设备文件(是字符设备文件为真)
-d 文件判断该文件是否存在,并且是否为目录文件(是目录为真)
-e 文件判断该文件是否存在(存在为真)
-f 文件判断该文件是否存在,并且是否为普通文件(是普通文件为真)
-L 文件判断该文件是否存在,并且是否为符号链接文件(是符号链接文件为真)
-p 文件判断该文件是否存在,并且是否为管道文件(是管道文件为真)
-s 文件判断该文件是否存在,并且是否为非空(非空为真)
-S 文件判断该文件是否存在,并且是否为套接字文件(是套接字文件为真)

范例:

~ [ -e /root/test ]
~ echo $?
0
#判断结果为0,/root/sh/目录是存在的

~ [ -e /root/sh ]
~ echo $?
1
#在/root/下并没有test文件或目录,所以“$?”的返回值为非零

#示例
~ [ -e /etc/passwd ] && echo "yes" || echo "no"
yes
~ [ -e /etc ] && echo "yes" || echo "no"
yes
~ [ -e /etc/abc ] && echo "yes" || echo "no"
no
~ cp /etc/passwd /root/sh/passwd
~ [ -s /root/sh/passwd ] && echo "yes" || echo "no"
yes
~ [ -s /root/sh/abc ] && echo "yes" || echo "no"
no

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

~ [ -e /root/sh ] && echo "yes" || echo "no"
yes

6.4.2 按照文件权限

test是非常完善的判断命令,还可以判断文件的权限,我们通过表来看看:

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

需要熟记。

比如:

~ ls -l student.txt
-rw-r--r-- 1 root root 96 Aug 20 18:34 student.txt

~ [ -w student.txt ] && echo "yes" || echo "no"
yes
#判断文件是拥有写权限的

#示例
~ [ -w abc ] && echo "yes" || echo "no"
yes
~ chmod u+s abc
~ ls -l abc
-rwSr--r-- 1 root root 0 Aug 21 02:18 abc
~ [ -u abc ] && echo "yes" || echo "no"
yes

6.4.3 两个文件之间进行比较

通过表来看看如何进行两个文件之间的比较:

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

我们一直很苦恼,到底该如何判断两个文件是否是硬链接呢﹖这时test就派上用场了:

~ ln /root/student.txt /tmp/stu.txt
#创建个硬链接把
[ /root/student.txt -ef /tmp/stu.txt ] && echo "yes" || echo "no"
yes
#用test进行测试下

6.4.4 两个整数之间比较

通过表来学习下如何在两个整数之间进行比较:

测试选项作用
整数1 -eq 整数2判断整数1是否和整数⒉相等(相等为真)
整数1 -ne 整数⒉判断整数1是否和整数2不相等(不相等位置)
整数1 -gt 整数判断整数1是否大于整数2(大于为真)
整数1 -lt 整数2判断整数1是否小于整数2(小于位置)
整数1 -ge 整数2判断整数1是否大于等于整数2(大于等于为真)
整数1 -le 整数2判断整数1是否小于等于整数2(小于等于为真)

整数1 -eq 整数2 等价于 字符串1 == 字符串2

整数1 -ne 整数2 等价于 字符串1 != 字符串2

举个例子:

~ [ 23 -ge 22 ] && echo "yes" || echo "no"
yes
#判断23是否大于等于22,当然是了

~ [ 23 -le 22 ] && echo "yes" || echo "no"
no
#判断23是否小于等于22,当然不是了

~ [ 50 -eq 40 ] && echo "yes" || echo "no"
no
~ [ 50 -ge 40 ] && echo "yes" || echo "no"
yes
~ [ 50 -le 40 ] && echo "yes" || echo "no"
no

6.4.5 字符串判断

通过表,我们来学习下字符串的判断:

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

字符串无法进行数值的大于小于等数值比较,只能进行字符串之间的是否相等。

举个例子:

~ name=sc
#给name变量赋值

~ [ -z "$name" ] && echo "yes" || echo "no"
no
#判断name变量是否为空,因为不为空,所以返回no

#示例
~ [ -z "$aa" ] && echo "yes" || echo "no"
no
~ [ -n "$aa" ] && echo "yes" || echo "no"
yes
#判断name变量是否为非空,因为不为空,所以返回yes

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

~ aa=11
~ bb=22
#给变量aa和变量bb赋值

~ [ "$aa" == "$bb" ] && echo "yes" || echo "no"
no
#判断两个变量的值是否相等,明显不相等,所以返回no

6.4.6 多重条件判断

通过表,来看看多重条件判断是什么样子的:

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

举个例子:

~ aa=11
#给变量aa赋值

~ [ -n $aa -a $aa -gt 23 ] && echo "yes" || echo "no"
no
#判断变量aa是否有值,同时判断变量aa 的是否大于23
#因为变量aa 的值不大于23,所以虽然第一个判断值为真,返回的结果也是假

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

~ aa=24

~ [ -n $aa -a $aa -gt 23 ] && echo "yes" || echo "no"
yes

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

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

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

6.5 流程控制

6.5.1 if 条件判断

1)、单分支 if 条件语句

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

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

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

  • if语句使用fi结尾,和一般语言使用大括号结尾不同
  • [条件判断式]就是使用test命令判断,所以中括号和条件判断式之间必须有空格
  • then后面跟符合条件之后执行的程序,可以放在[]之后,用“;”分割。也可以换行写入,就不需要“;”了,比如单分支if语句还可以这样写:
if [ 条件判断式 ]
  then
    程序
fi

范例:

~ vim if-df-warning.sh
#!/bin/bash
#统计根分区使用率
#Author: zhongzhiwei <E-mail:zhongzhiwei.kubernetes.com>

rate=$(df -h | grep "/dev/sda3" | awk '{print $5}' | cut -d "%"-f1)
#把根分区使用率作为变量值赋予变量rate
if [ $rate -ge 80 ] ; then
#判断rate的值如果大于等于80,则执行then程序
    echo -e "\E[1;31mThe / is used 80%,Immediately to deal with!\E[0m"
    echo "Warning ! /dev/sda3 is fulled!!!"
    #打印警告信息。在实际工作中,也可以向管理员发送邮件。
fi

#示例
~ vim if-df-warning.sh
#!/bin/bash
#统计根分区使用率

USED=$(df -aTh | awk -F " " '$7=="/" {print $6}' | cut -d "%" -f 1)
#把根分区使用率作为变量值赋予变量 USED
WARN=80

if [ ${USED} -ge ${WARN} ] ; then
    echo -e "\E[1;31mThe / is used 80%,Immediately to deal with!\E[0m"
else
    echo -e "\E[1;32mThe / is not used 80%,Relaxing!\E[0m"
fi

~ chmod 755 if-df-warning.sh
~ bash if-df-warning.sh
The / is not used 80%,Relaxing!

2)、双分支 if 条件语句

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

例子1:

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

例子1:备份mysql数据库
~ vim bakmysql.sh
#!/bin/bash
#备份 mysql 数据库
#Author: zhongzhiwei <E-mail:zhongzhiwei.kubernetes.com>

ntpdate ntp1.aliyun.com
#同步系统时间

DATE=$(date +%F)
#把当前系统时间按照“年月日”格式赋予变量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 -zcvf mysql-lib-${DATE}.tar.gz /var/lib/mysql dbinfo.txt &> /dev/null
    #打包压缩数据库与临时文件,把所有输出丢入垃圾箱(不想看到任何输出)
    rm -rf /tmp/dbbak/dbinfo.txt
    #删除临时文件
else
#创建该目录
    mkdir -p /tmp/dbbak
    #如果判断为真,执行以下脚本
    echo "Date: ${DATE}" >> /tmp/dbbak/dbinfo.txt
    #把当前日期写入临时文件
    echo "Data Size: ${SIZE}" >> /tmp/dbbak/dbinfo.txt
    #把数据库大小写入临时文件
    cd /tmp/dbbak
    #进入到备份目录
    tar -zcvf mysql-lib-${DATE}.tar.gz /var/lib/mysql dbinfo.txt &> /dev/null
    #打包压缩数据库与临时文件,把所有输出丢入垃圾箱(不想看到任何输出)
    rm -rf /tmp/dbbak/dbinfo.txt
    #删除临时文件
fi

例子2:

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

#例子2:判断apache是否启动,如果没有启动则自动启动
~ vim autostart-httpd.sh
#!/bin/bash
#判断apache是否启动,如果没有启动则自动启动
#Author: zhongzhiwei <E-mail:zhongzhiwei.kubernetes.com>

#netstat -auntlp | grep httpd
PORT=$(netstat -auntlp | awk '{print $4}' | rev | cut -d : -f "1" | rev | grep ^80$)
#判断80端口是否开启

if [ $PORT == "" ] ; then
#判断端口是否为空
    echo "Http is down , must restart"
    /etc/rc.d/init.d/httpd start &> /dev/null
    echo "Http is starting , You can see"
else
    echo "Http is start"
fi

直接检索80端口最简单,但是有可能会出现服务卡死的现象,即80端口还在,不应答的情况。

使用 nmap 进行扫描,那就更加准确,只有80端口应答的,nmap 才会得到有用"open"的信息,唯一的缺点就是 nmap 扫描起来会比较慢。

#例子2:判断apache是否启动,如果没有启动则自动启动
~ vim autostart-httpd.sh
#!/bin/bash
#判断apache是否启动,如果没有启动则自动启动
#Author: zhongzhiwei <E-mail:zhongzhiwei.kubernetes.com>

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

if [ "$PORT" == "open" ] ;then
#如果变量port的值是"open"
    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

以我们使用nmap端口扫描命令,nmap命令格式如下:

~ nmap -sT 域名或者IP
选项:
-s:扫描
-T:扫描所有开启的TCP端口

这条命令的执行结果如下:

~ nmap -sT 10.0.0.40
#可以看到这台服务器开启了如下的服务

Starting Nmap 5.51 ( http://nmap.org ) at 2022-08-21 20:06 CST
Nmap scan report for 10.0.0.40
Host is up (0.000091s latency).
Not shown: 996 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http    #apache的状态时 open
111/tcp  open  rpcbind
3306/tcp open  mysql

Nmap done: 1 IP address (1 host up) scanned in 0.08 seconds

知道了nmap命令的用法,我们在脚本中使用的命令就是为了截取http的状态,只要状态是"open"就证明apache启动正常,否则证明apache启动错误。来看看脚本中命令的结果:

~ nmap -sT 10.0.0.40 | grep tcp | grep http | awk '{print $2}'
#扫描指定计算机,提取包含tcp的行,在提取包含httpd 的行,截取第二列
open
#把截取的值赋予变量port

3)、多分支 if 条件语句

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

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

例子:判断用户输入的是什么文件
~ vim if-elif.sh
#!/bin/bash
#判断用户输入的是什么文件
#Author: zhongzhiwei <E-mail:zhongzhiwei.kubernetes.com>

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

#执行脚本
~ ./if-elif.sh
Please input a filename:
Error ,Please input the filename!

~ ./if-elif.sh
Please input a filename: abc.txt
Your input is not a file!

~ ./if-elif.sh
Please input a filename: /etc/passwd
/etc/passwd is a regulare file!

范例:四则运算练习题

~ vim count-if.sh
#!/bin/bash
#字符界面加减乘除计算器

read -t 30 -p "Please input number1: " num1
read -t 30 -p "Please input number2: " num2
#通过read命令接收要计算的数值,并赋予变量num1 和 num2
read -t 30 -p "Please input operator(+|-|*|/): " opt
#通过read命令接收要计算的符号,并赋予变量opt

if [ -n "$num1" -a -n "$num2" -a -n "$opt" ] ;then
#第一层判断,用来判断num1,num2和 opt有值
  test1=$(echo $num1 | sed 's/[0-9]//g')
  test2=$(echo $num2 | sed 's/[0-9]//g')
  #定义变量test1和test2的值为$(命令)的结果
  #后续命令作用是,把变量test1的值替换为空。如果能替换为空,证明num1的值为数字
  #如果不能替换为空,证明num1 的值为非数字。我们使用这种方法判断变量num1的值为数字
  #用同样的方法测试test2变量。
  if [ -z "$test1" -a -z "$test2" ] ;then
  #第二层判断,用来判断num1和num2为数值,
  #如果变量test1和 test2的值为空,则证明num1和num2是数字
    if [ "$opt" == '+' ] ;then
    #第三层判断用来确认运算符
    #测试变量$ope中是什么运算符
      value=$(( $num1 + $num2 ))
      #如果是加号则执行加法运算
    elif [ "$opt" == '-' ] ;then
      value=$(( $num1 - $num2 ))
      #如果是加号则执行减法运算
    elif [ "$opt" == '*' ] ;then
      value=$(( $num1 * $num2 ))
      #如果是加号则执行乘法运算
    elif [ "$opt" == '/' ] ;then
      value=$(( $num1 / $num2 ))
      #如果是加号则执行除法运算
    else
      echo "Please enter a valid symbol!"
      #则提示运算符不匹配,提示输入有效的符号
      exit 10
      #退出程序,返回错误代码10
    fi
  else
    echo "Please enter a valid value!"
    #则提示输入有效的数值
    exit 11
    #退出程序,返回错误代码11
  fi
else
  echo "Please enter relevant parameters!"
  exit 12
  #退出程序,返回错误代码12
fi

echo "$num1 $opt $num2 = $value"
#输出数值运算的结果

if 判断是否输入内容(为空为真);then

如果没有输入

exit 11

fi

if 判断是否为数字 ;then

如果不是纯数字

exit 12

fi

if 判断是否为运算符 ;then

elif …

elif …

如果不是运算符

exit 13

fi

示例:

#!/bin/bash
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>
#Shell ENV
COLOR='echo -e \E[1;32m'
COLORWARN='echo -e \E[1;31m'
COLOREND='\E[0m'

set -ue

#使用shell进行计算器实现
read -t 30 -p "请输入第一个数值: " NUM1
read -t 30 -p "请输入第二个数值: " NUM2
read -t 30 -p "请输入需要进行的运算(+|-|*|/|^|%): " OPTIONS

#判断数值以及运算符是否为空
if [ -n $NUM1 -a -n $NUM2 -a -n $OPTIONS ] ; then 
    #判断第一个数值是否为整数
    if [[ $NUM1 =~ ^[0-9]+$ ]] ; then
        #判断第二数值是否为整数
        if [[ $NUM2 =~ ^[0-9]+$ ]] ; then
            #判断运算符是否正确,并进行相应的运算操作
            if [ $OPTIONS == "+" ] ; then
                SUM=$(( $NUM1 + $NUM2 ))
                #进行加法运算
            elif [ $OPTIONS == "-" ] ; then
                SUM=$(( $NUM1 - $NUM2 ))
                #进行减法运算
            elif [ $OPTIONS == "*" ] ; then
                SUM=$(( $NUM1 * $NUM2 ))
                #进行乘法运算
            elif [ $OPTIONS == "/" ] ; then
                SUM=$(( $NUM1 / $NUM2 ))
                #进行除法运算
            elif [ $OPTIONS == "^" ] ; then
                #echo "$NUM1 $NUM2"
                for (( i=1,SUM=1 ; i<=$NUM2 ; i++ )) ; do
                    SUM=$(( $NUM1 * $SUM ))
                    #进行乘方运算
                done
            elif [ $OPTIONS == "%" ] ;then
                SUM=$(( $NUM1 % $NUM2 ))
                #进行取余运算
            else
                ${COLORWARN}"请正确输入运算符(目前只支持(+|-|*|/|**|%))"${COLOREND}
                exit 101
            fi
        else
            ${COLORWARN}"请正确输入第二个数值(非整数)"${COLOREND}
            exit 102
        fi
    else
        ${COLORWARN}"请正确输入第一个数值(非整数)"${COLOREND}
        exit 103
    fi
else
    ${COLORWARN}"请正确输入相应的数值或者运算符"${COLOREND}
    exit 104
fi

${COLOR} $NUM1 $OPTIONS $NUM2 = $SUM ${COLOREND}

6.5.2 多分支 case 条件语句

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

case $变量名 in
"值1")
  如果变量的值等于值1,则执行程序1
;;

"值2")
  如果变量的值等于值2,则执行程序2
;;

  ...省略其他分支...
*)
  如果变量的值都不是以上的值,则执行此程序
;;
esac

范例:

#!/bin/bash
#简单的Case Shell示例

echo "Want to BeiJing , please input 1"
echo "Want to ShenZhen , please input 2"
echo "Want to HaiNan , please input 3"

read -t 30 -p "Please input your choice: " CHOICE

case $CHOICE in
"1")
    echo "There are still tickets available for Beijing"
;;
"2")
    echo "There are still tickets available for Shenzhen"
;;
"3")
    echo "There are still tickets available for Hainan"
;;
*)
    echo "No other city Tours"
;;
esac

范例:使用 case 方式计算器

#!/bin/bash
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>
#Shell ENV
COLOR='echo -e \E[1;32m'
COLORWARN='echo -e \E[1;31m'
COLOREND='\E[0m'

set -ue

#使用shell进行计算器实现
read -t 30 -p "请输入第一个数值: " NUM1
read -t 30 -p "请输入第二个数值: " NUM2
read -t 30 -p "请输入需要进行的运算(+|-|*|/|^|%): " OPTIONS

#判断数值以及运算符是否为空
if [ -n $NUM1 -a -n $NUM2 -a -n $OPTIONS ] ; then 
    #判断第一个数值是否为整数
    if [[ $NUM1 =~ ^[0-9]+$ ]] ; then
        #判断第二数值是否为整数
        if [[ $NUM2 =~ ^[0-9]+$ ]] ; then
            #判断运算符是否正确,并进行相应的运算操作
            case $OPTIONS in
            "+")
                SUM=$(( $NUM1 + $NUM2 ))
                #进行加法运算
            ;;
            "-")
                SUM=$(( $NUM1 - $NUM2 ))
                #进行减法运算
            ;;
            "*")
                SUM=$(( $NUM1 * $NUM2 ))
                #进行乘法运算
            ;;
            "/")
                SUM=$(( $NUM1 / $NUM2 ))
                #进行除法运算
            ;;
            "^")
                for (( i=1,SUM=1 ; i<=$NUM2 ; i++ )) ; do
                    SUM=$(( $NUM1 * $SUM ))
                    #进行乘方运算
                done
            ;;
            "%")
                SUM=$(( $NUM1 % $NUM2 ))
                #进行取余运算
            ;;
            *)
                ${COLORWARN}"请正确输入运算符(目前只支持(+|-|*|/|**|%))"${COLOREND}
                exit 101
            ;;
            esac
        else
            ${COLORWARN}"请正确输入第二个数值(非整数)"${COLOREND}
            exit 102
        fi
    else
        ${COLORWARN}"请正确输入第一个数值(非整数)"${COLOREND}
        exit 103
    fi
else
    ${COLORWARN}"请正确输入相应的数值或者运算符"${COLOREND}
    exit 104
fi

${COLOR} $NUM1 $OPTIONS $NUM2 = $SUM ${COLOREND}

6.5.3 for 循环

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

语法一:

for 变量 in 值1 值2 值3 ......
  do 
      程序
  done

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

范例:

~ vim for1.sh
#!/bin/bash

for i in {1..100} ; do
    echo ${i}
done

~ chmod 755 for1.sh
~ bash for1.sh

语法二:

for ((初始值;循环控制条件;变量变化))
  do
    程序
  done

语法二中需要汪意;

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

1)、语法一举例:

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

~ vim for1.sh
#!/bin/bash

sum=0
for i in {1..100} ; do 
    sum=$(( $i + $sum))
done

echo "The sum of 1+2+......+100 is : $sum"
#输出1加到100的和
~ vim ./for-time.sh
#!/bin/bash
#打印时间
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

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

批量解压缩脚本就应用这样写:

#批量解压缩
~ vim for-auto-tar.sh
#!/bin/bash
#批量解压缩脚本
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

cd /tmp
#进入到压缩目录
ls *.tar.gz > ls.log
#把所有 tar.gz 结尾的文件的文件覆盖到 ls.log 临时文件中
ls *.tgz >> ls.log
#把所有 tgz 结尾的文件的文件覆盖到 ls.log 临时文件中
for i in $(cat ls.log) ; do
#读取ls.log 文件的内容,文件中有多少值,就会循环多少次,每次循环把文件名赋予变量
    tar -zxvf $i &> /dev/null
    #解压缩,并把所有输出丢到垃圾箱
done

rm -rf ls.log
#删除临时文件ls.log

2)、语法二举例

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

#例子1:从1加到100
~ vim for-number.sh
#!/bin/bash
#从1加到100
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

s=0
for (( i=1 ; i<=100 ; i=i+1 )) ; do 
#定义循环100次
    s=$(( $s + $i ))
    #每次循环给变量 s 赋值
done
echo "The sum of 1+2+......+100 is : $s"
#输出1加到100的和

批量解压缩脚本就应用这样写:

#批量解压缩
~ vim for-auto-tar.sh
#!/bin/bash
#批量解压缩脚本
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

cd /tmp ; mkdir tar
#进入到压缩目录
ls *.tar.gz > ls.log
#把所有 tar.gz 结尾的文件的文件覆盖到 ls.log 临时文件中
ls *.tgz >> ls.log &> /dev/null
#把所有 tgz 结尾的文件的文件追加到 ls.log 临时文件中
NUM=$( cat ls.log | wc -l )

for (( i=1 ; i<=$NUM ; i++ )) ; do 
    TAR=$( cat ls.log | awk 'NR=='$i'{print $1}' )
    tar -zxvf $TAR -C /tmp/tar
done

rm -rf ls.log
#删除临时文件ls.log

范例:for 循环之合法IP判断

cat > ip.txt <<EOF
192.168.1.200
202.106.0.20
300.36.190.5
222222222222
192.168.1.300
202.2.2
EOF

语法一:

#!/bin/bash

#检查IP地址格式
grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" /root/ip.txt > /root/ip_test.txt
#先通过正则,把明显不符合规则的IP地址进行过滤,把结果保存到临时文件中/root/ip_test.txt
echo "" > /root/ip_vaild.txt
#清空保存数据的文件

for i in $( cat /root/ip_test.txt ) ; do
    a=$( echo "$i" | cut -d '.' -f 1)
    b=$( echo "$i" | cut -d '.' -f 2)
    c=$( echo "$i" | cut -d '.' -f 3)
    d=$( echo "$i" | cut -d '.' -f 4)
    #分别把IP地址的四个数值分别读入变量a,b,c,d
    if [ "$a" -lt 1 -o "$a" -gt 255 ] ; then
    #如果第一个数值小于1或者大于255
      continue
      #则退出本次循环
    fi
    if [ "$b" -lt 0 -o "$b" -gt 255 ] ; then
    #如果第二个数值小于0或者大于255
      continue
      #则退出本次循环
    fi
    if [ "$c" -lt 0 -o "$c" -gt 255 ] ; then
    #如果第三个数值小于0或者大于255
      continue
      #则退出本次循环
    fi
    if [ "$d" -lt 0 -o "$d" -gt 255 ] ; then
    #如果第四个数值小于0或者大于255
      continue
      #则退出本次循环
    fi
    #依次判断四个IP数值十分超出范围,如果超出,退出本次循环
    echo "$i" >> /root/ip_vaild.txt
    #把合法IP地址写入到/root/ip_vaild.txt
done
rm -rf /root/ip_test.txt

语法二:

#!/bin/bash

#检查IP地址格式
grep -E "^(([0-9]\.)|([1-9][0-9]\.)|(1[0-9][0-9]\.)|(2[0-4][0-9]\.)|(25[0-5]\.)){3}(([0-9])|([1-9][0-9])|(1[0-9][0-9])|(2[0-4][0-9])|(25[0-5]))$" /root/ip.txt > /root/ip_test.txt
#先通过正则,把明显不符合规则的IP地址进行过滤,把结果保存到 /root/ip_test.txt
line=$( cat /root/ip_test.txt | wc -l )
#统计 test 中有几行IP地址
echo "" > /root/ip_correct.txt
#清空最终数据文件
for (( i=1 ; i<=$line ; i++ )) ; do
#有几行IP,循环几次
    cat /root/ip_test.txt | awk 'NR=='$i'{print}' > /root/ip_new.txt
    #第几次循环,就把第几行读入 /root/ip_new.txt 文件(此文件中只有一行IP)
    b=$( echo "$i" | cut -d '.' -f 2)
    c=$( echo "$i" | cut -d '.' -f 3)
    a=$( echo "$i" | cut -d '.' -f 1)
    d=$( echo "$i" | cut -d '.' -f 4)
    #分别把IP地址的四个数值分别读入变量a,b,c,d
    if [ $a -lt 1 -o $a -gt 255 ] ; then
    #如果第一个数值小于1或者大于255
      continue
      #则退出本次循环
    fi
    if [ $b -lt 0 -o $b -gt 255 ] ; then
    #如果第二个数值小于0或者大于255
      continue
      #则退出本次循环
    fi
    if [ $c -lt 0 -o $c -gt 255 ] ; then
    #如果第三个数值小于0或者大于255
      continue
      #则退出本次循环
    fi
    if [ $d -lt 0 -o $d -gt 255 ] ; then
    #如果第四个数值小于0或者大于255
      continue
      #则退出本次循环
    fi
    #依次判断四个IP数值十分超出范围,如果超出,退出本次循环
    cat /root/ip_new.txt >> /root/ip_correct.txt
    #如果四个IP数值都符合要求,则把合法IP记录在文件中
done
rm -rf /root/ip_test.txt
rm -rf /root/ip_new.txt
#删除临时文件

范例:批量添加指定数量的用户

网站是需要用户手工指定用户,是要求用户自身进行注册,而且还会对用户名进行重新检测。用户名重叠的现象会被杜绝。

#例子:批量添加指定数量的用户
~ vim useradd.sh
#!/bin/bash
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>
#批量添加指定数量的用户

read -t 30 -p "Please input user name: " NAME
#让用户输入用户名,把输入保存到变量 NAME
read -t 30 -p "Please input user number: " NUM
#让用户输入添加用户的数量,把输入保存到变量 NUM
#类似于test1..test30
read -t 30 -p "Please input user password: " PASS
#让用户输入初始密码,把输入保存如变量 PASS

if [ -n $NAME -a -n $NUM -a -n $PASS ] ; then
#判断三个变量不为空
    int=$( echo $NUM | sed 's/[0-9]//g' )
    #定义变量的值为后续命令的结果
    #后续命令作用是,把变量NUM 的值替换为空。如果能替换为空,证明 NUM 的值为数字
    #如果不能替换为空,证明NUM 的值为非数字。我们使用这种方法判断变量 NUM 的值为数字
    if [ -z "$int" ] ;then 
    #如果变量int的值为空,证明NUM变量是数字
        for (( i=1 ; i<=$NUM ; i++ )) ; do
            /usr/sbin/useradd $NAME$i &> /dev/null
            #添加用户,用户名为变量name的值加变量i的数字
            echo "$PASS" | /usr/sbin/useradd --stdin $NAME$i &> /dev/null
            #给用户设定初始密码为变量 PASS 的值
            #chage -d 0 $NAME$i &> /dev/null
            /usr/bin/passwd -e $NAME$i &> /dev/null
            #强制用户登录之后修改密码
        done
    else 
        echo -e "\E[1;31mError , Must Int! \E[0m"
    fi
else
    echo -e "\E[1;31mError!\E[0m"
fi

范例:

~ vim useradd-diy.sh
#!/bin/bash
#添加指定用户名
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

#创建一个临时存放用户名的文件
cat > useradd.txt <<EOF
zhangzong
huzong
linzong
EOF

echo -e "\E[1;32m脚本用于创建用户,如添加新用户需修改脚本\E[0m"
#让用户输入初始密码,把输入保存到变量PASSWORD
read -t 30 -p "请输入用户的初始密码: " PASSWORD

if [ -n $PASSWORD ] ; then
    for user in $( cat useradd.txt ) ; do
        if [ $( id $user &> /dev/null ; echo $? ) -eq 0 ]; then
            echo -e "\E[1;32m${user} 该用户已经存在,无法进行创建!\E[0m"
        else
            /usr/sbin/useradd $user &> /dev/null
            #添加用户,用户名为useradd.txt文件提供
            echo "$PASSWORD" | /usr/bin/passwd --stdin $user &> /dev/null
            #给用户设定初始密码为变量"$PASSWORD"
            /usr/bin/passwd -e $user &> /dev/null
            #强制用户登录之后修改密码
            echo -e "\E[1;32m添加用户: ${user} 成功,初始密码: ${PASSWORD}。"
        fi
    done
else
    echo -e "\E[1;31m错误,请输入用户的初始密码\E[0m"
fi

rm -rf useradd.txt
#删除临时文件

范例:批量删除系统普通用户

~ vim userdel.sh
#!/bin/bash
#批量删除的用户
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

user=$( cat /etc/passwd | grep "/bin/bash" | grep -v "root" | cut -d ":" -f 1 )
#读取用户信息文件,提取可以登录用户,取消root用户,截取第一列用户名
for i in $user ; do
    userdel -r $i
    #每次循环,删除指定普通用户
done

6.5.4 while 循环

while [ 条件判断式 ]
do
    程序
done

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

例子:1加到100
#!/bin/bash
#从1加到100
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

SUM=0
i=1
#给变量i和变量SUM赋值
while [ $i -le 100 ] ; do
#如果变量i的值小于等于100,则执行循环
    SUM=$(( $SUM + $i ))
    i=$(( i + 1 ))
done
echo "The sum is: $SUM"

范例:自定义while循环

~ vim ./while-diy.sh
#!/bin/bash
#从1加到100
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

#Shell ENV
SUM=0
i=1

#用户输入叠加的数值
read -p "请输入需要叠加的最后一个数值: " NUM

while [ $i -le $NUM ] ; do
    SUM=$(( $SUM + $i ))
    i=$(( i + 1 ))
done
echo "叠加之后的数值是:$SUM"

6.5.5 until 循环

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

until [ 条件判断式 ]
do
    程序
done

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

例子:1加到100
#!/bin/bash
#从1加到100
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

SUM=0
i=1
#给变量i和变量SUM赋值
until [ $i -gt 100 ] ; do
#如果变量i的值小于等于100,则执行循环
    SUM=$(( $SUM + $i ))
    i=$(( i + 1 ))
done
echo "The sum is: $SUM"

6.5.6 函数 Function

6.5.6.1 函数介绍

函数 function 是由若干条 shell 命令组成的语句块,实现代码重用和模块化编程。

它与 shell 程序形式上是相似的,不同的是它不是一个单独的进程,不能独立运行,而是 shell 程序的一部分。

函数和 shell 程序比较相似,区别在于

  • shell 程序在子 shell 中运行
  • shell 函数在当前 shell 中运行。因此在当前 shell 中,函数可对shell 中变量进行修改
6.5.6.2 管理函数

函数由两部分组成:函数名和函数体

帮助查看:help function

~ help function
function: function name { COMMANDS ; } or name () { COMMANDS ; }
    Define shell function.

    Create a shell function named NAME.  When invoked as a simple command,
    NAME runs COMMANDs in the calling shell's context.  When NAME is invoked,
    the arguments are passed to the function as $1...$n, and the function's
    name is in $FUNCNAME.

    Exit Status:
    Returns success unless NAME is readonly.
~ type function
function is a shell keyword
6.5.6.2.1 定义函数

格式:

# 语法一:
func_name (){
	...函数体...
}

# 语法二:
function func_name {
	...函数体...
}

# 语法三:
function func_name (){
	...函数体...
}

范例:

~ testfunc1() { 
echo testfunc1;
echo functions is excuted; 
}

范例:

#根据操作系统的类型安装httpd
~ vim function-installed-httpd.sh
#!/bin/bash
function os_type(){
#Check the operating system type
if grep -i -q ubuntu /etc/os-release ;then
    echo "ubuntu"
elif grep -i -q centos /etc/os-release ;then
    echo "centos"
else
    echo "Sorry, this script does not support this operating system"
fi
}

if [ `os_type` = centos ] ;then
    yum install -y httpd
elif [ `os_type` = ubuntu ] ;then
    apt install -y apache2
else
    echo "Sorry, this script does not support this operating system"
fi

#定义color.sh
~ vim color.sh
function color()
{
  RES_COL=60;
  MOVE_TO_COL="echo -en \\033[${RES_COL}G";
  SETCOLOR_SUCCESS="echo -en \\033[1;32m";
  SETCOLOR_FAILURE="echo -en \\033[1;31m";
  SETCOLOR_WARNING="echo -en \\033[1;33m";
  SETCOLOR_NORMAL="echo -en \E[0m";
  echo -n "$1" && $MOVE_TO_COL;
  echo -n "[";
  if [ $2 = "success" -o $2 = "0" ] ;then
    ${SETCOLOR_SUCCESS};
    echo -n $"  OK  ";
  elif [ $2 = "failure" -o $2 = "1" ] ;then
    ${SETCOLOR_FAILURE};
    echo -n $"FAILED";
  else
    ${SETCOLOR_WARNING};
    echo -n $"WARNING";
  fi
  ${SETCOLOR_NORMAL};
  echo -n "]";
  echo
}
#[ $# -eq 0 ] && echo "Usage: `basename $0` {success|failure|warning}"
#color $1 $2
6.5.6.2.2 查看函数
# 查看当前已经定义的函数名
declare -F 

# 查看当前已经定义的函数定义
declare -f

# 查看指定当前已经定义的函数名
declare -f func_name

# 查看当前已经定义的函数名定义
declare -F func_name
6.5.6.2.3 删除函数

格式:

unset func_name

6.5.7 特殊流程控制语句

6.5.7.1 exit 语句

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

exit [返回值]

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

~ vim exit.sh
#!/bin/bash
#演示 exit 的作用
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

read -t 30 -p "Please input a number: " num
#接收用户的输入,并把输入赋予变量 num
y=$( echo $num | sed 's/[0-9]//g' )
#如果变量num 的值是数字,则把num 的值替换为空,否则不替换
#把num内的数字替换为空,如果y的值是非空,则代表y的值是除了数字以外的其他字符
#把num内的数字替换为空,如果y的值是空,则代表num就只有数字
#把替换之后的值赋予变量y
#[ -n "$y" ] && echo "Error! Please input a number!" && exit 18
#判断变量y的值如果不为空,输出报错信息,退出脚本,退出返回值为18

#echo "The number is: $num"
#如果没有退出,则打印变量num中的数字
if [ -n "$y" ] ; then
    echo "Error! Please input a number!"
    exit 18 
else
    echo "The number is: $num"
fi

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

~ chmod 755 exit.sh
#给脚本赋予执行权限
~ ./exit.sh                   #执行脚本
Please input a number: test   #输入值不是数字,而是test
Error! Please input a number! #输出报错信息,而不会是输出test
~ echo $? #查看返回值
18        #返回值居然真是18
~ ./exit.sh
Please input a number: 10      #输入数字10
The number is: 10              #输出数字10
6.5.7.2 break 语句

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

img

举个例子:

~ vim break.sh
#!/bin/bash
#演示 break 跳出替换
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

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

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

~ chmod 755 break.sh
#赋予执行权限
~ ./break.sh
1
2
3
6.5.7.3 continue 语句

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

img

还是用刚刚的脚本,不过退出语句换成continue语句,看看会发生什么情况:

~ vim ./continue.sh
#!/bin/bash
#演示 break 跳出替换
#Author: zhongzhiwei <Email: zhongzhiwei@kubernetes.io>

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

执行结果:

~ chmod 755 ./continue.sh
#赋予执行权限
~ ./continue.sh
1
2
3
5  #少了 4 这个输出
6
7
8
9
10

continue只会退出单次循环,所以并不影响后续的循环,所以只会少4的输出。


  1. a-z ↩︎

  2. 0-9 ↩︎

  3. 0-9 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dark_Ice_

你的支持是我创作的核动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值