AWK

Awk是一种便于使用且表达能力强的程序设计语言,可应用于各种计算和数据处理任务。

起步

有用的awk程序往往很简短,仅仅一两行。假设你有一个名为 emp.data 的文件,其中包含员工的姓名、薪资(美元/小时)以及小时数,一个员工一行数据,如下所示:

Beth	4.00	0
Dan	3.75	0
kathy	4.00	10
Mark	5.00	20
Mary	5.50	22
Susie	4.25	18

现在你想打印出工作时间超过零小时的员工的姓名和工资(薪资乘以时间)。这种任务对于awk来说就是小菜一碟。输入这个命令行就可以了:

awk '$3 >0 { print $1, $2 * $3 }' emp.data

你应该会得到如下输出:

Kathy 40
Mark 100
Mary 121
Susie 76.5

该命令行告诉系统执行引号内的awk程序,从输入文件 emp.data 获取程序所需的数据。引号内的部分是个完整的awk程序,包含单个模式-动作语句。模式 $3>0 用于匹配第三列大于0的输入行,动作:

{ print $1, $2 * $3 }

打印每个匹配行的第一个字段以及第二第三字段的乘积。

如果你想打印出还没工作过的员工的姓名,则输入命令行::

awk ‘$3 == 0 { print $1 }’ emp.data 这里,模式 $3 == 0 匹配第三个字段等于0的行,动作:

$ awk ‘$3 > 0 { print $1, $2 * $3 }’ emp.data
Kathy 40
Mark 100
Mary 121
Susie 76.5
$ awk ‘$3 == 0 { print $1 }’ emp.data
Beth
Dan
$

行首的 $ 是系统提示符,也许在你的机器上不一样。

AWK程序的结构

上述的命令行中,引号之间的部分是awk编程语言写就的程序。

awk的基本操作是一行一行地扫描输入,搜索匹配任意程序中模式的行。词语“匹配”的准确意义是视具体的模式而言,对于模式 $3 >0 来说,意思是“条件为真”。

执行AWK程序

执行awk程序的方式有多种。你可以输入如下形式的命令行::

awk 'program' input files

从而在每个指定的输入文件上执行这个program。例如,你可以输入::

awk '$3 == 0 { print $1 }' file1 file2

打印file1和file2文件中第三个字段为0的每一行的第一个字段。

你可以省略命令行中的输入文件,仅输入::

awk 'program'

这种情况下,awk会将program应用于你在终端中接着输入的任意数据行,直到你输入一个文件结束信号(Unix系统上为control-d)。如下是Unix系统的一个会话示例:

$ awk ‘$3 == 0 { print $1 }’
Beth 4.00 0
Beth

Dan 3.75 0
Dan

Kathy 3.75 10
Kathy 3.75 0
Kathy

...

这个动作非常便于尝试awk:输入你的程序,然后输入数据,观察发生了什么。

注意命令行中的程序是用单引号包围着的。这会防止shell解释程序中 $ 这样的字符,也允许程序的长度超过一行。

当程序比较短小(几行的长度)的时候,这种约定会很方便。然而,如果程序较长,将程序写到一个单独的文件中会更加方便。假设存在程序 progfile ,输入命令行::

awk -f progfile     optional list of input files

其中 -f 选项指示awk从指定文件中获取程序。可以使用任意文件名替换 progfile 。

错误

如果你的awk程序存在错误,awk会给你一段诊断信息。例如,如果你打错了大括号,如下所示:

awk '$3 == 0 [ print $1 }' emp.data
你会得到如下信息:

awk: syntax error at source line 1
context is
$3 == 0 >>> [ <<<
extra }
missing ]
awk: bailing out at source line 1

“Syntax error”意味着在 »> «< 标记的地方检测到语法错误。“Bailing out”意味着没有试图恢复。有时你会得到更多的帮助-关于错误是什么,比如大括号或括弧不匹配。

因为存在句法错误,awk就不会尝试执行这个程序。然而,有些错误,直到你的程序被执行才会检测出来。

NF, 字段数量

Awk会对当前输入的行有多少个字段进行计数, 并且将当前行的字段数量存 储在一个内建的称作 NF 的变量中. 因此, 下面的程序

{ print NF, $1, $NF }

会依次打印出每一行的字段数量, 第一个字段的值, 最后一个字段的值.

打印行号

Awk提供了另一个内建变量, 叫做 NR, 它会存储当前已经读取了多少行的计数. 我们可以使用 NR 和 $0 给 emp.data 的没一行加上行号:

{ print NR, $0 }

打印的输出看起来会是这样:

1 Beth   4.00     0
2 Dan    3.75     0
3 Kathy  4.00    10
4 Mark   5.00    20
5 Mary   5.50    22
6 Susie  4.25   1 8

在输出中添加内容

你当然也可以在字段中间或者计算的值中间打印输出想要的内容:

{ print "total pay for", $1, "is", $2 * $3 }
输出

total pay for Beth is 0
total pay for Dan is 0
total pay for Kathy is 40
total pay for Mark is 100
total pay for Mary is 121
total pay for Susie is 76.5

