Coding Poet, Coding Science

Awk工具与编程语言

说明

刚接触unix相关的书籍的时候,里面有一章是专门介绍awk的使用的。但当时觉得它过于复杂,很难记住它是什么,能干什么。想来是因为我知识的储备还不够。Awk主要的思想就是,它的确是一种设计得非常精巧的编程语言,很好地满足了实际应用的同时,也能够让我们以软件工程,编译原理等理论性的东西欣赏它。因此,我就知道awk的使用不再是一件困难的事情。

awk基础

有些东西,理解它的过程,就好像是要进入一个豪华的庭院,要进入门内,非得绕过门前的屏风墙。不这样,就看不到里面的风景。

awk本身的语法是比较怪异的。然而外面的人看到的,往往是用awk写出来的程序,而非程序的规范。所以就不得要领。人们又常常抱着学习awk就直接是为了处理文本的功利目的,一旦不能一眼看出来它在处理文本上的直观,就放弃了。但是,绝大部分事情,是不能凭感官的。我们必须要有一种理论的指导。

让我们忽略掉awk的语法形式,单纯地考虑,如果是我们,将会怎样处理文本,特别是在C语言的框架下编写这样的程序。将能设想到,基本的方法,还是通过传递变量。也就是至多每读取一行,就自动将参数匹配到指定的位置。因此整个awk程序,首先看成是一个函数。宿主环境代理地读取目标文本,并在每次调用函数的时候得到参数。文本处理的思想,也就只能如此了。

另外,我们看到,awk在文本处理上做得更深一步。它本身是一个编译型的语言。也就是说,在文本没有读取之前,宿主环境先进行语法等方面的检查,通过之后才开始处理程序。这一点与 make 程序相似。因此,首先应当考虑到awk是如何检查脚本的,再考虑脚本如何执行文件。命令

awk -f AWK脚本.awk 数据文件

可以与

python py脚本.py 参数

相类比。

通常调用awk的时候,AWK脚本可以是即时编辑的,也就是脚本的内容直接出现在命令行的第一个参数中。把这个功能看成是一个语法糖,也许就顺畅多了。

awk程序的执行流程是:

  1. 读取指定的脚本命令并进行语法的检查;
  2. 从指定的数据文件中读取一个数据行;
  3. 自动更新相关的内建变量的值,如 $0 , $1 , NR
  4. 依次执行程序中每个 Pattern { Actions } 指令;
  5. 处理完当前行之后,读取下一个数据行,自动返回步骤三继续执行;
  6. 文件处理完毕,结束运行。

Awk主要概念

Awk程序的主要语法是 Pattern { Actions } 。一个Awk程序常常由许多这样结构的语句构成。在执行的时候, Pattern 主要是确定一个逻辑表达式,若数据行符合这样的条件,就执行相应的 Actions ;不符合条件则执行下一个 Pattern { Actions } 语句。如果 Pattern 为空,则表示无条件执行该语句的 Actions

Pattern 部分中,可能最需要介绍的部分是Awk的关系运算符。Awk本身是用C语言编写的,而且继承了其中常见的关系运算符,如 ><>=<===!= 。此外,Awk还用运算符与!表示字符串与正则表达式的匹配。 BEGINEND 是特殊定义的模式匹配语句,分别表示在第一个记录之前,与最后一个记录之后。

Actions 部分,主要是许多Awk指令。Awk的指令与C语言也十分相似,大多数时候,只是改用Shell那样的传参方式,而非C语言的函数。Awk的指令大致可分为普通指令与控制指令两类。前者如 print , getline ;后者如 if(...){...} else{...} , while(...){...}... 。除此之外,C语言赋值表达式往往也继续有效。

遇到 Pattern { Actions } 语句,Awk会先计算 Pattern 的值,若计算出的值为 true 、非零的数字或者非空字符串,则awk将执行后面的 Actions 。在其它条件下,将跳过相应的 Actions 。这一过程,可以与路由器上的ACL处理流程相类比。

