玩技术,Geeker
一个原创技术文章分享网站

玩玩awk

从一道面试题说起

那是难堪的一幕,记得那是一次面试,面试官问我:

如何查看Linux系统上的所有用户?

我答曰:看看/home路径下有哪些文件夹,每个用户都有一个以自己用户名为名字的主目录。

面试官反问到:如果我在/home下随便建立一个目录,那就是说又会增加一个新的用户喽?用户的主目录只能在/home目录下喽?

我不语,只是感觉到好尴尬。好吧,结果还是不错的,面试成功了。回来以后,Google了一下这个问题的答案。

是时候写篇总结来告诉大家,我也在玩awk。

我也玩玩awk

如果还不知道awk,去Google一下吧。可能会吓到你,可以说是上古的东西了,应该比你的年龄都大了吧。就是这么神奇,就这么个“老古董”,在新技术层出不穷的今天,依然保持着它的那份活力和自信——存在即合理。

awk的优点和使用场合非常明确,它非常适用于处理结构化数据和生成表单,与sed和grep有些类似,但是功能远强大于二者,由于awk具备脚本语言的特点,也算是一门脚本语言,这篇文章将按照下图所示的展开总结。

果冻想

还等什么,撸起袖子,开干吧。

awk初体验

话又说回来,接着文章开头的那道面试,下面使用awk来完成这道题目。

awk 'BEGIN{
    FS=":"
    printf("%-10s%-20s\n", "UserName", "HomeDir")
    print "=============================="
}
{
    printf("%-10s%-20s\n", $1, $6)
}
END{
    print "=============================="
    printf("User(s):%d\n", NR)
    print "=============================="
}' /etc/passwd

对于awk脚本的运行,有两种方式:

  1. awk [POSIX or GNU style options] -f progfile [–] file
  2. awk [POSIX or GNU style options] [–] ‘program’ file

我上面的脚本使用的是第二种方式,我们也可以将脚本单独写入一个文件:

BEGIN{
        FS=":"
        printf("%-10s%-20s\n", "UserName", "HomeDir")
        print "=============================="
}
{
        printf("%-10s%-20s\n", $1, $6)
}
END{
        print "=============================="
        printf("User(s):%d\n", NR)
        print "=============================="
}

将上述脚本写入listUser.awk这个文件,然后在命令行执行:

awk -f listUser.awk /etc/passwd

对于上面这段awk脚本看不懂不要紧,看完下面的内容,你再回过头来看这段脚本,就会豁然开朗。

透过现象看本质

我还是通过上面的awk代码来说说awk的原理和本质,只有掌握了原理,以后和awk打交道时才会游刃有余。还是先上一副图。

果冻想

从上图中可以看出,awk在工作时主要分为以下三个部分:

  • BEGIN,也就是所谓的初始化模块,比如定义分隔符,初始化一些值等,它会在数据处理部分执行之前执行,并且只执行一次,这一部分是可选的;
  • 数据处理,这一部分也就是awk脚本的核心部分,它是一对以模式pattern与大括号括起来的操作action组合而成的,二者可能会出现以下组合:
    • pattern {action} 记录匹配对应的模式,则执行大括号中的操作
    • pattern 记录匹配对应的模式,则直接打印记录
    • {action} 对每一条记录都执行大括号中的操作

    数据处理模块会循环读取待处理文件中的记录,每次读取一条记录,处理完一条以后再读取下一条记录,直至所有记录被读取完毕。

  • END,是最终的收尾处理模块,它会在所有数据处理完成以后才执行,并且只执行一次;比如我们处理完数据了,需要输出一共处理了多少条记录,多少个字段等信息,就可以在END部分进行输出,这一部分也是可选的。

如果你想说这对awk工作原理的总结不够透彻,但是掌握这些,你就已经强于80%的人了,不是么?如果你还想知道的更多,去Google吧,因为再深了,我也不会,文章标题就已经表明了,我是以“玩”的态度来学习awk的,不必过分的在于细节,不求甚解。

入门第一步——记录与字段

记录和字段是awk中非常重要的两个概念,在我们处理文件时,总是会说到读入一条记录,然后根据分隔符,分隔成字段,我们先来把awk中的这两个概念搞清楚喽。

果冻想

上面也说了,awk特别适用于处理结构型的数据;在决定是否选择awk来处理数据时,请检查数据是否具有结构性,而不只是一串无规则的字符。就如上图所示,jelly:b3SooaHxlCicE:202:20::/home/jelly:/sbin/sh这就是一条记录,我们可以将:作为分隔符,分隔这条记录,就可以得到图中所示各个字段,这就是对awk中记录与字段最直白的解释。

同时,为了更方便操作字段,在awk中可以使用字段操作符$来指定字段,在$后面跟一个数字或变量就表示对应字段的内容,比如:

