awk使用教程

Awk 是一种很是好的语言,同时有一个很是奇怪的名称。在本系列(共三篇文章)的第一篇文章中,Daniel Robbins 将使您迅速掌握 awk 编程技巧。随着本系列的进展,将讨论更高级的主题,最后将演示一个真正的高级 awk 演示程序。
捍卫 awk
在本系列文章中,我将使您成为精通 awk 的编码人员。我认可,awk 并无一个很是好听且又很是“时髦”的名字。awk 的 GNU 版本(叫做 gawk)听起来很是怪异。那些不熟悉这种语言的人可能据说过 "awk",并可能认为它是一组落伍且过期的混乱代码。它甚至会使最博学的 UNIX 权威陷于错乱的边缘(使他不断地发出 "kill -9!" 命令,就象使用咖啡机同样)。
的确,awk 没有一个动听的名字。但它是一种很棒的语言。awk 适合于文本处理和报表生成,它还有许多精心设计的特性,容许进行须要特殊技巧程序设计。与某些语言不一样,awk 的语法较为常见。它借鉴了某些语言的一些精华部分,如 C 语言、python 和 bash(虽然在技术上,awk 比 python 和 bash 早建立)。awk 是那种一旦学会了就会成为您战略编码库的主要部分的语言。
第一个 awk
让咱们继续,开始使用 awk,以了解其工做原理。在命令行中输入如下命令:

$ awk '{ print }' /etc/passwd
 
您将会见到 /etc/passwd 文件的内容出如今眼前。如今,解释 awk 作了些什么。调用 awk 时,咱们指定 /etc/passwd 做为输入文件。执行 awk 时,它依次对 /etc/passwd 中的每一行执行 print 命令。全部输出都发送到 stdout,所获得的结果与与执行catting /etc/passwd彻底相同。
如今,解释 { print } 代码块。在 awk 中,花括号用于将几块代码组合到一块儿,这一点相似于 C 语言。在代码块中只有一条 print 命令。在 awk 中,若是只出现 print 命令,那么将打印当前行的所有内容。
这里是另外一个 awk 示例,它的做用与上例彻底相同:

$ awk '{ print $0 }' /etc/passwd
 
在 awk 中,$0 变量表示整个当前行,因此 print 和 print $0 的做用彻底同样。
若是您愿意,能够建立一个 awk 程序,让它输出与输入数据彻底无关的数据。如下是一个示例:

$ awk '{ print "" }' /etc/passwd
 
只要将 "" 字符串传递给 print 命令,它就会打印空白行。若是测试该脚本,将会发现对于 /etc/passwd 文件中的每一行,awk 都输出一个空白行。再次说明, awk 对输入文件中的每一行都执行这个脚本。如下是另外一个示例:

$ awk '{ print "hiya" }' /etc/passwd
 
运行这个脚本将在您的屏幕上写满 hiya。
多个字段
awk 很是善于处理分红多个逻辑字段的文本,并且让您能够绝不费力地引用 awk 脚本中每一个独立的字段。如下脚本将打印出您的系统上全部用户账户的列表:

$ awk -F":" '{ print $1 }' /etc/passwd
 
上例中,在调用 awk 时,使用 -F 选项来指定 ":" 做为字段分隔符。awk 处理 print $1 命令时,它会打印出在输入文件中每一行中出现的第一个字段。如下是另外一个示例:

$ awk -F":" '{ print $1 $3 }' /etc/passwd
 
如下是该脚本输出的摘录:

halt7
operator11
root0
shutdown6
sync5
bin1
....etc.
 
如您所见,awk 打印出 /etc/passwd 文件的第一和第三个字段,它们正好分别是用户名和用户标识字段。如今,当脚本运行时,它并不理想 -- 在两个输出字段之间没有空格!若是习惯于使用 bash 或 python 进行编程,那么您会期望 print $1 $3 命令在两个字段之间插入空格。然而,当两个字符串在 awk 程序中彼此相邻时,awk 会链接它们但不在它们之间添加空格。如下命令会在这两个字段中插入空格:

$ awk -F":" '{ print $1 " " $3 }' /etc/passwd
 
以这种方式调用 print 时,它将链接 $一、" " 和 $3,建立可读的输出。固然,若是须要的话,咱们还能够插入一些文本标签:

