一、Awk介绍
Awk、sed与grep,俗称Linux下的三剑客,它们之间有很多相似点,但是同样也各有各的特色,相似的地方是它们都可以匹配文本,其中sed和awk还可以用于文本编辑,而grep则不具备这个功用。sed是一种非交互式且面向字符流的编辑器(a “non-interactive” stream-oriented editor),而awk则是一门模式匹配的编程语言,因为它的主要功能是用于匹配文本并处理,同时它有一些编程语言才有的语法,例如函数、分支循环语句、变量等等,当然比起我们常见的编程语言,Awk相对比较简单。
Awk其名称得自于它的创始人Alfred Aho 、Peter Weinberger和Brian Kernighan姓氏的首个字母。实际上 AWK 的确拥有自己的语言: AWK程序设计语言 , 三位创建者已将它正式定义为“样式扫描和处理语言”。它允许您创建简短的程序,这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表,还有无数其他的功能。AWK支持基本语言的变量,条件判断,循环,数组等,但是表现形式只有一行命令。
Awk有3个不同版本,分别为早期的awk、改版过的nawk和GNU的gawk,在Linux中awk实际上是gawk的一个符号链接。而gawk是也叫GNU awk是nawk的开源实现版本。
Awk就像个数据库,支持对记录和字段的处理。但是与数据库不同的是,它处理的是文本。默认的情况下,awk会将文本中的一行视为一个记录,而将一行中一个或多个非空白字符组成的单词视为字段,换句话说就是用空格来分隔字段。当然,这些都是默认情况。
使用Awk,我们可以做以下事情:
- 将文本文件视为由字段和记录组成的文本数据库;
- 在操作文本数据库的过程中能够使用变量;
- 能够使用数学运算和字符串操作
- 能够使用常见的编程结构,例如条件分支与循环;
- 能够格式化输出;
- 能够自定义函数;
- 能够在awk脚本中执行UNIX命令;
- 能够处理UNIX命令的输出结果;
Awk能够做得事情非常多,但千里之行,始于足下,我们首先从最基本的命令行语法开始,一步一步得走入awk的编程世界。
二、Awk语法介绍
命令语法:
1 |
awk [options] 'pattern{action}' file1,file2 |
Awk程序的核心思想是模式(pattern)/行为(action)对儿这个概念,也叫模式驱动编程。模式一般是关系式或正则表达式,用于与输入的每条记录进行匹配;Awk的输入被解析成多个记录(Record),默认情况下,记录的分隔符是\n,因此可以认为一行就是一个记录,记录的分隔符可以通过内置变量“RS”更改。当记录匹配某个pattern时,才会执行后续的action命令。模式或行为可以省略其中一个。如果省略模式,则行为将被应用到每条输入记录;如果省略行为,则默认操作是在标准输出上打印匹配到的记录。
而每个记录由进一步地被分隔成多个字段(Field),默认情况下字段的分隔符是空白符,例如空格、制表符等等,也可以通过“-F”选项或者内置变量“FS”更改。在awk中,可以通过$1,$2…来访问对应位置的字段,同时$0存放整个记录,这一点有点类似shell下的命令行位置参数。关于这些内容,我们会在下面详细介绍,这里你只要知道有这些东西就好。
Awk还提供了两种特殊的模式BEGIN和ENG。与BEGIN配对的行为只会被执行一次,且永远是最先执行的,而END则与BEGIN相反,永远是最后执行的且只执行一次,BEGIN/END都可以用来定义输出变量或输出信息,下图是awk的工作流程。
BEGIN和END模式可以放在awk程序的任何位置。但是由于人的惯性思维,通常会将BEGIN模式放在程序的最开头,而将END模式放在程序的结尾处。一个awk程序可以有多个BEGIN和END模式,这个时候就按照他们出现的顺序依次执行。提供这种特性时允许使用“-f”命令选项引入程序库,提供库的初始化和清除操作。
为了便于理解,这里举几个简单的例子。通过-F参数设置冒号:为分隔符,并打印各个字段:
1 2 |
$ echo "1:2:3" | awk -F: '{print $1 " and " $2 " and " $3}' 1 and 2 and 3 |
字段含义:
[options]
1 2 |
-F':' #设定字段分隔符,这里以冒号为分隔符,默认是以空格为空格符; -F'(' #以小括号为分隔符; |
/pattern/
- Regexp
正则表达式,格式为/regular exspression/
例如:
1 2 3 4 5 6 7 8 |
# 以:为分隔符显示第一个字段,且可以在每一行前加上自己的注释信息; $ awk -F: '{print "usernmae:"$1}' /etc/passwd # 以bash结尾为条件,显示字段1和7; $ awk -F: '/bash$/{print $1,$7}' /etc/passwd # 以#号或者空白字符开头的行都不予以显示; $ awk -F: '!/^#|^$/ {print $0}' /etc/inittab |
- 表达式操作符(expression)
算术操作符 | 负值(-x)、转换为数值(+x)、次方(**)、乘(*)、除(/)、加(+)、减(-) 、取余(%)。 |
赋值操作符 | 等于(=)、加等(+=)、减等(-=)、乘等(*=)、除等(/=)、**/ 、++、–。 |
比较操作符 | 小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、等于(==)、不等于(!=)、字符串匹配模式成功为真(~)、字符串匹配模式成功为假(!~)。 |
逻辑操作符 | 逻辑与(&&)、逻辑或(||)。 |
例如:
1 2 3 4 5 6 7 8 9 10 11 |
# 显示用户ID大于500的用户; $ awk -F: '$3>=500{print $1,$3}' /etc/passwd $ awk -F: '$3+1>=500{print $1,$3}' /etc/passwd # 显示第7个字段等于/bin/bash,或匹配模式以bash结尾的; $ awk -F: '$7=="/bin/bash"{print $1,$7}' /etc/passwd $ awk -F: '$7~"bash$"{print $1,$7}' /etc/passwd $ awk -F: '$7~"/bin/bash"{print $1,$7}' /etc/passwd # 取反; $ awk -F: '$7~"/bin/bash"{print $1,$7}' /etc/passwd |
- ranges
指定的匹配地址范围,如/part1/,/part2/
例如:
1 2 |
# 显示由root开头的行至ftp开头的行; $ awk -F: '/^root/,/^ftp/{print $1,$7}' /etc/passwd |
- BEGIN/END
特殊模式,仅在awk命令执行前运行一次或结束前运行一次。
1 2 3 4 5 6 7 8 9 10 11 |
# 给提取出来的字段各自加上一个开头和结尾标示符,使用print; $ awk -F: 'BEGIN{print "Username\n---------"}/^root/{print $1}END{print "---------\nTotal 1 User"}' /etc/passwd # 给提取出来的字段各自加上一个开头标示符,使用printf; # awk -F: 'BEGIN{print "Username shell\n------------------"}/^root/ {printf "%-10s%-20s\n",$1,$7}' /etc/passwd # 给切割之后的内容赋予新的分隔符予以显示; $ awk -F: 'BEGIN{OFS="-"} /^root/ {print $1,$2,$3}' /etc/passwd #在BEGIN模式下还支持做计算; $ awk 'BEGIN{print 2**3}' |
{action}
- Output statements
print的使用格式:{Print item1,item2…}
1、各项目之间使用逗号隔开而输出时则以空白字符隔开,否则则当成一个字段处理。
2、输出的item可以为字符串或数值,当前记录的字段(如$1),变量或awk的表达式,数值会先转换为字符串而后再输出。
3、Print命令后面的item可以省略,此时其功能相当于print $0,因此如果想输出空白行则需要使用print “”。
例如:
1 |
$ awk -F: '{print $1,"Hello",$2}' /etc/passwd |
printf的使用格式:{printf format,item1,itme2}
1、其与print命令的最大不同是,printf需要指定format。
2、Printf语句不会自动打印换行符,需要加\n换行。
3、format用于指定后面的每个item的输出格式,如:
格式 | 含义 |
%c | 显示字符的ASCII码 |
%d,%i | 十进制整数 |
%e,%E | 科学计数法显示数值 |
%f | 显示浮点数 |
%g,%G | 以科学计数法的格式或浮点数的格式显示数值 |
%s | 显示字符串 |
%u | 无符号整数 |
%% | 显示%自身 |
例如:
1 2 |
$ awk -F: '{printf "%15s\n",$1}' /etc/passwd $ awk 'BEGIN{printf "%f\n",'$Used'/'$MemTotal'}' |
修饰符:
1 2 3 |
10 #显示宽度; - #左对齐; + #显示数值符号; |
例如:
1 2 |
$ awk -F: '{printf "%-10s\n",'$1'}' /etc/passwd $ awk -F: '{printf "%-15s %i\n",$1,$3}' /etc/passwd |
- control statements(跟shell语法不同)
if语法:if (condition) {then-body} else {[else-body]}
1 2 3 4 5 6 7 8 |
# $1字段等于root就在后面注释为Admin,相反不为root用户就注释为Common User; $ awk -F: '{if ($1=="root") print $1, "Admin"; else print $1, "Common User"}' /etc/passwd # 功能跟上面一样,但是使用printf来格式化输出方式,更美观的显示; $ awk -F: '{if ($1=="root") printf "%-15s: %s\n", $1,"Admin"; else printf "%-15s: %s\n", $1, "Common User"}' /etc/passwd # 显示ID号大于500的用户有几个; $ awk -F: -v sum=0 '{if ($3>=500) sum++}END{print sum}' /etc/passwd |
while语法:while (condition) {statement1; statement2;…}
1 2 |
$ awk -F: '{i=1;while (i<=3) {print $i;i++}}' /etc/passwd $ awk -F: '{i=1;while (i<=NF) { if (length($i)>=4) {print $i}; i++ }}' /etc/passwd |
do while语法:do {statement1,statement2,…} while (condition)
1 |
$ awk -F: '{i=1;do {print $i;i++}while(i<=3)}' /etc/passwd |
for语法: for ( variable assignment ; condition; iteration process) {statment1,statment2,….}
1 2 |
$ awk -F: '{for(i=1;i<=3;i++) print $i}' /etc/passwd $ awk -F: '{for(i=1;i<=NF;i++) { if (length($i)>=4) {print $i}}}' /etc/passwd |
for (i in array) {statement1,statement2,…}
1 |
$ awk -F: '$NF!~/^$/{BASH[$NF]++}END{for(A in BASH){printf "%15s:%i\n",A,BASH[A]}}' /etc/passwd |
case语法: switch (expression) {case VALUE or /REGEXP/: statement1,statement2,…default:statement1,…}
PS:break和continue常用于循环和case语句中。
三、Awk内置变量
Awk在内部维护了许多内置变量,或者称为系统变量,例如之前提到的FS、RS等等。常见的内置变量如下表所示:
变量名 | 描述 |
ARGC | 命令行参数的各个,即ARGV数组的长度 |
ARGV | 存放命令行参数 |
CONVFMT | 定义awk内部数值转换成字符串的格式,默认值为”%.6g” |
OFMT | 定义输出时数值转换成字符串的格式,默认值为”%.6g” |
ENVIRON | 存放系统环境变量的关联数组 |
FILENAME | 当前被处理的文件名 |
NR | 记录的总个数 |
FNR | 当前文件中的记录的总个数 |
FS | 字段分隔符,默认为空白 |
NF | 每个记录中字段的个数 |
RS | 记录的分隔符,默认为回车 |
OFS | 输出时字段的分隔符,默认为空白 |
ORS | 输出时记录的分隔符,默认为回车 |
RLENGTH | 被match函数匹配的子串长度 |
RSTART | 被match函数匹配的子串位于目标字符串的起始下标 |
简单可分为如下几类:
- 记录变量
FS(field separator):输入使用的字段分隔符,默认是空格,可以自定义设置,如下:
1 |
$ awk 'BEGIN{FS=":"} $3>=500 {print $1}' /etc/passwd |
OFS(output field separator):输出使用的字段分隔符
1 |
$ awk -F: 'BEGIN{OFS="-"} /^root/ {print $1,$2,$3}' /etc/passwd |
RS(record separator):输入使用的换行符(awk每次读取一行数据就是根据设定的默认换行符来辨别的)
1 |
$ awk 'BEGIN{RS=" "}{print $0}' /etc/passwd |
ORS(output record separator):输出使用的换行符
1 |
$ awk 'BEGIN{ORS="!!!"}{print $0}' /etc/passwd |
- 数据变量
NR:记录所处理的行,如果有多个文件,这个数目会把处理的多个文件中行统一计数(注意,在awk中打印变量不需要加$符,如果加$符那就是打印字段了)
1 |
$ awk -F: '{print NR}' /etc/passwd /etc/fstab |
FNR:记录所处理的行,如果有多个文件,这个数目会把处理的多个文件各自计数
1 |
$ awk -F: '{print FNR}' /etc/passwd /etc/fstab |
NF:记录当前所处理文件行的字段数,(利用这个可以用来显示每行的最后一个字段)
1 2 |
$ awk -F: '{print NF}' /etc/passwd $ awk -F: '{print $NF}' /etc/passwd |
ARGV:数组,保存命令行本身这个字符串,如ARVG[0]显示awk命令,而ARGV[1]显示跟的文件名
1 |
$ awk 'BEGIN{print ARGV[0],ARGV[1]}' /etc/passwd |
- 用户自定义变量
gawk允许用户自定义自己的变量以便在程序代码中使用,变量名命名规则与大多数编程语言相同,只能使用字母、数字和下划线,且不能以数字开头,gawk变量名称区分字符大小写。
命令行中使用赋值变量,如:
1 |
$ awk -v var="variable testing" 'BEGIN{print var}' |
脚本中赋值变量,如:
1 |
$ awk 'BEGIN{var="variable testing";print var}' |
四、Awk数组使用
在awk中,数组跟shell中一样。但不同的是shell中数组下标一般都是从0开始而在awk中是从1开始。并且awk中的下标index可以使用任意字符串表示。如:
1 2 |
$ array[mon]=1 $ array[stu]=2 |
需要注意的是如果某数组元素事先不存在,那么在引用其时,awk会自动创建此元素并初始化为空串;因此要判断某数据组中是否存在某元素需要使用index in array的方式要遍历数组中的每一个元素需要使用如下的特殊结构For循环:
1 |
for (var in array) { statment1,…} |
其中var用于引用数组下标而不是元素值;如下:
1 2 3 4 5 6 7 8 |
# 每出现一被/^tcp/模式匹配到的行,数组S[$NF]就加1其初始值为0,NF为当前匹配到的行的最后一个字段,此处用其值做为数组S的元素索引; $ netstat -ant | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' # 使用表达式只统计ESTABLISHED状态的数值; $ netstat -anplt | awk '$6~"ESTABLISHED" {time[$6]++} END{for (i in time) print i,time[i]}' # 用于统计某日志文件中IP地的访问量。注意END的含义上面说过当数组都处理完之后,在进行for循环; $ awk '{counts[$1]++}; END {for(ip in counts) printf "%-20s:%d\n",ip,counts[ip]}' /var/log/httpd/access_log |
从关系数组中删除数组索引需要使用delete命令使用格式为Delete array[index]。
五、Awk内置函数
awk中包含大多数常见的字符串操作函数。
sub(ere, repl[, in])
描述:简单地说,就是将in中匹配ere的部分替换成repl,返回值是替换的次数。如果in参数省略,默认使用$0。替换的动作会直接修改变量的值。
1 2 |
$ echo "hello, world" | awk '{sub(/o/, "repl"); print}' hellrepl, world |
gsub(ere, repl[, in])
描述:同sub()函数功能类似,只不过是gsub()是全局替换,即替换所有匹配的内容。
1 2 |
$ echo "hello, world" | awk '{gsub(/o/, "repl"); print}' hellrepl, wreplrld |
index(s, t)
描述:返回字符串t在s中出现的位置,注意这里位置是从1开始计算的,如果没有找到则返回0。
1 2 |
$ awk 'BEGIN {print index("kodango", "w")}' 0 |
split(s, a[, fs])
描述:将字符串按照分隔符fs,分隔成多个部分,并存到数组a中。注意,存放的位置是从第1个数组元素开始的。如果fs为空,则默认使用FS分隔。函数返回值分隔的个数。
1 |
$ netstat -ant | awk '/:80\>/{split($5,clients,":");IP[clients[1]]++}END{for(i in IP){print IP[i],i}}' | sort -rn | head -50 |
length[([s])]
描述:返回字符串的长度,如果参数s没有指定,则默认使用$0作为参数。
1 2 |
$ echo "first line" | awk '{print length();}' 10 |
match(s, ere)
描述: 返回字符串s匹配ere的起始位置,如果不匹配则返回0。该函数会定义RSTART和RLENGTH两个内置变量。RSTART与返回值相同,RLENGTH记录匹配子串的长度,如果不匹配则为-1。
1 2 3 |
$ awk 'BEGIN {print match("kodango", /dango/); printf "Matched at: %d, Matched substr length: %d\n", RSTART, RLENGTH;}' 3 Matched at: 3, Matched substr length: 5 |
system([command])
描述:执行系统command并将结果返回至awk命令。
1 |
$ awk 'BEGIN {system("uname -r");}' |
sprintf(fmt, expr, expr, …)
描述:类似printf,只不过不会将格式化后的内容输出到标准输出,而是当作返回值返回。
1 2 |
$ awk 'BEGIN {var=sprintf("%s=%s", "name", "value");print var}' name=value |
tolower(s)
描述:将字符串转换成小写字符。
1 2 |
$ awk 'BEGIN {print tolower("HELLO");}' hello |
toupper(s)
描述:将字符串转换成大写字符。
1 2 |
$ awk 'BEGIN {print toupper("Hello");}' HELLO |
如果需要自定义函数使用function关键字即可,格式如下:
1 2 3 |
function F_NAME([variable]) { Statements… } |
函数还可以使用return语句返回值格式为”return value”。
Awk支持管道或重定向
1 2 3 |
$ print items > output-file $ print items >> output-file $ print items | command |
特殊设备文件描述符
1 2 3 |
/dev/stdin #标准输入; /dev/stdout #标准输出; /dev/stderr #错误输出; |
1 2 |
$ awk -F: '{printf "%-15s %i\n",$1,$3 > "/dev/stdin"}' /etc/passwd $ awk -F: '{printf "%-15s %i\n",$1,$3 | "grep ^root"}' /etc/passwd |