分类:
2009-08-15 16:50:50
循环结构
我们已经看到了 awk 的 while 循环结构,它等同于相应的 C 语言 while 循环。awk 还有 "do...while" 循环,它在代码块结尾处对条件求值,而不象标准 while 循环那样在开始处求值。它类似于其它语言中的 "repeat...until" 循环。以下是一个示例:
do...while 示例
{ count=1 do { print "I get printed at least once no matter what" } while ( count != 1 )}
与一般的 while 循环不同,由于在代码块之后对条件求值,"do...while" 循环永远都至少执行一次。换句话说,当第一次遇到普通 while 循环时,如果条件为假,将永远不执行该循环。
for 循环
awk 允许创建 for 循环,它就象 while 循环,也等同于 C 语言的 for 循环:
for ( initial assignment; comparison; increment ) { code block}
以下是一个简短示例:
for ( x = 1; x <= 4; x++ ) { print "iteration",x}
此段代码将打印:
iteration 1iteration 2iteration 3iteration 4
break 和 continue
此外,如同 C 语言一样,awk 提供了 break 和 continue 语句。使用这些语句可以更好地控制 awk 的循环结构。以下是迫切需要 break 语句的代码片断:
while 死循环
while (1) { print "forever and ever..."}
因为 1 永远代表是真,这个 while 循环将永远运行下去。以下是一个只执行十次的循环:
break 语句示例
x=1while(1) { print "iteration",x if ( x == 10 ) { break } x++}
这里,break 语句用于“逃出”最深层的循环。"break" 使循环立即终止,并继续执行循环代码块后面的语句。
continue 语句补充了 break,其作用如下:
x=1while (1) { if ( x == 4 ) { x++ continue } print "iteration",x if ( x > 20 ) { break } x++}
这段代码打印 "iteration 1" 到 "iteration 21","iteration 4" 除外。如果迭代等于 4,则增加 x 并调用 continue 语句,该语句立即使 awk 开始执行下一个循环迭代,而不执行代码块的其余部分。如同 break 一样,continue 语句适合各种 awk 迭代循环。在 for 循环主体中使用时,continue 将使循环控制变量自动增加。以下是一个等价循环:
for ( x=1; x<=21; x++ ) { if ( x == 4 ) { continue } print "iteration",x}
在 while 循环中时,在调用 continue 之前没有必要增加 x,因为 for 循环会自动增加 x。
数组
如果您知道 awk 可以使用数组,您一定会感到高兴。然而,在 awk 中,数组下标通常从 1 开始,而不是 0:
myarray[1]="jim"myarray[2]=456
awk 遇到第一个赋值语句时,它将创建 myarray,并将元素 myarray[1] 设置成 "jim"。执行了第二个赋值语句后,数组就有两个元素了。
数组迭代
定义之后,awk 有一个便利的机制来迭代数组元素,如下所示:
for ( x in myarray ) { print myarray[x]}
这段代码将打印数组 myarray 中的每一个元素。当对于 for 使用这种特殊的 "in" 形式时,awk 将 myarray 的每个现有下标依次赋值给 x(循环控制变量),每次赋值以后都循环一次循环代码。虽然这是一个非常方便的 awk 功能,但它有一个缺点 -- 当 awk 在数组下标之间轮转时,它不会依照任何特定的顺序。那就意味着我们不能知道以上代码的输出是
jim456
还是
456jim
套用 Forrest Gump 的话来说,迭代数组内容就像一盒巧克力 -- 您永远不知道将会得到什么。因此有必要使 awk 数组“字符串化”,我们现在就来研究这个问题。
数组下标字符串化
在我的前一篇文章中,我演示了 awk 实际上以字符串格式来存储数字值。虽然 awk 要执行必要的转换来完成这项工作,但它却可以使用某些看起来很奇怪的代码:
a="1"b="2"c=a+b+3
执行了这段代码后,c 等于 6。由于 awk 是“字符串化”的,添加字符串 "1" 和 "2" 在功能上并不比添加数字 1 和 2 难。这两种情况下,awk 都可以成功执行运算。awk 的“字符串化”性质非常可爱 -- 您可能想要知道如果使用数组的字符串下标会发生什么情况。例如,使用以下代码:
myarr["1"]="Mr. Whipple"print myarr["1"]
可以预料,这段代码将打印 "Mr. Whipple"。但如果去掉第二个 "1" 下标中的引号,情况又会怎样呢?
myarr["1"]="Mr. Whipple"print myarr[1]
猜想这个代码片断的结果比较难。awk 将 myarr["1"] 和 myarr[1] 看作数组的两个独立元素,还是它们是指同一个元素?答案是它们指的是同一个元素,awk 将打印 "Mr. Whipple",如同第一个代码片断一样。虽然看上去可能有点怪,但 awk 在幕后却一直使用数组的字符串下标!
了解了这个奇怪的真相之后,我们中的一些人可能想要执行类似于以下的古怪代码:
myarr["name"]="Mr. Whipple"print myarr["name"]
这段代码不仅不会产生错误,而且它的功能与前面的示例完全相同,也将打印 "Mr. Whipple"!可以看到,awk 并没有限制我们使用纯整数下标;如果我们愿意,可以使用字符串下标,而且不会产生任何问题。只要我们使用非整数数组下标,如 myarr["name"],那么我们就在使用关联数组。从技术上讲,如果我们使用字符串下标,awk 的后台操作并没有什么不同(因为即便使用“整数”下标,awk 还是会将它看作是字符串)。但是,应该将它们称作关联数组 -- 它听起来很酷,而且会给您的上司留下印象。字符串化下标是我们的小秘密。;)
数组工具
谈到数组时,awk 给予我们许多灵活性。可以使用字符串下标,而且不需要连续的数字序列下标(例如,可以定义 myarr[1] 和 myarr[1000],但不定义其它所有元素)。虽然这些都很有用,但在某些情况下,会产生混淆。幸好,awk 提供了一些实用功能有助于使数组变得更易于管理。
首先,可以删除数组元素。如果想要删除数组 fooarray 的元素 1,输入:
delete fooarray[1]
而且,如果想要查看是否存在某个特定数组元素,可以使用特殊的 "in" 布尔运算符,如下所示:
if (
下一篇
本文中,我们已经讨论了许多基础知识。下一篇中,我将演示如何使用 awk 的数学运算和字符串函数,以及如何创建您自己的函数,使您完全掌握 awk 知识。我还将指导您创建支票簿结算程序。那时,我会鼓励您编写自己的 awk 程序。请查阅以下参考资料。
在 awk 系列的这篇总结中,Daniel 向您介绍 awk 重要的字符串函数,以及演示了如何从头开始编写完整的支票簿结算程序。在这个过程中,您将学习如何编写自己的函数,并使用 awk 的多维数组。学完本文之后,您将掌握更多 awk 经验,可以让您创建功能更强大的脚本。
第三部分
格式化输出
虽然大多数情况下 awk 的 print 语句可以完成任务,但有时我们还需要更多。在那些情况下,awk 提供了两个我们熟知的老朋友 printf() 和 sprintf()。是的,如同其它许多 awk 部件一样,这些函数等同于相应的 C 语言函数。printf() 会将格式化字符串打印到 stdout,而 sprintf() 则返回可以赋值给变量的格式化字符串。如果不熟悉 printf() 和 sprintf(),介绍 C 语言的文章可以让您迅速了解这两个基本打印函数。在 Linux 系统上,可以输入 "man 3 printf" 来查看 printf() 帮助页面。
以下是一些 awk sprintf() 和 printf() 的样本代码。可以看到,它们几乎与 C 语言完全相同。
x=1b="foo"printf("%s got a %d on the last test\n","Jim",83)myout=("%s-%d",b,x)print myout
此代码将打印:
Jim got a 83 on the last testfoo-1
字符串函数
awk 有许多字符串函数,这是件好事。在 awk 中,确实需要字符串函数,因为不能象在其它语言(如 C、C++ 和 Python)中那样将字符串看作是字符数组。例如,如果执行以下代码:
mystring="How are you doing today?"print mystring[3]
将会接收到一个错误,如下所示:
awk: string.gawk:59: fatal: attempt to use scalar as array
噢,好吧。虽然不象 Python 的序列类型那样方便,但 awk 的字符串函数还是可以完成任务。让我们来看一下。
首先,有一个基本 length() 函数,它返回字符串的长度。以下是它的使用方法:
print length(mystring)
此代码将打印值:
24
好,继续。下一个字符串函数叫作 index,它将返回子字符串在另一个字符串中出现的位置,如果没有找到该字符串则返回 0。使用 mystring,可以按以下方法调用它:
print index(mystring,"you")
awk 会打印:
9
让我们继续讨论另外两个简单的函数,tolower() 和 toupper()。与您猜想的一样,这两个函数将返回字符串并且将所有字符分别转换成小写或大写。请注意,tolower() 和 toupper() 返回新的字符串,不会修改原来的字符串。这段代码:
print tolower(mystring)print toupper(mystring)print mystring
……将产生以下输出:
how are you doing today?HOW ARE YOU DOING TODAY?How are you doing today?
到现在为止一切不错,但我们究竟如何从字符串中选择子串,甚至单个字符?那就是使用 substr() 的原因。以下是 substr() 的调用方法:
mysub=substr(mystring,startpos,maxlen)
mystring 应该是要从中抽取子串的字符串变量或文字字符串。startpos 应该设置成起始字符位置,maxlen 应该包含要抽取的字符串的最大长度。请注意,我说的是最大长度;如果 length(mystring) 比 startpos+maxlen 短,那么得到的结果就会被截断。substr() 不会修改原始字符串,而是返回子串。以下是一个示例:
print substr(mystring,9,3)
awk 将打印:
you
如果您通常用于编程的语言使用数组下标访问部分字符串(以及不使用这种语言的人),请记住 substr() 是 awk 代替方法。需要使用它来抽取单个字符和子串;因为 awk 是基于字符串的语言,所以会经常用到它。
现在,我们讨论一些更耐人寻味的函数,首先是 match()。match() 与 index() 非常相似,它与 index() 的区别在于它并不搜索子串,它搜索的是规则表达式。match() 函数将返回匹配的起始位置,如果没有找到匹配,则返回 0。此外,match() 还将设置两个变量,叫作 RSTART 和 RLENGTH。RSTART 包含返回值(第一个匹配的位置),RLENGTH 指定它占据的字符跨度(如果没有找到匹配,则返回 -1)。通过使用 RSTART、RLENGTH、substr() 和一个小循环,可以轻松地迭代字符串中的每个匹配。以下是一个 match() 调用示例:
print match(mystring,/you/), RSTART, RLENGTH
awk 将打印:
9 9 3
字符串替换
现在,我们将研究两个字符串替换函数,sub() 和 gsub()。这些函数与目前已经讨论过的函数略有不同,因为它们确实修改原始字符串。以下是一个模板,显示了如何调用 sub():
sub(regexp,replstring,mystring)
调用 sub() 时,它将在 mystring 中匹配 regexp 的第一个字符序列,并且用 replstring 替换该序列。sub() 和 gsub() 用相同的自变量;唯一的区别是 sub() 将替换第一个 regexp 匹配(如果有的话),gsub() 将执行全局替换,换出字符串中的所有匹配。以下是一个 sub() 和 gsub() 调用示例:
sub(/o/,"O",mystring)print mystringmystring="How are you doing today?"gsub(/o/,"O",mystring)print mystring
必须将 mystring 复位成其初始值,因为第一个 sub() 调用直接修改了 mystring。在执行时,此代码将使 awk 输出:
HOw are you doing today?HOw are yOu dOing tOday?
当然,也可以是更复杂的规则表达式。我把测试一些复杂规则表达式的任务留给您来完成。
通过介绍函数 split(),我们来汇总一下已讨论过的函数。split() 的任务是“切开”字符串,并将各部分放到使用整数下标的数组中。以下是一个 split() 调用示例:
numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",")
调用 split() 时,第一个自变量包含要切开文字字符串或字符串变量。在第二个自变量中,应该指定 split() 将填入片段部分的数组名称。在第三个元素中,指定用于切开字符串的分隔符。split() 返回时,它将返回分割的字符串元素的数量。split() 将每一个片段赋值给下标从 1 开始的数组,因此以下代码:
print mymonths[1],mymonths[numelements]
……将打印:
Jan Dec
特殊字符串形式
简短注释 -- 调用 length()、sub() 或 gsub() 时,可以去掉最后一个自变量,这样 awk 将对 $0(整个当前行)应用函数调用。要打印文件中每一行的长度,使用以下 awk 脚本:
{ print length() }
财务上的趣事
几星期前,我决定用 awk 编写自己的支票簿结算程序。我决定使用简单的 tab 定界文本文件,以便于输入最近的存款和提款记录。其思路是将这个数据交给 awk 脚本,该脚本会自动合计所有金额,并告诉我余额。以下是我决定如何将所有交易记录到 "ASCII checkbook" 中:
23 Aug 2000 food - - Y Jimmy's Buffet 30.25
此文件中的每个字段都由一个或多个 tab 分隔。在日期(字段 1,$1)之后,有两个字段叫做“费用分类帐”和“收入分类帐”。以上面这行为例,输入费用时,我在费用字段中放入四个字母的别名,在收入字段中放入 "-"(空白项)。这表示这一特定项是“食品费用”。:) 以下是存款的示例:
23 Aug 2000 - inco - Y Boss Man
在这个实例中,我在费用分类帐中放入 "-"(空白),在收入分类帐中放入 "inco"。"inco" 是一般(薪水之类)收入的别名。使用分类帐别名让我可以按类别生成收入和费用的明细分类帐。至于记录的其余部分,其它所有字段都是不需加以说明的。“是否付清?”字段("Y" 或 "N")记录了交易是否已过帐到我的帐户;除此之外,还有一个交易描述,和一个正的美元金额。
用于计算当前余额的算法不太难。awk 只需要依次读取每一行。如果列出了费用分类帐,但没有收入分类帐(为 "-"),那么这一项就是借方。如果列出了收入分类帐,但没有费用分类帐(为 "-"),那么这一项就是贷方。而且,如果同时列出了费用和收入分类帐,那么这个金额就是“分类帐转帐”;即,从费用分类帐减去美元金额,并将此金额添加到收入分类帐。此外,所有这些分类帐都是虚拟的,但对于跟踪收入和支出以及预算却非常有用。
代码
现在该研究代码了。我们将从第一行(BEGIN 块和函数定义)开始:
balance,第 1 部分
#!/usr/bin/env awk -fBEGIN { FS="\t+" months="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"}function monthdigit(mymonth) { return (index(months,mymonth)+3)/4}
首先执行 "chmod +x myscript" 命令,那么将第一行 "#!..." 添加到任何 awk 脚本将使它可以直接从 shell 中执行。其余行定义了 BEGIN 块,在 awk 开始处理支票簿文件之前将执行这个代码块。我们将 FS(字段分隔符)设置成 "\t+",它会告诉 awk 字段由一个或多个 tab 分隔。另外,我们定义了字符串 months,下面将出现的 monthdigit() 函数将使用它。
最后三行显示了如何定义自己的 awk 。格式很简单 -- 输入 "function",再输入名称,然后在括号中输入由逗号分隔的参数。在此之后,"{ }" 代码块包含了您希望这个函数执行的代码。所有函数都可以访问全局变量(如 months 变量)。另外,awk 提供了 "return" 语句,它允许函数返回一个值,并执行类似于 C 和其它语言中 "return" 的操作。这个特定函数将以 3 个字母字符串格式表示的月份名称转换成等价的数值。例如,以下代码:
print monthdigit("Mar")
……将打印:
3
现在,让我们讨论其它一些函数。
财务函数
以下是其它三个执行簿记的函数。我们即将见到的主代码块将调用这些函数之一,按顺序处理支票簿文件的每一行,从而将相应交易记录到 awk 数组中。有三种基本交易,贷方 (doincome)、借方 (doexpense) 和转帐 (dotransfer)。您会发现这三个函数全都接受一个自变量,叫作 mybalance。mybalance 是二维数组的一个占位符,我们将它作为自变量进行传递。目前,我们还没有处理过二维数组;但是,在下面可以看到,语法非常简单。只须用逗号分隔每一维就行了。
我们将按以下方式将信息记录到 "mybalance" 中。数组的第一维从 0 到 12,用于指定月份,0 代表全年。第二维是四个字母的分类帐,如 "food" 或 "inco";这是我们处理的真实分类帐。因此,要查找全年食品分类帐的余额,应查看 mybalance[0,"food"]。要查找 6 月的收入,应查看 mybalance[6,"inco"]。
balance,第 2 部分
function doincome(mybalance) { mybalance[curmonth,$3] += amount mybalance[0,$3] += amount}function doexpense(mybalance) { mybalance[curmonth,$2] -= amount mybalance[0,$2] -= amount}function dotransfer(mybalance) { mybalance[0,$2] -= amount mybalance[curmonth,$2] -= amount mybalance[0,$3] += amount mybalance[curmonth,$3] += amount}
调用 doincome() 或任何其它函数时,我们将交易记录到两个位置 -- mybalance[0,category] 和 mybalance[curmonth, category],它们分别表示全年的分类帐余额和当月的分类帐余额。这让我们稍后可以轻松地生成年度或月度收入/支出明细分类帐。
如果研究这些函数,将发现在我的引用中传递了 mybalance 引用的数组。另外,我们还引用了几个全局变量:curmonth,它保存了当前记录所属的月份的数值,$2(费用分类帐),$3(收入分类帐)和金额($7,美元金额)。调用 doincome() 和其它函数时,已经为要处理的当前记录(行)正确设置了所有这些变量。
主块
以下是主代码块,它包含了分析每一行输入数据的代码。请记住,由于正确设置了 FS,可以用 $ 1 引用第一个字段,用 $2 引用第二个字段,依次类推。调用 doincome() 和其它函数时,这些函数可以从函数内部访问 curmonth、$2、$3 和金额的当前值。请先研究代码,在代码之后可以见到我的说明。