$ awk -F":" '{ print "username: " $1 "\t\tuid:" $3" }' /etc/passwd
 
这将产生如下输出:

username: halt uid:7
username: operator uid:11
username: root uid:0
username: shutdown uid:6
username: sync uid:5
username: bin uid:1
....etc.
 
外部脚本
将脚本做为命令行自变量传递给 awk 对于小的单行程序来讲是很是简单的,而对于多行程序,它就比较复杂。您确定想要在外部文件中撰写脚本。而后能够向 awk 传递 -f 选项,以向它提供此脚本文件:

$ awk -f myscript.awk myfile.in
 
将脚本放入文本文件还可让您使用附加 awk 功能。例如,这个多行脚本与前面的单行脚本的做用相同,它们都打印出 /etc/passwd 中每一行的第一个字段:

BEGIN {
FS=":"
}
{ print $1 }
 
这两个方法的差异在于如何设置字段分隔符。在这个脚本中,字段分隔符在代码自身中指定(经过设置 FS 变量),而在前一个示例中,经过在命令行上向 awk 传递 -F":" 选项来设置 FS。一般,最好在脚本自身中设置字段分隔符,只是由于这表示您能够少输入一个命令行自变量。咱们将在本文的后面详细讨论 FS 变量。
BEGIN 和 END 块
一般,对于每一个输入行,awk 都会执行每一个脚本代码块一次。然而,在许多编程状况中,可能须要在 awk 开始处理输入文件中的文本以前执行初始化代码。对于这种状况,awk 容许您定义一个 BEGIN 块。咱们在前一个示例中使用了 BEGIN 块。由于 awk 在开始处理输入文件以前会执行 BEGIN 块,所以它是初始化 FS(字段分隔符)变量、打印页眉或初始化其它在程序中之后会引用的全局变量的极佳位置。
awk 还提供了另外一个特殊块,叫做 END 块。awk 在处理了输入文件中的全部行以后执行这个块。一般,END 块用于执行最终计算或打印应该出如今输出流结尾的摘要信息。
规则表达式和块
awk 容许使用规则表达式,根据规则表达式是否匹配当前行来选择执行独立代码块。如下示例脚本只输出包含字符序列 foo 的那些行:

/foo/ { print }
 
固然,可使用更复杂的规则表达式。如下脚本将只打印包含浮点数的行:

/[0-9]+\.[0-9]*/ { print }
 
表达式和块
还有许多其它方法能够选择执行代码块。咱们能够将任意一种布尔表达式放在一个代码块以前,以控制什么时候执行某特定块。仅当对前面的布尔表达式求值为真时,awk 才执行代码块。如下示例脚本输出将输出其第一个字段等于 fred 的全部行中的第三个字段。若是当前行的第一个字段不等于 fred,awk 将继续处理文件而不对当前行执行 print 语句:

$1 == "fred" { print $3 }
 
awk 提供了完整的比较运算符集合,包括 "=="、"<"、">"、"<="、">=" 和 "!="。另外,awk 还提供了 "~" 和 "!~" 运算符,它们分别表示“匹配”和“不匹配”。它们的用法是在运算符左边指定变量,在右边指定规则表达式。若是某一行的第五个字段包含字符序列 root,那么如下示例将只打印这一行中的第三个字段:

$5 ~ /root/ { print $3 }
 
条件语句
awk 还提供了很是好的相似于 C 语言的 if 语句。若是您愿意,可使用 if 语句重写前一个脚本:

{
if ( $5 ~ /root/ ) {
print $3
}
}
 
这两个脚本的功能彻底同样。第一个示例中,布尔表达式放在代码块外面。而在第二个示例中,将对每个输入行执行代码块,并且咱们使用 if 语句来选择执行 print 命令。这两个方法均可以使用,能够选择最适合脚本其它部分的一种方法。
如下是更复杂的 awk if 语句示例。能够看到,尽管使用了复杂、嵌套的条件语句,if 语句看上去仍与相应的 C 语言 if 语句同样:

{
if ( $1 == "foo" ) {
if ( $2 == "foo" ) {
print "uno"
} else {
print "one"
}
} else if ($1 == "bar" ) {
print "two"
} else {
print "three"
}
}
 