为了方便对数据行的处理,Awk提供了许多内建的变量。常见的变量如下:

变量符号 含义
$0 awk当前所读入数据行的所有内容
$1 当前所读入数据行的第一个字段的内容,下个字段的变量依次类推
$NF Number of Fields。表示当前读入数据行 $0 含有的字段数
$NR Number of Records。表示已读入的数据行的数目
$FILENAME 当前文件的名称

注:awk的变量并不需要用$符号括住,用以字母打头的英文字符即可。$实际上是awk的一元运算符,它以一个数字为参数,表示操作“取字段”。

awk处理数据的时候,自动从数据文件中每次读取一行记录,然后将记录切分成若干个字段,程序可使用$1$2等直接取得字段中的内容。若要简单打印出相应字段,使用print $1, $2这样的语句即可。

此外,awk还提供 printf() 指令,它的使用方式与C语言更相近,用于格式化字符串。

比如:

awk '
{ printf("%d times %d is %d.\n", $1, $2 ,$1 * $2) }
'
234234234 234234234

将显示出两个字段的乘积。从上面的例子我们也看出,awk的整型运算功能是非常强大的,可以进行非常大的整数之间的运算。可以认为具有无限的精度。

作为一个领域专用的语言,在变量的使用上,要尽可能足够简单。所以awk的变量,不需要宣告就能够使用,类型是自动确定。并且,awk提供的“字典”(有人称为数组)类型,也可以自动使用。引用字典不存在的键值的时候,该键值会自动被创建。

awk与shell指令

在awk中使用shell指令是比较方便的。其思想是让字符串通过管道运算符|与awk内置的函数运算。字符串被当成指令执行,执行后的输出的结果,被当成内置函数的运算符。比如

"who" \| getline

getline 是awk的输入指令。当 PatternBEGINEND 的时候, getline 默认从 stdin 读取数据,否则从awk正处理的数据文件上读取数据。 若是后者,将会使数据文件中的两行当成一个记录来处理。

使用getline之后,变量 $0 , $1 等的内容会被刷新。

在awk中,输出文件则使用重定向运算符 >>> 。它可以用在任意 Action 之后。重定向运算符可以理解成一个普通的 Action 与一个字符串运算,而字符串被解释成文件的路径。

awk也提供了一个 system() 函数以调用shell命令。

注意,在执行shell指令的时候,相同的字符串代表同一个pipe名称,因此尽管出现在不同的 Actions 当中,shell命令可能只启动一次。具体意思是,比如 print $1, $2 > "sort -k 1" 这一语句,并非每执行一个 print ,就启动一个 sort -k 1 进程。而是所有 print 出来的内容,都送到 sort -k 1 这个程序的输入管道中,因而所有 print 出来的内容都输入同一文件流中,被 sort -k 1 处理。要想关闭此管道,必须显式地使用 close() 函数。

awk程序的结构

一般而言,awk当中都是 Pattern { Actions } 指令。但是还有其它类型的语句,包括注释与函数定义。注释以#号开头的行,而函数字义与C语言类似,比如 function double(x) {return 2*x}

其它

在写awk程序的时候,可能会用到许多字符串函数,如 substr() , length() 。一般而言,是不可缺少的。

awk读取文件的时候,使用FS作为字段分割符。它可以取一个正则表达式, 但默认情况下都是空白字符。若不想在启动awk时指定 FS ,可以在 BEGIN 对应的 Actions 中更改 FS 的值。

awk读取文件的时候,使用 RS 作为记录分割符。默认 RS 是字符 \n 。但是我们也可以改成其它的符号。特别地,当定义 RS="" 时,分割符是空白的行。此时,中间无论多少空白行,都被当成是记录分割符。

awk程序也可以有参数。这些参数在程序中通过 ARGV[]ARGC 标识。

awk中所有的变量都默认是全局变量,除了函数参数之外。因此在写递归程序的时候,有一些编程上的技巧。