2.1 Shell编程基础
Shell是核心程序Kernel之外的命令解析器,是一个程序,也是一种命令语言和程序设计语言。
作为一种命令语言,它可以交互式解析用户输入的命令。
作为一种程序设计语言,它定义了各种参数,并且提供了高级语言才有的程序控制结构,虽然它不是Linux核心系统的一部分,但是它调用了Linux核心的大部分功能来执行程序建立文件,并且通过并行的方式来协调程序的运行。
比如输入ls命令后,Shell会解析ls这个字符并向内核发出请求,内核执行这个命令之后把结果告诉Shell, Shell则会把结果输出到屏幕。
Shell相当于是Windows系统下的command.com,在Windows中只有一个这样的解析器,但在Linux中有多个,如sh、bash、ksh等。
可以通过echo $SHELL查看自己运行的Shell。在Shell中还可以运行子shell,直接输入csh命令以后就可以进入csh界面了。
Linux默认的Shell是bash,下面的内容基本以此为主(另外系统环境为CentOS 6.8 x86_64)。
2.1.1 Shell脚本基本元素
Shell脚本的第一行通常为如下内容:
#! /bin/bash //第一行 # //表示单行注释
如果是多行注释应该如何操作呢?如下所示:
:<<BLOCK 中间部分为要省略的内容 BLOCK
Shell脚本的第一行均包含一个以#!为起始标志的文本行,这个特殊的起始标志表示当前文件包含一组命令,需要提交给指定的Shell解释执行。紧随#!标志的是一个路径名,指向执行当前Shell脚本文件的命令解释程序。如:
#! /bin/bash
再比如:
#! /usr/bin/ruby
如果Shell脚本中包含多个特殊的标志行,那么只有一个标志行会起作用。
2.1.2 Shell基础正则表达式
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。并规定一些特殊语法表示字符类、数量限定符和位置关系,然后用这些特殊语法和普通字符一起表示一个模式,这就是正则表达式(Regular Expression)。
给定一个正则表达式和另一个字符串,我们可以达到如下目的:
❑ 给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”);
❑ 可以通过正则表达式,从字符串中获取我们想要的特定部分。
现对基础元字符及其在正则表达式上下文中的行为进行整理,如表2-1所示。
表2-1 基础元字符及其在正则表达式上下文中的行为
① 在bash中*代表通配符,用来表示任意个字符,但在正则表达式中其含义不同,*表示0个或多个字符,请注意区分。
② 只有连字符在字符组内部并出现在两个字符之间时,才能表示字符的范围;如果出字符组的开头,则只能表示连字符本身。
注意
如无特殊说明,下面的系统环境均为CentOS 6.8 x86_64。
再例如,可以使用([0-9]{1,3}\.){3}[0-9]{1,3}来匹配IP地址。
echo "192.168.1.1" | grep -E --color "([0-9]{1,3}\.){3}[0-9]{1,3}"
2.1.3 Shell特殊字符
Shell特殊字符及其作用,如表2-2所示。
表2-2 Shell特殊字符及其作用
2.1.4 变量和运算符
变量是放置在内存中的一定的存储单元,这个存储单元里存放的是这个单元的值,这个值是可以改变的,我们称之为变量。
其中,本地变量是在用户现有的Shell生命周期的脚本中使用的,用户退出后变量就不存在了,该变量只用于该用户。
下面都是跟变量相关的命令,这里只是大致说明下,会在后面的内容中详细说明,如下所示:
变量名="变量" readonly变量名="变量" 设置该变量为只读变量,则这个变量不能被改变。 echo $变量名 set 显示本地所有的变量 unset变量名清除变量 readonly显示当前shell下有哪些只读变量
环境变量用于所有用户进程(包括子进程)。Shell中执行的用户进程均称为子进程。不像本地变量只用于现在的Shell,环境变量可用于所有子进程,包括编辑器、脚本和应用。
环境变量主目录如下:
$HOME/.bash_profile(/etc/profile)
设置环境变量,例句如下所示:
export test="123"
查看环境变量,命令如下所示:
env
或者用如下命令:
export
本地变量中包含环境变量。环境变量既可以在父进程中运行,也可以在子进程中运行。本地变量则不能运行于所有的子进程中。
变量清除命令如下:
unset变量名
再来看看位置变量,在运行某些程序时,程序中会带一系列参数,若我们要使用这些参数,就会采用位置来表示,则这些变量被称为位置变量,目前在Shell中的位置变量有10个($0~$9),超过10个用其他方式表示。其中,$0表示整个SHELL脚本。
我们举例来说明位置变量的用法。比如,有如下test.sh脚本内容:
#! /bin/bash echo "第一个参数为": $0" echo "第二个参数为": $1" echo "第三个参数为": $2" echo "第四个参数为": $3" echo "第五个参数为": $4" echo "第六个参数为": $5" echo "第七个参数为": $6"
现在给予test.sh执行权限,命令如下:
chmod +x test.sh ./test.sh 1 2 3 4 5 6
命令结果显示如下:
第一个参数为: ./test.sh 第二个参数为: 1 第三个参数为: 2 第四个参数为: 3 第五个参数为: 4 第六个参数为: 5 第七个参数为: 6
值得注意的是,从第10个位置参数开始,必须使用花括号包含起来。如:${10}。
特殊变量$*和$@表示所有的位置参数,特殊变量$#表示位置参数的总数。
另外,介绍一下工作场景中关于位置参数的常见用法,例如,脚本publishconf依次对后面的IP进行操作,如下所示:
publishconf -p 192.168.11.2192.168.11.3192.168.11.4192.168.11.5
我们的需求是依次对后面的IP进行操作,这个时候我们可以利用shift命令,此shift命令用于对参数的移动(左移),此时,原先的$4会变成$3, $3会变成$2, $2会变成$1。部分业务代码摘录如下:
if [ $# >=3 ]; then shift 1 echo "此次需要更新的机器IP为:$@" for flat in $@ do echo "此次需要更新的机器IP为:$flat" 对相关IP机器进行的操作代码 then
我们将进一步详细说明Shell的知识要点。
1. 运行Shell脚本
Shell脚本有两种运行方式,第一种方式是利用sh命令,把Shell脚本文件名作为参数。这种执行方式要求Shell脚本文件具有“可读”的访问权限,然后输入sh test.sh即可执行。
第二种执行方式是利用chmod命令设置Shell脚本文件,使Shell脚本具有“可执行”的访问权限,然后直接在命令提示符下输入Shell脚本文件名,例如. /test.sh。
2. 调试Shell脚本
使用bash -x可以调试Shell脚本,bash会先打印出每行脚本,再打印出每行脚本的执行结果,如果只想调试其中的几行脚本,可以用set -x和set +x把要调试的部分包含进来,如下:
set -x 脚本部分内容 set +x
这个时候可以直接运行脚本,不需要再执行bash -x了。这个是工作中非常有用的功能,可以帮助我们调试变量并找出bug点,希望大家掌握。
3. 退出或出口状态
一个UNIX进程或命令终止运行时,将会自动向父进程返回一个出口状态。如果进程成功执行完毕,则返回一个数值为0的出口状态。如果进程在执行过程中出现异常而未正常结束,则返回一个非零值的出错代码。
在Shell脚本中,可以利用“exit[n]”命令在终止执行shell脚本的同时,向调用脚本的父进程返回一个数值为n的Shell脚本出口状态。其中,n必须是一个位于0~255范围内的整数值。如果Shell脚本以不带参数的exit语句结束执行,则Shell脚本的出口状态就是脚本中最后执行的一条命令的出口状态。
在UNIX系统中,为了测试一个命令或Shell脚本的执行结果,$?内部变量返回之前执行的最后一条命令的出口状态,其中,0才是正确值,其他非零的值都表示是错误的。
4. Shell变量
Shell变量名可以由字母、数字和下划线等字符组成,但第一个字符必须是字母或下划线。
Shell中的所有变量都是字符串类型的,它并不区分变量的类型,如果变量中包含下划线(_)就要注意了,有些脚本的区别就很大,比如脚本中$PROJECT_svn_$DATE.tar.gz与${PROJECT}_svn_${DATE}.tar.gz,注意变量${PROJECT_svn},如果不用{}将变量全部包括的话,Shell则会理解成变量$PROJECT,后面再接着_svn。
从用途上考虑,变量可以分为内部变量、本地变量、环境变量、参数变量和用户定义的变量,以下为它们的定义:
❑ 内部变量:为了便于Shell编程而由Shell设定的变量。如错误类型的ERRNO变量。
❑ 本地变量:在代码块或函数中定义的变量,且仅在定义的范围内有效。
❑ 参数变量:调用Shell脚本或函数时传递的变量。
❑ 环境变量:为系统内核、系统命令和用户命令提供运行环境而设定的变量。
❑ 用户定义的变量:为运行用户程序或完成某种特定任务而设定的普通变量或临时变量。
5. 变量的赋值
变量的赋值可以采用赋值运算符“=”实现,其语法格式为:
variable=value
注意
赋值运算符前后不能有空格,否则会报错,习惯Python后再写Shell脚本可能经常会犯这种错误。未初始化的变量值为null,使用下列变量赋值的形式即可声明一个未初始化的变量。
如果在“variable=value”语句的赋值运算符前后有空格,则报错信息如下:
err = 72 -bash: err: command not found
笔者也经常会犯这种错误,大家不要忘了,Shell的语法其实是很严谨的。
6. 内部变量
Shell提供了丰富的内部变量,为用户的Shell编程提供支持。如下:
❑ PWD:表示当前的工作目录,其变量值等同于pwd内部命令的输出。
❑ RANDOM:每次引用这个变量时都会生成一个均匀分布的0~32767范围内的随机整数。
❑ SCONDS:脚本已经运行的时间(单位:秒)。
❑ PPID:当前进程的父进程的进程ID。
❑ $? :表示最近一次执行的命令或shell脚本的出口状态。
7. 环境变量
主要环境变量如下所示:
❑ EDITOR:用于确定命令行编辑所用的编辑程序,通常为vim。
❑ HOME:用户主目录。
❑ PATH:指定命令的检索路径。
例如,要将/usr/local/mysql/bin目录添加进系统默认的PATH变量中,应该执行以下操作:
PATH=$PATH:/usr/local/mysql/bin export PATH echo $PATH
如果想让其重启或重开一个Shell也生效,又该如何操作呢?
Linux中含有两个重要的文件: /etc/profile和$HOME/.bash_profile,每当系统登录时都要读取这两个文件,用于初始化系统所用到的变量,其中/etc/profile是超级用户使用的,$HOME/.bash_profile是每个用户自己独立的,可以通过修改该文件来设置PATH变量。
注意
这种方法也只能使当前用户生效,并非所有用户。
如果要让所有用户都能用到此PATH变量,可以用vim命令打开/etc/profile文件,在适当位置添加PATH=$PATH:/usr/local/mysql/bin,然后执行source /etc/profile使其生效。
8. 变量的引用和替换
假定variable是一个变量,在变量名字前加上“$”前缀符号即可引用变量的值,表示使用变量中存储的值来替换变量名字本身。
引用变量的两种形式:$variable与${variable}。
注意
位于双引号中的变量可以进行替换,但位于单引号中的变量不能进行替换。
9. 变量的间接引用
假定一个变量的值是另一个变量的名字,则根据第一个变量可以获取第三个变量的值。举例说明如下:
a=123 b=a eval c=\${$b} echo $b echo $c
注意
工作中不推荐使用这种用法,写出来的脚本容易产生歧义,让人混淆,而且也不方便在团队里面交流工作。
10. 变量声明与类型定义
虽然Shell并未严格区分变量的类型,但在Bash中,可以使用typeset或declare命令定义变量的类型,并可在定义时进行初始化。举例说明,比如我们可以使用declare命令预先定义一个字典,命令如下所示:
declare -A dict dict=([key1]="value1" [key2]="value2" [key3]="value3")
11. 部分常用命令介绍
这里介绍工作中我们常用的部分Shell命令,如下所示:
(1)冒号
冒号(:)与true语句不执行任何实际的处理动作,但可用于返回一个出口状态为0的测试条件。这两个语句常用于While循环结构的无限循环测试条件,我们在脚本中经常会见到这样的使用:
while :
这表示是一个无限循环的过程,所以使用的时候要特别注意,不要形成了死循环,所以一般会定义一个sleep时间,可以实现秒级别的cron任务,其语法格式为:
while : do 命令语句 sleep自己定义的秒数 done
(2)echo与print命令
print的功能与echo的功能完全一样,主要用于显示各种信息。在工作中主要用于跟awk配合,输出截取的字段详细信息,如下所示:
ps aux | grep rsync-inotify.sh | grep -v grep | awk '{print $2}'
(3)read命令
read语句的主要功能是读取标准输入的数据,然后存储到变量参数中。如果read命令后面有多个变量参数,则输入的数据会按空格分隔单词顺序依次为每个变量赋值。read在交互式脚本中相当有用,建议大家掌握。
read命令用于接收标准输入(键盘)的输入,或其他文件描述符的输入会详细介绍。得到输入后,read命令会将数据放入一个标准变量中。下面是read命令的最简单形式:
#! /bin/bash echo -n "Enter your name:" #参数-n的作用是不换行,echo默认是换行 read name #从键盘输入 echo "hello $name, welcome to my program" #显示信息 exit 0 #退出shell程序
由于read命令提供了-p参数,允许在read命令行中直接指定一个提示,因此上面的脚本可以简写成下面的脚本:
#! /bin/bash read -p "Enter your name:" name echo "hello $name, welcome to my program" exit 0
(4)set与unset命令
set命令用于修改或重新设置位置参数的值。Shell规定,用户不能直接为位置参数赋值。使用不带参数的set将输出所有内部变量。
set --用于清除所有位置参数。
(5)unset命令
该命令用于清除Shell变量,把变量的值设置为null。这个命令并不影响位置参数,比如我们先设置一个变量为a=34,然后用unset变量清除,如下所示:
unset a
(6)expr命令
expr命令是一个手工命令行计数器,用于在Linux下求表达式变量的值,一般用于整数值,也可用于字符串。其格式为:
expr Expression
上述命令表示读入Expression参数,计算它的值,然后将结果写入到标准输出。
参数应用规则如下:
❑ 用空格隔开每个项;
❑ 用/(反斜杠)放在shell特定的字符前面;
❑ 对于包含空格和其他特殊字符的字符串要用引号包含起来。
expr命令支持的整数算术运算表达式如下:
❑ exp1+exp2,计算表达式exp1和exp2的和。
❑ exp1-exp2,计算表达式exp1和exp2的差。
❑ exp1/*exp2,计算表达式exp1和exp2的乘积。
❑ exp1/exp2,计算表达式exp1和exp2的商。
❑ exp1%exp2,计算表达式exp1与exp2的余数。
expr命令还可支持字符串比较表达式,如下:
str1=str2
这里为比较字符串str1和str2是否相等,如果计算结果真,同时输出1,则返回值为0。反之计算结果为假,则输出0,返回1。
要说明的是,expr默认是不支持浮点运算的,比如我们想在expr下面输出echo“1.2*7.8”的运算结果是不可能的,那该怎么办呢?这里可以使用bc计算器,举例如下:
echo "scale=2;1.2*7.8" |bc #这里用scale来控制小数点精度,默认为1
(7)let命令
let命令取代并扩展了expr命令的整数算术运算。let命令除了支持expr支持的五种算术运算外,还支持+=、-=、*=、/=、%=。
12. 数值常数
Shell脚本按十进制解释字符串中的数字字符,除非数字前有特殊的前缀或记号,若数字前有一个0则表示一个八进制的数,0x或0X则表示一个十六进制的数。
13. 命令替换
命令替换的目的是获取命令的输出,且为变量赋值或对命令的输出做进一步的处理。命令替换实现的方法为采用$(...)形式引用命令或使用反向引号引用命令’command'。如:
today=$(date) echo $today
文件filename中包含需要删除的文件列表时,采用如下命令:
rm $(cat filename)
14. test语句
test语句与if/then和case结构的语句一起构成了Shell编程的控制转移结构。
test命令的主要功能是计算紧随其后的表达式、检查文件的属性、比较字符串或比较字符串内涵的整数值,然后以表达式的计算结果作为test命令的出口状态。如果test命令的出口状态为真,则返回0;如果为假,则返回一个非0的数值。
test命令的语法格式有:
test expression
或
[ expression ]
注意上述代码中方括号内侧的两边必须各有一个空格。
[[ expression ]]是一种比[ expression ]更通用的测试结构,也用于扩展test命令。
15. 文件测试运算符
文件测试主要指文件的状态和属性测试,其中包括文件是否存在,文件的类型、文件的访问权限以及其他属性。
下面为文件属性测试表达式:
❑ -e file,如果给定的文件存在,则条件测试的结果为真。
❑ -r file,如果给定的文件存在,且其访问权限是当前用户可读的,则条件测试的结果为真。
❑ -w file,如果给定的文件存在,且其访问权限是当前用户可写的,则条件测试的结果为真。
❑ -x file,如果给定的文件存在,且其访问权限是当前用户可执行的,则条件测试的结果为真。
❑ -s file,如果给定的文件存在,且其大小大于0,则条件测试的结果为真。
❑ -f file,如果给定的文件存在,且是一个普通文件,则条件测试的结果为真。
❑ -d file,如果给定的文件存在,且是一个目录,则条件测试的结果为真。
❑ -L file,如果给定的文件存在,且是一个符号链接文件,则条件测试的结果为真。
❑ -c file,如果给定的文件存在,且是字符特殊文件,则条件测试的结果为真。
❑ -b file,如果给定的文件存在,且是块特殊文件,则条件测试的结果为真。
❑ -p file,如果给定的文件存在,且是命名的管道文件,则条件测试的结果为真。
常见代码举例如下:
BACKDIR=/data/backup [ -d ${BACKDIR} ] || mkdir -p ${BACKDIR} [ -d ${BACKDIR}/${DATE} ] || mkdir ${BACKDIR}/${DATE} [ ! -d ${BACKDIR}/${OLDDATE} ] || rm -rf ${BACKDIR}/${OLDDATE}
下面是字符串测试运算符:
❑ -z str,如果给定的字符串长度为0,则条件的结果为真。
❑ -n str,如果给定的字符串长度大于0,则条件测试的结果为真。注意,要求字符串必须加引号。
❑ s1=s2,如果给定的字符串s1等同于字符串s2,则条件测试的结果为真。
❑ s1! =s2,如果给定的字符串s1不等同于字符串s2,则条件测试的结果为真。
❑ s1<s2,如果给定的字符串s1小于字符串s2,则条件测试的结果为真。例:if[[ "$a"<"Sb" ]]
if[[ "$a"/<"$b" ]],在单方括号的情况下,字符“<”和“>”前需加转义符号。
❑ s1>s2,若给定的字符串s1大于字符串s2,则条件测试的结果为真。
在比较字符串的test语句中,变量或字符串表达式前后一定要加双引号。
再来看看整数值测试运算符。test语句中整数值的比较会自动采用C语言中的atoi()函数把字符转换成等价的ASC整数值,所以可以使用数字字符串和整数值进行比较。整数测试表达式为:-eq(等于), -ne(不等于), -gt(大于), -lt(小于), -ge(大于等于), -le(小于等于)。
16. 逻辑运算符
Shell中的逻辑运算符如下所示:
❑(expression):用于计算括号中的组合表达式,如果整个表达式的计算结果为真,则测试结果也为真。
❑! exp:可对表达式进行逻辑非运算,即对测试结果求反。例:test ! -f file1。
❑ 符号-a或&&:表示逻辑与运算。
❑ 符号-o或||:表示逻辑或运算。
Shell脚本中的用法可参考图2-1。
图2-1 &&与 ||指令说明
17. Shell中的自定义函数
自定义语法比较简单,如下:
function函数名() { action; [return数值;] }
具体说明如下:
❑ 自定义函数时可以带function函数名()进行定义,也可以直接使用函数名()定义,不带任何参数。
❑ 参数返回时,可以加return显式返回,如果不加,将以最后一条命令运行结果作为返回值。return后跟数值,取值范围为0~255。
例如,遍历/usr/local/src目录里面包含的所有文件(包括子目录),脚本内容如下:
#! /bin/bash function traverse(){ for file in 'ls $1' do if [ -d $1"/"$file ] then traverse $1"/"$file else echo $1"/"$file fi done } traverse "/usr/local/src"
18. Shell中的字符串截取
Shell截取字符串的方法有很多,常用的有以下几种。
先来看第一种方法,从不同的方向截取。
从左向右截取最后一个string后的字符串,命令如下:
${varible##*string}
从左向右截取第一个string后的字符串,命令如下:
${varible#*string}
从右向左截取最后一个string后的字符串,命令如下:
${varible%%string*}
从右向左截取第一个string后的字符串,命令如下:
${varible%string*}
下面是第二种方法。
${变量:n1:n2}:截取变量从n1开始的n2个字符,组成一个子字符串。可以根据特定字符偏移和长度,使用另一种形式的变量扩展方式来选择特定的子字符串,例如下面的命令:
${2:0:4}
这种形式的字符串截断非常简便,只需用冒号分开指定起始字符和子字符串长度,工作中用得最多的也是这种方式。
还有第三种方法。
这里使用cut命令获取后缀名,命令如下:
ls -al | cut -d "." -f2
19. Shell中的数组
Shell支持数组,但仅支持一维数组(不支持多维数组),并且没有限定数组的大小。类似于C语言,数组元素的下标由0开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于0。
(1)定义数组
在Shell中,用括号来表示数组,数组元素用“空格”符号分割开。定义数组的一般形式为:
array_name=(value1 ... valuen)
例如:
array_name=(value0 value1 value2 value3)
或者
array_name=( value0 value1 value2 value3 )
还可以单独定义数组的各个分量:
array_name[0]=value0 array_name[1]=value1 array_name[2]=value2
也可以不使用连续的下标,而且下标的范围没有限制。
(2)读取数组
读取数组元素值的一般格式是:
${array_name[index]}
例如:
valuen=${array_name[2]}
下面用一个Shell脚本举例说明上面的用法,脚本内容如下所示:
#! /bin/bash NAME[0]="yhc" NAME[1]="cc" NAME[2]="gl" NAME[3]="wendy" echo "First Index: ${NAME[0]}" echo "Second Index: ${NAME[1]}"
运行脚本,命令如下所示:
bash ./test.sh
输出结果如下所示:
First Index: yhc Second Index: cc
使用@或*可以获取数组中的所有元素,例如:
${array_name[*]} ${array_name[@]}
我们在上面的代码末尾加上两行,如下所示:
echo "${NAME[*]}" echo "${NAME[@]}"
运行脚本,输出:
First Index: yhc Second Index: cc yhc cc gl wendy yhc cc gl wendy
(3)获取数组的长度
获取数组长度的方法与获取字符串长度的方法相同,例如:
取得数组元素的个数,命令如下所示:
length=${#array_name[@]}
取得数组单个元素的长度,命令如下所示:
length=${#array_name[*]}
20. Shell中的字典
(1)定义字典
Shell也是支持字典的,不过要先进行声明,然后再定义,语法如下所示:
#必须先声明,然后再定义,这里定义了一个名为dic的字典 declare -A dic dic=([key1]="value1" [key2]="value2" [key3]="value3")
例子如下所示:
declare -A dic dic=([no1]="yhc" [no2]="yht" [no3]="cc")
(2)打印字典
打印指定key的value,例子如下所示:
echo ${dic[no3]}
打印所有key值:
echo ${! dic[*]}
打印所有value:
echo ${dic[*]}
遍历key值:
for key in $(echo ${! dic[*]}) do echo "$key : ${dic[$key]}" done
命令输出结果如下所示:
no3 : cc no2 : yht no1 : yhc
注意
当字典比较小时,用Shell和Python差别不大。但是,当字典比较大时,Shell的效率会明显差于Python。根源在于Shell在查字典时会采取遍历的算法,而Python用的是哈希算法。
2.2 Shell中控制流结构
Shell中的控制结构也比较清晰,如下所示:
❑ if ...then... else...fi语句
❑ case语句
❑ for循环
❑ until循环
❑ while循环
❑ break控制
❑ continue控制
工作中使用最多的就是if语句、for循环、while循环,以及case选择,大家可以把这几个作为重点对象进行学习。
if语句语法如下:
if then 命令1 else 命令2 fi
if语句的进阶用法:
if条件1 then 命令1 elseif条件2 then 命令2 else 命令3 fi
举例说明if语句的用法,如下:
#! /bin/bash if [ "10" -lt "12" ] then echo "10确实比12小" else echo "10不小于12" fi
case语句语法如下:
case值in 模式1) 命令1 ;; 模式2) 命令2 ;; *) 命令3 ;; esac
case取值后面必须为单词in,每一模式必须以右括号结束,取值可以为变量或常数。匹配发现取值符号某一模式后,其间所有命令开始执行直至;;。模式匹配符*表示任意字符,?表示任意单字符,[..]表示类或范围中任意字符。
case语句适合打印成绩或用于/etc/init.d/服务类脚本,用下述脚本举例说明:
#! /bin/bash #case select echo -n "Enter a number from 1 to 3:" read ANS case $ANS in 1) echo "you select 1" ;; 2) echo "you select 2" ;; 3) echo "you select 3" ;; *) echo "'basename $0': this is not between 1 and 3" exit; ;; esac
下面是稍为复杂的实例说明,/etc/init.d/syslog脚本的部分代码如下,大家注意case语句的用法,可以以此作为参考编写自己的case脚本:
case "$1" in start) start exit 0 ;; stop) stop exit 0 ;; reload|restart|force-reload) stop start exit 0 ;; **) echo "Usage: $0 {start|stop|reload|restart|force-reload}" exit 1 ;; esac
for循环语句的语法如下所示:
for变量名in列表 do 命令 done
若变量值在列表里,for循环即执行一次所有命令,并使用变量名访问列表且取值。命令可为任何有效的shell命令和语句,变量名为任意单词。in列表可以包含替换、字符串和文件名,还可以是数值范围,例如{100..200},举例说明:
#! /bin/bash for n in {100..200} do host=192.168.1.$n ping -c2 $host &>/dev/null if [ $? = 0 ]; then echo "$host is UP" else echo "$host is DOWN" fi done
while循环的语法如下所示:
while条件 do 命令 done
在Linux中有很多逐行读取一个文件的方法,其中最常用的就是下面脚本里的方法——管道法,而且这也是效率最高、使用最多的方法。为了给大家一个直观的感受,我们将通过生成一个大文件的方式来检验各种方法的执行效率,其中笔者最喜欢的就是管道法。
在脚本里,LINE这个变量是预定义的,并不需要重新定义,$FILENAME后面接系统中实际存在的文件名。
管道方法的命令语句为:
cat $FILENAME | while read LINE
脚本举例说明如下:
#! /bin/bash cattest.txt | while read LINE do echo $LINE done }
2.3 Sed的基础用法及实用举例
Sed是Linux平台下的轻量级流编辑器,一般用于处理文本文件。sed有许多很好的特性,首先,它十分小巧;其次,它可以配合强大的Shell完成许多复杂的功能。在笔者看来,我们完全可以把sed当作一个脚本解释器,它用类似于编程的手段来完成许多事情,我们也完全可以用sed的方式来处理日常工作中的大多数文档。它跟vim最大的区别是:它不需要像vim一样打开文件,可以直接在脚本里面操作文档,所以大家能发现它在Shell脚本里的使用频率是很高的。
2.3.1 Sed的基础语法格式
Sed的格式如下所示:
sed [-nefr] [n1, n2] 动作
其中:
❑ -n:安静模式,只有经过sed处理过的行才显示出来,其他不显示。
❑ -e:表示直接在命令行模式上进行sed操作。默认选项,不用写。
❑ -f:将sed的操作写在一个文件里,使用-f filename就可以按照内容进行sed操作了。
❑ -r:表示使sed支持扩展正则表达式。
❑ -i:直接修改读取的文件内容,而不是输出到终端。
❑ n1, n2:选择要进行处理的行,如10,20表示在10~20行之间处理不一定需要。
Sed格式中的动作支持如下参数:
❑ a:表示添加,后接字符串,添加到当前行的下一行。
❑ c:表示替换,后接字符串,用它替换n1到n2之间的行。
❑ d:表示删除符合模式的行,它的语法为sed '/regexp/d', //之间是正则表达式,模式在d前面,d后面一般不接任何内容。
❑ i:表示插入,后接字符串,添加到当前行的上一行。
❑ p:表示打印,打印某个选择的数据,通常与-n(安静模式)一起使用。
❑ s:表示搜索,还可以替换,类似与vim里的搜索替换功能。例如:1,20s/old/new/g表示替换1~20行的old为new, g在这里表示处理这一行所有匹配的内容。
注意
动作最好用单引号’ ’括起来,防止因空格导致错误。
sed的基础实例如下(下面所有实例在CentOS 6.8 x86_x64下已通过,这里提前将/etc/passwd拷贝到/tmp目录下)。
1)显示passwd内容,将2~5行删除后显示,命令如下所示:
cat-n/tmp/passwd |sed '2,5d’结果显示如下:
1 root:x:0:0:root:/root:/bin/bash 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 8 halt:x:7:0:halt:/sbin:/sbin/halt 9 mail:x:8:12:mail:/var/spool/mail:/sbin/nologin 10 uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin 11 operator:x:11:0:operator:/root:/sbin/nologin 12 games:x:12:100:games:/usr/games:/sbin/nologin 13 gopher:x:13:30:gopher:/var/gopher:/sbin/nologin 14 ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin 15 nobody:x:99:99:Nobody:/:/sbin/nologin 16 vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin 17 saslauth:x:499:76:Saslauthd user:/var/empty/saslauth:/sbin/nologin 18 postfix:x:89:89::/var/spool/postfix:/sbin/nologin 19 sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin 20 vagrant:x:500:500:vagrant:/home/vagrant:/bin/bash 21 vboxadd:x:498:1::/var/run/vboxadd:/bin/false
2)在第2行的后一行加上“hello, world”字符串,命令如下所示:
cat -n /tmp/passwd |sed '2a hello, world'
显示结果如下:
1 root:x:0:0:root:/root:/bin/bash 2 bin:x:1:1:bin:/bin:/sbin/nologin hello, world 3 daemon:x:2:2:daemon:/sbin:/sbin/nologin 4 adm:x:3:4:adm:/var/adm:/sbin/nologin 5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 8 halt:x:7:0:halt:/sbin:/sbin/halt 9 mail:x:8:12:mail:/var/spool/mail:/sbin/nologin 10 uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin 11 operator:x:11:0:operator:/root:/sbin/nologin 12 games:x:12:100:games:/usr/games:/sbin/nologin 13 gopher:x:13:30:gopher:/var/gopher:/sbin/nologin 14 ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin 15 nobody:x:99:99:Nobody:/:/sbin/nologin 16 vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin 17 saslauth:x:499:76:Saslauthd user:/var/empty/saslauth:/sbin/nologin 18 postfix:x:89:89::/var/spool/postfix:/sbin/nologin 19 sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin 20 vagrant:x:500:500:vagrant:/home/vagrant:/bin/bash 21 vboxadd:x:498:1::/var/run/vboxadd:/bin/false
3)在第2行的后一行加上两行字,例如:“this is first line! ”和“this is second line! ”,命令如下所示:
cat -n /tmp/passwd |sed '2a this is first line! \ #使用续航符\后按回车输入后续行 >this is second line! '
命令显示结果如下:
1 root:x:0:0:root:/root:/bin/bash 2 bin:x:1:1:bin:/bin:/sbin/nologin this is first line! this is second line! 3 daemon:x:2:2:daemon:/sbin:/sbin/nologin 4 adm:x:3:4:adm:/var/adm:/sbin/nologin 5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 8 halt:x:7:0:halt:/sbin:/sbin/halt 9 mail:x:8:12:mail:/var/spool/mail:/sbin/nologin 10 uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin 11 operator:x:11:0:operator:/root:/sbin/nologin 12 games:x:12:100:games:/usr/games:/sbin/nologin 13 gopher:x:13:30:gopher:/var/gopher:/sbin/nologin 14 ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin 15 nobody:x:99:99:Nobody:/:/sbin/nologin 16 vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin 17 saslauth:x:499:76:Saslauthd user:/var/empty/saslauth:/sbin/nologin 18 postfix:x:89:89::/var/spool/postfix:/sbin/nologin 19 sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin 20 vagrant:x:500:500:vagrant:/home/vagrant:/bin/bash 21 vboxadd:x:498:1::/var/run/vboxadd:/bin/false
4)将2~5行的内容替换成“I am a good man! ”:
cat -n /tmp/passwd | sed '2,5c I am a good man! '
显示结果如下:
1 root:x:0:0:root:/root:/bin/bash I am a good man! 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 8 halt:x:7:0:halt:/sbin:/sbin/halt 9 mail:x:8:12:mail:/var/spool/mail:/sbin/nologin 10 uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin 11 operator:x:11:0:operator:/root:/sbin/nologin 12 games:x:12:100:games:/usr/games:/sbin/nologin 13 gopher:x:13:30:gopher:/var/gopher:/sbin/nologin 14 ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin 15 nobody:x:99:99:Nobody:/:/sbin/nologin 16 dbus:x:81:81:System message bus:/:/sbin/nologin 17 usbmuxd:x:113:113:usbmuxd user:/:/sbin/nologin 18 rtkit:x:499:499:RealtimeKit:/proc:/sbin/nologin 19 avahi-autoipd:x:170:170:Avahi IPv4LL Stack:/var/lib/avahi-autoipd:/ sbin/nologin 20 vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin 21 abrt:x:173:173::/etc/abrt:/sbin/nologin 22 haldaemon:x:68:68:HAL daemon:/:/sbin/nologin 23 ntp:x:38:38::/etc/ntp:/sbin/nologin 24 apache:x:48:48:Apache:/var/www:/sbin/nologin 25 saslauth:x:498:76:Saslauthd user:/var/empty/saslauth:/sbin/nologin 26 postfix:x:89:89::/var/spool/postfix:/sbin/nologin 27 gdm:x:42:42::/var/lib/gdm:/sbin/nologin 28 pulse:x:497:496:PulseAudio System Daemon:/var/run/pulse:/sbin/nologin 29 sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin 30 tcpdump:x:72:72::/:/sbin/nologin 31 yhc:x:500:500:yhc:/home/yhc:/bin/bash
5)只显示5~7行,注意p与-n的配合使用,命令如下:
cat -n /tmp/passwd |sed -n '5,7p'
显示结果如下:
5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
6)使用ifconfig和sed组合列出特定网卡的IP,这里我们用一台线上的阿里云ECS机器举例说明。
如果我们只想获取eth0的IP地址(即内网IP地址),可以先用ifconfig eth0查看网卡eth0的地址,如下:
ifconfig eth0
命令显示结果如下:
eth0 Link encap:Ethernet HWaddr 00:16:3E:00:42:27 inet addr:10.168.26.245 Bcast:10.168.31.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:636577 errors:0 dropped:0 overruns:0 frame:0 TX packets:644337 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:102512163 (97.7 MiB) TX bytes:43675898 (41.6 MiB)
我们可以先用grep取出有IP的那一行,然后用sed去掉(替换成空)IP前面和后面的内容,命令如下:
ifconfig eth0 | grep "inet addr" | sed 's/^.*addr://g' | sed 's/Bcast.*$//g'
命令显示结果如下:
10.168.26.245
这行组和命令的解释如下:
grep后面紧跟"inet addr"是为了单独捕获包含ipv4的那行内容;'^.*addr:’ 表示从开头到addr:的字符串,将它替换为空;'Bcast.*$’ 表示从Bcast到结尾的串,也将它替换为空,然后就只剩下IPv4地址了。
另外一种更简便的方法是使用awk编辑器,命令如下:
ifconfig eth0 | grep "inet addr:"|awk -F[:" "]+ '{print $4}'
命令显示结果如下:
10.168.26.245
awk -F[:" "]意为以冒号或空格作为分隔符,然后打印出第四列,可能有些朋友会有疑惑,为什么不直接以如下方法获取IP呢?
ifconfig eth0 | grep "inet addr:" | awk -F: '{print $2}'
大家可以看下这种方式的运行结果:
10.168.26.245 Bcast
所以还需要再进行一步操作,如下:
ifconfig eth0 | grep "inet addr:" | awk -F: '{print $2}' | awk '{print $1}'
7)在/etc/man.config中,将有man的设置取出,但不要说明内容(即在抓取特定内容的同时,去掉以#号开头的内容和空行)。命令如下:
cat /etc/man.config |grep 'MAN'|sed 's/#.*$//g'|sed '/^$/d'
显示结果如下:
MANPATH/usr/man MANPATH/usr/share/man MANPATH/usr/local/man MANPATH/usr/local/share/man MANPATH/usr/X11R6/man MANPATH_MAP /bin /usr/share/man MANPATH_MAP /sbin /usr/share/man MANPATH_MAP /usr/bin /usr/share/man MANPATH_MAP /usr/sbin /usr/share/man MANPATH_MAP /usr/local/bin /usr/local/share/man MANPATH_MAP /usr/local/sbin /usr/local/share/man MANPATH_MAP /usr/X11R6/bin /usr/X11R6/man MANPATH_MAP /usr/bin/X11 /usr/X11R6/man MANPATH_MAP /usr/bin/mh /usr/share/man MANSECT 1:1p:8:2:3:3p:4:5:6:7:9:0p:n:l:p:o:1x:2x:3x:4x:5x:6x:7x:8x
注意
#不一定出现在行首。因此,/#.*$/表示#和后面的数据(直到行尾)是一行注释,将它们替换成空。/^$/表示空行,后接d表示删除空行。
希望大家根据这个例子好好总结一下sed的经典用法,第二种方法其实是awk的方法,它也是一种优秀的编辑器,现多用于对文本字段中列的截取。
以上就是sed几种常见的语法命令,希望大家结合下面的实例,多在自己的机器上进行演示,尽快掌握其相关用法。
2.3.2 Sed的用法举例说明
本节通过举例说明工作中常用的sed用法,如下所示。
1. sed的基础用法
1)删除行首空格,命令如下所示:
sed 's/^[[:space:]]*//g' filename
2)在行后和行前添加新行(这里的pattern指输入特定正则来指定的内容,其中,&代表pattern。)。
特定行后添加新行的命令如下:
sed 's/pattern/&\n/g' filename
特定行前添加新行的命令如下:
sed 's/pattern/\n&/g' filename
3)使用变量替换(使用双引号),代码如下:
sed -e "s/$var1/$var2/g" filename
4)在第一行前插入文本,代码如下:
sed -i '1 i\插入字符串’ filename
5)在最后一行插入字符串,代码如下:
sed -i '$ a\插入字符串’ filename
6)在匹配行前插入字符串,代码如下:
sed -i '/pattern/i "插入字符串"' filename
7)在匹配行后插入字符串,代码如下:
sed -i '/pattern/a "插入字符串"' filename
8)删除文本中空行和空格组成的行及#号注释的行,代码如下:
grep -v ^# filename | sed /^[[:space:]]*$/d | sed /^$/d
9)通过如下命令将目录/home/yhc下面所有文件中的zhangsan都修改成list(注意备份原文件),代码如下:
sed -i 's/zhangsan/list/g' 'grep zhangsan -rl /modules'
2. sed结合正则表达式批量修改文件名
笔者在工作中遇到了更改文件的需求,原来某文件test.txt中的链接地址为:
http://www.5566.com/produce/2007080412/315613171.shtml http://bz.5566.com/produce/20080808/311217.shtml http://gz.5566.com/produce/20090909/311412.shtml
现要求将http://*.5566.com更改为home/html/www.5566.com,选用sed结合正则表达示解决之,如下所示:
sed -i 's/http.*\.com/home\/html\/www.5566.com/g' test.txt
如果是用纯sed命令,方法更简单,如下所示:
sed -i 's@http://[^.]*.5566.com@/home/html/www.5566.com@g' test.txt
注意
sed是完全支持正则表达式的,在正则表达式里,[^.]表示为非“. ”的所有字符,换成[^/]也可以。另外,@是sed的分割符,我们也可以用其他符号“, ”比如“/”,但是如果要用到“/”的话就得用“\/”了,所以笔者工作中常用的方法是采用@作为分隔符,大家可以根据自己的习惯来选择。
3. 在配置. conf文件时,为相邻的几行添加#号
例如,我们要将test.txt文件中的31~36行加上#号,使这部分内容暂时失效,这该如何实现呢?
在vim中,可以执行:
:31,36 s/^/#/
使用sed将更加方便,如下所示:
sed -i '31,36s/^/#/' test.txt
反之,如果要将31~36行带#号的全部删除,用sed该如何实现呢?方法如下:
sed -i ‘31,36s/^#//' test.txt
很多人习惯在这个方法后面带个g,这里的g代表全局(global)的意思。事实上,如果没有g,则表示从行的左端开始匹配,每一行第一个与之匹配的会被换掉,如果有g,则表示每一行所有与之匹配的都会被换掉。
4. 利用sed分析日志
利用sed还可以很方便地分析日志。例如,在以下的secure日志文件中使用sed抓取12:48:48至12:48:55的日志:
Apr 17 05:01:20 localhost sshd[16375]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.37.226 user=root Apr 17 05:01:22 localhost sshd[16375]: Failed password for root from 222.186.37.226 port 60700 ssh2 Apr 17 05:01:22 localhost sshd[16376]: Received disconnect from 222.186.37.226: 11: Bye Bye Apr 17 05:01:22 localhost sshd[16377]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.37.226 user=root Apr 17 05:01:24 localhost sshd[16377]: Failed password for root from 222.186.37.226 port 60933 ssh2 Apr 17 05:01:24 localhost sshd[16378]: Received disconnect from 222.186.37.226: 11: Bye Bye Apr 17 05:01:24 localhost sshd[16379]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.37.226 user=root Apr 17 05:01:26 localhost sshd[16379]: Failed password for root from 222.186.37.226 port 32944 ssh2 Apr 17 05:01:26 localhost sshd[16380]: Received disconnect from 222.186.37.226: 11: Bye Bye Apr 17 05:01:27 localhost sshd[16381]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.37.226 user=root Apr 17 05:01:29 localhost sshd[16381]: Failed password for root from 222.186.37.226 port 33174 ssh2 Apr 17 05:01:29 localhost sshd[16382]: Received disconnect from 222.186.37.226: 11: Bye Bye Apr 17 05:01:29 localhost sshd[16383]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.37.226 user=root Apr 17 05:01:31 localhost sshd[16383]: Failed password for root from 222.186.37.226 port 33474 ssh2 Apr 17 05:01:31 localhost sshd[16384]: Received disconnect from 222.186.37.226: 11: Bye Bye Apr 17 05:01:32 localhost sshd[16385]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.37.226 user=root
则可以利用sed截取日志命令,如下所示:
cat /var/log/secure | sed -n '/12:48:48/, /12:48:55/p'
脚本结果如下所示:
Apr 23 12:48:48 localhost sshd[20570]: Accepted password for root from 220.249.72.138 port 27177 ssh2 Apr 23 12:48:48 localhost sshd[20570]: pam_unix(sshd:session): session opened for user root by(uid=0) Apr 23 12:48:55 localhost sshd[20601]: Accepted password for root from 220.249.72.138 port 59754 ssh2
sed的用法还有很多,这就靠大家在工作中归纳总结了。有兴趣的朋友还可以多了解下awk的用法,我们在工作中要频繁地分析日志文件,awk+sed是比较好的选择,下面介绍awk的基本使用方法。
2.4 awk的基础用法及实用举例
下面我们首先介绍awk,然后再介绍我们在工作中常用的awk用法。
1. awk工具简介
awk是一个强大的文本分析工具,相对于grep的查找以及sed的编辑,awk在对数据进行分析并生成报告时显得尤为强大。简单来说,awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各种分析处理。awk的名称源于它的创始人Alfred Aho、Peter Weinberger和Brian Kernighan姓氏的首个字母。实际上awk也拥有自己的语言:awk程序设计语言,三位创建者已将它正式定义为“样式扫描和处理语言”。
它允许我们创建简短的程序,这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表,还有无数其他的功能。
2. 使用方法
awk的命令格式如下:
awk'pattern {action}' filename
其中,pattern就是要表示的正则表达式,它表示awk在数据中查找的内容,而action是在找到匹配内容时所执行的一系列命令。
awk语言的最基本功能是在文件或字符串中基于指定规则浏览和抽取信息,在抽取信息后才能进行其他文本操作。完整的awk脚本通常用来格式化文本文件中的信息。
通常,awk是以文件的一行为处理单位的。awk每接收文件的一行后,就会执行相应的命令来处理文本。
下面介绍一下awk程序设计模型。
awk程序由三部分组成,分别为:
❑ 初始化:处理输入前做的准备,放在BEGIN块中。
❑ 数据处理:处理输入数据。
❑ 收尾处理:处理输入完成后要进行的处理,放到END块中。
其中,在“数据处理”过程中,指令被写成一系列模式/动作过程,模式是用于测试输入行的规则,以此确定是否将应用于这些输入行。
3. awk调用方式
awk主要有三种调用方式。
(1)命令行方式
命令行的具体方式如下:
awk [-F field-separator] 'commands' filename
其中,commands是真正的awk命令,[-F域分隔符]是可选的,filename是待处理的文件。
在awk文件的各行中,由域分隔符分开的每一项称为一个域。通常,在不指名-F域分隔符的情况下,默认的域分隔符是空格。
(2)使用-f选项调用awk程序
awk允许将一段awk程序写入一个文本文件,然后在awk命令行中用-f选项调用并执行这段程序,如下:
awk -f awk-script-file filename
(3)利用命令解释器调用awk程序
其中,-f选项加载awk-script-file中的awk脚本,filename表示文件名。
利用Linux系统支持的命令解释器功能可以将一段awk程序写入文本文件,然后在它的第一行加上如下代码:
#! /bin/awk -f
4. awk详细语法
与其他Linux命令一样,awk拥有自己的语法:
awk [ -F re] [parameter...] ['prog'] [-f progfile][in_file...]
❑ -F re:允许awk更改其字段分隔符。
❑ parameter:该参数帮助为不同的变量赋值。
❑ prog:awk的程序语句段。这个语句段必须用单引号‘和’括起,以防被shell解释。
前面已经提到过这个程序语句段的标准形式,如下所示:
awk 'pattern {action}' filename
其中pattern参数可以是egrep正则表达式中的任何一个,它可以使用语法/re/再加上一些样式匹配技巧构成。与sed类似,也可以使用逗号分开两种样式以选择某个范围。
action参数总是被大括号包围,它由一系列awk语句组成,各语句之间用分号分隔。awk会解释它们,并在pattern给定的样式匹配记录上执行相关操作。
事实上,在使用该命令时可以省略pattern和action其中的一个,但不能两者同时省略,当省略pattern时没有样式匹配,表示对所有行(记录)均执行操作,省略action时执行默认的操作——在标准输出上显示。
❑ -f progfile:允许awk调用并执行progfile指定的程序文件。progfile是一个文本文件,它必须符合awk的语法。
❑ in_file:awk的输入文件,awk允许对多个输入文件进行处理。值得注意的是awk不修改输入文件。
如果未指定输入文件,awk将接受标准输入,并将结果显示在标准输出上。
5. awk脚本编写
(1)awk的内置变量
awk的内置变量主要有:
❑ FS:输入数据的字段分割符
❑ RS:输入数据的记录分隔符
❑ OFS:输出数据的字段分割符
❑ ORS:输出数据的记录分隔符;另一类是系统自动改变的,比如:NF表示当前记录的字段个数,NR表示当前记录编号等。
举个例子,可用如下命令打印passwd中的第1个和第3个字段,这里用空格隔开,如下所示:
awk -F ":"'{ print $1 "" $3 }' /tmp/passwd
(2)pattern/action模式
awk程序部分采用了pattern/action模式,即针对匹配pattern的数据,使用action逻辑进行处理。下面来看两个例子。
判断当前是不是空格,命令如下:
/^$/ {print "This is a blank line! "}
判断第5个字段是否含有“MA”,命令如下:
$5~ /MA/ {print $1 ", " $3}
(3)awk与Shell混用
因为awk可以作为一个Shell命令使用,因此awk能与Shell脚本程序很好地融合在一起,这给实现awk与Shell程序的混合编程提供了可能。实现混合编程的关键是awk与Shell脚本之间的对话,换言之,就是awk与Shell脚本之间的信息交流:awk从Shell脚本中获取所需的信息(通常是变量的值)、在awk中执行Shell命令行、Shell脚本将命令执行的结果送给awk处理以及Shell脚本读取awk的执行结果等。另外还要注意一下在Shell脚本中读取awk变量的方式,一般会通过" '$变量名’ "的方式来读取Shell程序中的变量。
这里还可以使用awk -v的方式让awk采用Shell变量,如下所示:
TIMEOUT=60
awk -v time="$TIMEOUT" 'BEGIN{print time}’的结果显示为:
60
6. awk内置变量
awk有许多内置变量用于设置环境信息,这些变量可以被改变,下面给出了工作中最常用的一些awk变量,如下所示:
❑ ARGC:命令行参数个数。
❑ ARGV:命令行参数排列。
❑ ENVIRON:支持队列中系统环境变量的使用。
❑ FILENAME:awk浏览的文件名。
❑ FNR:浏览文件的记录数。
❑ FS:设置输入域分隔符,等价于命令行-F选项。
❑ NF:浏览记录的域的个数。
❑ NR:已读的记录数。
❑ OFS:输出域分隔符。
❑ ORS:输出记录分隔符。
❑ RS:控制记录分隔符。
此外,$0变量是指整条记录,$1表示当前行的第一个域,$2表示当前行的第二个域,以此类推。
7. awk中的print和printf
awk中同时提供了print和printf两种打印输出的函数。
其中print函数的参数可以是变量、数值或字符串。字符串必须用双引号引用,参数用逗号分隔。如果没有逗号,参数就串联在一起,无法区分。这里,逗号的作用与输出文件的分隔符作用是一样的,只是后者是空格而已。
printf函数,其用法和C语言中printf基本相似,可以格式化字符串,输出复杂时,printf的结果更加人性化。
使用示例如下:
awk -F ':' '{printf("filename:%10s, linenumber:%s, columns:%s, linecontent:%s\ n", FILENAME, NR, NF, $0)}' /tmp/passwd
命令结果如下所示:
file name:/tmp/passwd, linenumber:1, columns:7, linecontent:root:x:0:0:root:/root:/ bin/bash file name:/tmp/passwd, linenumber:2, columns:7, linecontent:bin:x:1:1:bin:/bin:/sbin/ nologin file name:/tmp/passwd, linenumber:3, columns:7, linecontent:daemon:x:2:2:daemon:/ sbin:/sbin/nologin file name:/tmp/passwd, linenumber:4, columns:7, linecontent:adm:x:3:4:adm:/var/adm:/ sbin/nologin file name:/tmp/passwd, linenumber:5, columns:7, linecontent:lp:x:4:7:lp:/var/spool/ lpd:/sbin/nologin file name:/tmp/passwd, linenumber:6, columns:7, linecontent:sync:x:5:0:sync:/sbin:/ bin/sync file name:/tmp/passwd, linenumber:7, columns:7, linecontent:shutdown:x:6:0:shutdown:/ sbin:/sbin/shutdown file name:/tmp/passwd, linenumber:8, columns:7, linecontent:halt:x:7:0:halt:/sbin:/ sbin/halt file name:/tmp/passwd, linenumber:9, columns:7, linecontent:mail:x:8:12:mail:/var/ spool/mail:/sbin/nologin file name:/tmp/passwd, linenumber:10, columns:7, linecontent:uucp:x:10:14:uucp:/var/ spool/uucp:/sbin/nologin file name:/tmp/passwd, linenumber:11, columns:7, linecontent:operator:x:11:0:operat or:/root:/sbin/nologin file name:/tmp/passwd, linenumber:12, columns:7, linecontent:games:x:12:100:games:/ usr/games:/sbin/nologin file name:/tmp/passwd, linenumber:13, columns:7, linecontent:gopher:x:13:30:gopher:/ var/gopher:/sbin/nologin filename:/tmp/passwd, linenumber:14, columns:7, linecontent:ftp:x:14:50:FTP User:/ var/ftp:/sbin/nologin file name:/tmp/passwd, linenumber:15, columns:7, linecontent:nobody:x:99:99:Nobo dy:/:/sbin/nologin file name:/tmp/passwd, linenumber:16, columns:7, linecontent:vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin file name:/tmp/passwd, linenumber:17, columns:7, linecontent:saslauth:x:499:76:Saslau thd user:/var/empty/saslauth:/sbin/nologin file name:/tmp/passwd, linenumber:18, columns:7, linecontent:postfix:x:89:89::/var/ spool/postfix:/sbin/nologin file name:/tmp/passwd, linenumber:19, columns:7, linecontent:sshd:x:74:74:Privilege- separated SSH:/var/empty/sshd:/sbin/nologin file name:/tmp/passwd, linenumber:20, columns:7, linecontent:vagrant:x:500:500:vagra nt:/home/vagrant:/bin/bash file name:/tmp/passwd, linenumber:21, columns:7, linecontent:vboxadd:x:498:1::/var/ run/vboxadd:/bin/false
8. 工作示例
截取出init中PID号的示例命令如下:
ps aux | grep init | grep -v grep | awk '{print $2}'
截取网卡ethp的ipv4地址,示例命令如下:
ifconfig eth0 |grep "inet addr:" | awk -F: '{print $2}' |awk '{print $1}'
找出当前系统的自启动服务,示例命令如下:
chkconfig --list |grep 3:on | awk '{print $1}'
取出vmstart第四项的平均值,示例命令如下:
vmstat 1 4 | awk '{sum+=$4} END{print sum/4}'
以l为分隔符,汇总/yundisk/log/hadoop/下的Hadoop第九项日志并打印,示例命令如下:
cat /yundisk/log/hadoop/hadoop_clk_*.log | awk -F '|' 'BEGIN{count=0} $2>0 {count=count+$9} END {print count}'
另外,应用程序的日志跟系统日志格式不一样,取时间范围的话不建议用sed,应改用awk的方式,部分脚本内容如下所示:
#取当前时间的精准分钟和秒数,方便取时间段的日志。 timenew='LC_ALL="C" date +%d/%b/%G' timebefore='date --date='5 minutes ago' +%H:%M:%S' timebefore_awk=[$timenew:$timebefore nowtime_awk=[$timenew:$nowtime cat /data/data/nginx_access.log | awk ' $4 >= "'${timebefore_awk}'" && $4 <= "'${nowtime_awk}'" '
更多awk的内容请大家参考文档:http://blog.pengduncun.com/?p=876。
2.5 Shell基础正则表达式举例
正则表达式只是一种表示法,只要工具支持这种表示法,那么该工具就可以处理正则表达式的字符串。awk、sed和grep都支持正则表达式,也正是因为它们支持正则表达式,所以它们处理文本和字符串的功能才如此强大。下面笔者以grep来举例说明Shell正则表达式的用法。
grep工具的语法格式为:
grep -[acinvE] ’搜索内容串’ filename
其中:
❑ -a:表示以文本文件方式搜索。
❑ -c:表示计算找到符合行的次数。
❑ -i:表示忽略大小写。
❑ -n:表示顺便输出行号。
❑ -v:表示反向选择,即找到没有搜索字符串的行。
❑ -E:使grep支持扩展正则表达式,作用等同于egrep。
另外,搜索内容串也可以使用正则表达式,下面来看几个示例,这里的测试文件为鸟哥的regular_express.txt,下载地址为:http://linux.vbird.org/linux_basic/0330regularex/regular_express.txt,文件内容如下所示:
"Open Source" is a good mechanism to develop programs. apple is my favorite food. Football game is not use feet only. this dress doesn't fit me. However, this dress is about $ 3183 dollars. GNU is free air not free beer. Her hair is very beauty. I can't finish the test. Oh! The soup taste good. motorcycle is cheap than car. This window is clear. the symbol '*' is represented as start. Oh! My god! The gd software is a library for drafting programs. You are the best is mean you are the no. 1. The world <Happy> is the same with "glad". I like dog. google is the best tools for search keyword. goooooogle yes! go! go! Let's go. # I am VBird
1. 搜索有或没有the的行
搜索有the的行并输出行号,如下所示:
grep -n 'the' regular_express.txt
命令结果如下所示:
8:I can't finish the test. 12:the symbol '*' is represented as start. 15:You are the best is mean you are the no. 1. 16:The world <Happy> is the same with "glad". 18:google is the best tools for search keyword.
搜索没有the的行并输出行号,如下所示:
grep -nv 'the' regular_express.txt
命令结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 4:this dress doesn't fit me. 5:However, this dress is about $ 3183 dollars. 6:GNU is free air not free beer. 7:Her hair is very beauty. 9:Oh! The soup taste good. 10:motorcycle is cheap than car. 11:This window is clear. 13:Oh! My god! 14:The gd software is a library for drafting programs. 17:I like dog. 19:goooooogle yes! 20:go! go! Let's go. 21:# I am VBird 22:
2. 利用[]搜索集合字符
[]表示其中的某一个字符,例如[ade] 表示a或d或e,下面的命令可以选择输出包含tast、tdst或test的行数,如下所示:
grep -n 't[ade]st' regular_express.txt
命令结果如下所示:
8:I can't finish the test. 9:Oh! the soup taste good!
此外,还可以用^符号做[]内的前缀,表示除[]内字符之外的字符。比如,要搜索oo前没有g的字符串所在的行,就可以使用’[^g]oo’作为搜索字符串,如下所示:
grep -n '[^g]oo' regular_express.txt
命令结果如下所示:
2:apple is my favorite food. 3:Football game is not use feet only. 18:google is the best tools for search keyword. 19:goooooogle yes!
[] 内也可以用范围来表示,比如[a-z] 表示小写字母,[0-9] 表示0~9的数字,[A-Z]表示大写字母。[a-zA-Z0-9]表示所有数字与英文字符。当然也可以配合^来排除字符。
搜索包含数字的行,示例如下:
grep -n '[0-9]' regular_express.txt
命令结果如下所示:
5:However , this dress is about $ 3183 dollars. 15:You are the best is menu you are the no.1.
3. 行首与行尾字符^ $
符号^表示行的开头,$表示行的结尾(不是字符,是位置),那么‘^$’表示空,因为只有行首和行尾。这里的符号^与[]里面所使用的^意义不同,它表示的是符号^后面的串是在行的开头。比如搜索the在开头的行,命令如下:
grep -n '^the' regular_express.txt
命令结果如下所示:
12:the symbol '*' is represented as star.
4. 搜索以小写字母开头的行
命令如下:
grep -n '^[a-z]' regular_express.txt
命令结果如下所示:
2:apple is my favorite food. 4:this dress doesn't fit me. 10:motorcycle is cheap than car. 12:the symbol '*' is represented as star. 18:google is the best tools for search keyword. 19:goooooogle yes! 20:go! go! Let's go.
5. 搜索开头不是英文字母的行
命令如下:
grep -n '^[^a-zA-Z]' regular_express.txt
命令结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 21:#I am VBird
$表示它前面的串是在行的结尾,如’\.’表示点(.)在一行的结尾。
搜索末尾是点(.)的行,如下所示:
grep -n '\.$' regular_express.txt
点(.)是正则表达式的特殊符号,所以要用\进行转义,命令显示结果如下:
1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 4:this dress doesn't fit me. 5:However, this dress is about $ 3183 dollars. 6:GNU is free air not free beer. 7:Her hair is very beauty. 8:I can't finish the test. 9:Oh! The soup taste good. 10:motorcycle is cheap than car. 11:This window is clear. 12:the symbol '*' is represented as start. 14:The gd software is a library for drafting programs. 15:You are the best is mean you are the no. 1. 16:The world <Happy> is the same with "glad". 17:I like dog. 18:google is the best tools for search keyword. 20:go! go! Let's go.
6. 搜索空行
'^$’即为表示只有行首行尾的空行。
搜索空行的命令如下:
grep -n '^$' regular_express.txt
命令结果如下所示:
22:
7. 搜索非空行
命令如下:
grep -vn '^$' regular_express.txt
显示结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 4:this dress doesn't fit me. 5:However, this dress is about $ 3183 dollars. 6:GNU is free air not free beer. 7:Her hair is very beauty. 8:I can't finish the test. 9:Oh! The soup taste good. 10:motorcycle is cheap than car. 11:This window is clear. 12:the symbol '*' is represented as start. 13:Oh! My god! 14:The gd software is a library for drafting programs. 15:You are the best is mean you are the no. 1. 16:The world <Happy> is the same with "glad". 17:I like dog. 18:google is the best tools for search keyword. 19:goooooogle yes! 20:go! go! Let's go. 21:# I am VBird
8. 正则中的重复字符“ *”与任意一个字符点“.”
在Bash中,*代表通配符,用于表示任意个字符,但是在正则表达式中,*表示有0个或多个某个字符,请大家注意区分一下。
例如,oo*表示第一个o一定存在,第二个o可以有一个或多个,也可以没有,因此代表至少有一个o,点“. ”代表一个任意字符,必须存在。
在下面的例子中,g??d可以用’g..d’ 表示,如good、gxxd、gabd都符合g??d形式。
grep -n 'g..d' regular_express.txt
显示结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 9:Oh! The soup taste good. 16:The world <Happy> is the same with "glad".
搜索有两个o以上的字符串,命令如下:
grep -n 'ooo*' regular_express.txt
显示结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 9:Oh! the soup taste good! 18:google is the best tools for search keyword. 19:goooooogle yes!
grep -n 'ooo*' regular_express.txt表示前两个o一定存在,第三个o可以没有,也可以有多个。
搜索以g开头和结尾,中间是至少一个o的字符串,如gog、goog、gooog等。示例命令如下:
grep -n 'goo*g' regular_express.txt
显示结果如下所示:
18:google is the best tools for search keyword. 19:goooooogle yes!
搜索以g开头和结尾的字符串所在的行,示例命令如下:
grep -n 'g.*g' regular_express.txt
显示结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 14:The gd software is a library for drafting programs. 18:google is the best tools for search keyword. 19:goooooogle yes! 20:go! go! Let's go.
9. 限定连续重复字符的范围时使用{ }
符号“. ”、“*”只能限制0个或多个字符,如果要确切地限制字符的重复数量,就要用{范围} 这种方式。范围是数字,用逗号隔开,比如“2,5”表示2~5个,2表示2个,“2, ”表示2到更多个。
注意
由于{ }在SHELL中有特殊意义,因此作为正则表达式用的时候要用\转义一下。
搜索包含两个o的字符串的行,示例命令如下:
grep -n 'o\{2\}' regular_express.txt
显示结果如下所示:
1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 9:Oh! the soup taste good! 18:google is the best tools for search keyword. 19:goooooogle yes!
搜索g后面跟2~5个o,再跟一个g的字符串的行,示例命令如下:
grep -n 'go\{2,5\}g' regular_express.txt
显示结果如下:
18:google is the best tools for search keyword.
搜索包含g,且后面跟2个以上的o,再跟g的行,示例命令如下:
grep -n 'go\{2, \}g' regular_express.txt
显示结果如下:
18:google is the best tools for search keyword. 19:goooooogle yes!
10. 符号^也可以放在[]中内容的后面
[]中的符号^表示否定的意思,也可以放在[]中内容的后面。'[^a-z\.! ^ -]’ 表示没有小写字母、没有点(.)、没有感叹号(!)、没有空格、没有-的串,注意[]里面有个小空格。另外Shell里面的反向选择为[! range],而在正则表达式里则是[^range],希望大家注意区分一下。
11. 扩展正则表达式egrep
扩展正则表达式是在基础正则表达式上添加了几个特殊符号,它令某些操作更加方便。
比如,要去除空白行和开头为#的行,可以使用如下命令:
grep -v '^$' regular_express.txt | grep -v '^#'
显示结果如下:
"Open Source" is a good mechanism to develop programs. apple is my favorite food. Football game is not use feet only. this dress doesn't fit me. However, this dress is about $ 3183 dollars. GNU is free air not free beer. Her hair is very beauty. I can't finish the test. Oh! The soup taste good. motorcycle is cheap than car. This window is clear. the symbol '*' is represented as start. Oh! My god! The gd software is a library for drafting programs. You are the best is mean you are the no. 1. The world <Happy> is the same with "glad". I like dog. google is the best tools for search keyword. goooooogle yes! go! go! Let's go.
然而使用支持扩展正则表达式的egrep与扩展特殊符号|,则会方便很多。
注意
grep只支持基础表达式,而egrep支持扩展,其实egrep是grep -E的别名,因此grep -E支持扩展正则表达式。
egrep用法举例如下:
egrep -v '^$|^#' regular_express.txt
命令结果如下所示:
"Open Source" is a good mechanism to develop programs. apple is my favorite food. Football game is not use feet only. this dress doesn't fit me. However, this dress is about $ 3183 dollars. GNU is free air not free beer. Her hair is very beauty. I can't finish the test. Oh! The soup taste good. motorcycle is cheap than car. This window is clear. the symbol '*' is represented as start. Oh! My god! The gd software is a library for drafting programs. You are the best is mean you are the no. 1. The world <Happy> is the same with "glad". I like dog. google is the best tools for search keyword. goooooogle yes! go! go! Let's go.
这里的符号|表示或的关系。即满足^$ 或^# 的字符串。
熟悉掌握Shell正则表达式,可以提高我们的工作效率,特别在文本处理和日志处理的相关工作上面。
2.6 Shell开发中应该掌握的系统知识点
在笔者利用Shell进行DevOps的实际开发工作中发现,很多时候需要掌握及深入一些系统的知识点,这样才能更好地结合业务与专业知识点,结合实际情况,实现工作需求。
1. Shell多进程并发
如果逻辑控制在时间上重叠,那么它们就是并发的(concurrent),这种常见的现象称为并发(concurrency),出现在计算机系统的许多不同层面上。
使用应用级并发的应用程度称之为并发程序。现代操作系统提供了三种基本的构造并发程度的方法,如下所示:
1)进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用进程间通信(IPC)。
2)I/O多路复用。这种形式的并发,应用程序在一个进程的上下文中显示调度自己的逻辑流。逻辑流被模拟为“状态机”,数据到达文件描述符后,主程序显示地从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享一个地址空间。
3)线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。线程可以看做是进程和I/O多路复用的合体,像进程一样由内核调度,像I/O多路复用一样共享一个虚拟地址空间。
默认情况下,Shell脚本中的命令是串行执行的,必须等到前一条命令执行完后才执行接下来的命令,但是如果有一大批的命令需要执行,而且互相没有影响的情况下,那么就要使用命令的并发执行了。
正常的程序echo_hello.sh如下所示:
#! /bin/bash for ((i=0; i<5; i++)); do { sleep 3 echo "hello, world">>aa && echo "done! " } done cat aa | wc -l rm aa
我们用time命令统计此脚本的执行时间,结果如下所示:
done! done! done! done! done! 5 real 0m15.016s user 0m0.004s sys 0m0.005s
并发执行的代码如下所示:
#! /bin/bash for ((i=0; i<5; i++)); do { sleep 3 echo "hello, world">>aa && echo "done! " }& done wait cat aa | wc -l rm aa
wait命令的一个重要用途就是在Shell的并发编程中,可以在Shell脚本中启动多个后台进程(使用&),然后调用wait命令,等待所有后台进程都运行完毕后再继续向下执行。我们继续用time命令进行统计,结果如下所示:
done! done! done! done! done! 10 real 0m3.007s user 0m0.002s sys 0m0.007s
当多个进程可能会对同样的数据执行操作时,这些进程需要保证其他进程没有在操作,以免损坏数据。通常,这样的进程会使用一个“锁文件”,也就是建立一个文件来告诉别的进程自己在运行,如果检测到那个文件存在则认为有操作同样数据的进程在工作。这样操作存在一个问题,如果进程不小心意外死亡了,没有清理掉那个锁文件,那么只能由用户手动清理了。
2. Shell脚本中执行另一个Shell脚本
运行Shell脚本时,有以下两种方式可调用外部的脚本,即exec方式和source方式。
1)exec方式:使用exec调用脚本,被执行的脚本会继承当前shell的环境变量。但事实上exec产生了新的进程,它会把主Shell的进程资源占用并替换脚本内容,继承原主Shell的PID号,即原主Shell剩下的内容不会执行。
2)source方式:使用source或者“. ”调用外部脚本,不会产生新的进程,继承当前Shell环境变量,而且被调用的脚本运行结束后,它拥有的环境变量和声明变量会被当前Shell保留,类似将调用脚本的内容复制过来直接执行。执行完毕后原主Shell继续运行。
3)fork方式:直接运行脚本,会以当前shell为父进程,产生新的进程,并继承主脚本的环境变量和声明变量。执行完毕后,主脚本不会保留其环境变量和声明变量。
工作中推荐使用source方式来调用外部的Shell脚本,稳定性高,不会出一些诡异的问题和bug,影响主程序的业务逻辑(大家也可以参考下Linux系统中的Shell脚本,如/etc/init.d/network等,基本上都是采用这种处理方式)。
3. flock文件锁
Linux中的例行性工作排程Crontab会定时执行一些脚本,但脚本的执行时间往往无法控制,当脚本执行时间过长,可能会导致上一次任务的脚本还没执行完,下一次任务的脚本又开始执行的问题。这种情况下可能会出现一些并发问题,严重时会导致出现脏数据或性能瓶颈的恶性循环。
通过使用flock建立排它锁可以规避这个问题,如果一个进程对某个加以独占锁(排他锁),则其他进程无法加锁,可以选择等待超时或马上返回。脚本file_lock.sh内容如下:
#! /bin/bash echo "----------------------------------" echo "start at 'date '+%Y-%m-%d %H:%M:%S'' ..." sleep 140s echo "finished at 'date '+%Y-%m-%d %H:%M:%S'' ..."
创建定时任务:测试排它锁
#crontab -e */1 * * * * flock -xn /dev/shm/test.lock -c "sh /home/yuhongchun/file_lock.sh >> /tmptest_tmp.log"
每隔一分钟执行一次该脚本,并将输出信息写入到/tmp/test_tmp.log, flock用到的选项也简单介绍下,如下所示:
-x, --exclusive: 获得一个独占锁 -n, --nonblock: 如果没有立即获得锁,直接失败而不是等待 -c, --command: 在shell中运行一个单独的命令
查看输出日志如下:
---------------------------------- start at 2017-02-25 11:30:01 ... finished at 2017-02-25 11:32:21 ... ---------------------------------- start at 2017-02-25 11:33:01 ... finished at 2017-02-25 11:35:22 ... ---------------------------------- start at 2017-02-25 11:36:01 ... finished at 2017-02-25 11:38:21 ... ---------------------------------- start at 2017-02-25 11:39:01 ... finished at 2017-02-25 11:41:21 ... ---------------------------------- start at 2017-02-25 11:42:01 ... finished at 2017-02-25 11:44:21 ... ---------------------------------- start at 2017-02-25 11:45:01 ... finished at 2017-02-25 11:47:21 ...
大家观察下输出日志,诸如#11:34:01和11:35:01的时间点应该是要启动定时任务,但由于无法获取锁,最终以失败而退出执行,直到11:36:01才获取到锁,然后正常执行脚本。
工作中如果有类似需求,可以参考下这种Crontab写法。
4. Linux中的信号及捕获
Linux下查看支持的信号列表,命令如下所示:
kill-l
结果如下所示:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
工作中常见信号的详细说明:
1)SIGHUP:本信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一Session内的各个作业,这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端S(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
2)SIGINT:程序终止(interrupt)信号,在用户键入INTR字符(快捷键通常为Ctrl+C)时发出。
3)SIGQUIT:和SIGINT类似,但由QUIT字符(快捷键通常为Ctrl+/)控制。进程在因收到SIGQUIT退出时会产生Core文件,在这个意义上类似于一个程序错误信号。
4)SIGFPE:在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等其他所有的算术错误。
5)SIGKILL:用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。
6)SIGALRM:时钟定时信号,计算的是实际时间或时钟时间。
7)~14):略。
15)SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。Shell命令kill缺省产生这个信号。
16)~64):略。
Linux中用trap来捕获信号,trap是一个Shell内建命令,它用于在脚本中指定信号的处理方式。比如,按Ctrl+C会使脚本终止执行,实际上系统发送了SIGINT信号给脚本进程,SIGINT信号的默认处理方式就是退出程序。如果要在按Ctrl+C时不退出程序,那么就得使用trap命令指定一下SIGINT的处理方式了。
trap命令不仅仅处理Linux信号,还能对脚本退出、调试、错误、返回等情况指定处理方式,其命令格式如下所示:
trap "commands" signals
当Shell接收到signals指定的信号时,执行commands命令。
工作中举例说明,部分Shell脚本逻辑摘录如下:
# 此临时文件$tmp_file的作用是防止多个脚本同时产生逻辑错误。如果出现中止进程的情况,捕捉异常信号, 清理临时文件。另外,程序在正常退出时(包括终端正常退出)也清理此临时文件 trap "echo ’程序被中止,开始清理临时文件’; rm -rf $tmp_file; exit" 1 2 3 rm -rf $tmp_file trap "rm -rf $tmp_file" exit
5. 什么是并行(parallellism)
就当前的计算机技术而言,目前大部分语言都能够满足并发执行,但是现在的多核CPU或者多CPU下开始产生并行的概念。
总体概念:在单CPU系统中,系统调度在某一时刻只能让一个线程运行,虽然这种调试机制有多种形式(大多数是时间片轮巡为主),但无论如何,要通过不断切换需要运行的线程让其运行的方式就称为并发(concurrent)。而在多CPU系统中,这种可以同时让两个以上线程同时运行的方式叫做并行(parallel)。
并发编程:“并发”在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替执行,从宏观来看,就像是这些进程都在执行。
使用多个线程可以帮助我们在单个处理系统中实现更高的吞吐量,如果一个程序是单线程的,这个处理器在等待一个同步I/O操作完成的时候,它仍然是空闲的。在多线程系统中,当一个线程等待I/O的同时,其他的线程也可以执行。
这个有点像一个厨师在做麻辣鸡丝的时候同时做香辣土豆条,这总比先做麻辣鸡丝再做香辣土豆条效率要高,因为可以交替进行。
上面这种是在单处理器(厨师)的系统处理任务(做菜)的情况,厨师只有一个,他在一个微观的时间点上,他只能做一件事情,这种情况就是虽然是多个线程,但是都在同一个处理器上运行。
但是多线程并不能一定能提高程序的执行效率,比如,你的项目经理给你分配了10个bug让你修改,你应该会一个一个去改,大家一般不会每个bug都去改5分钟,直到改完为止,如果这样的话,上次改到什么地方都记不得了。在这种情况下并发并没有提高程序的执行效率,反而因为过多的上下文切换引入了一些额外的开销。
因此在单CPU下只能实现程序的并发,无法实现程序的并行。
现在CPU到了多核的时代,那么就出现了新的概念:并行。
并行是真正细粒度上的同时进行,即同一时间点上同时发生着多个并发。更加确切地讲就是每个CPU上运行一个程序,以达到同一时间点上每个CPU上运行一个程序。
并行和并发的区别是:
解释一:并行是指两个或者多个事件在同一时刻发生,并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。
关于并行更多的内容,可参考如下文档:http://www.xue163.com/exploit/92/928818.html。
2.7 生产环境下的Shell脚本
生产环境下的Shell作用还是挺多的,这里根据2.1节介绍的日常工作中Shell脚本的作用,将生产环境下的Shell脚本分为备份类、监控类、运维开发类和自动化运维类。前面三个从字面意义上看比较容易理解,后面的稍微解释一下,运维开发类脚本是利用Shell或Python实现一些非系统类的管理工作,比如SVN的发布程序(即预开发环境和正式开发环境的切换实现)等;而自动化运维类脚本则利用Shell来自动替我们做一些繁琐的工作,比如系统上线前的初始化或自动安装LNMP环境等。下面会按这些分类举一些具体实例便于大家理解。另外值得说明的一点是,这些实例都源自于笔者个人的线上环境;大家拿过来稍微改动一下IP或备份目录基本上就可以使用了。
另外,因为现在线上部分业务采用的是AWS EC2机器,基本上都是采用的Amazon Linux系统,所以这里先跟大家简单介绍下Amazon Linux系统。
2.7.1 Amazon Linux系统简介
Amazon Linux由Amazon Web Services(AWS)提供。它旨在为Amazon EC2上运行的应用程序提供稳定、安全和高性能的执行环境。此外,它还包括能够与AWS轻松集成的软件包,比如启动配置工具和许多常见的AWS库及工具等。AWS为运行Amazon Linux的所有实例提供持续的安全性和维护更新。
1. 启动并连接到Amazon Linux实例
启动Amazon Linux实例需要使用Amazon Linux AMI(映像)。AWS向Amazon EC2用户提供Amazon Linux AMI,无需额外费用。找到需要的AMI后,记下AMI ID。然后就可以使用AMI ID启动并连接相应的实例了。
默认情况下,Amazon Linux不支持远程根SSH。此外,密码验证已禁用,以防止强力(brute-force)密码攻击。要在Amazon Linux实例上启用SSH登录,必须在实例启动时为其提供密钥对,还必须设置用于启动实例的安全组已允许SSH访问。默认情况下,唯一可以使用SSH进行远程登录的账户是ec2-user,此账户还拥有sudo特权。如果希望启动远程根登录,请注意,其安全性不及依赖密钥对和二级用户。
有关启动和使用Amazon Linux实例的信息,请参阅启动实例。有关连接到Amazon Linux实例的更多信息,请参阅连接到Linux实例。
2. 识别Amazon Linux AMI映像
每个映像都包含唯一的/etc/image-id,用于识别AMI。此文件包含有关映像的信息。
下面是/etc/image-id文件示例,命令如下:
cat /etc/image-id
命令结果如下所示:
image_name="amzn-ami-hvm" image_version="2015.03" image_arch="x86_64" image_file="amzn-ami-hvm-2015.03.0.x86_64.ext4.gpt" image_stamp="366c-fff6" image_date="20150318153038" recipe_name="amzn ami" recipe_id="1c207c1f-6186-b5c9-4e1b-9400-c2d8-a3b2-3d11fdf8"
其中,image_name、image_version和image_arch项目来自Amazon用于构建映像的配方。image_stamp只是映像创建期间随机生成的唯一十六进制值。image_date项目的格式为YYYYMMDDhhmmss,是映像创建时的UTC时间。recipe_name和recipe_id是Amazon用于构建映像的构建配方的名称和ID,用于识别当前运行的Amazon Linux版本。当我们从yum存储库安装更新时,此文件不会更改。
Amazon Linux包含/etc/system-release文件,用于指定当前安装的版本。此文件通过yum进行更新,是system-release RPM的一部分。
下面是/etc/system-release文件示例,命令如下:
cat /etc/system-release
命令结果如下所示:
Amazon Linux AMI release 2015.03
说明
Amazon Linux系统这部分内容摘录自http://docs.aws.amazon.com/zh_cn/AWSEC2/latest/UserGuide/AmazonLinuxAMIBasics. html#IdentifyingLinuxAMI_Images。
2.7.2 生产环境下的备份类脚本
俗话说得好:“备份是救命的稻草。”,特别是重要的数据和代码,这些都是公司的重要资产,所以必须进行备份。备份能在我们执行了一些毁灭性的工作之后(比如不小心删除了数据)进行恢复。许多有实力的公司在国内的多个地方都有灾备机房,而且用的都是价格不菲的EMC高端存储。可能会有朋友想问:如果我们没有存储怎么办?这可以参考一下笔者公司的备份策略:在执行本地备份的同时,让Shell脚本自动上传数据到另一台FTP备份服务器中,这种异地备份策略成本较低,无须存储,而且安全系数高,相当于双备份,本地和异地同时出现数据损坏的几率几乎为0。
另外,还可以将需要备份的数据备份至AWS的S3分布式文件系统里面(下文将详细介绍S3的资料),此双备策略的具体步骤如下。
首先,需要做好准备工作。先安装一台CentOS 6.4 x86_64的备份服务器,并安装vsftpd服务,稍微改动一下配置后启动。另外,关于vsftpd的备份目录,可以选择RAID1或RAID5的分区进行存储。
vsftpd服务的安装如下,CentOSOS 6.8 x86_64下自带的yum极为方便。
yum -y install vsftpd service vsftpd start chkconfig vsftpd on
vsftpd的配置比较简单,详细语法略过,这里只给出配置文件,我们可以通过组合使用如下命令直接得出vsftpd.conf中有效的文件内容:
grep -v "^#" /etc/vsftpd/vsftpd.conf | grep -v '^$' local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=YES chroot_local_user=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES
chroot_local_user=YES这句话要重点强调一下。它的作用是对用户登录权限进行限制,即所有本地用户登录vsftpd服务器时只能在自己的家目录下,这是基于安全的考虑,笔者在编写脚本的过程中也考虑到了这点,如果大家要移植此脚本到自己的工作环境中,不要忘了这句语法,否则异地备份极有可能失效。
另外,我们应该在备份服务器上建立备份用户,例如svn,并为其分配密码,还需将其家目录更改为备份目录,即/data/backup/svn-bakcup,这样的话更方便备份工作,以下备份脚本依此类推。
1. 版本控制软件SVN的代码库的备份脚本
版本控制软件SVN的重要性这里就不再多言,现在很多公司基本还是利用SVN作为提交代码集中管理的工具,所以做好其备份工作的重要性就不言而喻了。这里的轮询周期为30天一次,Shell会自动删除30天前的文件。在vsftpd服务器上建立相应备份用户svn的脚本内容如下(此脚本已在CentOS 6.8 x86_64下通过):
#! /bin/sh SVNDIR=/data/svn SVNADMIN=/usr/bin/svnadmin DATE='date +%Y-%m-%d' OLDDATE='date +%Y-%m-%d -d '30 days'' BACKDIR=/data/backup/svn-backup [ -d ${BACKDIR} ] || mkdir -p ${BACKDIR} LogFile=${BACKDIR}/svnbak.log [ -f ${LogFile} ] || touch ${LogFile} mkdir ${BACKDIR}/${DATE} for PROJECT in myproject official analysis mypharma do cd $SVNDIR $SVNADMIN hotcopy $PROJECT $BACKDIR/$DATE/$PROJECT --clean-logs cd $BACKDIR/$DATE tar zcvf ${PROJECT}_svn_${DATE}.tar.gz $PROJECT> /dev/null rm -rf $PROJECT sleep 2 done HOST=192.168.2.112 FTP_USERNAME=svn FTP_PASSWORD=svn101 cd ${BACKDIR}/${DATE} ftp -i -n -v << ! open ${HOST} user ${FTP_USERNAME} ${FTP_PASSWORD} bin cd ${OLDDATE} mdelete * cd .. rmdir ${OLDDATE} mkdir ${DATE} cd ${DATE} mput * bye !
2. MySQL数据备份至S3文件系统
首先跟大家介绍下亚马逊的分布式文件系统S3, S3为开发人员提供了一个高度扩展(Scalability)、高持久性(Durability)和高可用(Availability)的分布式数据存储服务。它是一个完全针对互联网的数据存储服务,应用程序通过一个简单的Web服务接口就可以在任何时候通过互联网访问S3上的数据。当然,我们存放在S3上的数据可以进行访问控制来保障数据的安全性。这里所说的访问S3包括读、写、删除等多种操作。在脚本最后,采用aws s3命令中的cp将MySQL上传至s3://example-shar这个bucket上面(更多S3详细资料请参考官方文档http://aws.amazon.com/cn/s3/),脚本内容如下所示(此脚本已在Amazon Linux AMI x86_64下通过):
#! /bin/bash # # Filename: # backupdatabase.sh # Description: # backup cms database and remove backup data before 7 days # crontab # 55 23 * * * /bin/sh /yundisk/cms/crontab/backupdatabase.sh >> /yundisk/cms/ crontab/backupdatabase.log 2>&1 DATE='date +%Y-%m-%d' OLDDATE='date +%Y-%m-%d -d '-7 days'' #MYSQL=/usr/local/mysql/bin/mysql #MYSQLDUMP=/usr/local/mysql/bin/mysqldump #MYSQLADMIN=/usr/local/mysql/bin/mysqladmin BACKDIR=/yundisk/cms/database [ -d ${BACKDIR} ] || mkdir -p ${BACKDIR} [ -d ${BACKDIR}/${DATE} ] || mkdir ${BACKDIR}/${DATE} [ ! -d ${BACKDIR}/${OLDDATE} ] || rm -rf ${BACKDIR}/${OLDDATE} mysqldump --default-character-set=utf8 --no-autocommit --quick --hex-blob --single-transaction -uroot cms_production | gzip > ${BACKDIR}/${DATE}/cms- backup-${DATE}.sql.gz echo "Database cms_production and bbs has been backup successful" /bin/sleep 5 aws s3 cp ${BACKDIR}/${DATE}/* s3://example-share/cms/databackup/
2.7.3 生产环境下的监控类脚本
在生产环境下,服务器的稳定情况会直接影响公司的生意和信誉,可见其有多重要。所以,我们需要即时掌握服务器的状态,一般我们会在机房部署Nagios或Zabbix作为监控程序,然后用SHELL和Python等脚本语言根据业务需求开发监控插件,实时监控线上业务。
1. Nginx负载均衡服务器上监控Nginx进程的脚本
由于笔者公司电子商务业务网站前端的LoadBalance用到了Nginx+Keepalived架构,而Keepalived无法进行Nginx服务的实时切换,所以用了一个监控脚本nginx_pid.sh,每隔5秒就监控一次Nginx的运行状态(也可以由Superviored守护进程托管),如果发现问题就关闭本机的Keepalived程序,让VIP切换到Nginx负载均衡器上。在对线上环境进行操作的时候,人为重启了主Master的Nginx机器,使Nginx机器在很短的时间内就接管了VIP地址,即网站的实际内网地址(此内网地址能过防火墙映射为公网IP),进一步证实了此脚本的有效性,脚本内容如下(此脚本已在CentOS6.8x86_64下通过):
#! /bin/bash while : do nginxpid='ps -C nginx --no-header | wc -l' if [ $nginxpid -eq 0 ]; then ulimit -SHn 65535 /usr/local/nginx/sbin/nginx sleep 5 if [ $nginxpid -eq 0 ]; then /etc/init.d/keepalived stop fi fi sleep 5 done
2. 系统文件打开数监测脚本
这个脚本比较方便,可用来查看Nginx进程下的最大文件打开数,脚本代码如下(此脚本已在CentOS 6.4 | 6.8 x86_x64、Amazon Linux AMI x86_64下通过):
#! /bin/bash for pid in 'ps aux |grep nginx |grep -v grep|awk '{print $2}'' do cat /proc/${pid}/limits |grep 'Max open files' done
运行结果如下所示:
Max open files 65535 65535 files Max open files 65535 65535 files Max open files 65535 65535 files Max open files 65535 65535 files Max open files 65535 65535 files
3. 监控Python程序是否正常运行
需求比较简单,主要是监控业务进程rsync_redis.py是否正常运行,有没有发生Crash的情况。另外建议类似于rsync_redis.py的重要业务进程交由Superviored守护进程托管。脚本内容如下所示(脚本已在Amazon Linux AMI x86_64下通过):
#! /bin/bash sync_redis_status='ps aux | grep sync_redis.py | grep -v grep | wc -l ' if [ ${sync_redis_status} ! = 1 ]; then echo "Critical! sync_redis is Died" exit 2 else echo "OK! sync_redis is Alive" exit 0 fi
4. 监测机器的IP连接数
需求较为简单,先编计IP连接数,如果ip_conns值小于15000则显示为正常,界于15000至20000之间为警告,如果超过20000则报警,脚本内容如下所示(脚本已在Amazon Linux AMI x86_64下通过):
#! /bin/bash #脚本的$1和$2报警阈值可以根据业务的实际情况调整。 #$1 = 15000, $2 = 20000 ip_conns='netstat -an | grep tcp | grep EST | wc -l' messages='netstat -ant | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'|tr -s '\n' ', ' | sed -r 's/(.*), /\1\n/g' ' if [ $ip_conns -lt $1 ] then echo "$messages, OK -connect counts is $ip_conns" exit 0 fi if [ $ip_conns -gt $1-a $ip_conns -lt $2 ] then echo "$messages, Warning -connect counts is $ip_conns" exit 1 fi if [ $ip_conns -gt $2 ] then echo "$messages, Critical -connect counts is $ip_conns" exit 2 fi
5. 监测机器的CPU利用率脚本
线上的bidder业务机器,在业务繁忙的高峰期会出现CPU利用率超过99.99%(sys%+ user%)的情况,导致后面进来的流量打在机器上面却发生完全进不来的情况,但此时机器系统负载及Nginx+Lua进程都是完全正常的,均能对外提供服务。所以需要开发一个CPU利用率脚本,在超过自定义阈值时报警,方便运维人员批量添加bidder机器以应对峰值,AWS EC2实例机器是可以以小时来计费的,大家在这里也要注意系统负载和CPU利用率之间的区别。脚本内容如下所示(脚本已在Amazon Linux AMI x86_64下通过):
#! /bin/bash # ============================================================================== # CPU Utilization Statistics plugin for Nagios # # USAGE : ./check_cpu_utili.sh [-w <user, system, iowait>] [-c <user, system, iowait>] ( [ -i <intervals in second> ] [ -n <report number> ]) # # Exemple: ./check_cpu_utili.sh # ./check_cpu_utili.sh -w 70,40,30-c 90,60,40 # ./check_cpu_utili.sh -w 70,40,30-c 90,60,40-i 3-n 5 #------------------------------------------------------------------------------- # Paths to commands used in this script. These may have to be modified to match your system setup. IOSTAT="/usr/bin/iostat" # Nagios return codes STATE_OK=0 STATE_WARNING=1 STATE_CRITICAL=2 STATE_UNKNOWN=3 # Plugin parameters value if not define LIST_WARNING_THRESHOLD="70,40,30" LIST_CRITICAL_THRESHOLD="90,60,40" INTERVAL_SEC=1 NUM_REPORT=1 # Plugin variable description PROGNAME=$(basename $0) if [ ! -x $IOSTAT ]; then echo "UNKNOWN: iostat not found or is not executable by the nagios user." exit $STATE_UNKNOWN fi print_usage() { echo "" echo "$PROGNAME $RELEASE - CPU Utilization check script for Nagios" echo "" echo "Usage: check_cpu_utili.sh -w -c (-i -n)" echo "" echo " -w Warning threshold in % for warn_user, warn_system, warn_iowait CPU (default : 70,40,30)" echo " Exit with WARNING status if cpu exceeds warn_n" echo " -c Critical threshold in % for crit_user, crit_system, crit_iowait CPU (default : 90,60,40)" echo " Exit with CRITICAL status if cpu exceeds crit_n" echo " -i Interval in seconds for iostat (default : 1)" echo " -n Number report for iostat (default : 3)" echo " -h Show this page" echo "" echo "Usage: $PROGNAME" echo "Usage: $PROGNAME --help" echo "" exit 0 } print_help() { print_usage echo "" echo "This plugin will check cpu utilization (user, system, CPU_Iowait in %)" echo "" exit 0 } # Parse parameters while [ $# -gt 0 ]; do case "$1" in -h | --help) print_help exit $STATE_OK ;; -v | --version) print_release exit $STATE_OK ;; -w | --warning) shift LIST_WARNING_THRESHOLD=$1 ;; -c | --critical) shift LIST_CRITICAL_THRESHOLD=$1 ;; -i | --interval) shift INTERVAL_SEC=$1 ;; -n | --number) shift NUM_REPORT=$1 ;; *) echo "Unknown argument: $1" print_usage exit $STATE_UNKNOWN ;; esac shift done # List to Table for warning threshold (compatibility with TAB_WARNING_THRESHOLD=('echo $LIST_WARNING_THRESHOLD | sed 's/, / /g'') if [ "${#TAB_WARNING_THRESHOLD[@]}" -ne "3" ]; then echo "ERROR : Bad count parameter in Warning Threshold" exit $STATE_WARNING else USER_WARNING_THRESHOLD='echo ${TAB_WARNING_THRESHOLD[0]}' SYSTEM_WARNING_THRESHOLD='echo ${TAB_WARNING_THRESHOLD[1]}' IOWAIT_WARNING_THRESHOLD='echo ${TAB_WARNING_THRESHOLD[2]}' fi # List to Table for critical threshold TAB_CRITICAL_THRESHOLD=('echo $LIST_CRITICAL_THRESHOLD | sed 's/, / /g'') if [ "${#TAB_CRITICAL_THRESHOLD[@]}" -ne "3" ]; then echo "ERROR : Bad count parameter in CRITICAL Threshold" exit $STATE_WARNING else USER_CRITICAL_THRESHOLD='echo ${TAB_CRITICAL_THRESHOLD[0]}' SYSTEM_CRITICAL_THRESHOLD='echo ${TAB_CRITICAL_THRESHOLD[1]}' IOWAIT_CRITICAL_THRESHOLD='echo ${TAB_CRITICAL_THRESHOLD[2]}' fi if [ ${TAB_WARNING_THRESHOLD[0]} -ge ${TAB_CRITICAL_THRESHOLD[0]} -o ${TAB_ WARNING_THRESHOLD[1]} -ge ${TAB_CRITICAL_THRESHOLD[1]} -o ${TAB_WARNING_ THRESHOLD[2]} -ge ${TAB_CRITICAL_THRESHOLD[2]} ]; then echo "ERROR : Critical CPU Threshold lower as Warning CPU Threshold " exit $STATE_WARNING fi CPU_REPORT='iostat -c $INTERVAL_SEC $NUM_REPORT | sed -e 's/, /./g' | tr -s ' ' '; ' | sed '/^$/d' | tail -1' CPU_REPORT_SECTIONS='echo ${CPU_REPORT} | grep '; ' -o | wc -l' CPU_USER='echo $CPU_REPORT | cut -d "; " -f 2' CPU_SYSTEM='echo $CPU_REPORT | cut -d "; " -f 4' CPU_IOWAIT='echo $CPU_REPORT | cut -d "; " -f 5' CPU_STEAL='echo $CPU_REPORT | cut -d "; " -f 6' CPU_IDLE='echo $CPU_REPORT | cut -d "; " -f 7' NAGI OS_STATUS="user=${CPU_USER}%, system=${CPU_SYSTEM}%, iowait=${CPU_ IOWAIT}%, idle=${CPU_IDLE}%" NAGI OS_DATA="CpuUser=${CPU_USER}; ${TAB_WARNING_THRESHOLD[0]}; ${TAB_CRITICAL_ THRESHOLD[0]};0" CPU_USER_MAJOR='echo $CPU_USER| cut -d "." -f 1' CPU_SYSTEM_MAJOR='echo $CPU_SYSTEM | cut -d "." -f 1' CPU_IOWAIT_MAJOR='echo $CPU_IOWAIT | cut -d "." -f 1' CPU_IDLE_MAJOR='echo $CPU_IDLE | cut -d "." -f 1' # Return if [ ${CPU_USER_MAJOR} -ge $USER_CRITICAL_THRESHOLD ]; then echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_CRITICAL elif [ ${CPU_SYSTEM_MAJOR} -ge $SYSTEM_CRITICAL_THRESHOLD ]; then echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_CRITICAL elif [ ${CPU_IOWAIT_MAJOR} -ge $IOWAIT_CRITICAL_THRESHOLD ]; then echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_CRITICAL elif [ ${CPU_USER_MAJOR} -ge $USER_WARNING_THRESHOLD ] && [ ${CPU_USER_MAJOR} -lt $USER_CRITICAL_THRESHOLD ]; then echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_WARNING elif [ ${CPU_SYSTEM_MAJOR} -ge $SYSTEM_WARNING_THRESHOLD ] && [ ${CPU_SYSTEM_ MAJOR} -lt $SYSTEM_CRITICAL_THRESHOLD ]; then echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_WARNING elif [ ${CPU_IOWAIT_MAJOR} -ge $IOWAIT_WARNING_THRESHOLD ] && [ ${CPU_IOWAIT_ MAJOR} -lt $IOWAIT_CRITICAL_THRESHOLD ]; then echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_WARNING else echo "CPU STATISTICS OK:${NAGIOS_STATUS} | CPU_USER=${CPU_USER}%;70;90;0;100" exit $STATE_OK fi
此脚本参考了Nagios的官方文档https://exchange.nagios.org/并进行了代码精简和移植,原代码是运行在ksh下面的,这里将其移植到了bash下面,ksh下定义数组的方式跟bash下还是有所区别的。另外有一点也值得大家注意,Shell本身不支持浮点运算,但可以通过awk的方式处理。
2.7.4 生产环境下的运维开发类脚本
Shell在DevOps(运维开发)工作中的比重其实不低,我们很多时候可以利用其写出对实际工作中有意义和帮助的脚本,这里举例说明。
1. 系统初始化脚本
此脚本用于新装Linux的相关配置工作,比如禁掉iptable、SElinux和ipv6,优化系统内核,停掉一些没必要启动的系统服务等。我们将此脚本用于公司内部的开发机器的批量部署。事实上,复杂的系统初始化initial脚本由于涉及多条产品线和多个业务平台,远比这里列出的开发环境下的初始化脚本复杂得多,而且代码量极大,基本上都是4000~5000行左右的Shell脚本,各功能模块以多函数的形式进行封装。下面只涉及了一些基础部分,希望大家注意。脚本代码如下所示(此脚本已在CentOS 6.8 x86_x64下已通过):
#! /bin/bash #添加epel外部yum扩展源 cd /usr/local/src wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm rpm -ivh epel-release-6-8.noarch.rpm #安装gcc基础库文件以及sysstat工具 yum -y install gcc gcc-c++ vim-enhanced unzip unrar sysstat #配置ntpdate自动对时 yum -y install ntp echo "01 01 * * * /usr/sbin/ntpdate ntp.api.bz >> /dev/null 2>&1" >> /etc/crontab ntpdate ntp.api.bz service crond restart #配置文件的ulimit值 ulimit -SHn 65535 echo "ulimit -SHn 65535" >> /etc/rc.local cat>> /etc/security/limits.conf << EOF * soft nofile 65535 * hard nofile 65535 EOF #基础系统内核优化 cat>> /etc/sysctl.conf << EOF fs.file-max=419430 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_syn_retries = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 1 net.ipv4.tcp_keepalive_time = 1200 net.ipv4.ip_local_port_range = 102465535 net.ipv4.tcp_max_syn_backlog = 16384 net.ipv4.tcp_max_tw_buckets = 36000 net.ipv4.route.gc_timeout = 100 net.ipv4.tcp_syn_retries = 1 net.ipv4.tcp_synack_retries = 1 net.core.somaxconn = 16384 net.core.netdev_max_backlog = 16384 net.ipv4.tcp_max_orphans = 16384 EOF /sbin/sysctl -p #禁用control-alt-delete组合键以防止误操作 sed -i 's@ca::ctrlaltdel:/sbin/shutdown -t3-r now@#ca::ctrlaltdel:/sbin/shutdown -t3-r now@' /etc/inittab #关闭SElinux sed -i 's@SELINUX=enforcing@SELINUX=disabled@' /etc/selinux/config #关闭iptables service iptables stop chkconfig iptables off #ssh服务配置优化,请至少保持机器中至少有一个具有sudo权限的用户,下面的配置会禁止root远程登录 sed -i 's@#PermitRootLogin yes@PermitRootLogin no@' /etc/ssh/sshd_config #禁止 root远程登录 sed -i 's@#PermitEmptyPasswords no@PermitEmptyPasswords no@' /etc/ssh/sshd_config #禁止空密码登录 sed -i 's@#UseDNS yes@UseDNS no@' /etc/ssh/sshd_config /etc/ssh/sshd_config service sshd restart #禁用ipv6地址 echo "alias net-pf-10 off" >> /etc/modprobe.d/dist.conf echo "alias ipv6 off" >> /etc/modprobe.d/dist.conf chkconfig ip6tables off #vim基础语法优化 echo "syntax on" >> /root/.vimrc echo "set nohlsearch" >> /root/.vimrc #精简开机自启动服务,安装最小化服务的机器初始可以只保留crond, network, rsyslog, sshd这四个服务。 for i in 'chkconfig --list|grep 3:on|awk '{print $1}''; do chkconfig --level 3 $i off; done for CURSRV in crond rsyslog sshd network; do chkconfig --level 3 $CURSRV on; done #重启服务器 reboot
2. 控制Shell多进程数量的脚本
下面的run.py是使用Python写的爬虫程序,经测试机器上面运行8个性能最好的时候,既能充分发挥机器性能,又不会导致机器响应速度过慢。有时为了避免并发进程数过多,导致机器卡死,需要限制并发的数量。下面的脚本可以实现这个需求,其代码如下所示:
#! /bin/bash #每5分钟运行一次脚本 CE_HOME='/data/ContentEngine' LOG_PATH='/data/logs' # 控制爬虫数量为8 MAX_SPIDER_COUNT=8 # current count of spider count='ps -ef | grep -v grep | grep run.py | wc -l' # 下面的逻辑是控制run.py进程数量始终为8,充分挖掘机器的性能,并且为了防止形成死循环,这里没有用 while语句。 try_time=0 cd $CE_HOME while [ $count -lt $MAX_SPIDER_COUNT -a $try_time -lt $MAX_SPIDER_COUNT ]; do let try_time+=1 python run.py >> ${LOG_PATH}/spider.log 2>&1 & count='ps -ef | grep -v grep | grep run.py | wc -l' done
3. 调用Ansible来分发多条线路的配置
这里的publishconf.sh文件为总控制逻辑文件,会调用Ansible进行电信、联通线路的配置下发工作,由于牵涉的业务较多,这里只摘录部分内容。另外,这里生成的hosts文件也是通过程序调用公司的CMDB资产管理系统的接口来生成另外hosts文件格式,内容如下所示:
[yd] 1.1.1.1 2.2.2.2 [wt] 3.3.3.3 4.4.4.4 [dx] 5.5.5.5 6.6.6.6
publishconf.sh部分内容如下所示:
# 如果hosts文件不存在,就调用touch命令建立;另外,这里要增加一个逻辑判断,即如果已经有人在发布 平台了,第二个运维人员发布的时候,一定要强制退出。 if [ ! -f "$hosts" ] then touch "$hosts" else echo "此平台已经有运维小伙伴在发布,请耐心等待!" exit fi #如果出现中止进程的情况,捕捉异常信号,清理临时文件。 trap "echo ’程序被中止,开始清理临时文件’; rm -rf $hosts; exit" 1 2 3 #进入public_conf目录,通过git pull获取gitlab上最新的相关文件配置 cd /data/conf /public_conf/ git pull origin master:master #配置文件也是通过内部的gitlab管理,这里没简化操作,例如git pull origin master或git pull的 时候,是防止此时可能会存在着多分支的情况导致运行报错 if [ $? == 0 ]; then echo "当前配置文件为最新版本,可以发布!" else echo "当前配置文件不是最新的,请检查后再发布" exit fi #此为发布单平台多IP的逻辑,$#判断参数个数,这里的逻辑判断为参数大于或等于3时就是单平台多IP发布。 if [ $# >=3 ]; then shift 1 #这里通过shift命令往左偏移一个位置参数,从而获取全部的IP。 echo "此次需要更新的机器IP为:$@" for flat in $@ do echo "此次需要更新的机器IP为:$flat" platform='awk '/\[/{a=$0}/'"$flat"'/{print a}' $hosts | head -n1' # 通过这段awk命令组和来获取当前的机器ip属于哪条线路,比如是移动或者网通或者电信,后续有相应 的措施。 if [[ $platform =~ "yd" ]]; then /usr/local/bin/ansible -i $hosts $flat -m shell -a "/home/fastcache_conf/ publish_fastcache.sh ${public_conf}_yd" elif [[ $platform =~ "wt" ]]; then /usr/local/bin/ansible -i $hosts $flat -m shell -a "/home/fastcache_conf/ publish_fastcache.sh ${public_conf}_wt" else /usr/local/bin/ansible -i $hosts $flat -m shell -a "/home/fastcache_conf/ publish_fastcache.sh ${public_conf}_dx" fi done fi #程序正常运行后,也要清理此临时文件,方便下次任务发布。 rm -rf $hosts trap "rm -rf $hosts" exit
2.8 小结
本章向大家详细说明了Shell的基础语法和系统相关知识点,以及sed和awk在日常工作中的使用案例,并用Shell命令grep结合正则表达式说明了Shell正则表达式的基础用法。在后面的实例中,又根据备份类、监控类、运维开发类向大家演示了在生产环境下我们经常用到的Shell脚本。希望大家可以结合本文提到的系统相关知识点,深入地了解和掌握Shell脚本的用法,这样我们的系统运维工作和DevOps工作会更加得心应手。