一、前言
gawk是GNU awk,是awk遵循开源的实现。而awk是一款功能强大的文本处理工具,这款软件自成体系,实际上就是一个用于处理文本的编程语言工具。
二、awk介绍
(一)awk语法
awk程序由一系列模式-动作对组成,写作 pattern { action }
awk程序把每一行看成一条记录,每一行又被分隔符分隔成不同的字段(或称域)。awk解释器会判断行中是否符合模式,如果符合模式则执行相应的动作。
语法格式
awk [options] ‘PATTERN{ACTION}FILE …
awk [options] -f ‘script-file’ FILE …
常用选项
-F --field-separator |
输入字段分隔符 |
-f program-file --file program-file |
从指定文件中读取程序 |
-v varname=val --assign varname=val |
自定义变量,BEGIN块可用 |
(二)内置变量
1、特殊变量
变量 | 说明 | 示例 |
$0 | 引用整行记录 | |
$1-$n | 分别代表引用对应的字段 | |
NR | 已输入的记录条目数 | |
NF | 当前记录中字段个数 | $NF引用最后一个字段值,$符号是字段引用操作符 |
FNR | 当前输入文件的总行数 | |
RS | 输入记录分隔符 | |
FS | 输入字段分隔符 | |
ORS | 输出记录分隔符 | |
OFS | 输入字段分隔符 | |
FILENAME | 当前输入文件的文件名 | |
ARGV | 索引从0开始的命令行参数数组 | awk -F: '/root/,/bin/ {print}' /etc/passwd /etc/group ARGV[0]是awk ARGV[ARGC-1]是/etc/group |
ARGC | 命令行参数个数,不包括选项和程序部分 | 上例中,参数总数为3 awk '{print ARGV[0],ARGV[2],ARGC}' /etc/passwd /etc/group |
2、自定义变量
可以在选项中定义,上面有介绍
awk -v a="hello" 'BEGIN{print a}'
模式-动作对中定义使用
awk -F: 'BEGIN{a="hello"}{print a,$1,$7}' /etc/passwd
awk -F: 'BEGIN{a="hello";print a}'
awk -F: '{a="hello";print a,$0}' /etc/issue
(三)数组
1、普通数组
array[index]
普通数组使用数字作为index的值,awk自动生成的索引从1开始。但是自定义就可以随意,例如:
awk 'BEGIN{a[5]="hello";print a[5]}'
数组索引为数字的,实际在引用的时候还是转换到了字符索引使用的。
2、关联数组
使用字符串作为数组的索引
awk 'BEGIN{a[test]="hello";print a[test]}'
3、数组元素测试
使用特殊操作符in可以用来测试数据是否是关联数组
if (val in array)
print array[val]
4、遍历关联数组
for (val in array) { process array[val] }
例如 for (var in array) {print i,array[i]}
5、删除数组或元素
delete array[index] 删除数据元素
delete array 删除数组
(四)操作符
$ | 引用操作符 |
! + - * / % ** ^ -val +val | 算术运算符 |
= += -+ *= /= %= ^= **= ^= | 赋值操作符 |
++ -- | 自增、自减单目运算符 |
&& || | 逻辑运算符 |
< <= > >= != == | 关系运算符 |
空 | 字符连接符 |
exp1?exp2:exp3 | 三目运算符。注意这里是表达式 |
in | 引用数组索引 |
(5)函数
1、内建函数
函数名 | 格式 | 说明 |
length | length([arg]) | 返回参数的长度。如果参数没有给出,将默认使用$0。如果参数是个数组名,返回数组长度(见文本综合示例) |
substr | substr(string,begin[,length]) | 返回从指定位置开始的子串。如果不给出长度,默认取到字符结束。开始位置必须从1开始 |
tolower | tolower(string) | 转换为小写 |
toupper | toupper(string) | 转换为大写 |
split | split(string, array [, sep]) | 以指定的分隔符(默认为FS)分隔字符串并保存在索引从1开始的数组中,并返回元素的个数。(综合示例中有例子) |
mktime | mktime(datespec) | 返回指定时间的时间戳 |
strftime | strftime([format [, timestamp[, utc-flag]]]) | 格式化时间戳 |
systime | systime() | 返回时间戳 |
2、自定义函数
格式为
function name([parameter-list])
{
body-of-function
}
可以定义函数,放在脚本文件中,使用-f选项读入。
(六)模式 PATTERN {ACTION}
名称 | 说明 |
BEGIN | 特殊模式。处理前执行一次,参见后文中的执行顺序 |
END | 特殊模式。处理的最后执行,参见后文中的执行顺序 |
/regular expression/ | 正则表达式的模式来匹配。 使用egrep风格,即扩展正则表达式ERE。 |
relational expression | 使用关系运算符的关系表达式作为匹配条件 |
pattern && pattern | 同时满足所有模式的匹配 |
pattern || pattern | 满足任意一个模式的匹配 |
pattern1 ? pattern2 : pattern3 | 三目运算,模式1为真使用模式2,为假使用模式3 |
(pattern) | |
! pattern | 模式取反的匹配 |
pattern1, pattern2 | 范围匹配,从模式1匹配行开始,到模式2匹配行为止 |
(七)动作 ACTION
1、表达式
基本的表达式都可以使用
2、控制语句
条件判断
if (condition) statement [ else statement ]
循环
while (condition) statement
do statement while (condition)
for (expr1; expr2; expr3) statement
循环控制
break
continue
next 结束当前行处理,直接处理下一行
数组操作
delete array[index]
delete array
for (var in array) statement
退出
exit [ expression ] 结束awk操作并退出
3、输入输出语句
(1)print
语法:print item1, item2, ...
还可以使用输出重定向和管道
print … >> file
print … | command
(2)printf
printf "format",arg1,arg2,…
常用格式符
格式符 | 说明 |
%c | 字符 |
%d,%i | 十进制 |
%o | 无符号八进制 |
%e,%E | 科学计数 |
%f | 显示浮点数 |
%g,%G | 科学计数或浮点数显示 |
%s | 字符串显示 |
%u | 无符号数 |
%% | 百分号本身 |
%x %X | 十六进制 |
#.# | 数字值代表输出宽度.精度。字符串前面代表最小长度,后面代表最大长度 |
- | 左对齐 |
+ | 显示数字符号 |
常用转义字符
\n | 换行 |
\t | tab |
\r | 回车 |
\\ \" \/ | 特殊字符转义 |
三、程序执行顺序
gawk执行程序的顺序是:
通过-v选项设定的变量赋值;
gawk编译程序成为一个内建形式;
开始执行BEGIN块代码
开始读取ARGV数组中的文件的每一个行,如果没有文件则读取标准输入
正常的文件处理完成后,则执行END块
四、举例
(一)打印数值
# awk 'BEGIN { print 042, 42, 0x42 }'
34 42 66
# awk 'BEGIN { printf "%d %d %d %o %x 0x%04X \n",042, 42, 0x42, 16, 34, 1203 }'
34 42 66 20 22 0x04B3
(二)稀疏数组
# awk 'BEGIN{a[5]="hello";a[test]="world";print a[5],a[test],length(a)}'
hello world 2
注意,这里用length函数返回数组元素的个数
# awk 'NR<2{lens=split($0,arr);printf "%s\ttotal=%d\t last=%s\n",$0,lens,arr[lens] }' /etc/issue
CentOS release 6.5 (Final) total=4 last=(Final)
注意:split函数的索引从1开始
(三)打印所有普通用户名称及其shell
# awk -F: '$3>=500{print $1,$7}' /etc/passwd
打印所有普通用户名称,且名称中不含有数字的,及其shell,要求格式化输出对齐,打印表头,表尾打印时间
# awk -F: 'BEGIN{printf "%-15s\t%-8s\t%-s\n","USERNAME","ID","SHELL"}$3>=500 && $1~/[[:alpha:]]+\>/{printf "%-15s\t%-8s\t%-s\n",$1,$3,$7}END{printf "%30s\n",strftime("%Y-%m-%d %H:%M:%S")}' /etc/passwd
[ 分析 ]
BEGIN块最先执行,然后每行都会匹配$3>=500 && $1~/[[:alpha:]]+\>/,这是个条件表达式的逻辑与表达式组成的模式PATTERN,要求同时成立,即前面要求第三字段大于等于500的,同时要求第一字段只能使用字母且至少有一个字母组成的名字。满足这两个条件的行才会被处理,使用printf格式输出。最后打印表尾,使用了格式化输出的内置函数。
(四)自定义分隔符
这个功能非常有用,看示例。
使用正则表达式作为分隔符
# ifconfig eth0 | awk -F'[ :]+' '/inet addr/{print $4}'
(五)数组高级应用
在另一个机器上使用ab压力测试
# ab -c 400 -n 12000 http://172.16.0.200/index.html
本机使用netstat做状态统计
1、统计连接的状态 小试牛刀
# netstat -tan | awk 'BEGIN{printf "%-15s %s\n","STATE","COUNT"}/^tcp\>/{stat[$NF]++}END{for(val in stat){printf "%-15s %s\n",val,stat[val]}}'
2、要求使用IP统计 增加难度
(1)采用选项重新定义FS
# netstat -tan | awk -F '[[:space:]:]+' 'BEGIN{printf "%-20s %s\n","IP","COUNT"}/^tcp\>/ && $(NF-3)~/.*\..*/ {stat[$(NF-3)]++}END{for(val in stat){printf "%-20s %s\n",val,stat[val]}}'
[ 分析 ]
找几行数据分析一下
tcp 0 0 0.0.0.0:2049 0.0.0.0:* LISTEN
tcp 0 52 172.16.0.200:22 172.16.0.100:1709 ESTABLISHED
tcp 0 0 :::35084 :::* LISTEN
tcp 0 0 ::ffff:172.16.0.200:80 ::ffff:172.16.0.210:50236 TIME_WAIT
按照默认分隔符,很难一下从上面几行中直接提取IP地址。
默认FS其实就是[[:space:]]+,就是多个空白字符作为分隔符的。
那么我们只需要将FS改为[[:space:]:]+,增加一个分号即可。这样从右边看过来,每一行从右数第4个数据就是IP。注意这里是4,不是3,$NF取的是最右边的空(NULL),因为状态值TIME_WAIT等右侧还有空白字符。
那么从右边看过来状态值就是$(NF-1),然后遇到了大片空白字符,这不算数因为是分隔符,然后遇到的就是:右边的端口号,不需要,然后遇到了一个冒号或者多个冒号,这都是分隔符,再向右就是IP地址。
所以IP地址是$(NF-3),但是有IPV6地址:::等的干扰,所以用了模式匹配/^tcp\>/ && $(NF-3)~/.*\..*/,要求tcp作为行首,且词尾锚定,同时要求IP地址的字段至少有一个.点号,当然你可以使用IPV4的正则表达式匹配,就是写出来有点吓人。
(2)采用split函数分隔处理
netstat -tan | awk 'BEGIN{printf "%-20s %s\n","IP","COUNT"}/^tcp\>/{lens=split($5,spe,":");stat[spe[lens-1]]++}END{for(val in stat){printf "%-20s %s\n",val,stat[val]}}'
[ 分析 ]
使用正常FS是以连续空白字符为分隔符的,遇到这样的地址比较头疼 ::ffff:172.16.0.210:50236 。
要再用split函数以:分号分隔,取倒数第二个,再次提醒,索引从1开始。split函数的返回值是切割得到元素的个数,所以spe数组的lens-1索引就是IP地址,然后把IP放在stat数组中作为关联数组索引。
stat[spe[lens-1]]++的意思是,如果地址是IP1,就stat[IP1]++,如果地址是IP2,就stat[IP2]++,用IP地址取出元素的值自增的。
最后和上例一样for循环打印出来。
为什么有空呢?看下图,你就会明白。原来使用split函数切割:::*的时候,倒数第二个是一个空NULL。
问题找到了,改进一下语句试一试
# netstat -tan | awk 'BEGIN{printf "%-20s %s\n","IP","COUNT"}/^tcp\>/{lens=split($5,spe,":");if(length(spe[lens-1])) stat[spe[lens-1]]++}END{for(val in stat){printf "%-20s %s\n",val,stat[val]}}'
ok,问题解决
3、要求按照IP和状态统计 来个挑战
# netstat -tan | awk -F '[[:space:]:]+' 'BEGIN{printf "%-30s %s\n","IP","COUNT"}/^tcp\>/ && $(NF-3)~/.*\..*/ {stat[$(NF-3),$(NF-1)]++}END{for(val in stat){printf "%-30s %s\n",val,stat[val]}}'
(六)最后做一个有趣的分析,来个思维的小小挑战
有下面三条语句来分析一下,NF是你想象的数字吗?
# echo "acadbbafdbaabbddabb " | awk -F "[ab]+" '{for(i=1;i<=NF;i++)print $i }END{print "NF="NF}'
# echo "acadbbafdbaabbddabb" | awk -F "[ab]+" '{for(i=1;i<=NF;i++)print $i }END{print "NF="NF}'
# echo "acadbbafdbaabbdd" | awk -F "[ab]+" '{for(i=1;i<=NF;i++)print $i }END{print "NF="NF}'
参考资料
官方资料
http://www.gnu.org/software/gawk/manual/gawk.html