Shell脚本命令的工作方式有两种:
交互式(Interactive):用户每输入一条命令就立即执行。
批处理(Batch):由用户事先编写好一个完整的Shell脚本,Shell会一次性执行脚本中诸多的命令。
一、编写简单的脚本
一个Shell脚本主要由三部分组成:脚本声明、脚本注释、脚本命令。
- 脚本声明:告诉系统使用哪种Shell解释器来执行该脚本,比如:#!/bin/bash
- 脚本注释:以#开头,主要是介绍脚本的功能和某些命令
- 脚本命令:需要被执行的Linux命令。
Shell脚本的名称可以任意,但为了方便用户辨认,建议加上.sh后缀以表示这是一个脚本文件。下面通过Vim编辑器简单编写一个Shell脚本:
[root@linuxprobe ~]# vim example.sh
#!/bin/bash //脚本声明
# for example by xuliang //脚本注释
pwd //脚本命令
ls -al
可以通过bash命令直接运行脚本文件,也可以通过输入完整路径的方式来执行,但是需要先对脚本文件添加可执行权限。
[root@linuxprobe ~]# bash example.sh //通过bash命令执行脚本文件
/root
total 21260
dr-xr-x---. 17 root root 4096 Feb 23 16:57 .
drwxr-xr-x. 17 root root 4096 Feb 23 10:34 ..
drwxr-xr-x. 3 root root 14 Feb 18 15:26 a
-rw-------. 1 root root 1032 Feb 18 2019 anaconda-ks.cfg
-rw-------. 1 root root 6039 Feb 23 10:57 .bash_history
---------------------省略部分输出内容------------------------------
[root@linuxprobe ~]# ./example.sh //通过完整路径执行脚本文件,需要可执行权限
-bash: ./example.sh: Permission denied
[root@linuxprobe ~]# chmod u+x example.sh //添加可执行权限
[root@linuxprobe ~]#
[root@linuxprobe ~]# ./example.sh //脚本执行成功
/root
total 21260
dr-xr-x---. 17 root root 4096 Feb 23 16:57 .
drwxr-xr-x. 17 root root 4096 Feb 23 10:34 ..
drwxr-xr-x. 3 root root 14 Feb 18 15:26 a
-rw-------. 1 root root 1032 Feb 18 2019 anaconda-ks.cfg
-rw-------. 1 root root 6039 Feb 23 10:57 .bash_history
-------------------省略部分输出内容-----------------------------
二、接收用户的参数
上面的脚本只能执行一些预先定义好的命令,未免太过于死板了。为了增加Shell脚本的灵活性,必须让脚本可以接收用户输入的参数。Linux系统中的Shell脚本语言已经内设了用于接受参数的变量,变量之间使用空格间隔,相关变量如下所示:
- $0:表示当前Shell脚本的名称;
- $#:表示总共有几个参数;
- $*:表示所有位置的参数值
- $1、$2、$3、$4.....:表示第N个位置的参数值。
“百闻不如一见,看书不如实践”,接下来通过编写一个脚本,引用上面的变量参数来看一下实际效果:
[root@linuxprobe ~]# vim example.sh
#!/bin/bash
# for example by xuliang
echo "当前脚本名称$0"
echo "总共有$#个参数,分别是$*"
echo "第一个参数为$1,第3为$3"
[root@linuxprobe ~]# bash example.sh 1 2 3 4 5 6 //传入6个参数
当前脚本名称example.sh
总共有6个参数,分别是1 2 3 4 5 6
第一个参数为1,第3为3
三、判断用户的参数
接下来学习如何处理接收到的用户参数。Shell脚本中的条件测试语法可以判断表达式是否成立,若条件成立则返回数字0,否则返回其他随机数字(一般都是返回1),条件测试语句的执行格式如下所示。切记,条件表达式两边必须要有一个空格。
按照测试对象来划分,条件测试语句可以分为4种:
- 文件测试语句;
- 逻辑测试语句;
- 整数值比较语句;
- 字符串比较语句。
1、文件测试语句
指使用指定条件来判断文件是否存在或权限是否满足等情况的运算符,具体参数如下所示:
运算符 | 作用 |
-d | 测试文件是否为目录类型 |
-e | 测试文件是否存在 |
-f | 判断是否为一般文件 |
-r | 测试当前用户是否有权限读取 |
-w | 测试当前用户是否有权限写入 |
-x | 测试当前用户是否有权限执行 |
实验1:使用文件测试语句判断/etc/fstab是否为一个目录类型文件,然后通过Shell解释器的内设$?变量来显示上一条命令执行后的返回值。如果上一条命令执行成功,则$?变量的数值为0;反之则为一个非零值(一般都是1)。
[root@linuxprobe ~]# [ -d /etc/fstab ]
[root@linuxprobe ~]# echo $? //显示上条命令执行结果
1 //非零值表示执行失败
[root@linuxprobe ~]#
实验2:使用条件测试语句判断/etc/fstab是否为一个一般文件。
[root@linuxprobe ~]# [ -f /etc/fstab ]
[root@linuxprobe ~]# echo $?
0 //执行成功
[root@linuxprobe ~]#
2、逻辑测试语句
指对测试结果进行逻辑分析,根据测试结果实现不同的效果。主要有3种逻辑运算符,如下所示:
运算符 | 作用 |
逻辑与(&&) | 表示前面的命令执行成功后,才执行后面的命令 |
逻辑或(||) | 表示前面的命令执行失败后,才执行后面的命令 |
逻辑非(!) | 表示把条件测试语句中的判断结果取相反值 |
实验1:判断/dev/cdrom文件是否存在,若存在则输出Exist字样。
[root@linuxprobe ~]# [ -e /dev/cdrom ] && echo "Exist" //逻辑与
Exist
实验2:判断当前登录的用户是否为管理员身份:
[root@linuxprobe ~]# su - linuxprobe //切换至linuxprobe用户
Last login: Mon Feb 24 10:26:51 BNT 2020 on pts/1
[linuxprobe@linuxprobe ~]$
[linuxprobe@linuxprobe ~]$ [ $USER = root ] || echo "not root" //逻辑或
not root
实验3:判断当前登录用户为非管理员身份:
[linuxprobe@linuxprobe ~]$ su - root //切换至root用户
Password:
Last login: Mon Feb 24 11:13:05 BNT 2020 on pts/1
[root@linuxprobe ~]# [ ! $USER = root ] || echo "It's root" //逻辑非
It's root
实验4:判断当前登录的用户,若是普通用户则输出“user”,若是管理员用户则输出“root”
[root@linuxprobe ~]# [ ! $USER = root ] && echo "user" || echo "It's root"
It's root //root用户
[root@linuxprobe ~]# su - linuxprobe //切换至linuxprobe用户
Last login: Mon Feb 24 14:41:29 BNT 2020 on pts/1
[linuxprobe@linuxprobe ~]$ [ ! $USER = root ] && echo "user" || echo "It's root"
user //普通用户
3、整数比较运算符
仅仅是对数字的操作,即运算符的两边必须是数字,不能将数字与字符串、文件等内容一起操作,而且一定要使用规范的整数比较运算符来进行操作。整数比较运算符如下所示:
运算符 | 作用 |
-eq | 是否等于 |
-ne | 是否不等于 |
-gt | 是否大于(greater than) |
-lt | 是否小于(less than) |
-le | 是否等于或小于 |
-ge | 是否大于或等于 |
实验:判断当前主机空闲内存是否小于1024M,若小于1024M,则输出“Insufficient Memory”的字样。
[linuxprobe@linuxprobe ~]$ FreeMem=`free -m | grep Mem: | awk '{print $4}'` //获取当前主机空闲内存值,注意赋值号的两边不能有空格
[linuxprobe@linuxprobe ~]$
[linuxprobe@linuxprobe ~]$ [ $FreeMem -lt 1024 ] && echo "Insuficient Memory"
Insuficient Memory
4、字符串比较语句
用于判断测试字符串是否为空值,或两个字符串是否相同,常见的字符串运算符如下所示:
运算符 | 作用 |
= | 比较字符串的内容是否相同 |
!= | 比较字符串的内容是否不同 |
-z | 判断字符串的内容是否为空 |
实验1:判断变量String是否空值,即判断是否定义了该变量。
[linuxprobe@linuxprobe ~]$ [ -z $String ]
[linuxprobe@linuxprobe ~]$ echo $?
0
实验2:判断当前LANG环境变量值是否为“en.US”。
[linuxprobe@linuxprobe ~]$ echo $LANG
en_US.UTF-8
[linuxprobe@linuxprobe ~]$ [ $LANG = "en.US" ] || echo "Not en.US"
Not en.US
四、流程控制语句
在Shell脚本中,我们可以通过if、for、while、case这4中流程控制语句来编写难度更大、功能更强的脚本,来匹配实际的生产需求。
1、if条件测试语句
if条件测试语句分为单分支结构、双分支结构、多分支结构,下面逐一进行介绍:
(1)单分支结构
由if、then、fi关键词组成,只有在判断条件成立之后才执行预设的命令,相当于口语的“如果........那么........”。语法格式如下所示:
实验:判断/root/test目录文件是否存在,如存在则结束Shell脚本,否则创建该目录。
[root@linuxprobe ~]# vim mkcdrom.sh
#!/bin/bash
DIR="/root/test"
if [ ! -e $DIR ]
then
mkdir -p $DIR
fi
[root@linuxprobe ~]# bash mkcdrom.sh //执行脚本
[root@linuxprobe ~]# ls -d /root/test/ //检查目录是否创建成功
/root/test/
(2)多分支结构
由if、then、else、fi关键词组成,它进行一次条件匹配判断,若匹配成功,则执行预设的命令,否则去执行匹配失败时的预设命令。语法格式如下所示:
实验:用户自行输入IP地址,并判断该主机是否在线。
[root@linuxprobe ~]# vim chkhost.sh
#!/bin/bash
ping -c 3 -i 0.2 -w 3 $1 &> /dev/null ###其中参数表示ping3次、每次间隔0.2秒、等待超时时间为3秒
if [ $? -eq 0 ]
then
echo "Host $1 is On-line"
else
echo "Host $1 is Off-line"
fi
[root@linuxprobe ~]# bash chkhost.sh 192.168.134.10
Host 192.168.134.10 is On-line //主机在线
[root@linuxprobe ~]# bash chkhost.sh 192.168.134.20
Host 192.168.134.20 is Off-line //主机不在线
[root@linuxprobe ~]#
(3)多分支结构
由if、then、else、elif、fi关键词组成,它进行多次条件匹配判断,这么多次判断中的任一项匹配成功都会执行相应的预设命令,相当于口语的“如果.....那么......如果.......那么.....”。多分支语句可以多次嵌套,语法格式如下所示:
实验:判断用户输入的分数在哪个区间,然后输出Excellent、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 "Excellent"
elif [ $GRADE -ge 70 ] && [ $GRADE -le 84 ]
then
echo "Pass"
else
echo "Fail"
fi
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):88
Excellent
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):73
Pass
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):60
Fail
2、for条件循环语句
for循环语句允许脚本一次性读取多个信息,然后逐一对信息进行操作处理。当要处理有范围的数据时,使用for循环语句再适合不过了,其语法格式如下所示:
实验1:从列表文件中读取多个用户名,然后为其逐一创建用户账号并设置密码。
[root@linuxprobe ~]# vim users.txt //首先创建包含用户名称的文件
zhangsan
lisi
wangwu
zhaoliu
[root@linuxprobe ~]# vim CreateUser.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
[root@linuxprobe ~]# bash CreateUser.sh //执行脚本
Enter the Users Password:123456
zhangsan create success
lisi create success
wangwu create success
zhaoliu create success
[root@linuxprobe ~]#
补充:假如用户名文件users.txt内容写成以下形式(即所有用户名写成一行,用空格分开),CreateUser.sh脚本同样执行成功。
[root@linuxprobe ~]# cat users.txt
andy barry carl
[root@linuxprobe ~]#
[root@linuxprobe ~]# bash CreateUser.sh
Enter the Users Password:123456
andy create success
barry create success
carl create success
实验2:读取文件中的主机列表,然后逐个测试这些主机是否在线。
[root@linuxprobe ~]# vim ipaddrs.txt //创建主机列表文件
192.168.134.10
192.168.134.20
192.168.134.30
192.168.134.40
[root@linuxprobe ~]# vim chkhosts.sh //编写脚本
#!/bin/bash
HLIST=$(cat ~/ipaddrs.txt) //其中$(命令)相当于`命令`
for IP in $HLIST
do
ping -c 3 -i 0.2 -w 3 $IP &> /dev/null
if [ $? -eq 0 ] ; then
echo "$IP is On-line"
else
echo "$IP is Off-line"
fi
done
[root@linuxprobe ~]# bash chkhosts.sh //运行脚本
192.168.134.10 is On-line
192.168.134.20 is Off-line
192.168.134.30 is Off-line
192.168.134.40 is Off-line
3、while条件循环语句
while条件语句是一种让脚本根据某些条件来重复执行命令的语句,它的循环结构往往在执行前并不确定最终执行的次数,它通过判断条件测试的真假来决定是否继续执行命令,条件为真就继续执行,为假就结束循环,语法结构如下所示:
实验:编写一个猜数的脚本,若用户输入的数值与脚本随机生成的数值一致,则结束游戏。
[root@linuxprobe ~]# vim guess.sh
#!/bin/bash
PRICE=$(expr $RANDOM % 1000) ##获取一个1000以内的随机数
TIMES=0
echo "商品的实际价格为0-999之间,请猜猜看是多少?"
while true
do
read -p "请输入你猜测的价格:" INT
let TIMES++ ##TIMES变量自增1
if [ $INT -eq $PRICE ] ; then
echo "恭喜你猜对啦,实际价格为 $PRICE"
echo "您总共猜了 $TIMES 次"
exit
elif [ $INT -gt $PRICE ] ; then
echo "您猜高了"
else
echo "您猜低了"
fi
done
[root@linuxprobe ~]# bash guess.sh
商品的实际价格为0-999之间,请猜猜看是多少?
请输入你猜测的价格:500
您猜低了
请输入你猜测的价格:800
您猜高了
-------------------省略部分内容---------------------
您猜高了
请输入你猜测的价格:516
您猜低了
请输入你猜测的价格:517
恭喜你猜对啦,实际价格为 517
您总共猜了 14 次
4、case条件测试语句
case语句是在多个范围内匹配数据,若匹配成功则执行相关命令并结束整个条件测试;如果数据在所列的范围内,则会去执行星号(*)中所定义的默认命令,语法结构如下所示:
实验:判断用户输入的数据是字母、数字还是其他字符。
[root@linuxprobe ~]# cat chkkeys.sh
#!/bin/bash
read -p "请输入一个字符:" KEY
case "$KEY" in
[a-z]|[A-Z])
echo "您输入的是字母"
;;
[0-9])
echo "您输入的是数字"
;;
*)
echo "您输入的是特殊字符"
esac
[root@linuxprobe ~]# bash chkkeys.sh
请输入一个字符:2
您输入的是数字
[root@linuxprobe ~]# bash chkkeys.sh
请输入一个字符:s
您输入的是字母
[root@linuxprobe ~]# bash chkkeys.sh
请输入一个字符:`
您输入的是特殊字符
[root@linuxprobe ~]#
补充:上述关于检查字符的脚本是有缺陷的,当用户输入两位及以上的数字或字母时,会提示输入的是特殊字符。
[root@linuxprobe ~]# bash chkkeys.sh
请输入一个字符:12
您输入的是特殊字符
解决方法:修改匹配条件,如下所示。但是当输入三位数字时,依然提示输入的是特殊字符,需要继续修改匹配条件。所以该方法只能算打补丁,不能算真正的解决方法。
[root@linuxprobe ~]# cat chkkeys.sh
#!/bin/bash
read -p "请输入一个字符:" KEY
case "$KEY" in
[a-z]|[A-Z])
echo "您输入的是字母"
;;
[0-9]|[0-9][0-9]) ##匹配一位或两位数字
echo "您输入的是数字"
;;
*)
echo "您输入的是特殊字符"
esac
[root@linuxprobe ~]# bash chkkeys.sh
请输入一个字符:34
您输入的是数字
四、计划任务服务程序
在实际的运维工作中,需要在指定的时间段自动启动或停止某些服务或命令,从而实现运维的自动化。接下来介绍如何设置服务器的计划任务服务,把周期性、规律性的工作交给系统自动完成。
计划任务分为一次性任务和长期性任务,如下所示:
- 一次性计划任务:比如今晚11点30分开启网站服务。
- 长期性计划任务:比如每周一的凌晨3点30分把/home/www目录的文件打包备份为backup.tar.gz。
1、一次性计划任务
顾名思义,一次性计划任务只执行一次,一般用于满足临时的工作需求,可以用at命令实现这种功能,相关功能如下所示:
- at 时间 :表示创建一个一次性计划任务。
- at -l :表示查看已经设置好但未执行的一次性计划任务。
- atrm 任务序号 : 表示删除一个一次性计划任务。
实验1:设置在今天11:24重启系统主机
[root@linuxprobe ~]# at 11:24
at> reboot
at> <此处按下Ctrl+d组合键来结束编写计划任务>
job 2 at Sat Feb 29 11:24:00 2020
[root@linuxprobe ~]# at -l
2 Sat Feb 29 11:24:00 2020 a root
[root@linuxprobe ~]#
实验2:at命令默认采用交互式的方式,接下使用非交互式的方法创建一个一次性计划任务。
[root@linuxprobe ~]# echo "systemctl restart network" | at 11:35
job 4 at Sat Feb 29 11:35:00 2020
[root@linuxprobe ~]#
[root@linuxprobe ~]# at -l
4 Sat Feb 29 11:35:00 2020 a root //其中"4"就是任务序号
实验3:删除一个一次性计划任务。
[root@linuxprobe ~]# atrm 4
[root@linuxprobe ~]# at -l
[root@linuxprobe ~]#
2、长期性计划任务
Linux系统中默认启动的crond服务能够周期性地、有规律地执行某些具体的任务,相关地命令如下所示:
命令 | 作用 |
crontab -e | 创建、编辑计划任务 |
crontab -l | 查看当前地计划任务 |
crontab -r | 删除某条计划任务 |
crontab -u | 编辑他人的计划任务(必须是root身份) |
使用crond服务设置计划任务时,语法格式为“分 时 日 月 星期 命令”。需要注意的是,如果有些字段没有设置,则必须使用星号(*)占位,如下图所示:
使用crond设置任务的参数字段说明,如下表所示:
字段 | 说明 |
分 | 取值为0~59的整数 |
时 | 取值为0~23的任意整数 |
日 | 取值为0~31的任意整数 |
月 | 取值1~12的任意整数 |
星期 | 取值0~7的任意整数,其中0与7均为星期日 |
命令 | 要执行的命令或脚本(必须要用绝对路径来写) |
实验1:设置在每周一、三、五的凌晨3点25分,使用tar命令打包网站的数据。
[root@linuxprobe ~]# crontab -e //默认进入Vim编辑器界面
no crontab for root - using an empty one
crontab: installing new crontab
[root@linuxprobe ~]# crontab -l
25 3 * * 1,3,5 /usr/bin/tar -czvf backup.tar.gz /home/wwwroot
[root@linuxprobe ~]#
补充说明:这里补充几点关于时间周期的设置,如下所示:
- 用逗号(,)来表示多个时间段,如“星期”字段“1,3,5”表示周一、周三和周五。
- 用减号(-)来表示一段连续的时间周期,如“月”字段“8-12”表示8~12月。
- 用除号(/)来表示执行任务的间隔时间,如“分”字段“*/2”表示每隔2分钟执行一次任务。
crond服务可以包含多条计划任务,注意每条计划任务占一行。比如我们要增加一条计划任务,它的功能是每周一至周五的凌晨1点自动清空/tmp目录内的所有文件。
[root@linuxprobe ~]# whereis rm //使用whereis命令查询绝对路径
rm: /usr/bin/rm /usr/share/man/man1/rm.1.gz /usr/share/man/man1p/rm.1p.gz
[root@linuxprobe ~]#
[root@linuxprobe ~]# crontab -e
crontab: installing new crontab
[root@linuxprobe ~]# crontab -l
25 3 * * 1,3,5 /usr/bin/tar -czvf backup.tar.gz /home/wwwroot
0 1 * * 1-5 /usr/bin/rm -rf /tmp/*
注意事项:
(1)在Vim编辑器中配置计划任务时,可以以#号开头写上注释信息。
(2)计划任务中的“分”字段必须有数值,绝对不能为空或*号。
(3)“日”和“星期”字段不能同时使用,否则会发生冲突。