山地人

Linux awk命令

山地人
山地人
2021-05-24

这是一篇关于awk的快速入门教程,通过学习这篇教程,你会收获这些技能:

  1. 为何要学awk?
  2. 理解awk的内在逻辑。
  3. 如何使用awk帮你处理业务?

体验”awk在线成绩”查询

先来直观体验下下面这个awk写的小程序,也许能改变一些你对awk的印象。

点击启动,然后点击运行,启动我们的模拟查分应用

是不是有点意思,原来awk可以这样用。是不是迫不及待要搞清楚这个程序是怎么写的。

如果你对awk完全是个新手,那么学完这篇awk教程,你也能做出这样的小应用了。

awk的由来

awk这个工具的名称并没有实际的含义,awk这个名字取自最初的三位开发者:Alfred Aho,Peter Weinberger 和 Brian Kernighan(注意我加粗的三个字母,合起来就是awk)。

学awk的意义

既然你正在阅读这篇文章,那说明你对awk多少有一些了解。

这是一门非常强大的文本处理工具或者说处理文本并输出报告的编程语言。没错它是一门编程语言。在awk中你可以使用C语言的运算符,它也可以处理字符串,awk内置了一个伪C的解释器。因此awk异常灵活强大,当然在拥有这么多好处的同时,它最大的缺点就是语法相对复杂,有一定的学习成本。

也正因如此,本篇教程的目的就是帮助你跨过awk的这道门槛,带你入门awk

理解awk的模式

很多awk的教程在教你使用awk的时候都只告诉你具体的使用方式,按照这些现有的例子,虽然你也能实现一些功能。但是你并没有真正理解awk的内在设计,因此当你想要灵活应用的时候,就变得困难了。为了让你更好的学会awk,我会努力把awk的一些设计上的原理尽量传达给你,让你能够理解awk,最终真正掌握awk

awk的结构

awk命令即可以以命令行的方式直接在终端运行,也可以写在脚本文件中通过执行脚本来运行。

下面我们先看看命令行模式的awk的语法结构:

awk 参数 '开始代码块 主体代码块 结束代码块' 待处理文件
  • 参数 可以给awk设置一些特定功能的参数。
  • ' 脚本内容 '每条awk命令,都必须放置在一对单引号之间。
  • 一条完成的awk命令,包含三块代码:开始代码块主体代码块结束代码块
    • 开始代码块用于在正式开始处理文本之前,做一些事前动作,比如打印个表格头部。 开始代码块的格式是:BEGIN { awk命令 }
    • 主体代码块是用来处理文本的主体代码,对于读取的文本的每一行,先进行模式匹配,对比配的内容,使用后续{}中的命令进行处理。 主体代码块的格式:模式 { awk命令 }
    • 结束代码块用来做文本处理后的一些善后工作,比如输出一个表位。 结束代码块的格式是:END { awk命令 }
  • 待处理文件是指定要被处理的文件的路径

上面的awk所涉及的每个部分都是可选的,

到这里,你应该对awk的结构有了大致的了解,带着这些结构的概念,我们从最简单的awk开始讲起,循序渐进直到完全掌握awk

最简单的awk

既然awk是一门编程语言,遵照编程语言的传统,先来打印一个awk版的Hello World

Hell World

这里使用BEGIN代码块来输出Hello World

awk 'BEGIN { print "Hello World" }'

启动终端,试试这条命令。

打印文件里的Hello World

假设有一个hello.txt文件,这个文件的内容是

Hello World

下面我们要用awk输出这个hello.txt文件中的内容。

awk '{ print }' hello.txt

启动终端,试试这条命令。(注意:hello.txt已经为你在当前目录下创建好了)

如果出现错误,注意引号要用英文输入法下的单引号。实在不行,你也可以复制上面的命名进行试验。

相信你已经完成了第一个Hello World输出的练习。

这里对这条命名稍微做下解释:

awk '{ print }' hello.txt
  • '{ print }' 利用的是前面讲过的主体代码块部分,并且只使用里里面的命令print
  • 结尾的hello.txt指定了要处理的文件的路径,是当前目录下的hello.txt

打印管道传来的Hello World

