awk - 模式扫描与处理语言

8:53:00 PM 0 Comments

1. 介绍

awk 是设计用来使很多常见的信息检索和文本操作任务易于陈述和进行的一门编程语言。

awk 的基本操作是依次扫描一组输入文件,查找匹配与用户已经指定的模式集合中的任何一个模式相匹配的行。对于每个模式,都可以指定一个动作;这个动作将在匹配这个模式每一行上进行。

尽管读者熟悉的 UNIX ® 程序 grep 也认可这种方式,在 awk 中的模式可能比 grep 中的模式更加一般性,而且允许的动作比只是打印匹配的行更加复杂。例如,awk 程序

{print $3, $2}

依次打印一个表格的第三和第二列。程序

$2 ~ /A|B|C/

打印在第二列是 A、B 或 C 的所有输入行。程序

$1 != prev { print; prev = $1 }

打印第一个字段不同于前面的第一个字段的所有的行。
1.1. 用法

命令

awk program [files]

在指名的一组文件上,或标准输入上、如果没有指定文件的话,执行字符串 program 中的 awk 命令。语句也可以放置到一个文件 pfile 中,并用如下命令执行。

awk -f pfile [files]

1.2. 程序结构

awk 程序是如下形式的语句序列:


模式 { 动作 }
模式 { 动作 }
...

输入的每行都要依次针对每个模式做匹配。对于每个匹配的模式,执行相关的动作。在所有模式都已经测试过了的时候,取回下一行并从头开始做匹配。

模式或动作二者都可以但不能同时省略。如果一个模式没有动作,简单的把匹配的行复制到输出。(所以匹配多个模式的行可能被打印多次)。如果一个动作没有模式,则这个动作在所有输入上进行。不匹配模式的行被忽略。

因为模式和动作都是可选的,动作必须被包围在花括号中来区别于模式。
1.3. 记录和字段

awk 输入被分解成了终止于记录分隔符的“记录”。缺省的记录分隔符是换行,所以缺省的 awk 一次处理它的输入中的一行。当前记录的数可在命名为 NR 的变量中得到。

每个输入记录被当作分解成了“字段”。字段通常用空白也就是空格或 tab 来分隔,但是输入字段分隔符是可以变更的,这在后面会有所描述。字段被引用为 $1、$2 ,以此类推。这里的 $1 是第一个字段,而 $0 是整个输入记录自身。字段可以被赋值。在当前记录中字段的数目可以在命名为 NF 的变量中得到。

变量 FS 和 RS 分别指定输入字段和记录分隔符;它们可以在任何时候被改变为任何的单一字符。也可以使用可选的命令行参数 −Fc 来设置 FS 为字符 c。

如果记录分隔符为空,把空输入行作为记录分隔符,并把空格、tab 和换行作为字段分隔符处理。

变量 FILENAME 包含当前输入文件的名字。
1.4. 打印

一个动作可以没有模式,在这种情况下动作在所有行上执行。最简单的动作是打印某些或所有的记录;这可以通过 awk 命令 print 来完成。awk 程序

{ print }

打印每个记录,也就是把输入完好的复制到输出。更有用的是打印来自每个记录的一个字段或某些字段。例如

print $2, $1

按逆序打印前两个字段。在 print 语句中用逗号分隔的项,在输出的时候会用当前输出字段分隔符分隔开。没有用逗号分隔的项会串联起来,所以

print $1 $2

把第一个和第二个字段合在一起。

可以使用预定义的变量 NF 和 NR;例如

{ print NR, NF, $0 }

打印出前导了记录数和字段数的每个记录。

输出可以被转向到多个文件中;程序

{ print $1 >"foo1"; print $2 >"foo2" }

写第一个字段 $1 到文件 foo1 中,写第二个字段到文件 foo2 中。还可以使用 >> 符号:

print $1 >>"foo"

添加输出到文件 foo。(在每种情况下,输出文件都在必要时建立)。文件名可以是一个变量或字段,同常量一样;例如

print $1 >$2

使用字段 2 的内容作为文件名字。

自然的,有对输出文件数目的限制,目前是 10 个。