在打印语句中, 双引号内的文字将会在字段和计算的值中插入输出.

高级输出

print 语句可用于快速而简单的输出。若要严格按照你所想的格式化输出,则需要使用 printf 语句。 printf 几乎可以产生任何形式的输出

printf 语句的形式如下::

printf(format, value1, value2, ..., valuen)

其中 format 是字符串,包含要逐字打印的文本,穿插着 format 之后的每个值该如何打印的规格(specification)。一个规格是一个 % 符,后面跟着一些字符,用来控制一个 value 的格式。第一个规格说明如何打印 value1 ,第二个说明如何打印 value2 ,… 。因此,有多少 value 要打印,在 format 中就要有多少个 % 规格。

使用 printf 打印每位员工的总薪酬:

{ printf("total pay for %s is $%.2f\n", $1, $2 * $3) }

printf 语句中的规格字符串包含两个 % 规格。第一个是 %s ,说明以字符串的方式打印第一个值 $1 。第二个是 %.2f ,说明以数字的方式打印第二个值 $2*$3 ,并保留小数点后面两位。规格字符串中其他东西,包括美元符号,仅逐字打印。字符串尾部的 \n 代表开始新的一行,使得后续输出将从下一行开始。以 emp.data 为输入,该程序产生:

total pay for Beth is $0.00
total pay for Dan is $0.00
total pay for Kathy is $40.00
total pay for Mark is $100.00
total pay for Mary is $121.00
total pay for Susie is $76.50

printf 不会自动产生空格或者新的行,必须是你自己来创建,所以不要忘了 \n 。

打印每位员工的姓名与薪酬:

{ printf("%-8s $%6.2f\n", $1, $2 * $3) }

第一个规格 %-8s 将一个姓名以字符串形式在8个字符宽度的字段中左对齐输出。第二个规格 %6.2f 将薪酬以数字的形式,保留小数点后两位,在6个字符宽度的字段中输出。

Beth     $  0.00
Dan      $  0.00
Kathy    $ 40.00
Mark     $100.00
Mary     $121.00
Susie    $ 76.50

之后我们将展示更多的 printf 示例。一切精彩尽在2.4小节。

排序输出

awk '{ printf("%6.2f    %s\n", $2 * $3, $0) }' emp.data | sort

将awk的输出通过管道传给 sort 命令,输出为:

  0.00    Beth  4.00 0
  0.00    Dan   3.75 0
 40.00    Kathy 4.00 10
 76.50    Susie 4.25 18
100.00    Mark  5.00 20
121.00    Mary  5.50 22

选择

对比选择

$2 * $3 > 50 { printf("$%.2f for %s\n", $2 * $3, $1) }

打印出总薪资超过50美元的员工的薪酬。

文本内容选择

$1 == "Susie"

操作符 == 用于测试相等性。你也可以使用称为 正则表达式 的模式查找包含任意字母组合,单词或短语的文本。这个程序打印任意位置包含 Susie 的行:

/Susie/ 输出为这一行:

Susie   4.25    18

正则表达式可用于指定复杂的多的模式

模式组合

可以使用括号和逻辑操作符与 && , 或   , 以及非 ! 对模式进行组合。程序:
$2 >= 4 || $3 >= 20

会打印 $2 (第二个字段) 大于等于 4 或者 $3 (第三个字段) 大于等于 20 的行:

Beth    4.00    0
kathy   4.00    10
Mark    5.00    20
Mary    5.50    22
Susie   4.25    18

BEGIN与END

特殊模式 BEGIN 用于匹配第一个输入文件的第一行之前的位置, END 则用于匹配处理过的最后一个文件的最后一行之后的位置。这个程序使用 BEGIN 来输出一个标题::

BEGIN { print "Name    RATE    HOURS"; print ""}
      { print }

输出为:

NAME    RATE    HOURS

Beth    4.00    0
Dan     3.75    0
Kathy   4.00    10
Mark    5.00    20
Mary    5.50    22
Susie   4.25    18

程序的动作部分你可以在一行上放多个语句,不过要使用分号进行分隔。注意 普通的 print 是打印当前输入行,与之不同的是 print “” 会打印一个空行。

使用AWK进行计算

计数

这个程序使用一个变量 emp 来统计工作超过15个小时的员工的数目::

$3 > 15 { emp = emp + 1 }
END     { print emp, "employees worked more than 15 hours" }

对于第三个字段超过15的每行, emp 的前一个值加1。以 emp.data 为输入,该程序产生:

3 employees worked more than 15 hours

用作数字的awk变量的默认初始值为0,所以我们不需要初始化 emp 。

求和与平均值

为计算员工的数目,我们可以使用内置变量 NR ,它保存着到目前位置读取的行数;在所有输入的结尾它的值就是所读的所有行数。

END { print NR, "employees" }

输出为:

6 employees

如下是一个使用 NR 来计算薪酬均值的程序::

    { pay = pay + $2 * $3 }
END { print NR, "employees"
      print "total pay is", pay
      print "average pay is", pay/NR
    }

第一个动作累计所有员工的总薪酬。 END 动作打印出