除了在awk末尾指定hello.txt这种具体的文件句方式,你也可以通过Linux里的管道|命令,将要处理的内容通过|管道发送给后面的awk命令来处理。

echo "Hello World" | awk '{ print }'

启动终端,试试这种写法。

带头尾的Hello World

这次,我们要对这个Hello World显示进行升级,我们希望最终可以打印出下面这样的更美观的版本:

-==========-
Hello World
-==========-

也就是在Hello World正文的前后分别加上一行-==========-的装饰。

这里你是否已经想到,利用前面说过的头部代码块尾部代码块

awk 'BEGIN {print "-==========-"} { print } END {print "-==========-"}' hello.txt

启动终端,进行试验。

到这里,我们已经接触过了awk脚本的三个主要模块了:BEGIN { 命令 }、主体块模式 { 命令 }END { 命令 }

awk实战

下面,我们进一步深入,做一些实战应用。

选择指定列

下面有个学生成绩表scores.txt,现在需要只显示展示语数英三门科目的成绩。

学号 姓名 语文 数学 英语 化学 物理 历史 地理
1. 张三 85 93 78 89 78 92 84
2. 李四 93 76 90 75 92 77 90
3. 王五 95 65 88 86 84 81 85
4. 赵六 76 97 78 71 77 90 79
5. 孙七 84 68 86 94 68 84 92

awk会按照空格,将一行数据分成多列,对于其中的每一列数据,awk都有一个变量与值对应。 第一列是$1,第二列是$2,以此类推。至此,我们这个问题有了解决思路了。

awk '{print $1 $2 $3 $4 $5 }' scores.txt

启动终端,进行试验。(注意:scores.txt以放置在)

做完实验,是不是发现了什么不对劲的地方。每一列的文字都在了一起。

输出列间隙

试试下面这个写法,在每一列之间加一个,英文的逗号。

awk '{print $1,$2,$3,$4,$5 }' scores.txt

启动终端,再次实验。(每一次的练习,都会让你加深理解)