类似的,输出可以用管道导入到(只在 UNIX 上的)其他进程;例如,

print | "mail bwk"

把输入邮递给 bwk。

可以使用变量 OFS 和 ORS 来改变当前输出字段分隔符和输出记录分隔符。输出记录分隔符被添加到 print 语句的输出后面。

awk 还提供 printf 语句用于输出格式化:

printf format expr, expr, ...

依据在 format 中的规定格式化在列表中的表达式并打印它们。例如,

printf "%8.2f %10ld\n", $1, $2

打印 $1 为 8 位宽的小数点后有两位的浮点数,打印 $2 为 10 位长的长十进制数,并跟随着一个换行。不自动生成输出分隔符;你必须自己增加它们,如这个例子那样。这个版本的 printf 同于 C 语言所使用的。
2. 模式

在动作之前的模式充当决定一个动作是否执行的选择者。有多种多样的表达式可以被用做模式: 正则表达式,算术关系表达式,字符串值的表达式,和它们的任意的布尔组合。
2.1. BEGIN 和 END

特殊模式 BEGIN 匹配输入的开始,在第一个记录被读取之前。模式 END 匹配输入的结束,在最后一个记录已经被处理之后。BEGIN 和 END 从而提供了在处理之前和之后获得控制的方式,用来做初始化和总结。

作为一个例子,可以如下这样把字段分隔符设置为冒号

BEGIN { FS = ":" }
... 余下的程序 ...

或如下这样输出输入行的计数

END { print NR }

如果 BEGIN 出现,它必须是第一模式;END 必须是最后一个模式,如果用到了的话。
2.2. 正则表达式

最简单的正则表达式是包围在斜杠内的文字的字符串,如

/smith/

这实际上是个完整的 awk 程序,它将打印包含名字“smith”的任何出现的所有行。如果一行包含“smith”作为一个大单词的一部分,它也会被打印,比如

blacksmithing

awk 正则表达式包括在 UNIX 文本编辑器 ed 和 grep 中能找到的正则表达式形式(没有后引用)。此外同 lex 一样,awk 允许采用圆括号用做组合,| 用做选择,+ 用做“一或多个”,? 用于“零或一个”。字符类可以简写: [a−zA−Z0−9] 是所有字母和数字的集合。作为例子,awk 程序

/[Aa]ho|[Ww]einberger|[Kk]ernighan/

将打印包含名字“Aho”、“Weinberger”或“Kernighan”中任何一个、不论首字母是否大写的所有行。

(带有上述扩展的)正则表达式必须包围在斜杠中,同 ed 和 sed 一样。在正则表达式内,空白和正则表达式元字符是有意义的。要去掉某个正则表达式字符的特殊意义,可前导一个反斜杠。一个例子模式

/\/.*\//

它匹配包围在斜杠内的任何字符串。

你还可以通过算符 ~ 和 !~ 指定任何字段或变量匹配(或不匹配)一个正则表达式。程序

$1 ~ /[jJ]ohn/

打印第一个字段匹配“john”或“John”的所有行。注意它还会匹配“Johnson” 和“St. Johnsbury”等等。要精确的限制它为 [jJ]ohn,使用

$1 ~ /^[jJ]ohn$/

这个脱字符号 ^ 指称一行或一个字段的开始处;美元号 $ 指称结束处。
2.3. 关系表达式

awk 模式可以是涉及常用的关系算符 <、<=、==、!=、>=、> 的关系表达式。 例子

$2 > $1 + 100

它选择第二个字段至少比第一个字段大 100 的行。类似的

NF % 2 == 0

打印有偶数个字段的行。

在关系测试中,如果操作数(operand)都不是数值,则做字符串比较;否则做数值比较。所以

$1 >= "s"

选择开始于 s、t、u 等字符的行。在缺乏任何其他信息的情况下,字段被当作字符串,所以程序

$1 > $2

将进行字符串比较。
2.4. 模式的组合

模式可以是模式的使用算符 ||(或)、&&(与)和 !(非)的任意布尔组合。例如