6 employees
total pay is 337.5
average pay is 56.25

很明显, printf 可用来产生更简洁的输出。并且该程序也有个潜在的错误:在某种不太可能发生的情况下, NR 等于0,那么程序会试图执行零除,从而产生错误信息。

字符串连接

可以合并老字符串来创建新字符串。这种操作称为 连接(concatenation) 。程序

    { names = names $1 " "}
END { print names }

通过将每个姓名和一个空格附加到变量 names 的前一个值, 来将所有员工的姓名收集进单个字符串中。最后 END 动作打印出 names 的值:

Beth Dan Kathy Mark Mary Susie

awk程序中,连接操作的表现形式是将字符串值一个接一个地写出来。对于每个输入行,程序的第一个语句先连接三个字符串: names 的前一个值、当前行的第一个字段以及一个空格,然后将得到的字符串赋值给 names 。

因此,读取所有的输入行之后, names 就是个字符串,包含所有员工的姓名,每个姓名后面跟着一个空格。用于保存字符串的变量的默认初始值是空字符串(也就是说该字符串包含零个字符),因此这个程序中的 names 不需要显式初始化。

内置函数

我们已看到awk提供了内置变量来保存某些频繁使用的数量,比如:字段的数量和输入行的数量。类似地,也有内置函数用来计算其他有用的数值。除了平方根、对数、随机数诸如此类的算术函数,也有操作文本的函数。其中之一是 length ,计算一个字符串中的字符数量。例如,这个程序会计算每个人的姓名的长度::

{ print $1, length($1) }
结果::

Beth 4
Dan 3
Kathy 5
Mark 4
Mary 4
Susie 5

行、单词以及字符的计数

控制语句

Awk为选择提供了一个 if-else 语句,以及为循环提供了几个语句,所以都效仿C语言中对应的控制语句。它们仅可以在动作中使用。

if-else语句

如下程序将计算时薪超过6美元的员工的总薪酬与平均薪酬。它使用一个 if 来防范计算平均薪酬时的零除问题。

$2 > 6 { n = n + 1; pay = pay + $2 * $3 }
END    { if (n > 0)
            print n, "employees, total pay is", pay,
                     "average pay is", pay/n
         else
             print "no employees are paid more than $6/hour"
        }

emp.data 的输出是::

no employees are paid more than $6/hour

if-else 语句中,if 后的条件会被计算。如果为真,执行第一个 print 语句。否则,执行第二个 print 语句。注意我们可以使用一个逗号将一个长语句截断为多行来书写。

while语句

一个 while 语句有一个条件和一个执行体。条件为真时执行体中的语句会被重复执行。这个程序使用公式

value=amount(1+rate)yearsvalue=amount(1+rate)years

来演示以特定的利率投资一定量的钱,其数值是如何随着年数增长的。

# interest1 - 计算复利
#   输入: 钱数    利率    年数
#   输出: 复利值

{   i = 1
    while (i <= $3) {
        printf("\t%.2f\n", $1 * (1 + $2) ^ i)
        i = i + 1
    }
}

条件是 while 后括弧包围的表达式;循环体是条件后大括号包围的两个表达式。 printf 规格字符串中的 \t 代表制表符; ^ 是指数操作符。从 # 开始到行尾的文本是注释,会被awk忽略,但能帮助程序的读者理解程序做的事情。

for语句

另一个语句, for ,将大多数循环都包含的初始化、测试、以及自增压缩成一行。如下是之前利息计算的 for 版本::

# interest1 - 计算复利
#   输入: 钱数    利率    年数
#   输出: 每年末的复利

{ for (i = 1; i <= $3; i = i + 1)
    printf("\t%.2f\n", $1 * (1 + $2) ^ i)
}

初始化 i = 1 只执行一次。接下来,测试条件 i <= $3 ;如果为真,则执行循环体的 printf 语句。循环体执行结束后执行自增 i = i + 1 ,接着由另一次条件测试开始下一个循环迭代。代码更加紧凑,并且由于循环体仅是一条语句,所以不需要大括号来包围它。

数组

awk为存储一组相关的值提供了数组。虽然数组给予了awk很强的能力,但在这里我们仅展示一个简单的例子。如下程序将按行逆序打印输入。第一个动作将输入行存为数组 line 的连续元素;即第一行放在 line[1] ,第二行放在 line[2] , 依次继续。 END 动作使用一个 while 语句从后往前打印数组中的输入行::

# 反转 - 按行逆序打印输入

    { line[NR] = $0 }  # 记下每个输入行

END { i = NR           # 逆序打印
      while (i > 0) {
        print line[i]
        i = i - 1
      }
    }
    

以 emp.data 为输入,输出为

Susie    4.25   18
Mary     5.50   22
Mark     5.00   20
Kathy    4.00   10
Dan      3.75   0
Beth     4.00   0

如下是使用 for 语句实现的相同示例:


# 反转 - 按行逆序打印输入

    { line[NR] = $0 }   # 记下每个输入行

END { for (i = NR; i > 0; i = i - 1)
        print line[i]
    }