Awk是一种便于使用且表达能力强的程序设计语言,可应用于各种计算和数据处理任务。
起步
有用的awk程序往往很简短,仅仅一两行。假设你有一个名为 emp.data 的文件,其中包含员工的姓名、薪资(美元/小时)以及小时数,一个员工一行数据,如下所示:
1 | Beth 4.00 0 |
现在你想打印出工作时间超过零小时的员工的姓名和工资(薪资乘以时间)。这种任务对于awk来说就是小菜一碟。输入这个命令行就可以了:
1 | awk '$3 >0 { print $1, $2 * $3 }' emp.data |
你应该会得到如下输出:
1 | Kathy 40 |
该命令行告诉系统执行引号内的awk程序,从输入文件 emp.data 获取程序所需的数据。引号内的部分是个完整的awk程序,包含单个模式-动作语句。模式 $3>0 用于匹配第三列大于0的输入行,动作:
1 | { print $1, $2 * $3 } |
打印每个匹配行的第一个字段以及第二第三字段的乘积。
如果你想打印出还没工作过的员工的姓名,则输入命令行::
awk ‘$3 == 0 { print $1 }’ emp.data
这里,模式 $3 == 0 匹配第三个字段等于0的行,动作:
1 | $ awk ‘$3 > 0 { print $1, $2 * $3 }’ emp.data |
行首的 $ 是系统提示符,也许在你的机器上不一样。
AWK程序的结构
上述的命令行中,引号之间的部分是awk编程语言写就的程序。
awk的基本操作是一行一行地扫描输入,搜索匹配任意程序中模式的行。词语“匹配”的准确意义是视具体的模式而言,对于模式 $3 >0 来说,意思是“条件为真”。
执行AWK程序
执行awk程序的方式有多种。你可以输入如下形式的命令行::
1 | awk 'program' input files |
从而在每个指定的输入文件上执行这个program。例如,你可以输入::
1 | awk '$3 == 0 { print $1 }' file1 file2 |
打印file1和file2文件中第三个字段为0的每一行的第一个字段。
你可以省略命令行中的输入文件,仅输入::
1 | awk 'program' |
这种情况下,awk会将program应用于你在终端中接着输入的任意数据行,直到你输入一个文件结束信号(Unix系统上为control-d)。如下是Unix系统的一个会话示例:
1 | $ awk ‘$3 == 0 { print $1 }’ |
这个动作非常便于尝试awk:输入你的程序,然后输入数据,观察发生了什么。
注意命令行中的程序是用单引号包围着的。这会防止shell解释程序中 $ 这样的字符,也允许程序的长度超过一行。
当程序比较短小(几行的长度)的时候,这种约定会很方便。然而,如果程序较长,将程序写到一个单独的文件中会更加方便。假设存在程序 progfile ,输入命令行::
1 | awk -f progfile optional list of input files |
其中 -f 选项指示awk从指定文件中获取程序。可以使用任意文件名替换 progfile 。
错误
如果你的awk程序存在错误,awk会给你一段诊断信息。例如,如果你打错了大括号,如下所示:
1 | awk '$3 == 0 [ print $1 }' emp.data |
“Syntax error”意味着在 >>> <<< 标记的地方检测到语法错误。“Bailing out”意味着没有试图恢复。有时你会得到更多的帮助-关于错误是什么,比如大括号或括弧不匹配。
因为存在句法错误,awk就不会尝试执行这个程序。然而,有些错误,直到你的程序被执行才会检测出来。
NF, 字段数量
Awk会对当前输入的行有多少个字段进行计数, 并且将当前行的字段数量存 储在一个内建的称作 NF 的变量中. 因此, 下面的程序
1 | { print NF, $1, $NF } |
会依次打印出每一行的字段数量, 第一个字段的值, 最后一个字段的值.
打印行号
Awk提供了另一个内建变量, 叫做 NR, 它会存储当前已经读取了多少行的计数. 我们可以使用 NR 和 $0 给 emp.data 的没一行加上行号:
1 | { print NR, $0 } |
打印的输出看起来会是这样:
1 | 1 Beth 4.00 0 |
在输出中添加内容
你当然也可以在字段中间或者计算的值中间打印输出想要的内容:
1 | { print "total pay for", $1, "is", $2 * $3 } |
在打印语句中, 双引号内的文字将会在字段和计算的值中插入输出.
高级输出
print 语句可用于快速而简单的输出。若要严格按照你所想的格式化输出,则需要使用 printf 语句。 printf 几乎可以产生任何形式的输出
printf 语句的形式如下::
1 | printf(format, value1, value2, ..., valuen) |
其中 format 是字符串,包含要逐字打印的文本,穿插着 format 之后的每个值该如何打印的规格(specification)。一个规格是一个 % 符,后面跟着一些字符,用来控制一个 value 的格式。第一个规格说明如何打印 value1 ,第二个说明如何打印 value2 ,… 。因此,有多少 value 要打印,在 format 中就要有多少个 % 规格。
使用 printf 打印每位员工的总薪酬:
1 | { printf("total pay for %s is $%.2f\n", $1, $2 * $3) } |
printf 语句中的规格字符串包含两个 % 规格。第一个是 %s ,说明以字符串的方式打印第一个值 $1 。第二个是 %.2f ,说明以数字的方式打印第二个值 $2*$3 ,并保留小数点后面两位。规格字符串中其他东西,包括美元符号,仅逐字打印。字符串尾部的 \n 代表开始新的一行,使得后续输出将从下一行开始。以 emp.data 为输入,该程序产生:
1 | total pay for Beth is $0.00 |
printf 不会自动产生空格或者新的行,必须是你自己来创建,所以不要忘了 \n 。
打印每位员工的姓名与薪酬:
1 | { printf("%-8s $%6.2f\n", $1, $2 * $3) } |
第一个规格 %-8s 将一个姓名以字符串形式在8个字符宽度的字段中左对齐输出。第二个规格 %6.2f 将薪酬以数字的形式,保留小数点后两位,在6个字符宽度的字段中输出。
1 | Beth $ 0.00 |
之后我们将展示更多的 printf 示例。一切精彩尽在2.4小节。
排序输出
1 | awk '{ printf("%6.2f %s\n", $2 * $3, $0) }' emp.data | sort |
将awk的输出通过管道传给 sort 命令,输出为:
1 | 0.00 Beth 4.00 0 |
选择
对比选择
1 | $2 * $3 > 50 { printf("$%.2f for %s\n", $2 * $3, $1) } |
打印出总薪资超过50美元的员工的薪酬。
文本内容选择
1 | $1 == "Susie" |
操作符 == 用于测试相等性。你也可以使用称为 正则表达式 的模式查找包含任意字母组合,单词或短语的文本。这个程序打印任意位置包含 Susie 的行:
/Susie/
输出为这一行:
1 | Susie 4.25 18 |
正则表达式可用于指定复杂的多的模式
模式组合
可以使用括号和逻辑操作符与 && , 或 || , 以及非 ! 对模式进行组合。程序:
1 | $2 >= 4 || $3 >= 20 |
会打印 $2 (第二个字段) 大于等于 4 或者 $3 (第三个字段) 大于等于 20 的行:
1 | Beth 4.00 0 |
BEGIN与END
特殊模式 BEGIN 用于匹配第一个输入文件的第一行之前的位置, END 则用于匹配处理过的最后一个文件的最后一行之后的位置。这个程序使用 BEGIN 来输出一个标题::
1 | BEGIN { print "Name RATE HOURS"; print ""} |
输出为:
1 | NAME RATE HOURS |
程序的动作部分你可以在一行上放多个语句,不过要使用分号进行分隔。注意 普通的 print 是打印当前输入行,与之不同的是 print “” 会打印一个空行。
使用AWK进行计算
计数
这个程序使用一个变量 emp 来统计工作超过15个小时的员工的数目::
1 | $3 > 15 { emp = emp + 1 } |
对于第三个字段超过15的每行, emp 的前一个值加1。以 emp.data 为输入,该程序产生:
1 | 3 employees worked more than 15 hours |
用作数字的awk变量的默认初始值为0,所以我们不需要初始化 emp 。
求和与平均值
为计算员工的数目,我们可以使用内置变量 NR ,它保存着到目前位置读取的行数;在所有输入的结尾它的值就是所读的所有行数。
1 | END { print NR, "employees" } |
输出为:
6 employees
如下是一个使用 NR 来计算薪酬均值的程序::
1 | { pay = pay + $2 * $3 } |
第一个动作累计所有员工的总薪酬。 END 动作打印出
1 | 6 employees |
很明显, printf 可用来产生更简洁的输出。并且该程序也有个潜在的错误:在某种不太可能发生的情况下, NR 等于0,那么程序会试图执行零除,从而产生错误信息。
字符串连接
可以合并老字符串来创建新字符串。这种操作称为 连接(concatenation) 。程序
1 | { names = names $1 " "} |
通过将每个姓名和一个空格附加到变量 names 的前一个值, 来将所有员工的姓名收集进单个字符串中。最后 END 动作打印出 names 的值:
Beth Dan Kathy Mark Mary Susie
awk程序中,连接操作的表现形式是将字符串值一个接一个地写出来。对于每个输入行,程序的第一个语句先连接三个字符串: names 的前一个值、当前行的第一个字段以及一个空格,然后将得到的字符串赋值给 names 。
因此,读取所有的输入行之后, names 就是个字符串,包含所有员工的姓名,每个姓名后面跟着一个空格。用于保存字符串的变量的默认初始值是空字符串(也就是说该字符串包含零个字符),因此这个程序中的 names 不需要显式初始化。
内置函数
我们已看到awk提供了内置变量来保存某些频繁使用的数量,比如:字段的数量和输入行的数量。类似地,也有内置函数用来计算其他有用的数值。除了平方根、对数、随机数诸如此类的算术函数,也有操作文本的函数。其中之一是 length ,计算一个字符串中的字符数量。例如,这个程序会计算每个人的姓名的长度::
1 | { print $1, length($1) } |
行、单词以及字符的计数
控制语句
Awk为选择提供了一个 if-else 语句,以及为循环提供了几个语句,所以都效仿C语言中对应的控制语句。它们仅可以在动作中使用。
if-else语句
如下程序将计算时薪超过6美元的员工的总薪酬与平均薪酬。它使用一个 if 来防范计算平均薪酬时的零除问题。
1 | $2 > 6 { n = n + 1; pay = pay + $2 * $3 } |
interest1 - 计算复利
输入: 钱数 利率 年数
输出: 复利值
{ i = 1
while (i <= $3) {
printf(“\t%.2f\n”, $1 * (1 + $2) ^ i)
i = i + 1
}
}
1 |
|
interest1 - 计算复利
输入: 钱数 利率 年数
输出: 每年末的复利
{ for (i = 1; i <= $3; i = i + 1)
printf(“\t%.2f\n”, $1 * (1 + $2) ^ i)
}
1 |
|
反转 - 按行逆序打印输入
{ line[NR] = $0 } # 记下每个输入行
END { i = NR # 逆序打印
while (i > 0) {
print line[i]
i = i - 1
}
}
1 |
|
Susie 4.25 18
Mary 5.50 22
Mark 5.00 20
Kathy 4.00 10
Dan 3.75 0
Beth 4.00 0
1 |
|
反转 - 按行逆序打印输入
{ line[NR] = $0 } # 记下每个输入行
END { for (i = NR; i > 0; i = i - 1)
print line[i]
}