$1 >= "s" && $1 < "t" && $1 != "smith" 选择第一字段开始于“s”而不是“smith”的行。&& 和 || 保证它们的操作数会被从左至右的求值;在确定了真或假之后求值立即停止。 2.5. 模式范围 选择一个动作的“模式”还可以由用逗号分隔的两个模式组成,比如 pat1,{ ... } pat2 在这种情况下,这个动作在 pat1 的一个出现和 pat2 的下一个出现之间(包含它们)的每个行上进行。例如, /start/, /stop/ 打印在 start 和 stop 之间的所有行。而 NR == 100, NR == 200 { ... } 在输入的从 100 到 200 的行上进行这个动作。 3. 动作 awk 动作是用换行或分号终止的动作语句的序列。这些动作语句可以被用来做各种各样的簿记和字符串操纵任务。 3.1. 内置函数 awk 提供了一个“长度”函数来计算字符串的长度。下面这个程序打印每个记录,每个都前导它的长度: {print length, $0} length 自身是个“伪变量”,它生成当前记录的长度;length(参数)生成它的参数的长度,下面的程序等价于上个程序 {print length($0), $0} 参数可以是任何表达式。 awk 还提供算术函数 sqrt、log、exp 和 int,分别得到它们参数的平方根、自然对数、指数和整数部分。 某个内置函数的名字,不带有参数或圆括号,表示这些函数在整个记录上的值。程序 length <> 20

打印长度小于 10 或大于 20 的行。

函数 substr(s, m, n) 生成 s 的开始于位置 m(起始于 1)的最多 n 个字符长的子串。如果省略了 n,子串到达 s 的结束处。函数 index(s1, s2) 返回字符串 s2 在 s1 出现的位置,如果未出现则为零。

函数 sprintf(f, e1, e2, ...) 在 f 指定的 printf 格式中生成表达式 e1、e2 等的值。所以例子

x = sprintf("%8.2f %10ld", $1, $2)

设置 x 为格式化 $1 和 $2 的值所生成的字符串。
3.2. 变量、表达式和赋值

awk 变量依据上下文而被接纳为数值(浮点数)或字符串值。例如

x = 1

x 明显的是个数,而

x = "smith"

明显的是个字符串。在上下文需要的时候,把字符串转换为数或反之。例如

x = "3" + "4"

把 7 赋值给 x。在数值上下文中,不能被解释为数的字符串一般会有为零的数值,但是依靠这种行为是愚蠢的。

缺省的,(不是内置的)变量被初始化为空字符串,它有为零的数值;这消除了大多数对 BEGIN 段落的需要。例如,前两个字段的总和可以用下列程序计算

{ s1 += $1; s2 += $2 }
END { print s1, s2 }

算术在内部以浮点数的方式计算。算术算符有 +、-、*、/、%(模)。C 语言的增加 ++ 和减少 −− 算符也可用,还有赋值算符 +=、-=、*=、/=、%=。这些算符都可以用于表达式中。
3.3. 字段变量

awk 中的字段在本质上享有变量的所有性质 — 他们可以用在算术或字符串运算/操作中,并可以被赋值。所以你可以把第一个字段替代为一个序号,比如:

{ $1 = NR; print }

或累计前两个字段到第三个字段中,比如:

{ $1 = $2 + $3; print $0 }

或把一个字符串赋值到一个字段:

{ if ($3 > 1000)
$3 = "too big"
print
}

它把第三个字段替代为“too big”,在它很长的时候,并在这种情况下,打印这个记录。

字段引用可以是数值表达式,比如

{ print $i, $(i+1), $(i+n) }

一个字段被认为是数值还是字符串依赖于上下文;在有歧义的情况下比如

if ($1 == $2) ...

字段被当作字符串。

每个输入行都在需要的时候被自动分解到字段。还可以把任意变量或字段分解到字段:

n = split(s, array, sep)

把字符串 s 分解到 array[1], ..., array[n]。返回找到的元素数目。如果提供了 sep 参数,则把它用做字段分隔符;否则使用 FS 作为分隔符。
3.4. 字符串连接

字符串可以被串接。例如

length($1 $2 $3)

返回前三个字段的长度。还有在 print 语句中

print $1 " is " $2

