全部博文(245)
分类:
2008-05-17 13:15:16
在这篇 awk简介的续集中,Daniel Robbins 继续探索awk(一种很棒但有怪异名称的语言)。Daniel将演示如何处理多行记录、使用循环结构,以及创建并使用 awk数组。阅读完本文后,您将精通许多 awk的功能,而且可以编写您自己的功能强大的 awk 脚本。
awk 是一种用于读取和处理结构化数据(如系统的 /etc/passwd 文件)的极佳工具。/etc/passwd 是 UNIX 用户数据库,并且是用冒号定界的文本文件,它包含许多重要信息,包括所有现有用户帐户和用户标识,以及其它信息。在我的 前一篇文章 中,我演示了 awk 如何轻松地分析这个文件。我们只须将 FS(字段分隔符)变量设置成 ":"。
正确设置了 FS 变量之后,就可以将 awk 配置成分析几乎任何类型的结构化数据,只要这些数据是每行一个记录。然而,如果要分析占据多行的记录,仅仅依靠设置 FS 是不够的。在这些情况下,我们还需要修改 RS 记录分隔符变量。RS 变量告诉 awk 当前记录什么时候结束,新记录什么时候开始。
譬如,让我们讨论一下如何完成处理“联邦证人保护计划”所涉及人员的地址列表的任务:
Jimmy the Weasel |
理论上,我们希望 awk 将每 3 行看作是一个独立的记录,而不是三个独立的记录。如果 awk 将地址的第一行看作是第一个字段 ($1),街道地址看作是第二个字段 ($2),城市、州和邮政编码看作是第三个字段 $3,那么这个代码就会变得很简单。以下就是我们想要得到的代码:
BEGIN { |
在上面这段代码中,将 FS 设置成 "\n" 告诉 awk 每个字段都占据一行。通过将 RS 设置成 "",还会告诉 awk 每个地址记录都由空白行分隔。一旦 awk 知道是如何格式化输入的,它就可以为我们执行所有分析工作,脚本的其余部分很简单。让我们研究一个完整的脚本,它将分析这个地址列表,并将每个记录打印在一行上,用逗号分隔每个字段。
BEGIN { |
如果这个脚本保存为 address.awk,地址数据存储在文件 address.txt 中,可以通过输入 "awk -f address.awk address.txt" 来执行这个脚本。此代码将产生以下输出:
Jimmy the Weasel, 100 Pleasant Drive, San Francisco, CA 12345 |
|
在 address.awk 的 print 语句中,可以看到 awk 会连接(合并)一行中彼此相邻的字符串。我们使用此功能在同一行上的三个字段之间插入一个逗号和空格 (", ")。这个方法虽然有用,但比较难看。与其在字段间插入 ", " 字符串,倒不如让通过设置一个特殊 awk 变量 OFS,让 awk 完成这件事。请参考下面这个代码片断。
print "Hello", "there", "Jim!" |
这行代码中的逗号并不是实际文字字符串的一部分。事实上,它们告诉 awk "Hello"、"there" 和 "Jim!" 是单独的字段,并且应该在每个字符串之间打印 OFS 变量。缺省情况下,awk 产生以下输出:
Hello there Jim! |
这是缺省情况下的输出结果,OFS 被设置成 " ",单个空格。不过,我们可以方便地重新定义 OFS,这样 awk 将插入我们中意的字段分隔符。以下是原始 address.awk 程序的修订版,它使用 OFS 来输出那些中间的 ", " 字符串:
BEGIN { |
awk 还有一个特殊变量 ORS,全称是“输出记录分隔符”。通过设置缺省为换行 ("\n") 的 OFS,我们可以控制在 print 语句结尾自动打印的字符。缺省 ORS 值会使 awk 在新行中输出每个新的 print 语句。如果想使输出的间隔翻倍,可以将 ORS 设置成 "\n\n"。或者,如果想要用单个空格分隔记录(而不换行),将 ORS 设置成 ""。
|
假设我们编写了一个脚本,它将地址列表转换成每个记录一行,且用 tab 定界的格式,以便导入电子表格。使用稍加修改的 address.awk 之后,就可以清楚地看到这个程序只适合于三行的地址。如果 awk 遇到以下地址,将丢掉第四行,并且不打印该行:
Cousin Vinnie |
要处理这种情况,代码最好考虑每个字段的记录数量,并依次打印每个记录。现在,代码只打印地址的前三个字段。以下就是我们想要的一些代码:
BEGIN { |
首先,将字段分隔符 FS 设置成 "\n",将记录分隔符 RS 设置成
"",这样 awk 可以象以前一样正确分析多行地址。然后,将输出记录分隔符
ORS 设置成 "",它将使 print
语句在每个调用结尾
不 输出新行。这意味着如果希望任何文本从新的一行开始,那么需要明确写入
print "\n"
。
在主代码块中,创建了一个变量 x 来存储正在处理的当前字段的编号。起初,它被设置成 1。然后,我们使用 while 循环(一种 awk 循环结构,等同于 C 语言中的 while 循环),对于所有记录(最后一个记录除外)重复打印记录和 tab 字符。最后,打印最后一个记录和换行;此外,由于将 ORS 设置成 "",print 将不输出换行。程序输出如下,这正是我们所期望的:
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345 |
|
我们已经看到了 awk 的 while 循环结构,它等同于相应的 C 语言 while 循环。awk 还有 "do...while" 循环,它在代码块结尾处对条件求值,而不象标准 while 循环那样在开始处求值。它类似于其它语言中的 "repeat...until" 循环。以下是一个示例:
{ |
与一般的 while 循环不同,由于在代码块之后对条件求值,"do...while" 循环永远都至少执行一次。换句话说,当第一次遇到普通 while 循环时,如果条件为假,将永远不执行该循环。
for 循环
awk 允许创建 for 循环,它就象 while 循环,也等同于 C 语言的 for
循环:
for ( initial assignment; comparison; increment ) { |
以下是一个简短示例:
for ( x = 1; x <= 4; x++ ) { |
此段代码将打印:
iteration 1 |
|
此外,如同 C 语言一样,awk 提供了 break 和 continue 语句。使用这些语句可以更好地控制 awk 的循环结构。以下是迫切需要 break 语句的代码片断:
while (1) { |
while 死循环
1
永远代表是真,这个 while 循环将永远运行下去。以下是一个只执行十次的循环:
x=1 |
这里,break 语句用于“逃出”最深层的循环。"break" 使循环立即终止,并继续执行循环代码块后面的语句。
continue 语句补充了 break,其作用如下:
x=1 |
这段代码打印 "iteration 1" 到 "iteration 21","iteration 4" 除外。如果迭代等于 4,则增加 x 并调用 continue 语句,该语句立即使 awk 开始执行下一个循环迭代,而不执行代码块的其余部分。如同 break 一样,continue 语句适合各种 awk 迭代循环。在 for 循环主体中使用时,continue 将使循环控制变量自动增加。以下是一个等价循环:
for ( x=1; x<=21; x++ ) { |
在 while 循环中时,在调用 continue 之前没有必要增加
x
,因为 for 循环会自动增加
x
。
|
如果您知道 awk 可以使用数组,您一定会感到高兴。然而,在 awk 中,数组下标通常从 1 开始,而不是 0:
myarray[1]="jim" |
awk 遇到第一个赋值语句时,它将创建
myarray
,并将元素
myarray[1]
设置成
"jim"。执行了第二个赋值语句后,数组就有两个元素了。
数组迭代
定义之后,awk 有一个便利的机制来迭代数组元素,如下所示:
for ( x in myarray ) { |
这段代码将打印数组
myarray
中的每一个元素。当对于
for 使用这种特殊的 "in" 形式时,awk 将
myarray
的每个现有下标依次赋值给
x
(循环控制变量),每次赋值以后都循环一次循环代码。虽然这是一个非常方便的
awk 功能,但它有一个缺点 -- 当 awk
在数组下标之间轮转时,它不会依照任何特定的顺序。那就意味着我们不能知道以上代码的输出是:
jim |
还是
456 |
套用 Forrest Gump 的话来说,迭代数组内容就像一盒巧克力 -- 您永远不知道将会得到什么。因此有必要使 awk 数组“字符串化”,我们现在就来研究这个问题。
|
在我的
前一篇文章 中,我演示了 awk
实际上以字符串格式来存储数字值。虽然 awk
要执行必要的转换来完成这项工作,但它却可以使用某些看起来很奇怪的代码:
a="1" |
执行了这段代码后,
c
等于
6
。由于 awk
是“字符串化”的,添加字符串 "1" 和 "2" 在功能上并不比添加数字 1 和
2 难。这两种情况下,awk 都可以成功执行运算。awk
的“字符串化”性质非常可爱 --
您可能想要知道如果使用数组的字符串下标会发生什么情况。例如,使用以下代码:
myarr["1"]="Mr. Whipple" |
可以预料,这段代码将打印 "Mr. Whipple"。但如果去掉第二个 "1" 下标中的引号,情况又会怎样呢?
myarr["1"]="Mr. Whipple" |
猜想这个代码片断的结果比较难。awk 将
myarr["1"]
和
myarr[1]
看作数组的两个独立元素,还是它们是指同一个元素?答案是它们指的是同一个元素,awk
将打印 "Mr.
Whipple",如同第一个代码片断一样。虽然看上去可能有点怪,但 awk
在幕后却一直使用数组的字符串下标!
了解了这个奇怪的真相之后,我们中的一些人可能想要执行类似于以下的古怪代码:
myarr["name"]="Mr. Whipple" |
这段代码不仅不会产生错误,而且它的功能与前面的示例完全相同,也将打印
"Mr. Whipple"!可以看到,awk
并没有限制我们使用纯整数下标;如果我们愿意,可以使用字符串下标,而且不会产生任何问题。只要我们使用非整数数组下标,如
myarr["name"]
,那么我们就在使用
关联数组 。从技术上讲,如果我们使用字符串下标,awk
的后台操作并没有什么不同(因为即便使用“整数”下标,awk
还是会将它看作是字符串)。但是,应该将它们称作
关联数组 --
它听起来很酷,而且会给您的上司留下印象。字符串化下标是我们的小秘密。;)
|
谈到数组时,awk
给予我们许多灵活性。可以使用字符串下标,而且不需要连续的数字序列下标(例如,可以定义
myarr[1]
和
myarr[1000]
,但不定义其它所有元素)。虽然这些都很有用,但在某些情况下,会产生混淆。幸好,awk
提供了一些实用功能有助于使数组变得更易于管理。
首先,可以删除数组元素。如果想要删除数组
fooarray
的元素
1
,输入:
delete fooarray[1] |
而且,如果想要查看是否存在某个特定数组元素,可以使用特殊的 "in" 布尔运算符,如下所示:
if ( 1 in fooarray ) {
print "Ayep! It's there."
} else {
print "Nope! Can't find it."
}