$0 #输出整个记录的值:jelly:b3SooaHxlCicE:202:20::/home/jelly:/sbin/sh
$1 #输出字段1的值:jelly
$6 #输出字段6的值:/home/jelly
$(1+2) #输出字段3的值:202

入门第二步——表达式

在awk中,我们可以向其它高级语言那样,很轻松的就可以使用表达式来进行计算、检索等操作。表达式由数字和字符串常量、变量、操作符、函数和正则表达式组成。

由于awk是脚本语言,没有高级语言中那些复杂的数据类型定义。在awk中定义变量时,每个变量都有一个字符串类型值和数字类型值,awk将根据表达式的前后关系选择合适的值,变量也可以不用初始化,awk自动将它们初始化为空字符串,如果当做数字使用时,其值则为0。例如以下代码:

a = 1 #将一个数字赋值给变量
b = "Hello World" #将一个字符串赋值给变量

定义变量以后,就需要说说各种运算操作了。先来看看算术操作符,在awk常用的算术操作符有以下这些:

操作符 描述 举例
+ awk ‘{x=2; y=3; print x+y}’
awk ‘{x=2; y=3; print x-y}’
* awk ‘{x=2; y=3; print x*y}’
/ awk ‘{x=2; y=3; print x/y}’
% 取模运算 awk ‘{x=7; y=3; print x%y}’
^ 幂运算 awk ‘{x=2; y=3; print x^y}’

再来看看和其它高级语言一样拥有的高级赋值操作符:

操作符 描述 举例
++ 变量加1,分为前置加和后置加 awk ‘{print x++}’
– – 变量减1,分为前置减和后置减 awk ‘{print –x}’
+= 将加的结果赋值给变量 awk ‘{print x+=1}’
-= 将减的结果赋值给变量 awk ‘{print x-=1}’
*= 将乘的结果赋值给变量 awk ‘{x=2; print x*=2}’
/= 将除的结果赋值给变量 awk ‘{x=4; print x/=2}’
%= 将取模的结果赋值给变量 awk ‘{x=5; print x%=2}’
^= 将幂运算的结果赋值给变量 awk ‘{x=2; print x^=3}’

很多时候,我们需要将两个字符串连接在一起,怎么搞呢?看看吧。

awk '{x="Hello" "World"; print x}'

在awk中就是通过空格来连接字符串,就是如此简单。

入门第三步——系统变量

在awk中有很多的系统变量,这些系统变量在我们编写awk脚本的时候会经常使用到,我现在将经常使用到的系统变量列举出来,并做简要说明。

变量名 描述 举例
$0 当前记录内容 awk ‘{print $0}’
$1~$n 分别保存着当前记录的字段1到字段n的内容 awk ‘{print $1, $2, $3}’
FS 字段的分隔符,默认是空格或Tab awk ‘BEGIN{FS=”:”}{print $1,$3,$6}’ /etc/passwd
NF 记录当前记录中的字段个数 awk ‘{print $0} END{printf(“Total Field(s):%d\n”, NF)}’ 201509.log
NR 已经读出的行数,从1开始计数;对于多个文件的情况下,该值会持续累加 awk ‘{print NR}’ 201508.log 201509.log
FNR 对于当前处理的文件来说,已经读出的行数;对于多个文件的情况下,该值是各个文件独自对应的行号 awk ‘{print FNR}’ 201508.log 201509.log
RS 输入的记录分隔符, 默认为换行符 awk ‘BEGIN{RS=” “}{print FNR}’ 201508.log
OFS 输出字段分隔符, 默认也是空格 awk ‘BEGIN{OFS=”\t”}{print 1,2, $3}’ 201509.log
ORS 输出的记录分隔符,默认为换行符 一般用的很少,此处不举例说明了
FILENAME 当前输入文件的名字 awk ‘{print FILENAME}’ 201509.log

入门第四步——操作符

这里主要总结awk中的关系操作符和布尔操作符,在两个表达式之间进行比较操作时,经常会用到这里总结的操作符。

操作符 描述
< 小于
> 大于
<= 小于等于
>= 大于等于
== 等于
!= 不等于
~ 匹配

布尔操作符

操作符 描述
|| 逻辑或
&& 逻辑与
! 逻辑非

由于操作符经常用于流程判断,在后面的流程判断中将结合这里总结的操作符进行实例分析。

入门第五步——格式化输出

我们使用awk的目的是什么?从复杂的数据中抽取出我们需要的数据,并展现出来,以便阅读。为了方便阅读,就需要输出的格式是可控的,是可自定义的。为此,在awk中也有一个和C语言一样的函数——printf。我们可以使用这个函数来进行输出格式控制。至于具体的控制选项,自己Google研究去吧。