使用 if 语句还能够将代码:

! /matchme/ { print $1 $3 $4 }
 
转换成:

{
if ( $0 !~ /matchme/ ) {
print $1 $3 $4
}
}
 
这两个脚本都只输出不包含 matchme 字符序列的那些行。此外,还能够选择最适合您的代码的方法。它们的功能彻底相同。
awk 还容许使用布尔运算符 "||"(逻辑与)和 "&&"(逻辑或),以便建立更复杂的布尔表达式:

( $1 == "foo" ) && ( $2 == "bar" ) { print }
 
这个示例只打印第一个字段等于 foo 且第二个字段等于 bar 的那些行。
数值变量!
至今,咱们不是打印字符串、整行就是特定字段。然而,awk 还容许咱们执行整数和浮点运算。经过使用数学表达式,能够很方便地编写计算文件中空白行数量的脚本。如下就是这样一个脚本:

BEGIN { x=0 }
/^$/ { x=x+1 }
END { print "I found " x " blank lines. " }
 
在 BEGIN 块中,将整数变量 x 初始化成零。而后,awk 每次遇到空白行时,awk 将执行 x=x+1 语句,递增 x。处理完全部行以后,执行 END 块,awk 将打印出最终摘要,指出它找到的空白行数量。
字符串化变量
awk 的优势之一就是“简单和字符串化”。我认为 awk 变量“字符串化”是由于全部 awk 变量在内部都是按字符串形式存储的。同时,awk 变量是“简单的”,由于能够对它执行数学操做,且只要变量包含有效数字字符串,awk 会自动处理字符串到数字的转换步骤。要理解个人观点,请研究如下这个示例:

x="1.01"
# We just set x to contain the *string* "1.01"
x=x+1
# We just added one to a *string*
print x
# Incidentally, these are comments
 
awk 将输出:

2.01
 
有趣吧!虽然将字符串值 1.01 赋值给变量 x,咱们仍然能够对它加一。但在 bash 和 python 中却不能这样作。首先,bash 不支持浮点运算。并且,若是 bash 有“字符串化”变量,它们并不“简单”;要执行任何数学操做,bash 要求咱们将数字放到丑陋的 $( ) ) 结构中。若是使用 python,则必须在对 1.01 字符串执行任何数学运算以前,将它转换成浮点值。虽然这并不困难,但它还是附加的步骤。若是使用 awk,它是全自动的,而那会使咱们的代码又好又整洁。若是想要对每一个输入行的第一个字段乘方并加一,可使用如下脚本:

{ print ($1^2)+1 }
 
若是作一个小实验,就能够发现若是某个特定变量不包含有效数字,awk 在对数学表达式求值时会将该变量看成数字零处理。
众多运算符
awk 的另外一个优势是它有完整的数学运算符集合。除了标准的加、减、乘、除,awk 还容许使用前面演示过的指数运算符 "^"、模(余数)运算符 "%" 和其它许多从 C 语言中借入的易于使用的赋值操做符。
这些运算符包括先后加减(i++、--foo)、加/减/乘/除赋值运算符( a+=三、b*=二、c/=2.二、d-=6.2)。不只如此 -- 咱们还有易于使用的模/指数赋值运算符(a^=二、b%=4)。
字段分隔符
awk 有它本身的特殊变量集合。其中一些容许调整 awk 的运行方式,而其它变量能够被读取以收集关于输入的有用信息。咱们已经接触过这些特殊变量中的一个,FS。前面已经提到过,这个变量让您能够设置 awk 要查找的字段之间的字符序列。咱们使用 /etc/passwd 做为输入时,将 FS 设置成 ":"。当这样作有问题时,咱们还能够更灵活地使用 FS。
FS 值并无被限制为单一字符;能够经过指定任意长度的字符模式,将它设置成规则表达式。若是正在处理由一个或多个 tab 分隔的字段,您可能但愿按如下方式设置 FS:

FS="\t+"
 
以上示例中,咱们使用特殊 "+" 规则表达式字符,它表示“一个或多个前一字符”。
若是字段由空格分隔(一个或多个空格或 tab),您可能想要将 FS 设置成如下规则表达式:

FS="[[:space:]+]"
 
