编写Shell脚本
我经常把Shell终端解释器形容是人与计算机硬件的“翻译官”,它作为用户与Linux系统内部通讯的媒介,除了允许了各种变量与参数外还提供了诸如循环、分支等高级语言才有的控制结构特性,如何正确的使用这些功能,准确下达命令尤为重要。Shell脚本命令的工作方式有两种,首先是前面所接触的交互方式(Interactive),即当用户每输入一条命令就执行一次,而批处理(Batch)则是由用户事先编写好一个完整的Shell脚本,Shell会一次性执行脚本中诸多的命令。因此在Shell脚本中不仅需要用到很多前面学习过的Linux命令以及正则表达式、管道符、数据流重定向等语法规则,还需要把内部功能模块化后通过逻辑语句进行加工,最终才能成为日常所见的Shell脚本程序。
查看系统默认解释器
可以通过SHELL变量来查看到当前系统已经默认使用bash解释器作为命令行终端了:
echo $SHELL
小试牛刀
需求:
想查看当前所在工作路径并列出当前目录下所有文件及属性信息。
[centos7@localhost test]$ sudo vim example.sh
然后编辑内容:
#!/bin/bash
# For Example BY jayafs
pwd
ls -al
Shell脚本文件的名称是可以任意起,但为了避免其他同事误以为是普通文件,咱们应该遵守行业人员大众的规范把.sh后缀写上,这样让其他人一看就知道是个脚本文件,与人方便自己方便。在这个脚本中实际上出现了三种不同的元素,第一行脚本声明(#!)是用来告知系统用何种shell解释器来执行本脚本程序,第二行注释信息(#)是对程序功能和某些命令的介绍信息,使得自己或他人再次看到这个脚本内容时可以快速知道这些功能的作用或一些警告信息,第三、四行可执行语句也就是咱们平时执行的Linux命令啦
。这么简单就编写出来了一个脚本程序,那来执行看一看吧:
第二种运行脚本程序的方法是以输入完整路径的方式来执行,但默认会因为权限不足而提示报错信息,这种情况只需要为脚本文件增加执行权限即可。
接受用户参数
但是像上面这样的脚本程序在功能上真的太过于“死板”,为了能够让Shell脚本程序更好的满足用户对灵活完成工作的热切需要,必须要让脚本程序能够像咱们以前执行命令时那样来接收用户输入进来的参数。
其实Shell脚本早就考虑到了这些,已经在脚本中定义好了很多变量功能,例如$0对应当前Shell脚本程序的名称,$#对应总共有几个参数,$*对应所有位置的参数值,而$1,$2,$3……依次类推则分别对应着第N个位置的参数,如图:
我们来实际操作下,修改example.sh
[centos7@localhost test]$ sudo vim example.sh
内容为:
#!/bin/bash
echo "当前脚本名称为$0"
echo "总共有$#个参数,分别是$*。"
echo "第一个参数$1,第5个参数为$5。"
运行脚本:
entos7@localhost test]$ sh example.sh one two three four five six
结果:
判断语句
有时咱们也需要像mkdir
命令一样来判断用户输入的信息,从而判断用户指定的文件夹名称是否已经存在,已存在则提示报错,不存在则自动的创建。
条件判断语句
条件测试语法能够判断表达式是否成立,若条件成立则返回数字0,否则便返回其他随机数值。
格式
注意:
表达式在中括号里面,并且表达式前后都有一个空格
。
条件判断语句按照测试对象可分为:
- 文件测试
- 逻辑测试
- 整数值比较
- 字符串比较
文件测试
文件测试即用来按照指定条件来判断文件是否存在或权限是否满足。
参数:
操作符 | 作用 |
---|---|
-d | 测试是否为目录。 |
-e | 测试文件或目录是否存在。 |
-f | 判断是否为文件。 |
-r | 测试当前用户是否有权限读取。 |
-w | 测试当前用户是否有权限写入。 |
-x | 测试当前用户是否有权限执行。 |
好啦,那么先通过文件测试语句来判断/etc/fstab是否为一个目录文件,然后通过 $?
变量来显示上一条命令执行后的返回值,这样就可以通过返回的非零值判断目录是不存在的了(即文件测试语句判断结果不符合):
说明:
1、测试结果是不会直接输出的、必须通过$?
来查看结果。
2、如果检测,测试成功返回 0;反之返回非0(根据系统决定)。
逻辑测试
逻辑测试则是用于判断用户给出的条件是为真还是假,从而把条件测试语句与逻辑语句相搭配结合使用可以实现一个更高级的使用方法。
例如在Shell终端中逻辑“与”符号是&&,它代表当前面的命令执行成功后才会执行后面的命令,因此可以用来判断/dev/cdrom设备是否存在,若存在时才输出Exist字样。
[centos7@localhost test]$ [ -e /dev/cdrom ] && echo "Exist"
Exist
除了“与”逻辑测试符号外还有“或”逻辑测试,在Linux系统中的逻辑“或”符号为“||”,它代表当前面的命令执行失败后才会执行后面的命令,因此可以结合系统环境变量USER来判断当前登录的用户是否为非超级管理员身份:
[centos7@localhost test]$ echo $USER
centos7
[centos7@localhost test]$ [ $USER = root ] || echo "user"
user
除了基本的“与”、“或”逻辑符号外,还有逻辑“非”符号,在Linux系统中逻辑“非”的符号就是一个叹号,它代表把条件测试中的判断结果取相反值,也就是说原本测试的结果是正确,则变成错误,而错误的结果会变成正确,有一种负负为正的感觉。例如现在切换到一个普通用户的身份后再来判断当前用户是不是一个非超级管理员的用户,判断结果因为两次否定而变成正确,因此会正常的输出预设信息:
[centos7@localhost test]$ [ $USER != centos7 ] || echo "administrator"
administrator
整数值比较
整数比较运算符是仅对数字的测试操作,不能把数字与字符串、文件等内容一起操作,而且不能想当然的使用日常生活中的等号、大于号、小于号等来做判断,因为等号与是赋值命令符冲突,大于号和小于号分别是和输出重定向命令符和输入重定向命令符冲突。虽然有时候碰巧也能执行成功,但是在后面脚本程序中普遍会产生错误,一定要使用规范的整数比较运算符来进行操作。
参数:
操作符 | 作用 |
---|---|
-eq | 判断是否是等于 |
-ne | 判断是否不等于 |
-gt | 判断是否大于 |
-lt | 判断是否小于 |
-le | 判断是否等于或小于 |
-ge | 判断是否大于或等于 |
小试牛刀
咱们先小试牛刀的测试下10是否大于10以及10是否等于10,依次通过判断输出的返回值内容来进行判断:
[centos7@localhost test]$ [ 10 -gt 10 ]
[centos7@localhost test]$ echo $?
1
注意
:
比较结果不会直接输出,必须通过$?
来获取。
字符串比较
字符串比较是判断测试字符串是否为空值,或两个字符串是否相同的操作,常常用来判断某个变量是否未被定义(即内容为空值)。
常见的运算符:
操作符 | 作用 |
---|---|
= | 比较字符串内容是否相同 |
!= | 比较字符串内容是否不同 |
-z | 判断字符串内容是否为空 |
咱们可以通过判断String变量是否为空值,进而判断是否未被定义:
[centos7@localhost test]$ [ -z $String ]
[centos7@localhost test]$ echo $?
0
最后再尝试把逻辑运算符引入来试试,当判断用于保存当前语系的环境变量值LANG不是为英语(en.US)则会满足逻辑条件并输出非英语的字样:
[centos7@localhost test]$ echo $LANG
en_US.UTF-8
[centos7@localhost test]$ [ $LANG != "en.US" ] && echo "Not en.US"
Not en.US
流程控制语句
前面学的判断语句,这种脚本暂时并不能适用于日常生产环境的工作,首先是它不能根据实际工作内容来调整具体的执行命令内容,也不能根据某些条件来实现自动循环执行,例如需要批量的创建一千个用户,首先您要能判断这些用户是否已经存在了,若不存在然后再通过循环语句让脚本自动化的依次创建他们。
语句:
- if
- for
- while
- case
if条件测试语句
if条件语句可以让脚本根据实际情况的不同而自动切换命令执行方案。
- 单分支
- 双分支
- 多分支
单分支
单分支的if条件语句结构,这种结构仅用if、then、fi关键词组成,只在条件成立后才执行预设命令,相当于口语的“如果……那么……”,属于最简单的一种条件判断结构,操作语法:
需求:
使用单分支的if条件语句来判断某个目录是否存在,若已经存在就结束条件判断和整个Shell脚本,而如果不存在则去创建这个目录。
[root@linuxprobe ~]# vim mkcdrom.sh
#!/bin/bash
DIR="/media/cdrom"
if [ ! -e $DIR ]
then
mkdir -p $DIR
fi
执行脚本:
[centos7@localhost test]$ bash mkcdrom.sh
[centos7@localhost test]$ ls -d /media/cdrom
/media/cdrom
注意:
if后面接判断条件,中括号中字符串前后一定要有空格
。
双分支
双分支的if条件语句结构,这种结构仅用if、then、else、fi关键词组成,进行两次条件判断匹配,两次判断中任何一项匹配成功后都会执行预设命令,相当于口语的“如果……那么……或者……那么……”。
操作语法:
使用双分支的if条件语句来验证某个主机是否在线,然后根据判断执行返回值结果分别给予对方主机是在线还是不在线的提示信息。脚本中我主要是使用ping命令来测试与对方主机的网络联通性,而linux系统中的ping命令不像windows系统一样仅会尝试四次就结束,因此为了避免用户等待时间过长,而通过-c参数来规定尝试的次数,-i参数定义每个数据包的发送间隔时间以及-W参数定义最长的等待超时时间。
[root@linuxprobe ~]# vim chkhost.sh
#!/bin/bash
ping -c 3 -i 0.2 -W 3 $1 &> /dev/null
if [ $? -eq 0 ]
then
echo "Host $1 is On-line."
else
echo "Host $1 is Off-line."
fi
若上一条语句是顺利执行成功的则会返回数字0,而若上一条语句执行是失败的则返回一个非零的数字(随系统版本差异可能会是1或者2都有可能),因此可以通过用数字条件测试的方法判断$?变量是否等于零来获知上一条语句的最终判断情况,192.168.10.10是服务器本机地址,验证下脚本的效果吧:
[centos7@localhost test]$ bash chkhost.sh 192.168.10.10
Host 192.168.10.10 is Off-line
分析:
linux系统中的ping命令不像windows系统一样仅会尝试四次就结束。它接受对应胡参数:
- -c:规定尝试次数
- -i:每个数据包的发送间隔时间
- -W:最长等待超时时间
多分支
多分支的if条件语句结构,这种结构需要使用if、then、else、elif、fi关键词组成,进行多次条件判断匹配,多次判断中任何一项匹配成功后都会执行预设命令,相当于口语的“如果……那么……如果……那么……N次等等”,这是一种工作中最常使用的条件判断结构,虽然相对复杂但更加灵活。
操作语法:
使用多分支的if条件语句来判断用户输入的分数在那个成绩区间内,然后输出如优秀、合格、不合格等提示信息。read是用来读取用户输入信息的命令,它能够把接收到的用户输入信息赋值给后面的指定变量,而-p参数则是给予了用户一定的提示信息。下面实例中判断用户输入的分数是否同时具备大于等于85分且小于等于100分,这样的话才输出Excellent字样,若上一条件没有匹配成功则继续判断用户输入分数是否大于等于70分且小于等于84分,这样的话输出Pass字样,如果两次都落空没有匹配成功,则最终输出Fail字样:
[root@linuxprobe ~]# vim chkscore.sh
#!/bin/bash
read -p "Enter your score(0-100):" GRADE
if [ $GRADE -ge 85 ] && [ $GRADE -le 100 ] ; then
echo "$GRADE is Excellent"
elif [ $GRADE -ge 70 ] && [ $GRADE -le 84 ] ; then
echo "$GRADE is Pass"
else
echo "$GRADE is Fail"
fi
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):88
88 is Excellent
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):80
80 is Pass
如果用户输入的分数并没有满足第一项匹配条件,则会自动进行下面的匹配流程:
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):30
30 is Fail
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):200
200 is Fail
您会不会很奇怪,为什么我输入200分的超高成绩却依然提示了Fail字样?其实原因是很简单明显的,咱们的条件判断语句两条都没有匹配成功,因此自动执行了最终的兜底策略。
此脚本不是很完美,还可以所有大于100分或小于0分的用户输入都设置提示下Error字样。
for条件循环语句
for循环语句可以让脚本一次性读取多个信息值,然后逐一对信息值进行循环操作处理,因此当您要处理的数据是有目标和范围时简直再适合不过了。
处理流程:
例如使用for循环语句来从列表文件中读取多个用户名,然后逐一创建用户帐号并为其设置密码。
首先创建用户名称的列表文件,把每个用户名称单独占一行,当然具体的用户名称和个数都是可以由自己来决定的:
[root@linuxprobe ~]# vim users.txt
andy
barry
carl
duke
eric
george
Shell脚本中使用read命令来读取用户输入的密码值后赋值给PASSWD变量,并通过-p参数来显示一段给用户的提示内容,告诉用户正在输入的内容即将作为帐号密码。当下面的脚本执行后会自动的用users.txt列表文件中获取到所有的用户名称值,然后逐一使用id 用户名的方式查看用户的信息,并使用$?变量判断这条命令是否执行成功,也就是判断该用户是否已经存在。而/dev/null是被称作Linux的黑洞的文件,把输出信息重定向到这个文件后等同于删除数据(没有回收功能的垃圾箱),让用户的屏幕窗口保持简洁。
[root@linuxprobe ~]# vim Example.sh
#!/bin/bash
read -p "Enter The Users Password : " PASSWD
for UNAME in `cat users.txt`
do
id $UNAME &> /dev/null
if [ $? -eq 0 ]
then
echo "Already exists"
else
useradd $UNAME &> /dev/null
echo "$PASSWD" | passwd --stdin $UNAME &> /dev/null
if [ $? -eq 0 ]
then
echo "$UNAME , Create success"
else
echo "$UNAME , Create failure"
fi
fi
done
执行批量创建用户的Shell脚本程序,在输入为帐户设定的密码口令后将由脚本全自动的检查并创建这些帐号,因为已经把多余的信息通过输出重定向转移到了黑洞文件中,因此屏幕窗口除了用户创建成功的提示后不会有其他的内容。/etc/passwd是用来保存Linux系统中用户帐号信息的文件,因此如果不放心的话可以手动的再看下这个文件中有无这些新用户的信息,同时这也又回归到了前面3章中重复提到了一个概念——Linux系统中的一切都是文件。
[root@linuxprobe ~]# bash Example.sh
Enter The Users Password : linuxprobe
andy , Create success
barry , Create success
carl , Create success
duke , Create success
eric , Create success
george , Create success
[root@linuxprobe ~]# tail -6 /etc/passwd
andy:x:1001:1001::/home/andy:/bin/bash
barry:x:1002:1002::/home/barry:/bin/bash
carl:x:1003:1003::/home/carl:/bin/bash
duke:x:1004:1004::/home/duke:/bin/bash
eric:x:1005:1005::/home/eric:/bin/bash
george:x:1006:1006::/home/george:/bin/bash
while条件循环语句
这是一种让脚本根据某些条件来重复执行命令的条件循环语句,而这种循环结构往往在执行前并不确定最终执行的次数,完全不同于for循环语句中有目的、有范围的使用场景。而while循环语句判断是否继续执行命令的依据一般是检查若条件为真就继续执行,而条件为假就结束循环。
结构:
接下来就来利用多重分支的if条件测试语句与while条件循环语句来结合写一个用来判断数值的脚本吧,脚本中会使用$RANDOM变量来调取出一个随机的数值(范围:0--32767),然后通过expr命令计算取整出1000以内的一个随机数值,用这个数值来跟用户通过read命令输入的数值做比较判断。判断语句结构分为三项,分别是判断是否相等、是否大于随机值以及是否小于随机值,但这不是重点~关键是在于while条件循环语句的判断值为true,因此会无限的运行下去,直到猜中后运行exit 0命令才终止脚本。
[root@linuxprobe ~]# vim Guess.sh
#!/bin/bash
PRICE=$(expr $RANDOM % 1000)
TIMES=0
echo "商品实际价格为0-999之间,猜猜看是多少?"
while true
do
read -p "请输入您猜测的价格数目:" INT
let TIMES++
if [ $INT -eq $PRICE ] ; then
echo "恭喜您答对了,实际价格是 $PRICE"
echo "您总共猜测了 $TIMES 次"
exit 0
elif [ $INT -gt $PRICE ] ; then
echo "太高了!"
else
echo "太低了!"
fi
done
通过给脚本加上解释说明后整个内容开始变得丰满起来,互动感也变得很强,每当循环到let TIMES++这个命令时都会让TIMES变量内数值加上1,这样用来统计总共循环次数的功能更是画龙点睛,让操作者可以知道猜对价格最终使用了几次机会。
[root@linuxprobe ~]# bash Guess.sh
商品实际价格为0-999之间,猜猜看是多少?
请输入您猜测的价格数目:500
太低了!
请输入您猜测的价格数目:800
太高了!
请输入您猜测的价格数目:650
太低了!
请输入您猜测的价格数目:720
太高了!
请输入您猜测的价格数目:690
太低了!
请输入您猜测的价格数目:700
太高了!
请输入您猜测的价格数目:695
太高了!
请输入您猜测的价格数目:692
太高了!
请输入您猜测的价格数目:691
恭喜您答对了,实际价格是 691
您总共猜测了 9 次
case条件测试语句
如果您学习过C语言,此刻一定是会心一笑,这不就是switch语句吗?是的,功能非常相似!case条件测试语句是在多个范围内匹配数据,若匹配到则执行相关命令并结束整个条件测试,而如果数据不在所列出的范围内,则会去执行*)中所规定的默认命令。
结构:
刚刚学习的脚本普遍有一个致命的弱点,不信您就输入一个字母或乱码试一试脚本立即就崩溃了。这是由于字母是不能跟数字做大小比较的,例如a是否大于等于3,这样的命题完全错误,变量操作会直接导致系统崩溃。咱们必须马上想出一个办法来判断用户的输入内容,一旦碰到字母或乱码也能予以提示,不至于因错误输入而崩溃,因此这样的需求用case条件测试语句和第3章节中学习的通配符来一起组合写一个脚本简直再适合不过了提示用户输入一个字符并将其赋值给变量KEY,判断变量KEY为何种字符后分别输出是字母、数字还是其他字符:
[root@linuxprobe ~]# vim Checkkeys.sh
#!/bin/bash
read -p "请输入一个字符,并按Enter键确认:" KEY
case "$KEY" in
[a-z]|[A-Z])
echo "您输入的是 字母。"
;;
[0-9])
echo "您输入的是 数字。"
;;
*)
echo "您输入的是 空格、功能键或其他控制字符。"
esac
[root@linuxprobe ~]# bash Checkkeys.sh
请输入一个字符,并按Enter键确认:6
您输入的是 数字。
[root@linuxprobe ~]# bash Checkkeys.sh
请输入一个字符,并按Enter键确认:p
您输入的是 字母。
[root@linuxprobe ~]# bash Checkkeys.sh
请输入一个字符,并按Enter键确认:^[[15~
您输入的是 空格、功能键或其他控制字符。