Shell脚本的重要性我想应该不需要我在重复了,本文的目的是向读者介绍Shell编程的一些固定套路,当然,也可以称之为技巧,而crontab计划任务又和Shell脚本密切相关(一般简单任务当然是写一条命令啦,如果比较复杂的场景还是脚本比较合事宜,并且,脚本更为灵活,拓展性更强啦)。
一,脚本的结构
就是第一个行,#!
被称为shebang符号,用来声明所使用的解释器,一旦执行某个脚本,CPU看到第一行,就会去执行该解释器,再由解释器去找对应的命令程序(这个要圈起来,要考~~)比如,bash解释器/bin/bash(最常用的)csh解释器,zsh解释器,ash解释器等等。- 一般是第二行,脚本编写者信息,同样#!开始,可写,可不写,不过建议写上,表明著作权还是应该的,至少,某些人看到这个脚本,会知道谁tm写的这么烂的脚本~~~哈哈
- 一般是第三行,同样#!开始,脚本的功能大致介绍,相当于一本书的前言,也建议写上,万一,n个月后翻出来这么一个脚本,不至于一脸懵逼::),尤其是比较复杂的大型的脚本,否则只好慢慢阅读后面的内容,然后才可能知道这脚本干什么用的。相信我,这是一个好的习惯,防止尴尬,不会让你看到自己写的脚本后进入十脸懵逼的状态。
- 一般是第四行,同样#!开始,脚本的编写日期。这个也是脚本的介绍功能,能清楚知道哪一天写的,如果,刚巧那天头脑发昏,而又记得那一天,会理解自己怎么写出这么烂的脚本的哦。(当然,这个是玩笑话,其实,有个日期是方便以后脚本写的越来越多的时候,进行归档的。)
- 一般是第五行,这里就不能#!开头了,是脚本的运行方式指定,比如 set -e(脚本遇到错误就停止) set -x(debug模式运行脚本) 等等这些命令了。 详细介绍了set -e 等等命令的用法。
- 第五行后,如果是比较复杂的功能相对比较多的脚本,很可能会使用不少全局变量了,这个时候,变量的定义就放这了,也就是脚本的前一部分,
- 在往后就是脚本的功能实现段了,也就是你要用脚本干什么,比如,执行命令,怎么执行命令,是否需要逻辑判断,也就是逻辑内容,需要输出什么,等等都在这了,直到脚本末尾。
- 稍作总结。。。。。。。。。其实一个好的脚本至少得需要四行,脚本的功能实现段还应该有详细的注释。
为了说明脚本的结构,现拿pycharm的启动脚本来做一示范(官方脚本,最为致命,最为标准吧)
该脚本第二行开始就是注释了,因为,此脚本的用途是启动脚本,并不是个人编写的,推测是团队编写,不好署名啊,日期也不太好写,当然,这个脚本有用到全局变量,只是先写了一个自定义函数。第六行也就是message()这一行开始,就是shell函数了。UNAME=$(command -v uname)这一行是该脚本的集中定义的全局变量。总的来说,这个脚本设计比较巧妙。(截取了一部分,只演示了部分结构,让大家对脚本有一个大致印象,以免篇幅太长,引起大家的不适)
#!/bin/sh
# Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
# ---------------------------------------------------------------------
# PyCharm startup script.
# ---------------------------------------------------------------------
message()
{
TITLE="Cannot start PyCharm"
if [ -n "$(command -v zenity)" ]; then
zenity --error --title="$TITLE" --text="$1" --no-wrap
elif [ -n "$(command -v kdialog)" ]; then
kdialog --error "$1" --title "$TITLE"
elif [ -n "$(command -v notify-send)" ]; then
notify-send "ERROR: $TITLE" "$1"
elif [ -n "$(command -v xmessage)" ]; then
xmessage -center "ERROR: $TITLE: $1"
else
printf "ERROR: %s\n%s\n" "$TITLE" "$1"
fi
}
UNAME=$(command -v uname)
GREP=$(command -v egrep)
CUT=$(command -v cut)
READLINK=$(command -v readlink)
XARGS=$(command -v xargs)
DIRNAME=$(command -v dirname)
MKTEMP=$(command -v mktemp)
RM=$(command -v rm)
CAT=$(command -v cat)
SED=$(command -v sed)
if [ -z "$UNAME" ] || [ -z "$GREP" ] || [ -z "$CUT" ] || [ -z "$DIRNAME" ] || [ -z "$MKTEMP" ] || [ -z "$RM" ] || [ -z "$CAT" ] || [ -z "$SED" ]; then
message "Required tools are missing - check beginning of \"$0\" file for details."
exit 1
fi
# shellcheck disable=SC2034
GREP_OPTIONS=''
OS_TYPE=$("$UNAME" -s)
。。。。略略略
我们如果自己编写脚本,当然不能和官方一样,因为目的和用途不同嘛,像编写者署名,编写日期,脚本大致介绍这些内容应该是要有的,这是一个良好的习惯,一个好的习惯是一件事成功的一半嘛,对吧。
脚本大体可以分为(1)安装部署类脚本,比如,自动安装某个软件,自动部署某个环境,例如,自动部署lnmp,自动安装MySQL数据库到一个自定义的位置等等。(2)功能类脚本。仅仅实现一个或者多个功能,比如,实现自动清理多余的日志文件,清理磁盘空间,批量改名,批量建立用户,检测某个服务是否正常,如果服务异常就重启服务,等等功能多个或者单个功能的脚本。(3)启动类脚本,比如,自动启动某个自编服务,启动某个MySQL数据库,停止某个redis数据库,这样的一些流程化,制度化的脚本。
那么,流程化,制度化的脚本又通常需要任务计划程序crontab这样的服务来编排调度,比如,现在我安装了某个集群,集群的启动停止是有先后顺序的,那么,无疑,编写一个或者n个shell脚本让脚本来做这些事会更有效率,也更有意义的。而有些服务长期不重启,这里重点要说一下Java编写的服务(Java让人诟病的内存管理问题::)),通常都有内存脏数据的问题,那么,定期的重启服务,使得服务器一直保持良好的运行是一个十分必要的事情。通常,服务的启停重启会选择在半夜来做,那么,人是会有累的时候,如果使用脚本,定时在半夜某个点利用脚本重启服务,无疑是更科学,更人性的一件事情。 或者,某些常常要做得事情,比如,定期备份数据库,备份重要文件,清理多余的垃圾文件,这些繁琐的事情,交给脚本处理,也无疑是一个正确的选择。因此,脚本与计划任务结合势在必行,也是一个必须掌握的事情。
说了这么多,也就是说,第一,我们需要能够编写高质量的脚本,第二,我们能够灵活的根据实际情况,结合crontab计划任务来编排调度使用这些脚本,让脚本灵活的为我们所用。
重要的事情在强调一次,脚本编写和合理的计划任务调度。
编写脚本的技巧:
脚本编写技巧应该有以下几点:
1,从实际需求出发,构思准确无误,其内部逻辑应该准确无误。
2,有一个沙盒测试环境用来测试脚本,防止对生产环境造成破坏。
3,脚本有健壮性测试,要考虑脚本如果运行成功自然是好,如果失败了,有相应的处理,比如,提示某某功能失败。要考虑在别的机器的不同的环境,或者与本机比更复杂的环境,脚本的执行成功率,相应的策略。脚本对系统可能带来的后果,比如,有些脚本里变量的使用不规范,造成某个变量失效,而刚好某个删除命令rm -rf这种的用到了此变量,没有对变量值检测,那么,服务器文件会完全删除的。
4,有一个规范的调试流程,调试语句比如echo "第一步已完成"这样的,在沙盒测试环境可以使用,在生产环境请去掉。
5,bash -x 脚本名称 脚本参数1 脚本参数2.。。其实就是调试模式,但是,如果脚本是安装部署类脚本,会真的执行,并且会告诉你执行的详细情况,所以沙盒环境是十分重要的哦。
6,注意发现并修改脚本的逻辑错误,在编写的时候就要有清晰的逻辑。
7,合理使用shell的函数功能,可以使得脚本更简洁,代码更复用,效率更高。
一,脚本如何构思
首先,需要明确你要写的这个脚本主要功能是什么,是什么类型的脚本,比如,添加用户脚本,明确添加几个用户,涉及哪些 服务器,用户和密码需要对应,新建的这些用户打算是做什么用的,根据用途设计它的shell是nologin还是可登录,可登录用户密码是否一致还。也就是从需求出发,对于脚本如何运行有一个大致的构想,然后开始编写脚本。例如我写的这样一个脚本:
#!/bin/bash
#!author zsk_john
#!date:2021-08-30
#!quickly add user for linux server!!!
if [ $# -eq 0 ];
then
echo "please use xargs"
#提示请使用参数,该脚本是带参才可运行
exit 1
#给定一个返回码,如果脚本运行到这
fi
if [ ! -f $1 ];
then
echo "file is not found,请提供一个写有需要添加的用户列表的文件"
exit 2
#给定一个返回码,如果脚本运行到这
fi
while read line
do
egrep "^$line" /etc/passwd >& /dev/null
if [ $? -ne 0 ];then
useradd -s /sbin/login $line &> /dev/null
echo "user $line is add success!!!"
else
echo "user $line is exits"
#这一行是添加用户命令,做了静默运行处理,&》 /dev/null 所有结果不显示
#-s 是添加用户命令指定用户的bin,该命令可以指定用户目录位置,用户id和用户所属组id等
#根据实际需要吧
fi
done<$1
二,脚本的健壮性测试
脚本的健壮性指的是一个良好的脚本应该能够在目标平台上无错误的反复运行而不会对服务器造成伤害,例如,自动化编译安装MySQL5.7版本这样的一个脚本,应该是可以反复运行都不会报错,不会对系统环境造成污染,比如产生多余的安装包文件,/etc/profile这样的环境配置文件不会被脚本反复注入重复的环境变量。例如下面这个脚本就不太符合健壮性测试,因为,环境变量会随着每次的运行反复注入环境配置文件。
#!/bin/bash
wget http://dl.mycat.org.cn/jdk-8u20-linux-x64.tar.gz
tar -zxf `find / -name jdk-8u231-linux-x64.tar.gz` -C /usr/local/
mv /usr/local/jdk1.8.0_231 /usr/local/jdk
echo "JAVA_HOME=/usr/local/jdk
PATH=.\$PATH:\$JAVA_HOME/bin
CLASSPATH=\$JAVA_HOME/jre/lib/ext:\$JAVA_HOME/lib/tools.jar
export CLASSPATH PATH JAVA_HOME">>/etc/profile
source /etc/profile
这样的脚本如果反复在同一个服务器上运行,会产生很多多余的垃圾文件和多余的配置文件。如果这样的脚本有很多,那么无疑对系统是一场灾难啦~~~~
三,脚本的测试与调试流程
首先,脚本编写完毕后,应该是在沙盒测试环境运行(沙盒测试环境可以简单理解为高仿生产环境),并对各个功能进行验证是否符合脚本编写构想,对于沙盒测试环境本文不过多探讨。
那么,脚本其实很多情况下,是比较简单的命令集合,在编写脚本前,对于每一个命令可以单独执行一下,看是否可以正常执行。对于脚本内的逻辑单元测试,需要请出set -x 或者 脚本运行时 bash -x 脚本名称 脚本参数。。。 这样的形式,跟踪每一个变量,每一个逻辑。例如,下面这个脚本,echo $i (这一行,将每一次循环的变量i的值打印,这个是和其它编程语言一样的通用做法,比如Java的print函数,一般都是测试时打印输出一些程序的反馈,在shell里就是echo命令啦):
#!/bin/bash
#!author zsk_john
#!date 2021-08-30
set -x
A=2
for i in {1..100}
do
echo $i
echo $[$i*$A]
done
执行结果如下:
+ echo 194
194
+ for i in '{1..100}'
+ echo 98
98
+ echo 196
196
+ for i in '{1..100}'
+ echo 99
99
+ echo 198
198
+ for i in '{1..100}'
+ echo 100
100
+ echo 200
200
bash -x 脚本名称 脚本参数1 脚本参数2.。。其实就是调试模式,但是,如果脚本是安装部署类脚本,会真的执行,并且会告诉你执行的详细情况,所以沙盒环境是十分重要的哦。
在调试脚本的时候,多使用echo命令 输出自己定义的内容,这样,脚本运行正不正常会一目了然。比如,下面这个脚本(还是前面那个添加用户脚本,其中有逻辑判断,那么,我们将逻辑判断更改为错误的,会发生什么奇妙的情况呢?):
#!/bin/bash
#!author zsk_john
#!date:2021-08-30
#!quickly add user for linux server!!!
if [ $# -eq 0 ];
then
echo "please use xargs"
#提示请使用参数,该脚本是带参才可运行
exit 1
#给定一个返回码,如果脚本运行到这
fi
if [ ! -f $1 ];
then
echo "file is not found,请提供一个写有需要添加的用户列表的文件"
exit 2
#给定一个返回码,如果脚本运行到这
fi
while read line
do
egrep "^$line" /etc/passwd >& /dev/null
if [ $? -ge 0 ];then
useradd -s /sbin/login $line &> /dev/null
echo "user $line is add success!!!"
else
echo "user $line is exits"
#这一行是添加用户命令,做了静默运行处理,&》 /dev/null 所有结果不显示
#-s 是添加用户命令指定用户的bin,该命令可以指定用户目录位置,用户id和用户所属组id等
#根据实际需要吧
fi
done<$1
调试模式输出如下(可以看到,即使zsk这个用户存在,也会继续useradd 用户zsk ,else不会执行,并不会警告用户已存在,很明显这是不对的。):
[root@hdp-1 ~]# id zsk
uid=1003(zsk) gid=1003(zsk) groups=1003(zsk)
[root@hdp-1 ~]# bash -x aa.sh user
+ '[' 1 -eq 0 ']'
+ '[' '!' -f user ']'
+ read line
+ egrep '^zsk' /etc/passwd
+ '[' 0 -ge 0 ']'
+ useradd -s /sbin/login zsk
+ echo 'user zsk is add success!!!'
user zsk is add success!!!
+ read line
+ egrep '^zsk1' /etc/passwd
+ '[' 0 -ge 0 ']'
+ useradd -s /sbin/login zsk1
+ echo 'user zsk1 is add success!!!'
user zsk1 is add success!!!
+ read line
+ egrep '^zsk2' /etc/passwd
+ '[' 0 -ge 0 ']'
+ useradd -s /sbin/login zsk2
+ echo 'user zsk2 is add success!!!'
user zsk2 is add success!!!
+ read line
这里告诉我们一个道理,逻辑错误是十分隐蔽的,调试手段并不会百试不爽,本例脚本不等于零也就是非零范围比大于零的范围要大,因此,实际合理的判断符号是-gt,-ne勉强符合逻辑。
合理使用函数,提高代码复用率,前面的那个pycharm的启动脚本就很简洁,里面用到了大量的自定义函数,那么,函数如何使用?其实shell脚本也是需要一个长期的实践熟练过程,如果想要快速掌握shell函数,那么,平常编写小脚本的时候就多封装几个自定义函数,多用,多练,自然成功。
shell脚本与crontab计划任务的结合:
可能很多同学都有一个误区,crontab编排任务的时候,把命令朝里面一扔了事,能不能成看天意嘛。这种思想要不得的,不管什么时候,都要时刻有一个封装思想,把每样东西做成一个模块,会让事情变得更有条理性,把要做成计划的命令封装到脚本里,脚本按照上面的那些技巧编写,脚本调试到成功运行,然后,在编排计划任务会更简单,也会少更多的麻烦(因为,计划任务里各种变量和环境变量以及某些命令执行和在shell里是不一样的,我是遇到过这样的情况。)。
好了,现在有一个新的问题,那就是比如,某个服务器的Java服务需要定时的在午夜某一个时间点重启,是不是编写一个重启服务脚本,然后计划任务里设定在指定时间执行脚本会更好呢?如果,重启服务后,由于某种原因并没有达到预期的效果,你会怎么做呢??
标签:脚本,shell,用户,crontab,echo,command,user,Linux,line From: https://blog.51cto.com/u_15966109/6171358