这个赋值表达式也有问题,它并不是必要。为何?由于缺省状况下,FS 设置成单一空格字符,awk 将这解释成表示“一个或多个空格或 tab”。在这个特殊示例中,缺省 FS 设置偏偏是您最想要的!
复杂的规则表达式也不成问题。即便您的记录由单词 "foo" 分隔,后面跟着三个数字,如下规则表达式仍容许对数据进行正确的分析:

FS="foo[0-9][0-9][0-9]"
 
字段数量
接着咱们要讨论的两个变量一般并非须要赋值的,而是用来读取以获取关于输入的有用信息。第一个是 NF 变量,也叫作“字段数量”变量。awk 会自动将该变量设置成当前记录中的字段数量。可使用 NF 变量来只显示某些输入行:

NF == 3 { print "this particular record has three fields: " $0 }
 
固然,也能够在条件语句中使用 NF 变量,以下:

{
if ( NF > 2 ) {
print $1 " " $2 ":" $3
}
}
 
记录号
记录号 (NR) 是另外一个方便的变量。它始终包含当前记录的编号(awk 将第一个记录算做记录号 1)。迄今为止,咱们已经处理了每一行包含一个记录的输入文件。对于这些状况,NR 还会告诉您当前行号。然而,当咱们在本系列之后部分中开始处理多行记录时,就不会再有这种状况,因此要注意!能够象使用 NF 变量同样使用 NR 来只打印某些输入行:

(NR < 10 ) || (NR > 100) { print "We are on record number 1-9 or 101+" }
 
另外一个示例:

{
#skip header
if ( NR > 10 ) {
print "ok, now for the real information!"
}
}
 
awk 提供了适合各类用途的附加变量。咱们将在之后的文章中讨论这些变量。
如今已经到了初次探索 awk 的尾声。随着本系列的开展,我将演示更高级的 awk 功能,咱们将用一个真实的 awk 应用程序做为本系列的结尾。同时,若是急于学习更多知识,请参考如下列出的参考资料。
在这篇 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
100 Pleasant Drive
San Francisco, CA 12345
Big Tony
200 Incognito Ave.
Suburbia, WA 67890
 
 
理论上,咱们但愿 awk 将每 3 行看做是一个独立的记录,而不是三个独立的记录。若是 awk 将地址的第一行看做是第一个字段 ($1),街道地址看做是第二个字段 ($2),城市、州和邮政编码看做是第三个字段 $3,那么这个代码就会变得很简单。如下就是咱们想要获得的代码:

BEGIN {
FS="\n"
RS=""
}
 
 
在上面这段代码中,将 FS 设置成 "\n" 告诉 awk 每一个字段都占据一行。经过将 RS 设置成 "",还会告诉 awk 每一个地址记录都由空白行分隔。一旦 awk 知道是如何格式化输入的,它就能够为咱们执行全部分析工做,脚本的其他部分很简单。让咱们研究一个完整的脚本,它将分析这个地址列表,并将每一个记录打印在一行上,用逗号分隔每一个字段。
address.awk
BEGIN {
FS="\n"
RS=""
}
{
print $1 ", " $2 ", " $3
}
 
 
若是这个脚本保存为 address.awk,地址数据存储在文件 address.txt 中,能够经过输入 "awk -f address.awk address.txt" 来执行这个脚本。此代码将产生如下输出:

Jimmy the Weasel, 100 Pleasant Drive, San Francisco, CA 12345
Big Tony, 200 Incognito Ave., Suburbia, WA 67890
 
 
OFS 和 ORS
在 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 来输出那些中间的 ", " 字符串:
address.awk 的修订版
BEGIN {
FS="\n"
RS=""
OFS=", "
}
{
print $1, $2, $3
}
 
 
awk 还有一个特殊变量 ORS,全称是“输出记录分隔符”。经过设置缺省为换行 ("\n") 的 OFS,咱们能够控制在 print 语句结尾自动打印的字符。缺省 ORS 值会使 awk 在新行中输出每一个新的 print 语句。若是想使输出的间隔翻倍,能够将 ORS 设置成 "\n\n"。或者,若是想要用单个空格分隔记录(而不换行),将 ORS 设置成 ""。
将多行转换成用 tab 分隔的格式
假设咱们编写了一个脚本,它将地址列表转换成每一个记录一行,且用 tab 定界的格式,以便导入电子表格。使用稍加修改的 address.awk 以后,就能够清楚地看到这个程序只适合于三行的地址。若是 awk 遇到如下地址,将丢掉第四行,而且不打印该行:

