Awk实例 第一部分


起因:近日google一把awk的教程,翻到IBM developerworks上一个初级的awk系列文章,是Gentoo Linux创始人Daniel Robbins在多年前写的。细看一下觉得很不错,可developerworks中国上面的中文版却是缺斤少量,比起原文整段整段的丢失,让人看得糊涂。不知是机器翻译问题还是版面问题?于是只有去翻看英文版(本人英语那个差啊,逼着看),遂有重新翻译一遍,权当为自己保留,也为其他有需要的朋友保留吧。尽管是初级内容,还是很值得一看,确实没想到Gentoo的创始人会写这么基础的东西来普渡众生。

原文地址: http://www.ibm.com/developerworks/linux/library/l-awk1/index.html

Common threads: Awk by example, Part 1

一个名字怪怪的强大语言介绍

概要:awk是一个有着奇怪名字的强大语言。本文是这个系列中的第一篇,本系列共包括三篇文章。本篇中Daniel将带你快速掌握awk的编程技巧,随着系列的进展,将包括更多高级的主题,最终演示一个真实的高级awk应用程序。

为AWK辩护

       在这一系列文章中,我将带领你成为一个熟练的awk程序员。我承认awk并没有一个漂亮又时尚的名字,并且GNU版的awk(名叫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命令。所有的输出都发送到标准输出,我们得到的结果跟执行cat /etc/passwd一样。现在解释一下{ print }代码块。类似C语言一样,awk中使用花括号组织语句块。在我们的代码块中只有一个print命令。awk中只有print命令单独出现的时候,当前行的所有内容都会被打印出来。

下面还有另外一个awk例子,他干上面那个语句所做的相同的事情:

$ awk '{ print $0 }' /etc/passwd 

在awk中,$0代表当前行整行,所以print和print $0实际上是干一样的事情。如果你喜欢,你也能创建一个awk程序来输出与输入数据完全不相干的数据。下面就是一个例子:

$ awk '{ print "" }' /etc/passwd 

每当你传递“”字符串给print命令时,他打印一个空白行。如果你测试上面这个脚本,你将发现awk为/etc/passwd中的每一行都输出一个空白行。这是因为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文件中的第一个和第三个字段,刚好是用户名和uid字段。现在,尽管这个脚本工作了,但他并不完美——两个输出字段间没有任何空格!如果你习惯在bash或python中编程,你可能期望print $1 $3命令在两个字段间插入空格。然而,awk程序中当两个字符串相邻出现时,awk不会增加中间空格来连接他们。下面的命令将在两个字段间插入空格:

$ awk -F":" '{ print $1 " " $3 }' /etc/passwd 

当你使用这种方式调用print时,他将连接$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.

外部脚本

对于小的单行脚本来说,作为命令行参数传给awk非常方便,但当脚本变成复杂的多行程序,你绝对想要将脚本组织在一个外部文件中。然后通过-f选项将该脚本文件传给awk:

$ awk -f myscript.awk myfile.in 

将脚本放到单独的文本文件中也允许你利用awk额外的特性。例如,下面这个多行脚本与我们早前的一个单行脚本干一样的事:打印/etc/passwd中每行的的第一个字段:

BEGIN { 
FS=":"
}
{ print $1 }

这两种方法的不同之处在于我们如何设置分割符。这个脚本中,在代码内部指定分隔符(通过设置FS变量),而前面的例子则通过在awk命令行中传递-F":"选项。仅因为这样做你可以得到少记一个命令行参数的好处,因此在脚本中设置分隔符通常也是最好的。我们将在这篇文章中更详细的介绍FS变量

BEGIN和END块

通常情况,awk在输入文件的每行上执行脚本中每个代码块。还有在许多编程环境下,你需要在开始处理输入文件的文本前执行一些初始化动作。对于这种情况,awk允许你定义BEGIN块。我们在前面的例子中已经用过一次BEGIN块。因为BEGIN块是在awk开始处理输入文件前执行的,因此它是进行诸如初始化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并且第二个字段为two的行。

数值变量

到目前为止,我们用awk打印了字符串、整行或者特定字段。然而,awk也允许我们进行整数和浮点数的的数学运算。使用算术表达式,写一个脚本来统计一个文件中的空白行数很容易。这儿就是一个干这件事的例子:

BEGIN { x=0 } 
/^$/  { x=x+1 } 
END   { print "I found " x " blank lines. :)" } 

在BEGIN块中我们将整型变量x初始化为0。 然后,每当awk遇到空白行都将执行x=x+1语句,将x的值加1。在所有行都处理完成后,END块内的语句执行,awk将输出最后的总结,指出他找到的空白行数。

串型变量

关于awk变量的一个巧妙的事情是变量是“简单和串型的(simple and stringy)“。我之所以说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

有趣吧!尽管我们给变量x赋了一个字符串值1.01,我们仍然能够给x加1。在bash或python中就不能这么干。首先,bash不支持浮点算术,其次,尽管bash有字符串型变量但他们不是"简单"的;要执行任何算术操作,bash要求我们使用一个丑陋的$()结构把表达式括起来。如果我们使用python,我们还必须在进行算术运算前显示的将字符串”1.01“转换为浮点数。有了awk,这一切都是自动进行的,这会让我们的代码看起来更清晰干净。如果我们想给每个输入行的第一个字段进行平方并加1,我们可以用下面的这个脚本:

{ print ($1^2)+1 } 

做一个小小的尝试,你会发现在进行算术运算时,如果一个特定的变量并不包含有效的数值,awk将认为该变量是数值0。

大量的运算符

awk的另一个好的地方是,他实现了完全的算术运算符。除了标准的加、减、乘、除,awk也给允许我们使用指数运算符"^"(上个例子已用过)、求模运算符"%"和一堆从C语言中借用过来的其他很方便的赋值运算符。

包括前置和后置的自增自减( i++, --foo ),加/减/乘/除赋值运算符( a+=3, b*=2, c/=2.2, d-=6.2 )。这还不是全部--我们也能得到方便的求模/指数赋值运算符(a^=2,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 == 3 { print "this particular record has three fields: " $0 } 

当然,你也能在条件语句中使用NF变量,如下:

{ 
  if ( NF > 2 ) { 
          print $1 " " $2 ":" $3 
  } 
} 

记录号

记录号(NR)是另一个方便的变量。它总是包含当前记录编号(awk处理第一个记录的编号为1)。直到现在,我们 处理的输入文件中每行为一个记录。在这种情况下,NR将告诉你当前的行号。然而,当我们后面开始处理多行时,将不再是这种情况,所以要小心!NR能像NF一样涌来打印特定的行:

(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应用来结束这个系列。与此同时,如果你渴望学的更多,看看下面列出的这些资源吧。

Resources

    • If you'd like a good old-fashioned book, O'Reilly's sed & awk, 2nd Edition is a wonderful choice.

    • Be sure to check out the comp.lang.awk FAQ. It also contains lots of additional awk links.

    • Patrick Hartigan's awk tutorial is packed with handy awk scripts.

    • Thompson's TAWK Compiler compiles awk scripts into fast binary executables. Versions are available for Windows, and DOS.

    • The GNU Awk User's Guide is available for online reference.

 第一部分结束!

相关内容