前言
一、木石前盟
众所周知,我们大中华上下5千年历史,其中当属“四大名著”最让人津津乐道。《红楼梦》一书更为四大名著之首,被评为中国古典章回小说的巅峰之作,是中国最受重视的一部文学作品。此书由清代作家曹雪芹所作,故事主线为贾宝玉、林黛玉及薛宝钗三人的爱情与婚姻悲剧,以及贾府等四大家族的没落。
故事开头即提到木石前盟一事。说到林黛玉的前身是绛珠仙草,贾宝玉的前身是神瑛侍者。《红楼梦》第一回“甄士隐梦幻识通灵 贾雨村风尘怀闺秀 ”中有这样一段描写:
西方灵河岸上三生石畔,有绛珠草一株,时有赤瑕宫神瑛侍者,日以甘露灌溉,这绛珠草始得久延岁月。后来既受天地精华,复得雨露滋养,遂得脱却草胎木质,得换人形,仅修成个女体,终日游于离恨天外。饥则食蜜青果为膳,渴则饮灌愁海水为汤。只因尚未酬报灌溉之德,故其五内便郁结着一段缠绵不尽之意。恰这神瑛侍者凡心偶炽,乘此昌明太平朝世,意欲下凡造历幻缘,已在警幻仙子案前挂了号。警幻亦曾问及,灌溉之情未偿,趁此倒可了结的。那绛珠仙子道:“他是甘露之惠,我并无此水可还。他既下世为人,我也去下世为人,但把我一生所有的眼泪还他,也偿还得过他了。”
而今天我们要讲的可不是贾宝玉和林黛玉之间的情愫姻缘,我们今天的主角是awk,一种优良的文本处理工具,而且它还有另外一个名字:gawk。
GNU awk:
文本处理三工具:grep, sed, awk
grep, egrep, fgrep:文本过滤工具;pattern
sed: 行编辑器
模式空间、保持空间
awk(实际上是编程语言的解释器,也可以说是一个编程语言):报告生成器,格式化文本输出;AWK: Aho, Weinberger, Kernighan --> New AWK, NAWK GNU awk(awk 是gawk的链接文件), gawk (也是一次读取一行文本,跟sed一样)
gawk - pattern scanning and processing language (模式扫描及实现处理语言)
基本用法:gawk [options] 'program' FILE …
program: PATTERN{ACTION STATEMENTS(语句)} PATTERN:模式有多种,有地址定界作用,不同于sed或grep里面的概念; ACTION STATEMENTS:动作语句,可有多个语句,但语句之间用分号分隔; 可认为是脚本里的调用命令,这不过用的是awk内建命令;如: print, printf (主要功能:实现文本格式化输出)
选项:
-F:指明输入时用到的字段分隔符; -v var=value: 自定义变量
1、print
print item1, item2, ... 默认输出分隔符是空白字符;
要点:
(1) 逗号分隔符;
(2) 输出的各item可以是字符串,也可以是数值;当前记录的字段、变量($1,$2,..)或awk的表达式
;
(3) 如省略item,相当于print $0($0代表一整行内容,默认显示打印整行内容);
]# tail -5 /etc/fstab |awk '{print "hello:$1"}' 放在引号里变量不会做替换; 显示内容: hello:$1 hello:$1 hello:$1 hello:$1 hello:$1 ------------------------------------------------------------------------------- ]# tail -5 /etc/fstab |awk '{print "hello:"$1}' 显示内容: hello:# hello:UUID=7db66111-7836-4dd8-8b10-d26b45faebb9 hello:UUID=f004f1af-6d47-4b6b-94d8-95fb2d58ae42 hello:UUID=c396e749-0a90-4b07-92ad-18627d4f8f4e hello:UUID=f4999e79-e3f1-45ed-bcb3-90493dadc48f 注意:在awk中,变量做替换是不用引号的; -------------------------------------------------------------------------------- ]# tail -5 /etc/fstab |awk '{print " "}' 显示空白字符,如果每一行都有空白,那就每一行都显示空白;
2、变量
FS:input field seperator(输入时字段的分隔符),默认为空白字符;
OFS:output field seperator(输出时字段的分隔符),默认为空白字符;
RS:input record seperator,输入时的换行符;一行为一条record;默认为行分隔符;
ORS:output record seperator,输出时的换行符;一行为一条record;默认为行分隔符;2.1 内建变量
例如:
]# awk '{print $1}' /etc/passwd 默认以空白符为分隔符,显示第1字段; 显示部分内容: aaa6:x:1017:1017::/home/aaa6:/bin/bash aaa7:x:1018:1018::/home/aaa7:/bin/bash aaa8:x:1019:1019::/home/aaa8:/bin/bash aaa9:x:1020:1020::/home/aaa9:/bin/bash aaa10:x:1021:1021::/home/aaa10:/bin/bash-------------------------------------------------------------------------------]# awk -v FS=':' '{print $1}' /etc/passwd 以冒号为分隔符,显示第1字段;以默认空格为分隔符输出;显示部分内容: aaa6 aaa7 aaa8 aaa9 aaa10-------------------------------------------------------------------------------]# awk -F:'{print $1}' /etc/passwd 以冒号为分隔符,显示第1字段;以默认空格为分隔符输出;显示部分内容: aaa6 aaa7 aaa8 aaa9 aaa10
]# awk -v RS=' ' '{print $0}' /etc/passwd 以空白字符为输入换行符;$0为显示整行内容,为默认显示可省略;输出时,把已有输出换行符,在加上定义的空白字符为输入换行符都当做了输出换行符;
]# awk -v RS=' ' -v ORS='#' '{print}' /etc/passwd 以空格为输入换行符,以#为输出换行符;则输出时把每行空格替换成了#,比较诡异;
数值统计的内建变量:在awk中引用变量不加符号;符号;1,$2,..是字段的固定使用的变量除外;
NF:number of field,字段数量,即每行的字段数量; 注意:{print NF}, {print $NF} (加$ 即显示最后的字段)
例如:
]# awk '{print NF}' /etc/fstab 显示fstab文件中每一行有多少字段;]# awk '{print $NF}' /etc/fstab 表示为显示每行最后一个字段;]# awk '{print NR}' /etc/fstab 统计行数,显示行号;
NR:number of record, 行数;即对文件中的行进行编号;
FNR:各文件分别计数;行数;
例如:
]# awk '{print NR}' /etc/fstab /etc/issue 显示内容:两文件的总行数;]# awk '{print FNR}' /etc/fstab /etc/issue 显示内容:两文件分别显示行数;
FILENAME:当前文件名;处理一行显示一次;
例如:
]# awk '{print FILENAME}' /etc/fstab 显示内容:每行的文件名;
ARGC:命令行参数的个数;
ARGV:数组,保存的是命令行所给定的各参数;
例如:
]# awk '{print ARGC}' /etc/fstab:显示文件的每一行在命令行中参数的个数; 显示内容:即每一行执行命令时,都有2个参数,即awk和/etc/fstab;---------------------------------------------------------------------------------------------]# awk 'BEGIN{print ARGC}' /etc/fstab:仅显示一次在命令行中参数的个数; 显示内容:2;---------------------------------------------------------------------------------------------]# awk 'BEGIN{print ARGV}' /etc/fstab:显示数组,命令行中所给定的各参数,即显示数组中各元素; 显示内容:awk: cmd. line:1: fatal: attempt to use array `ARGV' in a scalar context--------------------------------------------------------------------------------------------]# awk 'BEGIN{print ARGV[0]}' /etc/fstab:显示命令行中所给定的各参数的数组中第1个元素; 显示内容:awk;--------------------------------------------------------------------------------------------]# awk 'BEGIN{print ARGV[1]}' /etc/fstab:显示命令行中所给定的各参数的数组中第2个元素;
2.2 自定义变量
(1) -v var=value
变量名区分字符大小写;(2) 在program中直接定义
注意:定义变量,同bash中一样,用时定义即可;
例如:
]# awk -v test='hello gawk' '{print test}' /etc/fstab:文件此处没什么用,只有一个显示行数作用;即文件有12行 显示了每行显示一次,显示了12行;-------------------------------------------------------------------------------------------]# awk -v test='hello gawk' 'BEGIN{print test}':定义test变量并赋值为hello gawk,且显示一次test变量的内容;------------------------------------------------------------------------------------------]# awk 'BEGIN{test="hello gawk";print test}':直接在程序中定义变量(赋值语句),多个语句之间使用分号分隔,表示意义同上;
3、printf命令
格式化输出:printf FORMAT, item1, item2, …
FORMAT是格式符,为每个item按位占一个位留一个特殊符号,所以item最终会显示在format指定格式符号的位置上;
要点:
(1) FORMAT必须给出;
(2) 不会自动换行,需要显示给出换行控制符,\n;
(3) FORMAT中需要分别为后面的每个item指定一个格式化符号;
格式符:
例如:
%c: 显示字符的ASCII码;
%d, %i: 显示十进制整数;
%e, %E: 科学计数法数值显示;
%f:显示为浮点数;
%g, %G:以科学计数法或浮点形式显示数值;
%s:显示字符串;
%u:无符号整数;
%%: 显示%自身;
]# awk -F: '{printf "%s",$1}' /etc/passwd:以字符串显示每行的第1字段;显示内容:rootbindaemonadmlpsyncshutdownhaltmailoperatorgamesftpnobodyavahiautoipddbuspolkitdabrtcolordlibstoragemgmtrpcrtkitusbmuxdchroymysqltssntppulsegdmrpcusernfsnobodypostfixsshdtcpdumplsjtestuser1centosubuntuubuntomagedusparkmageeduaaabbbcccapacheaaa1aaa2aaa3aaa4aaa5aaa6aaa7aaa8aaa9aaa10--------------------------------------------------------------------------------------------]# awk -F: '{printf "%s\n",$1}' /etc/passwd::以字符串显示每行的第1字段;每字段一行; 显示部分内容:aaa6aaa7aaa8aaa9aaa10--------------------------------------------------------------------------------------------]# awk -F: '{printf "username:%s\n",$1}' /etc/passwd:显示指定字符串即username:,同时以字符串显示每行的第1字段;每字段一行;--------------------------------------------------------------------------------------------]# awk -F: '{printf "username:%s, UID:%d\n",$1,$3}' /etc/passwd 显示部分内容:username:aaa6, UID:1017username:aaa7, UID:1018username:aaa8, UID:1019username:aaa9, UID:1020username:aaa10, UID:1021
修饰符:
例如:
#[.#]:第一个数字控制显示的宽度;第二个#表示小数点后的精度;默认右对齐;
%3.1f:3表示显示3个字符的宽度;
-: 左对齐
+:显示数值的符号 (bash也支持printf语句)
]# awk -F: '{printf "username:%15s,UID:%d\n",$1,$3}' /etc/passwd:设定username:为15个字符宽度,默认右对齐; 显示部分内容:username: aaa6,UID:1017username: aaa7,UID:1018username: aaa8,UID:1019username: aaa9,UID:1020username: aaa10,UID:1021---------------------------------------------------------------------------------------------]# awk -F: '{printf "username:%-15s,UID:%d\n",$1,$3}' /etc/passwd:同上但是为左对齐;--------------------------------------------------------------------------------------------]# awk -F: '{printf "username:%-15s,UID:%+d\n",$1,$3}' /etc/passwd:显示第2字段正负号; 显示部分内容:username:aaa6 ,UID:+1017username:aaa7 ,UID:+1018username:aaa8 ,UID:+1019username:aaa9 ,UID:+1020username:aaa10 ,UID:+1021
4、操作符
算术操作符:
字符串操作符:没有符号的操作符,字符串连接(一般使用内建函数进行字符串切片)
赋值操作符:
比较操作符:
模式匹配符:
逻辑操作符:
函数调用:
条件表达式:
selector?if-true-expression:if-false-expression (正确执行第一个expr,错误执行第二个expr) //:selector(条件表达式)
function_name(argu1, argu2, ...) (规范的调用使用方式)
&&
||
!
~:是否匹配
!~:是否不匹配
>, >=, <, <=, !=, ==(等值比较)
=, +=, -=, *=, /=, %=, ^=(增强相赋值)
++, --(自增、自减运算)
x+y, x-y, x*y, x/y, x^y, x%y
-x (把正数转换为负数)
+x: 转换为数值;
例子:判断一个用户类型,id大于1000则是普通用户(Common User),小于则是系统用户(Sysadmin or SysUser)。
# awk -F: '{$3>=1000?usertype="Common User":usertype="Sysadmin or SysUser";printf "%15s:%-s\n",$1,usertype}' /etc/passwd【判断用户id是否>=1000,是在定义变量usertype为Common User,否则定义变量usertype为Sysadmin or SysUser,显示第1字段为15个字符宽度,usertype为左对齐;】
5、PATTERN (能实现地址定界功能)
(1) empty:空模式,匹配每一行;**
(2) /regular expression/:仅处理能够被此处的(正则表达式)模式匹配到的行;前面加!表示对模式过滤的条件取反;**
~]# awk '/^UUID/{print $1}' /etc/fstab:显示UUID开头的行的第1字段;
(3) relational expression: 关系(比较)表达式;结果有“真”有“假”;结果为“真”才会被处理;
真(两种):1.结果为非0值,2.非空字符串;
假:0,空字符串;
]# awk '!/^UUID/{print $1}' /etc/fstab:表示取反,显示UUID开头的以外的行的第1字段;-------------------------------------------------------------------------------------------]# awk -F: '$3>=1000{print $1,$3}' /etc/passwd:以冒号为分隔符,id号大于1000的用户,显示第1,3字段;-------------------------------------------------------------------------------------------]# awk -F: '$NF=="/bin/bash"{print $1,$NF}' /etc/passwd:显示默认为shell类型为bash的用户;-------------------------------------------------------------------------------------------]# awk -F: '$NF~/bash$/{print $1,$NF}' /etc/passwd:功能同上;显示以bash结尾的用户,第1和最后一个字段;-------------------------------------------------------------------------------------------]# awk -F: '/^root/,/^mysql/{print $1}' /etc/passwd:从以root开头的行开始,到以mysql开头的行结束的行,显示第1字段;--------------------------------------------------------------------------------------------]# awk -F: 'NR>=2&&NR<=10{print $1}' /etc/passwd:显示从第2行到第10行的第1字段;
(4) line ranges:行范围,
startline,endline:/pat1/,/pat2/ (第一次由模式匹配的行开始到第一次由模式到匹配的行结束)
注意: 不支持直接给出数字的格式
~]# awk -F: '(NR>=2&&NR<=10){print $1}' /etc/passwd 显示从第2行到第10行的第1字段;
(5) BEGIN/END模式
BEGIN{program}: 仅在开始处理文件中的文本之前执行一次;显示表头
END{program}:仅在文本处理完成之后命令结束之前执行一次;
]# awk -F: 'BEGIN{print " username uid \n--------------"}{print $1,$3}' /etc/passwd:显示表头;--------------------------------------------------------------------------------------------]# awk -F: '{print "username uid \n--------------";print $1,$3}' /etc/passwd显示部分内容:即每行都显示一遍;主程序执行前显示;--------------------------------------------------------------------------------------------]# awk -F: 'BEGIN{print " username uid \n--------------"}{print $1,$3}END{print "==========\n end"}' /etc/passwd:设置尾部显示;主程序结束后显示;
6、常用的action
(1) Expressions (表达式)
(2) Control statements(控制语句):if, while等;
(3) Compound statements:组合语句; —-把多个语句放在代码块的语句
(4) input statements (输入语句)
(5) output statements
7、控制语句
if(condition) {statments} condition (条件) if(condition) {statments} else {statements} (这即组合语句) while(conditon) {statments} do {statements} while(condition) for(expr1;expr2;expr3) {statements} break continue delete array[index] (从数组中删除指定的元素) delete array ( 删除整个数组) exit { statements } (多个语句写成代码组合在一块形成组合语句时,需要用{}括起来)
7.1 if-else语句,支持双分支if语句,完成条件判断;
语法格式:if(condition) statement [else statement] (一个语句不用加括号,双分支else语句 true和else后面都要加{})
使用场景:对awk取得的整行或某个字段做条件判断时使用;
例如:
~]# awk -F: '{if($3>=1000) print $1,$3}' /etc/passwd (表示用 :做分隔符)以冒号为分隔符,判断用户ID>=1000,显示第1,3字段;(在centos 7)-----------------------------------------------------------------------------------------~]# awk -F: '{if($3>=1000) {printf "Common user: %s\n",$1} else {printf "root or Sysuser: %s\n",$1}}' /etc/passwd以冒号为分隔符,判断ID>=1000的用户ID为普通用户;
~]# awk -F: '{if($NF=="/bin/bash") print $1}' /etc/passwd (以冒号为分隔符,判断默认shell为bash的用户,有哪些;)---------------------------------------------------------------------------------------------~]# awk '{if(NF>5) print $0}' /etc/fstab (以默认空格为分隔符,判断某行的字段数是否大于5个才显示,$0可省略,默认显示整行)---------------------------------------------------------------------------------------------~]# df -h | awk -F[%] '/^\/dev/{print $1}' | awk '{if($NF>=20) print $1}' 以%为分隔符,/dev开头的行取第1字段,再判断最后一个字段内容是否>=20;即判断磁盘使用超过20%的分区;
使用场景:对awk取得的整行或某个字段做条件判断;
7.2 while循环
语法:while(condition) statement
条件“真”,进入循环;条件“假”,退出循环;语句如果有多个,需要用{}括起来; (注意:第一为假时则不进入循环 )
使用场景:对一行内的多个字段逐一类似处理时使用;对数组中的各元素逐一处理时使用;
例如:
~]# awk '/^[[:space:]]*linux16/{i=1;while(i<=NF) {print $i,length($i); i++}}' /etc/grub2.cfg 【以空格开头0次或多次,后跟linux16的行,以空格为分隔符,显示每行中各字段的长度;】
~]# awk '/^[[:space:]]*linux16/{i=1;while(i<=NF) {if(length($i)>=7) {print $i,length($i)}; i++}}' /etc/grub2.cfg 【以空格开头0次或多次,后跟linux16的行,以空格为分隔符,只显示每行中字段的长度>=7字段;】
7.3 do-while循环 (不常用)
语法:do statement while(condition)
意义:无论条件真假,至少执行一次循环体7.4 for循环
语法:for(expr1;expr2;expr3) statement
expr1:控制变量初始化;expr2:条件判断;expr3:控制变量的数值修正表达式;
for(variable assignment;condition;iteration process) {for-body} //:variable assignment【变量赋值】;condition【条件判断表达式】;iteration process【变量修正表达式】;for-body【循环体语句】
~]# awk '/^[[:space:]]*linux16/{for(i=1;i<=NF;i++) {print $i,length($i)}}' /etc/grub2.cfg 【以空格开头0次或多次,后跟linux16的行,以空格为分隔符,显示每行中各字段的长度;同上while语句;】]# awk '/^[[:space:]]*linux16/{for(i=1;i<=NF;i++) {if(length($i)>=7) {print $i,length($i)}}}' /etc/grub2.cfg 【以空格开头0次或多次,后跟linux16的行,以空格为分隔符,只显示每行中字段的长度>=7的字段;同上while语句;】
for循环特殊用法:
能够遍历数组中的元素语法:for(var in array) {for-body} //var in array:var【自定义的变量】in【关键字】 array【数组名】
7.5 switch语句(在awk中用的不多)
语法:switch(expression) {case VALUE1 or /REGEXP/: statement; case VALUE2 or /REGEXP2/: statement; …; default: statement}
即:switch(表达式) {case 值1或/正则式1/: 语句1; case 值1或/正则式1/: 语句2; …; default: 语句n}
判断表达式的值符合case里面的哪个条件,可以等于case里的值,也可以是被case里的正则表达式匹配;如果符合条件就执行该case里的语句,不会往下判断了;类似case语句,只不过是关键字写法不同;// case:关键字 default:statement【默认分支】
7.6 break和continue
break [n]continue
在awk中能实现2重循环,awk本身可对文件每行循环,使用循环语句是为了遍历一行中的每个字段,或数组中的每个元素;
7.7 next
提前结束对本行的处理而直接进入下一行;(类似于continue,continue是用来控制行内字串间跳转的,next是控制awk的本身循环的)例子:显示用户的id号为偶数的用户
~]# awk -F: '{if($3%2!=0) next; print $1,$3}' /etc/passwd 【显示用户ID号为偶数的行;即判断id号是否能被2整除,如果不等于0,直接结束本行,直接进行下一行;】
8、array (数组)
关联数组:array[index-expression] // array[](数组名);index-expression(索引表达式)
(1) 可使用任意字符串;字符串要使用双引号;
(2) 如果某数组元素事先不存在,在引用时,awk会自动创建此元素,并将其值初始化为“空串”;
index-expression:
若要判断数组中是否存在某元素,要使用"index in array"格式进行;
例如:
]# awk 'BEGIN{weekdays["mon"]="Monday";weekdays["tue"]="Tuesday";print weekdays["mon"]}' //注意:字符串要使用双引号;引用数组中的元素不需使用$符号;
若要遍历数组中的每个元素,要使用for循环;
for(var in array) {for-body}即for(变量名 in 数组名) {循环体}变量名中保存的是数组的索引,而不是数组中的元素;注意:不要使用$符号;
例如:
~]# awk 'BEGIN{weekdays["mon"]="Monday";weekdays["tue"]="Tuesday";for(i in weekdays) {print weekdays[i]}}' 【遍历数组中的每个元素;要使用for循环;显示的次序可能不太和想象的一样;】
注意:对应的变量var会遍历array的每个索引;
某个数组元素不存在,直接引用后会被创建且为空,做数值操作时当做0使用;
使用数组统计每一类数值各自出现次数时非常有用;例如:
state["LISTEN"]++state["ESTABLISHED"]++ (先显示索引,再显示索引元素的值)~]# netstat -tan | awk '/^tcp\>/{state[$NF]++}END{for(i in state) { print i,state[i]}}' 【显示tcp每个状态分别出现的次数;】
]# systemctl status httpd.service]# systemctl start httpd.service]# vim /var/www/html/index.html 编辑内容:Test page 使用网页浏览可见此内容;]# tail /var/log/httpd/access_log:查看服务器访问日志;可对每一个ip地址做统计;
例如:统计每个ip地址访问服务器的次数;
~]# awk '{ip[$1]++}END{for(i in ip) {print i,ip[i]}}' /var/log/httpd/access_log
练习1:统计/etc/fstab文件中每个文件系统类型出现的次数;
~]# awk '/^UUID/{fs[$3]++}END{for(i in fs) {print i,fs[i]}}' /etc/fstab
练习2:统计指定文件中每个单词出现的次数;
~]# awk '{for(i=1;i<=NF;i++){count[$i]++}}END{for(i in count) {print i,count[i]}}' /etc/fstab
9、函数
9.1 内置函数
数值处理:
rand():返回0和1之间一个随机数;
例如:
]# awk 'BEGIN{print rand()}':执行多次都是同样的随机数;
字符串处理:
length([s]):返回指定字符串的长度;sub(r,s,[t]):以r表示的模式来查找t所表示的字符中的匹配的内容,并将其第一次出现替换为s所表示的内容;[t]表示可选的 字符串;gsub(r,s,[t]):以r表示的模式来查找t所表示的字符中的匹配的内容,并将其所有出现均替换为s所表示的内容;split(s,a[,r]):以r为分隔符切割字符s,并将切割后的结果保存至a所表示的数组中;
例如:
]# awk -F: '{print sub(o,O,$1)}' /etc/passwd //把每行的第1字段中,第一次出现的小写o替换为大写O;注意:仅替换每行一次出现的;
gsub(r,s,[t]):以r表示的模式来查找t所表示的字符中的匹配的内容,并将其所有出现的内容均替换为s所表示的内容;
split(s,a[,r]):以r为分隔符切割字符s,并将切割后的结果保存至a所表示的数组中;数组元素从1开始编号;
例如:
]# netstat -tan | awk '/^tcp\>/{split($5,ip,":");count[ip[1]]++}END{for (i in count) {print i,count[i]}}' 【显示来访的主机地址连接的次数;】]# ab -c 100 -n 1000 http://172.18.252.22/index.html
解法:①
②
9.2 自定义函数
《sed和awk》