shell编程入门
最近由于工作原因,写了几个脚本,然后再linux中用定时任务去跑,记录一下shell脚本的学习过程吧!
首先知道shell第一行#!/bin/bash表示这是一个shell脚本,例如下面这个简单的脚本:
#!/bin/bash #author:Gary #date:20200313 #description:用于清理14天之前日志文件夹 #这里就是要进行进行清理的nas中的目录 baseDir="/usr/local/java/shellscript/store" #服务器中不存在该目录就退出 if [ ! -d ${baseDir} ];then echo "不存在目录:${baseDir}" exit 1 fi #这里指定前段时间第n天日志需要进行清理 num=14 #需要清理的日期转换成时间戳 barrierDate=`date -d "$num days ago" +%F` barrierDateStamp=`date -d "${barrierDate}" +%s` #遍历目录下的所有日志文件进行正则匹配 for file in `ls ${baseDir}`;do #获取目录中文件名中的日期 dirDate=`expr "${file}" : '\([0-9]\{4\}\-[0-9]\{2\}\-[0-9]\{2\}\)'` #目录中的日志文件名没有日期在其中,说明不需要删除,就跳过,进行下一次循环 if [ -z "${dirDate}" ];then continue fi #获取该文件文件名的时间戳,当该时间符合需要清理的时间的时候,再判断一次该目录是否存在,保险一点!存在的话就删除就行了 dirDateTimeStamp=`date -d "${dirDate}" +%s` if [ ${dirDateTimeStamp} -lt ${barrierDateStamp} ];then echo "当前遍历的目录是:${file},目录时间是:${dirDate}" #当前日志目录所在的绝对路径 fileAbsPath="${baseDir}/${file}" if [ -d ${basePath} ];then echo "对${file}文件进行删除" rm -rf ${fileAbsPath} echo "日志目录删除成功:${fileAbsPath}" else echo "删除目录发生错误:${fileAbsPath}" fi fi done
上面的目录中存放的就是每天生成的日志文件,粗略看一下上面这个简单的脚本,由于本人写脚本的水平抠脚,只能用这种很直白的写法,没有任何花里胡哨.....
就是按照java代码那样的逻辑写的,应该都能看得懂,基本上是一些for循环和if判断;还有一个echo语句表示在命令行中输出相关信息,如果使用echo "xxx" >> /usr/local/file_name.log表示将echo输出的信息追加到/usr/local/file_name.log文件中;
注意>和>>的区别,>是覆盖的意思,比如用命令清空一个文件,最简单的就是echo “” > a.txt;而>>表示在文件后面追加,不会覆盖
1.先说说for循环
下面这个就是遍历一个目录下的子目录的,不能遍历文件的哦!那个反引号,表示让系统去执行ls /etc命令,什么叫做让系统去执行呢?就跟我们手动敲这条命令去执行一样;还有一点,就是shell中要使用一个声明好的变量,需要用${xxx}表示,可以不加大括号,习惯加上好看点;
最后就是要以done表示这个for循环结束;
for file in `ls /etc`;do echo ${file} done
2.if语句
if和for写法差不多,下面这个作用是:当根目录下没有Top这个目录,就创建Top目录;注意,if后面的中括号那么多空格,不能少!!!if语句要以fi结尾;
那么问题来了,那个-d是干嘛的呀?下面列出来了很多-xx的,写脚本的时候拿出来看一看就行;发现判断文件或者是字符串是不是空都可以用这种-xx的方式判断;
if [ ! -d "/Top" ]; then mkdir -p /Top fi
[ -a FILE ] 如果 FILE 存在则为真。 [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真。 [ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真。 [ -d FILE ] 如果 FILE 存在且是一个目录则为真。 [ -e FILE ] 如果 FILE 存在则为真。 [ -f FILE ] 如果 FILE 存在且是一个普通文件则为真。 [ -g FILE ] 如果 FILE 存在且已经设置了SGID则为真。 [ -h FILE ] 如果 FILE 存在且是一个符号连接则为真。 [ -k FILE ] 如果 FILE 存在且已经设置了粘制位则为真。 [ -p FILE ] 如果 FILE 存在且是一个名字管道(F如果O)则为真。 [ -r FILE ] 如果 FILE 存在且是可读的则为真。 [ -s FILE ] 如果 FILE 存在且大小不为0则为真。 [ -t FD ] 如果文件描述符 FD 打开且指向一个终端则为真。 [ -u FILE ] 如果 FILE 存在且设置了SUID (set user ID)则为真。 [ -w FILE ] 如果 FILE 如果 FILE 存在且是可写的则为真。 [ -x FILE ] 如果 FILE 存在且是可执行的则为真。 [ -O FILE ] 如果 FILE 存在且属有效用户ID则为真。 [ -G FILE ] 如果 FILE 存在且属有效用户组则为真。 [ -L FILE ] 如果 FILE 存在且是一个符号连接则为真。 [ -N FILE ] 如果 FILE 存在 and has been mod如果ied since it was last read则为真。 [ -S FILE ] 如果 FILE 存在且是一个套接字则为真。 [ FILE1 -nt FILE2 ] 如果 FILE1 has been changed more recently than FILE2, or 如果 FILE1 exists and FILE2 does not则为真。 [ FILE1 -ot FILE2 ] 如果 FILE1 比 FILE2 要老, 或者 FILE2 存在且 FILE1 不存在则为真。 [ FILE1 -ef FILE2 ] 如果 FILE1 和 FILE2 指向相同的设备和节点号则为真。 [ -o OPTIONNAME ] 如果 shell选项 “OPTIONNAME” 开启则为真。 [ -z STRING ] 字符串“STRING” 的长度为零则为真。 或者字符串为NULL时也为真。 [ -n STRING ] 和-z相反,默认不加-n也行,也就是说这个写法和[STRING]是一样的 [ STRING1 == STRING2 ] 如果2个字符串相同。 “=” may be used instead of “==” for strict POSIX compliance则为真。 [ STRING1 != STRING2 ] 如果字符串不相等则为真。
3.数组的遍历
可以看到数组中每个元素用空格分隔就行了,如果是字符串数组也是一样的,例如:dirs=("/log1" "/log2");
注意下面的for循环中有两个小括号啊,括号里面的${#arr(*)}表示数组里面元素的个数,注意和${arr[*]}的区别,这个表示数组中所有的实际元素
arr=(1 2 3 4) for((i=0;${#arr[*])};i++));do echo ${arr[i]} done
上面的for循环也可以用for in循环表示,,如下所示,看到这里应该很多人微微一笑,很多变成语言应该都知道这两种循环吧!
arr=(1 2 3 4) for a in ${arr[*]};do echo ${a} done
4.比较运算
很多时候我们需要比较两个字符串是否相同,如下所示,注意,中括号两边的空格啊!==号两边空格,不要吝啬空格,不然你会发现一些奇葩的错误;
if [ ${a} == ${b} ];then echo "相同" else echo "不相同" fi
那么又有人要问了,数字的比较呢?注意,此时是没有大于号,小于号这种东西的(这里不用转义符号。。)
左边等于右边: $a -eq $b; 左边不等于右边: $a -ne $b; 左边大于右边: $a -gt $b; 左边小于右边: $a -lt $b; 左边大于等于右边: $a -ge $b; 左边小于等于右边: $a -le $b;
这里注意一点东西,=,==和-eq都可以用来比较是否相等,都可以用在if后面的中括号中;在[ ]中=和==效果一样,在(( ))中=表示赋值,而==表示比较;那么eq和==的区别在哪呢?==可以比较字符串和数字,而eq只能比较数字(eq可以比较这样的字符串[ "20" -eq "20" ],不能比较[ "hello" -eq "hello" ],会报错),所以尽量用==;
#!/bin/bash #不会报错,打印false if [ "a" == "" ];then echo "true" else echo "false" fi #会报错 if [ "a" -eq "" ];then echo "true" else echo "false" fi
另外,注意[[ ]]和[ ]的区别,简单说一下,只要你使用[[ ]]那么你想表示&&和||的关系,你可使用这两个符号,也可以使用对应的-a和-o;但是如果你使用的是[ ]那么你只能使用-a和-o,还只能在中括号里面,例如[ 5 -lt 3 ] -o [ 7 -gt 6 ]就会报错;
if [[ 5 -lt 3 || 7 -gt 6 ]];then echo "true" else echo "false" fi if [[ 5 -lt 3 ]] || [[ 7 -gt 6 ]];then echo "true" else echo "false" fi if [ 5 -lt 3 -o 7 -gt 6 ];then echo "true" else echo "false" fi if [ 5 -lt 3 ] && [ 7 -gt 6 ];then echo "true" else echo "false" fi
5.时间
比较常见的就是脚本中处理时间:
获取今天的日期:todayDate=`date -d now +%Y-%m-%d`或者`date +%F` 明天日期:`date -d next-day +%Y-%m-%d`或者`date -d tomorrow +%Y-%m-%d` 昨天日期:`date -d "1 years ago" +%Y-%m-%d` 第n天前的日期:`date -d "n days ago" +%F` 转换成时间戳:stamp=`date -d "${todayDate}" +s`
6.正则匹配
例如一个日志文件是这样的sgffg.log.2020-02-05.2,那么用下面这个正则去匹配,其中${file}表示该日志文件名;
fileDate=`expr "${file}" : '.*\([0-9]\{4\}\-[0-9]\{2\}\-[0-9]\{2\}\).*'`
7.日志脚本
#!/bin/bash #author:Gary #date:20200218 #description:主要用于定期备份过期日志 #这里表示可以备份多个目录下的日志文件 baseDirs=( "/home/path1" "/home/path2" "/home/path3" ) #想要备份的地方 storePath="/home/store/path" #这里指定前段时间第n天日志需要进行备份 num=7 #备份成功文件的个数 storeLogFileNum=0 taskStartTime=`date "+%Y-%m-%d %H:%M:%S"` #因为需要把日志按照时间进行分类的,这里获取需要清理日志的那天日期以及时间戳 barrierDate=`date -d "$num days ago" +%F` barrierDateStamp=`date -d "${barrierDate}" +%s` #路径不存在就创建该目录 if [ ! -d ${storePath}/${barrierDate} ];then mkdir -p ${storePath}/${barrierDate} || exit 1 fi #将脚本文件中echo输出的信息追加到文件中,这里该文件不存在就会创建 storeRecord=${storePath}/${barrierDate}/storeRecord.log echo "当前时间为${taskStartTime},对日志文件时间是${barrierDate}的文件开始备份..." >> ${storeRecord} #开始遍历所有需要备份日志的目录 for ((i=0;i<${#baseDirs[*]};i++));do basePath=${baseDirs[i]} #对存日志的路径判断进行处理,该目录不存在的话就进行下一次循环 if [ ! -d ${basePath} ];then # echo "日志文件目录不存在,basePath:${basePath}" continue fi echo "开始对目录${basePath}下的日志文件进行处理" >> ${storeRecord} #判断日志目录下有没有日志文件 folder=`ls ${basePath}` if [ -z ${folder} ];then echo "目录${basePath}中没有任何文件" >> ${storeRecord} continue fi #遍历目录下的所有日志文件进行正则匹配 for file in ${folder};do #获取文件名中的日期 fileDate=`expr "${file}" : '.*\([0-9]\{4\}\-[0-9]\{2\}\-[0-9]\{2\}\).*'` #目录中的日志文件不符合条件筛选条件,说明该文件不需要备份,跳过,进行下一次循环 #这个很重要!!!!!!! if [ -z "${fileDate}" ];then continue fi #不为空就获取该文件文件名的时间戳,当该时间符合需要清理的时间的时候,就移动到目标目录中存起来 fileDateTimeStamp=`date -d "${fileDate}" +%s` if [ ${fileDateTimeStamp} -eq ${barrierDateStamp} ];then #当前日志文件所在的绝对路径 fileAbsPath="${basePath}/${file}" #如果在目标目录中有重名的,这里的-b参数会对目标目录中的同名文件进行备份,不会覆盖 echo "对${file}文件进行备份" >> ${storeRecord} mv -b ${fileAbsPath} ${storePath}/${barrierDate} #日志文件移动成功的计数器 ((storeLogFileNum++)) echo "该文件备份成功" >> ${storeRecord} fi done echo "目录${basePath}下的日志文件处理完毕" >> ${storeRecord} done taskEndTime=`date "+%Y-%m-%d %H:%M:%S"` startSeconds=$(date --date="${taskStartTime}" +%s); endSeconds=$(date --date="${taskEndTime}" +%s); #执行该任务所花费时间,精确到秒 runTime=$((endSeconds-startSeconds))"s" echo "日志备份整理完毕,备份完成时间为${taskEndTime},花费了${runTime},共备份了${storeLogFileNum}个日志文件" >> ${storeRecord} echo >> ${storeRecord} exit 0
我这个脚本在linux定时任务中是每天执行一次,将前面第七天的日志备份,例如今天是3月8号清理3月1号的,3月9号清理3月2号的...,始终保证最新的一个星期的日志不被清理;
备份之后生成的目录是这样的:
8.定时任务
我们肯定不会自己手动去执行这个shell脚本吧!这个时候就要用到linux的定时任务,最重要的时cron表达式;
定时任务分两种,一种是系统级别的定时任务,可以通过vim /etc/crontab打开,但是不建议使用这个,这个文件必须要有root权限才能修改;另外一种是用户级别的定时任务,默认就是当前用户,使用crontab -e打开;
那么问题来了,cron表达式怕写错了怎么办?肯定不会自己傻乎乎的等啊,这里有个在线的cron表达式测试工具https://tool.lu/crontab,可以直接看看你的任务啥时候执行;
最后,注意一点,使用crontab -e打开之后,需要配置你的shell脚本绝对路径,例如0 1 * * * /usr/local/del_file.sh,注意哦!!!要给你的脚本添加可执行权限啊!
chmod +x del_file.sh