还是有个小问题,如果你是哪种追求完美的人,可能觉得每一列不够对齐。这时我们需要在每一列之间加入’\t`制表符。

控制对齐列

在awk中,我们可以结合\t来完成这个任务。

awk '{print $1,"\t",$2,"\t",$3,"\t",$4,"\t",$5 }' scores.txt

启动终端,再来一次。

这次,awk工整地打印出了我们所期望的效果。

另外,我们也可以使用printf函数结合\t来完成这个任务。

awk '{printf "%s\t%s\t%s\t%s\t%s\n",$1,$2,$3,$4,$5 }' scores.txt

这里需要注意,使用printf时,后面的字符串结尾的换行是需要我们主动添加\n来换行的,而之前使用的print是会自动加上换行的。

你可以在上面的终端里进行测试。

过滤行

这时,又有了一个新的需求,我们想要查看某个同学的成绩,比如:查找赵六的全部课程的成绩。如果这是一张全年级的成绩表。手工查找比较慢,我们希望利用awk来帮我们做这件事。

这里就要用到模式匹配了。我们需要匹配第2列的值为赵六这个词的行,所以最终的语句如下:

awk '$2=="赵六" { print }' scores.txt
  • 这里的$2=="赵六"就是一个模式匹配,匹配第二列值为"赵六"的行记录。
  • { print }是匹配后的执行语句,打印整行数据。

启动终端,试一试。

是不是一下子就找出了赵六的成绩了。

可能对于追求完美的人来说,可能希望能够保留第一行的标题。这里有一种思路,继续使用模式匹配,如果第二列的值是学号或者赵六这样的行就整行打印出来。基于这种思路,就有了下面的命令语句。

awk '$2=="姓名" || $2=="赵六" { print }' scores.txt

这里的||是或者的意思,如果你学过一些编程,应该会了解这就是编程里的运算符。||前后两个条件有一个满足条件,这个过滤就通过了,当前这行记录就会被打印。

启动终端,抓紧测试一下。

当然,你可以继续对这行语句进行优化,输出列对齐更好的版本。

统计数据

这时,我们收到一个新的需求,语数英三门主课老师想要知道每门课的平均分。我们需要对除了第一行的其它行进行数据统计。最后把统计结果输出。

这里我们可以利用BEGIN代码块做变量初始化,然后在主体代码块收集每一列的数据,最后在END代码块输出统计结果。

因此,我们得到了下面的这个awk脚本。

awk 'BEGIN {
chinese=0
math=0
english=0
count = 0
print "学号\t姓名\t语文\t数学\t英语"
}
$2!="姓名" {
chinese += $3
math += $4
english += $5
count += 1
print $1,"\t",$2,"\t",$3,"\t",$4,"\t",$5
}
END {
print "-------------------------------------------"
print "平均分", "\t", "共" count "人", "\t" , chinese/count , "\t" , math/count , "\t" , english/count
}' scores.txt

分析过程

乍一看代码有些长,但自学分析,其实都是应用我们之前学过的知识点。

  • 首先在BEGIN代码块中初始化要做统计的变量,并打印输出表头。
  • 主题代码块中统计行数count和累计每一门课程的得分,并输出对应行记录。
  • END代码块中输出平均得分情况。

启动终端,进行实验。

制作脚本文件

对于这种比较长的脚本,我们可以将其保存成一个脚本来执行,方便修改以及日后再次使用。

内置变量

前面我们已经学过了一种内置变量$n,在awk中内置了很多内置变量,你可以通过查看awk的手册,找到这些内置变量。

man awk

这里我做了一个整理:

内置变量英文全称说明
ARGCarguments count命令行参数数量
ARGVarguments value命令行参数数组
CONVFMTconvert format数字转换为字符串的内部转换值,初始值为 %.6g
ENVIRONenviroment环境变量构成的索引数组,ENVIRON[var] = value
FILENAMEfile name当前输入文件的文件名
FNRfile number of records当前文件的当前行记录值
FSfield separator字段分割符默认是空格
NFnumber of fields当前记录的分段数(列数)
NRtotal number of records已读出记录的当前行数
OFMToutput format数字的输出格式(默认为 %.6g )
OFSoutput field separator输出字段分隔符,默认和输入字段分隔符一致(空格)
ORSoutput record separator输出记录分隔符,默认是换行符\n
RLENGTHmatch函数匹配的字符串长度
RS[input] record separator输入记录分隔符,默认是换行符\n
RSTARTmatch函数匹配的字符串的起始位置
SUBSEP构建多个数组下边,初始值\034

乍一看,这个表格里的内容有些不好理解,我们还是通过实验来搞清楚每一项的含义:

打印命令行参数信息

打印当前文件信息

FILENAME这个变量必须在awk正在处理文件时才能获取,因此必须在主体代码块中测试。

打印全部环境变量值

下面会使用for in语句来遍历ENVIRON中的每一项。

打印其余信息

到这里,我们已经把内置变量都理解了一遍,剩下的就是在编程的过程中,对这些内置变量进行灵活运用。

内置函数

awk中的内置了5大类函数:算术函数、字符串相关函数、时间函数、位操作函数和一些其他函数。

算术函数

函数说明
int(x)对x取整数
sqrt(x)求x的平方根
exp(x)求e^x
log(x)求x的对数
sin(x)求x的正弦值
cos(x)求x的余弦值
atan2(y,x)求x的反正切
rand()0-1之前的随机数
srand(expr)设置随机种子

字符串函数

函数说明
gsub(r,s,t) gsub(r,s)全局搜索子字符串,t中每个匹配正则表达式r的值都用s来替代,最后函数返回替换的次数。如果t未指定,则使用$0替代。
index(s,t)如果t是s的子字符串,返回在s中出现的第一个t的位置,否则返回0,s中第一个字符的位置记为1
length(s)返回字符串或数组长度
match(s,r)返回s中最长匹配r正则的子字符串,并会把RSTART和RLENGTH值设置为匹配的子字符串的起始位置和长度
split(s,A,r) split(s,A)使用r正则对s进行分割,分割后的元素放入A数组中,并返回分割成的元素的数量
sprintf(format, expr-list)根据format格式返回一个字符串,
sub(r,s,t) sub(r,s)只搜索一次子字符串,功能类似于gsub
substr(s,i,n) substr(s,i)返回新子字符串,内容为原s中起始索引为i,长度为n的子字符串,无没有提供n,则返回起始索引为i一直到源字符串末尾的子字符串
tolower(s)返回新字符串,将s中的字母全转为小写
toupper(s)返回新字符串,将s中的字母全转为大写

动手试试各种字符串相关函数

时间日期函数

函数说明
mktime(specification)用指定格式创建一个当前时间的时间戳YYYY-MM-DD HH:MM:SS
strftime([format [, timestamp [, utc ]]])使用指定格式,对给定时间戳构建时间字符串
systime()返回系统当前时间(秒数),从1970-01-01 00:00:00 UTC至今

动手试试时间函数

正则表达式

输入和输出

awk中的输出函数,我们前面已经使用过很多次了,printprintf函数。

输入函数,通过getline实现。

# 直接读取到$0中
getline
# 从file路径中读取到$0
getline < file
# 从file文件读取到var变量中
getline var < file
# 通过管道将command命令结果读取到$0
command | getline
# 通过管道将command命令结果读取到var
command | getline var

自定义函数

在awk中,我们可以定义函数。需要注意的是函数不能定义在BEGIN{}{}END{}这种语句块内。

function 函数名(参数){
return 返回值
}

下面我们定义一个sum求和函数

控制语句

awk支持四种主流的逻辑控制语句:if-elsewhiledo-whilefor。如果你有C/C++JavaScript或者Java这些主流编程语言。那这些逻辑控制语句你也就已经掌握了。当然为了照顾零基础的阅读者,我还是有必要列举下这几种控制语句的常用场景。

if-else语句

对于if语句中只有一行语句的情况,if后面的那对{}中括号可以省略。

awk 'BEGIN {
score=80
if(score>=60)
print "合格"
else
print "不合格"
}'

启动终端,动手试试

for循环语句

awk支持两种for循环语句

普通的for语句和for in语句。

awk 'BEGIN {
nums[0] = 100
nums[1] = 200
nums[2] = 300
nums[3] = 400
for(i=0;i<length(nums);i++)
print nums[i]
}'

另外如果你对语句顺序没有要求,也可以使用更为方便的for in语句。

awk 'BEGIN {
nums[0] = 100
nums[1] = 200
nums[2] = 300
nums[3] = 400
for(key in nums)
print nums[key]
}'

启动终端,动手试试

while和do-while循环语句

while循环

while条件一直成立时,循环就会一直执行,直到最终条件不成立,退出循环。

while(条件){
动作语句
}

我们使用awkwhile循环连续输出几个数字

awk 'BEGIN {
i = 1
while(i<10){
print i
i++
}
}'

do-while循环

和while循环的区别是,do-while循环是先进入循环体内部,在每次循环结束时进行条件检查。而while是先检查条件是否成立,再进入循环体内。

do{
动作语句
}while(条件)

上面的例子改成do-while语句是这样:

awk 'BEGIN {
i = 1
do{
print i
i++
}while(i<10)
}'

启动终端,动手试试。

break 和 continue

在所有的循环语句中,都可以使用breakcontinue

下面的例子中原本while是一个无限循环语句,因为while(1==1)这个条件永远成立,通过break语句的接入,在循环体内当i==10时,break语句会跳出外层的while循环。

continue语句则是忽略本次循环体内continue之后的语句,执行下一次循环。

awk 'BEGIN {
i = 1
do{
print i
i++
if(i==10) break
}while(1==1)
}'

启动终端,动手试试。

exit

这个函数可以用来立即退出当前awk程序,并可以设置一个退出返回码。

exit(返回码)

返回码如果设置为0表示正常退出,非零表示异常退出。

至此,本篇教程也到了该和你说再见的时候了,我们下期再见。

学完本篇互动教程,如果你觉得体验不错,可以把网页链接发送给你的小伙伴,让他/她也来感受一下。当然,你也可以继续看看网站上其他的的互动教程,希望`idev365`能够给你带来收获。

学习教程的过程中碰到了问题,或者对idev365有什么改进意见和想法,欢迎加入idev365微信内测群,和山地人交流你的想法。