Cousin Vinnie
Vinnie's Auto Shop
300 City Alley
Sosueme, OR 76543
 
 
要处理这种状况,代码最好考虑每一个字段的记录数量,并依次打印每一个记录。如今,代码只打印地址的前三个字段。如下就是咱们想要的一些代码:
适合具备任意多字段的地址的 address.awk 版本
BEGIN {
FS="\n"
RS=""
ORS=""
}
{
x=1
while ( x<NF ) {
print $x "\t"
x++
}
print $NF "\n"
}
 
 
首先,将字段分隔符 FS 设置成 "\n",将记录分隔符 RS 设置成 "",这样 awk 能够象之前同样正确分析多行地址。而后,将输出记录分隔符 ORS 设置成 "",它将使 print 语句在每一个调用结尾不输出新行。这意味着若是但愿任何文本重新的一行开始,那么须要明确写入 print "\n"。
在主代码块中,建立了一个变量 x 来存储正在处理的当前字段的编号。起初,它被设置成 1。而后,咱们使用 while 循环(一种 awk 循环结构,等同于 C 语言中的 while 循环),对于全部记录(最后一个记录除外)重复打印记录和 tab 字符。最后,打印最后一个记录和换行;此外,因为将 ORS 设置成 "",print 将不输出换行。程序输出以下,这正是咱们所指望的:
咱们想要的输出。不算漂亮,但用 tab 定界,以便于导入电子表格
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345
Big Tony 200 Incognito Ave. Suburbia, WA 67890
Cousin Vinnie Vinnie's Auto Shop 300 City Alley Sosueme, OR 76543
 
 
循环结构
咱们已经看到了 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 1
iteration 2
iteration 3
iteration 4
 
 
break 和 continue
此外,如同 C 语言同样,awk 提供了 break 和 continue 语句。使用这些语句能够更好地控制 awk 的循环结构。如下是迫切须要 break 语句的代码片段:
while 死循环
while (1) {
print "forever and ever..."
}
 
 
由于 1 永远表明是真,这个 while 循环将永远运行下去。如下是一个只执行十次的循环:
break 语句示例
x=1
while(1) {
print "iteration",x
if ( x == 10 ) {
break
}
x++
}
 
 
这里,break 语句用于“逃出”最深层的循环。"break" 使循环当即终止,并继续执行循环代码块后面的语句。
continue 语句补充了 break,其做用以下:

x=1
while (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 在数组下标之间轮转时,它不会依照任何特定的顺序。那就意味着咱们不能知道以上代码的输出是:

jim
456
 
 
仍是

456
jim
 
 
套用 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 ( 1 in fooarray ) {
print "Ayep! It's there."
} else {
print "Nope! Can't find it."
}
 
 
下一篇
本文中,咱们已经讨论了许多基础知识。下一篇中,我将演示如何使用 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=1
b="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 test
foo-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 mystring
mystring="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 2001.00
 

在这个实例中,我在费用分类账中放入 "-"(空白),在收入分类账中放入 "inco"。"inco" 是通常(薪水之类)收入的别名。使用分类账别名让我能够按类别生成收入和费用的明细分类账。至于记录的其他部分,其它全部字段都是不需加以说明的。“是否付清?”字段("Y" 或 "N")记录了交易是否已过账到个人账户;除此以外,还有一个交易描述,和一个正的美圆金额。
用于计算当前余额的算法不太难。awk 只须要依次读取每一行。若是列出了费用分类账,但没有收入分类账(为 "-"),那么这一项就是借方。若是列出了收入分类账,但没有费用分类账(为 "-"),那么这一项就是贷方。并且,若是同时列出了费用和收入分类账,那么这个金额就是“分类账转账”;即,从费用分类账减去美圆金额,并将此金额添加到收入分类账。此外,全部这些分类账都是虚拟的,但对于跟踪收入和支出以及预算却很是有用。
代码
如今该研究代码了。咱们将从第一行(BEGIN 块和函数定义)开始:
balance,第 1 部分

#!/usr/bin/env awk -f
BEGIN {
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、$二、$3 和金额的当前值。请先研究代码,在代码以后能够见到个人说明。
balance,第 3 部分

{
curmonth=monthdigit(substr($1,4,3))
amount=$7
#record all the categories encountered
if ( $2 != "-" )
globcat[$2]="yes"
if ( $3 != "-" )
globcat[$3]="yes"
#tally up the transaction properly
if ( $2 == "-" ) {
if ( $3 == "-" ) {
print "Error: inc and exp fields are both blank!"
exit 1
} else {
#this is income
doincome(balance)
if ( $5 == "Y" )
doincome(balance2)
}
} else if ( $3 == "-" ) {
#this is an expense
doexpense(balance)
if ( $5 == "Y" )
doexpense(balance2)
} else {
#this is a transfer
dotransfer(balance)
if ( $5 == "Y" )
dotransfer(balance2)
}
}
 
 

在主块中,前两行将 curmonth 设置成 1 到 12 之间的整数,并将金额设置成字段 7(使代码易于理解)。而后,是四行有趣的代码,它们将值写到数组 globcat 中。globcat,或称做全局分类账数组,用于记录在文件中遇到的全部分类账 -- "inco"、"misc"、"food"、"util" 等。例如,若是 $2 == "inco",则将 globcat["inco"] 设置成 "yes"。稍后,咱们可使用简单的 "for (x in globcat)" 循环来迭代分类账列表。
在接着的大约二十行中,咱们分析字段 $2 和 $3,并适当记录交易。若是 $2=="-" 且 $3!="-",表示咱们有收入,所以调用 doincome()。若是是相反的状况,则调用 doexpense();若是 $2 和 $3 都包含分类账,则调用 dotransfer()。每次咱们都将 "balance" 数组传递给这些函数,从而在这些函数中记录适当的数据。
您还会发现几行代码说“if ( $5 == "Y" ),那么将同一个交易记录到 balance2 中”。咱们在这里究竟作了些什么?您将回忆起 $5 包含 "Y" 或 "N",并记录交易是否已通过账到账户。因为仅当过账了交易时咱们才将交易记录到 balance2,所以 balance2 包含了真实的账户余额,而 "balance" 包含了全部交易,无论是否已通过账。可使用 balance2 来验证数据项(由于它应该与当前银行账户余额匹配),可使用 "balance" 来确保没有透支账户(由于它会考虑您开出的还没有兑现的全部支票)。
生成报表
主块重复处理了每一行记录以后,如今咱们有了关于比较全面的、按分类账和按月份划分的借方和贷方记录。如今,在这种状况下最合适的作法是只须定义生成报表的 END 块:
balance,第 4 部分

END {
bal=0
bal2=0
for (x in globcat) {
bal=bal+balance[0,x]
bal2=bal2+balance2[0,x]
}
printf("Your available funds: %10.2f\n", bal)
printf("Your account balance: %10.2f\n", bal2)
}
 
 

这个报表将打印出汇总,以下所示:
Your available funds:1174.22
Your account balance:2399.33
 

在 END 块中,咱们使用 "for (x in globcat)" 结构来迭代每个分类账,根据记录在案的交易结算主要余额。实际上,咱们结算两个余额,一个是可用资金,另外一个是账户余额。要执行程序并处理您在文件 "mycheckbook.txt" 中输入的财务数据,将以上全部代码放入文本文件 "balance",执行 "chmod +x balance",而后输入 "./balance mycheckbook.txt"。而后 balance 脚本将合计全部交易,打印出两行余额汇总。
升级
我使用这个程序的更高级版原本管理个人我的和企业财务。个人版本(因为篇幅限制不能在此涵盖)会打印出收入和费用的月度明细分类账,包括年度总合、净收入和其它许多内容。它甚至以 HTML 格式输出数据,所以我能够在 Web 浏览器中查看它。 若是您认为这个程序有用,我建议您将这些特性添加到这个脚本中。没必要将它配置成要 记录任何附加信息;所需的所有信息已经在 balance 和 balance2 里面了。只要升级 END 块就万事具有了!
我但愿您喜欢本系列。有关 awk 的详细信息,请参考如下列出的参考资料