打印用“ is ”分隔的两个字段。变量和数值表达式也可以在连接中出现。
3.5. 数组

数组元素不用声明;在被提及到的时候才导致它的存在。下标可以有任何非空的值,包括非数值的字符串。作为常规的数值下标的例子,语句

x[NR] = $0

把当前输入记录赋值到数组 x 的第 NR 个元素。实际上,在原理上(尽管可能很慢)用 awk 程序按随机的次序处理整个输入是可能的

{ x[NR] = $0 }
END { ... 程序 ... }

第一动作只是把每个输入行记录到数组 x 中。

数组元素可以用非数值的值来命名,这给予 awk 非常象 Snobol 语言的关联内存表的能力。假设输入包含的字段带有象 apple、orange 等等这样的值。则程序

/apple/ { x["apple"]++ }
/orange/ { x["orange"]++ }
END { print x["apple"], x["orange"] }

增加指名的数组元素的计数,并在输入结束时打印它们。
3.6. 控制流语句

awk 提供了同 C 语言一样的基本控制流语句 if-else、while、for,和使用花括号的语句组合。我们在章节 3.3 展示了 if 语句而没有描述它。求值在圆括号中的条件;如果为真,则执行在跟随在 if 后面的语句。else 部分是可选的。

while 语句完全同 C 语言的一样。例如,要一行一个打印所有输入字段

i = 1
while (i <= NF) { print $i ++i } for 语句也完全同 C 的一样: for (i = 1; i <= NF; i++) print $i 同上面的 while 语句做同样的工作。 for 语句还有一种可选的形式,它适合于访问关联数组的元素: for (i in array) 语句 把 i 依次设置为 array 的每个元素并重复执行后面的语句。元素是按明显的随机次序访问的。如果在循环期间 i 被改变了,或者访问了新元素,就会出现混乱。 在 if、while、for 的条件部分中的表达式可以包括关系算符如 <、<=、>、>=、==(“等于”)、!=(“不等于”);带有匹配算符 ~ 和 !~ 表示匹配的正则表达式;逻辑算符 ||、&& 和 !;当然还有用于组合的圆括号。

break 语句导致从围绕它 while 或 for 中立即退出,continue 语句导致开始下一次重复。

next 语句导致立即跳转到下一个记录并从头开始扫描模式。exit 语句导致程序表现得如同已经到达了输入的结束。

在 awk 程序中可以放置注释: 它们开始于字符 # 并结束于本行的结束处。比如

print x, y # 这是一个注释

4. 设计

UNIX 系统已经提供一些程序,它们通过传递输入经过某种选择机制而进行操作。grep 是最早和最简单的,它只打印匹配一个单一的指定模式的所有行。egrep 提供了更一般的模式,就是说,完全一般性的正则表达式;fgrep 通过特别快的算法查找关键字的集合。

sed 提供了编辑器 ed 的大多数编辑设施,并应用于输入流之上。这些程序都不提供数值功能、逻辑关系或变量。

lex 提供了一般性的正则表达式的识别能力,并充当 C 程序生成器,在能力上是没有限制的。但使用 lex 需要 C 编程的知识,并且 lex 程序必须必须在使用之前编译和装载,所以不鼓励在简短的应用中使用。

awk 尝试填充可能性矩阵中的空白。它提供了一般性的正则表达式能力和隐含的输入/输出循环。它还提供方便的数值处理、变量、更一般性的选择和在动作中的控制流。它不需要编译和 C 语言知识。最后,awk 提供了访问行中字段的方便的方式;在这方面它是唯一的。

awk 还尝试完全整合字符串和数值,通过把所有数量都作为既是字符串又是数处理,尽可能晚的确定哪个表示是合适的。在大多数情况下用户可以简单的忽略这种区别。

开发 awk 的多数努力在于确定 awk 应该做什么与不应该做什么(例如,它不做字符串替换),和应当采用什么语法(没有显式的连接算符),而不是书写和调试代码。我们尝试使语法强力但易于使用并适于扫描文件。例如,缺乏声明和隐含的初始化,尽管对于通用编程语言是个坏主意,但对意图用于甚至是在命令行上合成的小程序的一门语言而言是需要的。