入门第六步——参数传递

我们可以向Shell脚本传递参数,这样可以带来极大的扩展性和代码的复用,那么如何向awk脚本传递参数呢?

在awk中,参数将值赋给一个变量,这个变量可以在awk脚本中访问,具体的使用方式如下:

awk [POSIX or GNU style options] -f progfile var1=value1 var2=value2 file ...

awk [POSIX or GNU style options] 'program' var1=value1 var2=value2 file ...

其中的var1为参数变量,value1为参数值;以此类推,var2为第二个参数变量,value2为第二个参数的值。这些变量必须要放在脚本的后面,待处理文件名的前面,而且在给变量赋值时,等号的两边不允许出现空格。这就是向awk传递参数的方法。

有的时候,我们在Shell脚本中会使用awk脚本,我们可以将Shell脚本的参数直接传递给awk脚本,例如:

awk -f script.awk "var1=$1" "var2=$2" file

其中$1$2分别为Shell脚本的第一个参数和第二个参数;是的,我们可以直接这么传递参数,没有任何问题。

虽然我们可以向awk脚本中传递参数,但是有一个不幸的消息——我们传递的参数在BEGIN过程中是不可用的。也就是说,我们从命令行传递的任何参数值,在BEGIN过程中,都无法得到我们传递的值,直到首行输入完成以后,命令行参数才是可用的。所以呢,你不要想着写出类似下面的代码:

awk -f script.awk var=';' file

script.awk脚本

BEGIN{FS=var1}{....}

但是,解决问题的方法还是有的,如果你感兴趣,可以去看看-v选项。

进阶第一步——流程判断

awk中的流程判断和C语言几乎是一致的,这让熟悉C语言的人非常容易上手。

if (expression1)
{
    action1
}
else if (expression2)
{
    action2
}
else
{
    action3
}

在上面总结了那么多操作符,这里重点对~匹配操作符进行演示:

awk '{
    if ($0 ~ /[Yy](es)?/)
    {
    	print "Yes"
    }
        else if ($0 ~ /[Nn]o?/)
    {
        print "No"
    }
    else
    {
        print "No Match"
    }
}'

当输入的记录匹配指定的正则表达式,则表达式的值为真,否则为假;这种匹配玩法在C中可是没有的哦。

在awk中还提供了一个三元操作符,这让我很吃惊,的确很方便:

expr ? action1 : action2

简单的说就是如果expr为真,就返回action1的执行结果,否则返回action2的执行结果。

awk '{
    result = (var >= 60 ? "OK" : "FAILED")
    print result
}' var=50

进阶第二步——循环

在awk中支持whiledo{...} whilefor循环。

# while循环
while (condition)
{
    action
}

# do{...} while循环
do
{
    action
} while(condition)

# for循环
for (i = 1; i < 10; ++i)
{
    print i
}

还是和C语言那样,awk中也有影响循环的continuebreak关键字。

进阶第三步——数组

在awk中,所有的数组都是关联数组。关联数组的特点是它的下标可以是一个字符或一个数值。我们定义一个数组时,不必指明数组的大小,只需要为数组指定下标,然后赋值即可。比如:

awk '{
    array[1]="HelloWorld"
    array["Jelly"]="JellyThink";
    print array[1]
    print array["Jelly"]
}'

下面创建一个数组,并使用一种类似高级语言中的枚举器的东西来打印数组:

awk '{
    for (i = 1; i <= 10; ++i)
    {
        array[i] = i * 2
    }

    for (value in array)
    {
        print array[value]
    }
}'

有的时候,我们需要判断数组是否包含指定的下标,我们可以使用以下代码来实现:

awk '{
    for (i = 1; i <= 10; ++i)
    {
        array[i] = i * 2
    }

    if (2 in array)
    {
        print "FOUND"
    }
    else
    {
        print "NOT FOUND"
    }
}'

进阶第四步——函数

函数的内容不少,请参见这篇《awk中的函数》。

总结

总结这篇文章花了好长的时间啊,总算把awk相关的东西都总结的差不多了,自己也算把awk又重新学习了一遍,希望我的这篇文章也能够帮助到你。

2015年10月19日 于呼和浩特。

打赏

未经允许不得转载:果冻想 » 玩玩awk

分享到:更多 ()

评论 5

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #2

    不应该是/etc/passwd么

    bobo2年前 (2015-12-03)回复
  2. #1

    cut -d: -f 1 /etc/passwd,博主回答问题,让我想起了多年前,朋友问我,怎么修改系统默认的shell,我答:每个系统默认shell不一样,修改得重装系统吧。。。。

    博客很赞1个月前 (06-13)回复

在这里玩技术,享受技术带来的疯狂

捐赠名单关于果冻