awk 实例
Awk 是一种名称奇怪但功能强大的语言。本文是一个包含三部分的系列的第一篇。在本文中,DanielRobbins 将使您迅速掌握 awk 编程技巧。随着本系列的进展,将讨论更高级的主题,最后将演示一个真实的高级 awk 应用程序。
捍卫 awk
在这一系列的文章中,我将使您成为精通 awk 的编程人员。我承认,awk 并没有一个非常好听且又非常 “时髦” 的名字。awk 的
GNU 版本(叫作 gawk)听起来非常怪异。那些不熟悉这种语言的人可能听说过
"awk",而且可能认为它是一组落伍且过时的混乱代码。它甚至会使最博学的 UNIX 权威陷于混乱的边缘(使他不断地发出 "kill -9!"
命令,就象使用咖啡机一样)。
的确,awk 没有一个动听的名字。但它是一种很棒的语言。awk
适合于文本处理和报表生成,它还有许多精心设计的特性,允许进行多种方式的编程。与某些语言不同,awk
的语法较为常见。它借鉴了某些语言的一些精华部分,如 C 语言、python 和 bash(虽然在技术上,awk 比 python 和 bash
早创建)。awk 是一种一旦学会就会成为您战略代码库的重要部分的语言。
第一个 Awk
您应该会看到 /ect/passwd 文件中的内容,本文使用该文件来解释 awk 的工作原理。当调用 awk 时,我们指定
/etc/passwd 作为输入文件。Awk 在执行期间对 /etc/passwd 文件中的每一行依次执行 print 命令。所有输出都发送到
stdout,可以得到类似 cat 命令的结果。
现在解释代码块 { print }。在 Awk 中,花括号用于将代码分块,这与 C 语言类似。我们的代码块中只有一条 print 命令。在 Awk 中,当 print 命令单独出现时,将打印当前行的全部内容。
$ awk '{ print $0 }' /etc/passwd
在 Awk 中,变量 $0 表示整个当前行,因此 print 和 print $0 的作用完全相同。
$ awk '{ print "" }' /etc/passwd
$ awk '{ print "hiya" }' /etc/passwd
运行该脚本,屏幕上讲显示多行 hiya。:)
多个字段
print $1 $ awk -F":" '{ print $1 $3 }' /etc/passwd
halt7
operator11
root0
shutdown6
sync5
bin1
....etc.
print $1 $3 $ awk -F":" '{ print $1 " " $3 }' /etc/passwd
$1 $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.
外部脚本
BEGIN {
FS=":"
}
{ print $1 }
两种方法的区别在于如何设置字段分隔符。在该脚本中,(通过设置 FS 变量)在代码中指定字段分隔符,而前一示例通过在命令行向
awk 传递 -F":" 选项来设置 FS。一般而言,最好在脚本内部设置字段分隔符,因为这样可以少输入一个命令行参数。本文稍后将深入讲解 FS
变量。
BEGIN 与 END 代码块
通常,awk 会针对每个输入行执行一次每个代码块。但是,在许多编程情形下,可能需要在 awk 开始处理输入文件的文本之前
执行初始化代码。对这种情况,awk 支持定义 BEGIN 代码块。前一示例使用了这种代码块。因为 BEGIN 代码块在 awk
开始处理输入文件之前执行,因此它是初始化 FS(字段分隔符)变量、打印页眉或者初始化在后续程序中将要引用的其他全局变量的绝佳位置。
另外,awk 还提供了另一种称为 END 的专用代码块。在输入文件的所有行处理完毕之后,awk 执行这个代码块。通常,END 代码块用于进行最终计算或者打印应该在输出流结尾处出现的汇总信息。
正则表达式与代码块
/foo/ { print }
/[0-9]+\.[0-9]*/ { print }
表达式与代码块
fred print $1 == "fred" { print $3 }
root $5 ~ /root/ { print $3 }
条件语句
if {
if ( $5 ~ /root/ ) {
print $3
}
}
两个脚本的作用相同。第一个示例的布尔表达式位于代码块外,而第二个示例的代码块会针对每个输入行执行一次,本文使用 if 语句有选择地执行打印命令。两种方法都可以使用,可以选择与脚本的其他部分最匹配的方法。
if 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 字符序列的行。也可以选择最适合您的代码的方法。它们的功能完全相同。
( $1 == "foo" ) && ( $2 == "bar" ) { print }
该示例只打印第一个字段等于 foo 且 第二字段等于 bar 的行。
数值变量!
在 BEGIN 代码块中,我们将整型变量 x 初始化为零。这样,awk 每次遇到空白行时都将执行 x=x+1 语句,递增 x 值。在所有行都处理完毕之后,awk 执行 END 代码块,并打印最终的汇总信息,以显示它找到的空白行数。
字符串化变量
2.01
1.01 x $( ) 1.01 { print ($1^2)+1 }
稍做试验就可以发现,如果特定变量不包含效数字,那么 awk 在计算数学表达式时将该变量当作数值零处理。
众多运算符
Awk 的另一个优点是它拥有全面的数学运算符。除了标准的加减乘除,awk 还支持前面演示的指数运算符 “^”、求模(余数)运算符 “%” 和借鉴自 C 语言的大量方便的赋值运算符。
其中包括前后加/减(i++、--foo),加减乘除赋值运算符(a+=3、b*=2、c/=2.2、d-=6.2)。而且,这仅仅是一部分 —— 我们还能使用方便的求模/指数赋值运算符(a^=2、b%=4)。
字段分隔符
Awk 有其自己的特殊变量集合。其中一些变量支持调优 awk
性能,而且可以读取另一些变量来收集关于输入的重要信息。前面已经接触过特殊变量 FS。如前所述,这个变量支持设置 awk
期望在字段中找到的字符序列。当我们使用 /ect/passwd 作为输入时,FS 设为 ":"。尽管这样做可以解决问题,但 FS
还支持更高的灵活性。
FS="\t+"
上面使用特殊的正则表达式字符 “+”,表示 “一个或多个前一字符”。
FS="[[:space:]+]"
尽管该赋值运算符能够解决问题,但是并非必要。为什么呢?因为在默认情况下,FS 被设为单个空格字符,awk 将其解释为 “一个或多个空格或制表符”。在这个特定的示例中,默认的 FS 设置恰好是您最想要的设置。
FS="foo[0-9][0-9][0-9]"
字段数目
{
if ( NF > 2 ) {
print $1 " " $2 ":" $3
}
}
记录数目
{
#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 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..."
}
while 死循环 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."
}