在实践中,awk 的使用适合两个广泛的范畴。其一可以叫做“报表生成”— 处理一个输入,提取计数,总和等。这也包括写琐碎的数据验证程序,比如校验一个字段只包含数值信息或特定分界符是正确配对的。文本和数值处理的组合在这种情况下是没有价值的。

第二个用途是做数据转换器,从一个程序生成的一种形式转换成另一个程序期望的另一种形式。最简单的例子只是选择字段,可能再做些重新安排。
5. 实现

awk 语言的实际实现利用了 UNIX 操作系统上可用的开发工具。文法使用 yacc 规定;词法分析使用 lex;正则表达式识别器是直接从这些表达式构造出来的确定有限自动机。awk 程序被翻译成一个分析树,并接着直接用一个简单的解释器执行它。

awk 是为易于使用而不处理速度而设计;变量类型的延迟评估和分解到字段的需要使在任何情况下都难于达到高速。尽管如此,程序不是慢得不能工作。

下面的表 I 展示了在 PDP-11/70 上 UNIX 程序 wc、grep、egrep、fgrep、sed、lex 和 awk 在下列简单任务上的执行(用户+系统)时间:

* 1. 计数行数。
* 2. 打印包含“doug” 的所有行。
* 3. 打印包含“doug”、 “ken” 或“dmr”的所有行。
* 4. 打印每行的第三个字段。
* 5. 依次打印每行的第三和第二个字段。
* 6. 分别把包含“doug”、“ken”和“dmr”的所有行添加到文件“jdoug”、“jken”和 “jdmr”。
* 7. 打印每行并前导上“行号 :”。
* 8. 总和一个表的第四列。

程序 wc 只计数它输入中的字、行和字符;其他的我们都提到过。在所有情况下,输入都是使用命令 ls −l 建立的包含 10,000 行的文件;每行都有如下形式

-rw-rw-rw- 1 ava 123 Oct 15 17:05 xxx

这个输入的总长度是 452,960 个字符。lex 的时间不包括编译和装载。

如同预期的一样,awk 不如特殊工具 wc、sed 或 grep 家族程序那么快,但是比更一般性的工具 lex 要快。在所有情况下,这些任务表达为 awk 程序同表达为其他语言一样容易;涉及字段的任务相当易于表达为 awk 程序。某些测试程序同时用 awk、sed 和 lex 展示。

任务
程序 1 2 3 4 5 8 7 8
wc 8.6






grep 11.7 13.1





egrep 6.2 11.5 11.6




fgrep 7.7 13.8 16.1




sed 10.2 11.6 15.8 29.0 30.5 16.1

lex 65.1 150.1 144.2 67.3 70.3 104.0 81.7 92.8
awk 15.0 25.6 29.9 33.3 38.9 46.4 71.4 31.1

表 I. 程序的执行时间。(单位是秒)

下面展示完成某些任务的程序。lex 程序一般长得难以展示。

awk:

1. END {print NR}
2. /doug/
3. /ken|doug|dmr/
4. {print $3}
5. {print $3, $2}
6. /ken/ {print >"jken"}
/doug/ {print >"jdoug"}
/dmr/ {print >"jdmr"}
7. {print NR ": " $0}
8. {sum = sum + $4}
END{print sum}

SED:


1. $=
2. /doug/p
3. /doug/p
/doug/d
/ken/p
/ken/d
/dmr/p
/dmr/d
4. /[^ ]* [ ]*[^ ]* [ ]*\([^ ]*\) .*/s//\1/p
5. /[^ ]* [ ]*\([^ ]*\) [ ]*\([^ ]*\) .*/s//\2 \1/p
6. /ken/w jken
/doug/w jdoug
/dmr/w jdmr

LEX:

1. %{
int i;
%}
%%
\n i++;
. ;
%%
yywrap() {
printf("%d\n", i);
}
2. %%
^.*doug.*$ printf("%s\n", yytext);
. ;
\n ;

Some say he’s half man half fish, others say he’s more of a seventy/thirty split. Either way he’s a fishy bastard.