书摘:Linux命令、编辑器和Shell编程实用指南
本文节选自Mark Sobell的新版(第二版)著作Linux命令、编辑器和Shell编程实用指南,由Prentice Hall Professional于2009年11月出版,ISBN为0131367366,版权归2010年Mark G. Sobell所有。如需更多章节内容示例,请访问出版社网站:www.informit.com/title/0131367366
第12章:AWK模式处理语言
AWK是一种模式扫描和处理语言,它在一个或多个文件中搜索与指定模式匹配的记录(通常是行)。 每当找到匹配项时,它通过执行诸如将记录写入标准输出或递增计数器之类的操作来处理行。 与过程式语言不同,AWK是数据驱动的:您描述要处理的数据,并告诉AWK找到数据后该怎么做。
您可以使用AWK生成报告或过滤文本。 它可以很好地处理数字和文本; 当您将两者混合时,AWK通常会得出正确的答案。 AWK的作者(Alfred V. Aho,Peter J. Weinberger和Brian W. Kernighan)将该语言设计为易于使用。 为了实现这一目标,他们在最初的实现中牺牲了执行速度。
AWK从C编程语言中获取了许多构造。 它包括以下功能
灵活的格式
条件执行
循环语句
数值变量
字符串变量
正则表达式
关系表达式
C的printf
协进程执行(仅gawk)
网络数据交换(仅gawk)
语法
gawk命令行具有以下语法
gawk [options] [program] [file-list] gawk [options] –f program-file [file-list]
gawk实用程序从您在命令行上指定的文件或从标准输入获取其输入。 一个高级命令getline,使您可以更多地选择输入来源以及gawk如何读取它(第558页)。 使用协进程,gawk可以与另一个程序交互或通过网络交换数据(第560页;在awk或mawk下不可用)。 gawk的输出转到标准输出。
参数
在前面的语法中,program是您包含在命令行中的gawk程序。 program-file是保存gawk程序的文件名。 将程序放在命令行上可以使您编写简短的gawk程序,而不必创建单独的program-file. 要防止shell将gawk命令解释为shell命令,请将program括在单引号内。 将长或复杂的程序放在文件中可以减少错误和重新键入。
file-list包含gawk处理的普通文件的路径名。 这些文件是输入文件。 如果您未指定file-list,则gawk从标准输入或如getline或协进程所指定的那样获取输入。
选项
以双连字符(– –)开头的选项仅在gawk下有效。 它们在awk和mawk下不可用。
– –field-separator fs
–F fs
使用fs作为输入字段分隔符(FS变量)的值。
– –file program-file
–f program-file
从名为program-file的文件而不是命令行读取gawk程序。 您可以在命令行上多次指定此选项。 请参见示例。
– –help
–W help
总结了如何使用gawk(仅gawk)。
– –lint
–W lint
警告有关可能不正确或不可移植的gawk构造(仅gawk)。
– –posix
–W posix
运行符合POSIX的gawk版本。 此选项引入了一些限制; 有关详细信息,请参见gawk手册页(仅gawk)。
– –traditional
–W traditional
忽略gawk程序中的新GNU功能,使程序符合UNIX awk(仅gawk)。
– –assign var =value
–v var =value
将value分配给变量var。 分配发生在gawk程序执行之前,并且在BEGIN模式(第535页)中可用。 您可以在命令行上多次指定此选项。
笔记
有关AWK实现的信息,请参见上一页的提示。
为方便起见,许多Linux系统提供了从/bin/awk到/bin/gawk或/bin/mawk的链接。 因此,您可以使用任一名称运行该程序。
语言基础
gawk程序(来自命令行上的program或来自program-file)由一行或多行包含以下格式的pattern和/或action组成
pattern { action }
pattern从输入中选择行。 gawk实用程序对pattern选择的所有行执行action。 括住action的花括号使gawk能够将其与pattern区分开。 如果程序行不包含pattern,则gawk选择输入中的所有行。 如果程序行不包含action,则gawk将选择的行复制到标准输出。
首先,gawk将第一行输入(来自file-list或标准输入)与程序中的每个pattern进行比较。 如果pattern选择该行(如果存在匹配项),则gawk将执行与该pattern关联的action。 如果未选择该行,则gawk不执行action。 当gawk完成其对第一行输入的比较后,它将对下一行输入重复该过程。 它将继续此过程,比较后续的输入行,直到读取完所有输入为止。
如果多个patterns选择同一行,则gawk按照patterns在程序中出现的顺序执行与每个patterns关联的actions。 gawk有可能将输入中的单行多次发送到标准输出。
模式~ 和 !~
您可以使用正则表达式(附录A),用斜杠括起来,作为pattern. ~运算符测试字段或变量是否与正则表达式匹配(第543页上的示例)。 !~运算符测试是否不匹配。 您可以使用表12-1中列出的关系运算符执行数值和字符串比较。 您可以使用布尔运算符||(OR)或&&(AND)组合任何patterns。
表12-1 关系运算符
关系运算符 |
含义 |
< |
小于 |
<= |
小于或等于 |
= = |
等于 |
!= |
不等于 |
>= |
大于或等于 |
> |
大于 |
BEGIN 和 END
两个唯一的patterns, BEGIN 和 END,在gawk开始处理输入之前和完成处理输入之后执行命令。 gawk实用程序在处理所有输入之前执行与BEGIN pattern关联的actions,并在处理完所有输入之后执行与END pattern关联的actions。 请参见示例。
, (逗号)
逗号是范围运算符。 如果您在单个gawk程序行上用逗号分隔两个patterns,则gawk选择一系列行,从与第一个pattern匹配的第一行开始。 gawk选择的最后一行是与第二个pattern匹配的下一个后续行。 如果没有行与第二个pattern匹配,则gawk选择直到输入结尾的每一行。 在gawk找到第二个pattern之后,它通过再次寻找第一个pattern来重新开始该过程。 请参见示例。
操作gawk命令的action部分使gawk在匹配pattern时执行该action。 如果您未指定action,则gawk执行默认的action,即print命令(明确表示为{print})。 此action将记录(通常是一行;请参见“记录分隔符”)从输入复制到标准输出。
如果在print命令后跟参数,则gawk仅显示您指定的参数。 这些参数可以是变量或字符串常量。 您可以将print命令的输出发送到文件(在gawk程序中使用>),将其附加到文件(>>),或通过管道将其发送到另一个程序的输入(|)。 协进程(|&amp;)是与在后台运行的程序交换数据的双向管道(仅在gawk下可用)。
除非您用逗号分隔print命令中的项目,否则gawk会将其连接起来。 逗号使gawk用输出字段分隔符(OFS,通常是SPACE)分隔项目。
您可以通过用分号分隔它们来在一行上包含多个actions。
注释gawk实用程序忽略程序行中井号(#)后面的所有内容。 您可以通过在此符号之前添加注释来记录gawk程序。
变量尽管您无需在使用前声明gawk变量,但如果需要,可以为其分配初始值。 未分配的数值变量初始化为0;字符串变量初始化为空字符串。 除了支持用户变量外,gawk还维护程序变量。 您可以在gawk程序的pattern和action部分中使用用户和程序变量。 表12-2列出了一些程序变量。
表12-2 变量
变量 |
含义 |
$0 |
当前记录(作为单个变量) |
$1–$n |
当前记录中的字段 |
FILENAME |
当前输入文件的名称(标准输入为空) |
FS |
输入字段分隔符(默认值:空格或TAB) |
NF |
当前记录中的字段数 |
NR |
当前记录的记录号 |
OFS |
输出字段分隔符 |
ORS |
输出记录分隔符(默认值:换行符) |
RS |
输入记录分隔符(默认值:换行符) |
除了在程序中初始化变量之外,您还可以使用– –assign(–v)选项在命令行上初始化变量。 当变量的值在gawk的两次运行之间发生变化时,此功能非常有用。
默认情况下,输入和输出的记录分隔符是换行符。因此,gawk 将每行输入视为一个单独的记录,并在每个输出记录的末尾附加一个换行符。默认情况下,输入字段分隔符是空格和制表符;默认的输出字段分隔符是空格。您可以通过在程序中或从命令行使用 – –assign (–v) 选项为其关联的变量分配一个新值,随时更改任何分隔符的值。
函数表 12-3 列出了一些 gawk 提供的用于操作数字和字符串的函数。
表 12-3 函数
函数 |
含义 |
length(str ) |
返回 str 中的字符数;如果没有参数,则返回当前记录中的字符数(第 545 页) |
int(num) |
返回 num 的整数部分 |
index(str1,str2 ) |
返回 str1 中 str2 的索引,如果 str2 不存在,则返回 0 |
split(str,arr,del ) |
将由 del 分隔的 str 的元素放入数组 arr [1]...arr [n ] 中;返回数组中元素的数量(第 556 页) |
sprintf(fmt,args) |
根据 fmt 格式化 args 并返回格式化的字符串;模仿 C 编程语言的同名函数 |
substr(str,pos,len) |
返回 str 的子字符串,该子字符串从 pos 开始,长度为 len 个字符 |
tolower(str ) |
返回 str 的副本,其中所有大写字母都替换为它们的小写对应字母 |
toupper(str ) |
返回 str 的副本,其中所有小写字母都替换为它们的大写对应字母 |
表 12-4 中列出的 gawk 算术运算符来自 C 编程语言。
表 12-4 算术运算符
运算符 |
含义 |
** |
将运算符前面的表达式提高到运算符后面的表达式的幂 |
* |
将运算符前面的表达式乘以运算符后面的表达式 |
/ |
将运算符前面的表达式除以运算符后面的表达式 |
% |
取运算符前面的表达式除以运算符后面的表达式后的余数 |
+ |
将运算符前面的表达式添加到运算符后面的表达式 |
– |
从运算符前面的表达式中减去运算符后面的表达式 |
= |
将运算符后面的表达式的值赋给运算符前面的变量 |
++ |
递增运算符前面的变量 |
– – |
递减运算符前面的变量 |
+= |
将运算符后面的表达式添加到运算符前面的变量,并将结果赋给运算符前面的变量 |
– = |
从运算符前面的变量中减去运算符后面的表达式,并将结果赋给运算符前面的变量 |
= |
将运算符前面的变量乘以运算符后面的表达式,并将结果赋给运算符前面的变量 |
/= |
将运算符前面的变量除以运算符后面的表达式,并将结果赋给运算符前面的变量 |
%= |
将运算符前面的变量除以运算符后面的表达式后的余数赋给运算符前面的变量 |
关联数组是 gawk 最强大的功能之一。这些数组使用字符串作为索引。使用关联数组,您可以通过使用数字字符串作为索引来模仿传统数组。在 Perl 中,关联数组称为哈希。
您可以使用以下语法将值赋给关联数组的元素
array[string] = value
其中 array 是数组的名称,string 是您要向其赋值的数组元素的索引,而 value 是您要赋给该元素的值。
使用以下语法,您可以将 for 结构与关联数组一起使用
for (elem in array) action
其中 elem 是一个变量,当 for 结构循环遍历它们时,它采用数组的每个元素的值,array 是数组的名称,action 是 gawk 对数组中的每个元素执行的操作。您可以在此 action 中使用 elem 变量。
有关使用关联数组的示例程序,请点击此处。
printf您可以使用 printf 命令代替 print 来控制 gawk 生成的输出的格式。gawk 版本的 printf 类似于 C 语言中的 printf。printf 命令具有以下语法
printf "control-string", arg1, arg2, ..., argn
control-string 确定 printf 如何格式化 arg1, arg2, ..., argn. 这些参数可以是变量或其他表达式。在 control-string 中,您可以使用 \n 来指示换行符,使用 \t 来指示制表符。control-string 包含转换规范,每个参数对应一个。转换规范具有以下语法
%[–][x[.y]]conv
其中 – 使 printf 左对齐参数,x 是最小字段宽度,.y 是数字中小数点右边的位数。conv 指示数字转换的类型,可以从表 12-5 中的字母中选择。请参阅使用 printf 的示例程序。
表 12-5 数字转换
conv |
转换类型 |
d |
十进制 |
e |
指数符号 |
f |
浮点数 |
g |
使用 f 或 e,以较短者为准 |
o |
无符号八进制 |
s |
字符串 |
x |
无符号十六进制 |
控制(流)语句改变 gawk 程序中命令的执行顺序。本节详细介绍了 if...else、while 和 for 控制结构。此外,break 和 continue 语句与控制结构一起工作以改变命令的执行顺序。有关控制结构的更多信息,请参见第 398 页。当您指定单个简单命令时,无需在 commands 周围使用大括号。
if...else
if...else 控制结构测试 condition 返回的状态,并根据此状态转移控制。if...else 结构的语法如下所示。else 部分是可选的。
if (condition) {commands} [else {commands}]
此处显示的简单 if 语句未使用大括号
if ($5 <= 5000) print $0
接下来是一个 gawk 程序,它使用一个简单的 if...else 结构。同样,没有大括号。
$ cat if1 BEGIN { nam="sam" if (nam == "max") print "nam is max" else print "nam is not max, it is", nam } $ gawk -f if1 nam is not max, it is sam
while
只要 condition 为真,while 结构就会循环执行 commands。while 结构的语法是
while (condition) {commands}
下一个 gawk 程序使用一个简单的 while 结构来显示 2 的幂。此示例使用大括号,因为 while 循环包含多个语句。此程序不接受输入;当 gawk 执行与 BEGIN 模式关联的语句时,会发生所有处理。
$ cat while1 BEGIN { n = 1 while (n <= 5) { print "2^" n, 2n n++ } } $ gawk -f while1 1^2 2 2^2 4 3^2 8 4^2 16 5^2 32
for
for 控制结构的语法是
for (init; condition; increment) {commands}
for 结构首先执行 init 语句,该语句通常将计数器设置为 0 或 1。然后,只要 condition 保持真,它就会循环执行 commands。在每次循环之后,它执行 increment 语句。for1 gawk 程序执行与前面的 while1 程序相同的事情,除了它使用 for 语句,这使得程序更简单
$ cat for1 BEGIN { for (n=1; n <= 5; n++) print "2^" n, 2n } $ gawk -f for1 1^2 2 2^2 4 3^2 8 4^2 16 5^2 32
gawk 实用程序支持用于处理关联数组的另一种 for 语法
for (var in array) {commands}
此 for 结构循环遍历名为 array 的关联数组的元素,每次循环都将 array 的每个元素的索引的值赋给 var。以下代码行演示了一个 for 结构
END {for (name in manuf) print name, manuf[name]}
break
break 语句将控制权转移出 for 或 while 循环,终止其出现的内层循环的执行。
continue
continue 语句将控制权转移到 for 或 while 循环的末尾,导致其出现的内层循环的执行继续进行下一次迭代。
示例
cars 数据文件
本节中的许多示例都使用 cars 数据文件。从左到右,文件中的列包含每辆车的品牌、型号、制造年份、以千英里为单位的里程和价格。此文件中的所有空白均由单个制表符组成(该文件不包含任何空格)。
$ cat cars plym fury 1970 73 2500 chevy malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850 ford thundbd 2003 15 10500 chevy malibu 2000 50 3500 bmw 325i 1985 115 450 honda accord 2001 30 6000 ford taurus 2004 10 17000 toyota rav4 2002 180 750 chevy impala 1985 85 1550 ford explor 2003 25 9500
缺少模式
一个简单的 gawk 程序是
{ print }
该程序由一行程序行组成,该行是一个 action。因为 pattern 缺失,gawk 选择所有输入行。当不带任何参数使用 print 命令时,该命令将完整显示每个选定的行。该程序将输入复制到标准输出。
$ gawk '{ print }' cars plym fury 1970 73 2500 chevy malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850 ...
缺少操作
下一个程序有一个 pattern 但没有明确的 action。斜杠表示 chevy 是一个正则表达式。
/chevy/
在这种情况下,gawk 仅从输入中选择包含字符串 chevy 的那些行。当您不指定 action 时,gawk 假定 action 是 print。以下示例将包含字符串 chevy 的所有输入行复制到标准输出
$ gawk '/chevy/' cars chevy malibu 1999 60 3000 chevy malibu 2000 50 3500 chevy impala 1985 85 1550
单引号
虽然 gawk 和 shell 语法都不需要在命令行上使用单引号,但使用它们仍然是一个好主意,因为它们可以防止出现问题。如果您在命令行上创建的 gawk 程序包含空格或对 shell 来说是特殊的字符,则必须引用它们。始终将程序用单引号引起来是确保您已引用任何需要引用的字符的最简单方法。
字段
下一个例子选择文件中的所有行(它没有模式)。花括号括起动作; 你必须始终使用花括号来分隔动作,以便 gawk 可以将其与模式区分开来。此示例显示第三个字段($3)、一个空格(输出字段分隔符,用逗号表示)以及每个所选行的第一个字段($1)
$ gawk '{print $3, $1}' cars 1970 plym 1999 chevy 1965 ford 1998 volvo ...
下一个示例包含一个模式和一个动作,选择所有包含字符串 chevy 的行,并显示所选行的第三个和第一个字段
$ gawk '/chevy/ {print $3, $1}' cars 1999 chevy 2000 chevy 1985 chevy
在下面的示例中,gawk 选择包含正则表达式 h 的匹配项的行。由于没有显式的动作,gawk 会显示它选择的所有行。
$ gawk '/h/' cars chevy malibu 1999 60 3000 ford thundbd 2003 15 10500 chevy malibu 2000 50 3500 honda accord 2001 30 6000 chevy impala 1985 85 1550
~ (匹配运算符)
下一个模式使用匹配运算符 (~) 来选择第一个字段中包含字母 h 的所有行
$ gawk '$1 ~ /h/' cars chevy malibu 1999 60 3000 chevy malibu 2000 50 3500 honda accord 2001 30 6000 chevy impala 1985 85 1550
正则表达式中的插入符号 (^) 强制在行的开头或在本例中,在第一个字段的开头进行匹配
$ gawk '$1 ~ /^h/' cars honda accord 2001 30 6000
方括号括起字符类定义。在下一个示例中,gawk 选择第二个字段以 t 或 m 开头的行,并显示第三个和第二个字段、一个美元符号和第五个字段。因为 "$" 和 $5 之间没有逗号,所以 gawk 不会在输出中在它们之间放置一个空格。
$ gawk '$2 ~ /^[tm]/ {print $3, $2, "$" $5}' cars 1999 malibu $3000 1965 mustang $10000 2003 thundbd $10500 2000 malibu $3500 2004 taurus $17000
美元符号
下一个示例显示了美元符号在 gawk 程序中可以扮演的三个角色。首先,后跟数字的美元符号命名一个字段。其次,在正则表达式中,美元符号强制在行或字段的末尾进行匹配 (5$)。第三,在字符串中,美元符号表示自身。
$ gawk '$3 ~ /5$/ {print $3, $1, "$" $5}' cars 1965 ford $10000 1985 bmw $450 1985 chevy $1550
在下一个示例中,等于关系运算符 (= =) 使 gawk 在每行中的第三个字段与数字 1985 之间执行数值比较。对于比较结果为 *true* 的每一行,gawk 命令采用默认动作,print。
$ gawk '$3 == 1985' cars bmw 325i 1985 115 450 chevy impala 1985 85 1550
下一个示例查找所有价格低于或等于 3,000 美元的汽车。
$ gawk '$5 <= 3000' cars plym fury 1970 73 2500 chevy malibu 1999 60 3000 bmw 325i 1985 115 450 toyota rav4 2002 180 750 chevy impala 1985 85 1550
文本比较
当您使用双引号时,gawk 会通过使用 ASCII(或其他本地)排序序列作为比较的基础来执行文本比较。在以下示例中,gawk 显示 *字符串* 450 和 750 落在位于 *字符串* 2000 和 9000 之间的范围内,这可能不是预期的结果。
$ gawk '"2000" <= $5 && $5 < "9000"' cars plym fury 1970 73 2500 chevy malibu 1999 60 3000 chevy malibu 2000 50 3500 bmw 325i 1985 115 450 honda accord 2001 30 6000 toyota rav4 2002 180 750
当您需要执行数值比较时,请不要使用引号。下一个示例给出了预期的结果。它与前一个示例相同,只是省略了双引号。
$ gawk '2000 <= $5 && $5 < 9000' cars plym fury 1970 73 2500 chevy malibu 1999 60 3000 chevy malibu 2000 50 3500 honda accord 2001 30 6000
, (范围运算符)
范围运算符 (,) 选择一组行。它选择的第一行是逗号之前由模式指定的行。最后一行是逗号之后由模式选择的行。如果没有任何行与逗号后的模式匹配,gawk 将选择输入中的每一行。下一个示例选择所有行,从包含 volvo 的行开始,到包含 bmw 的行结束。
$ gawk '/volvo/ , /bmw/' cars volvo s80 1998 102 9850 ford thundbd 2003 15 10500 chevy malibu 2000 50 3500 bmw 325i 1985 115 450
范围运算符找到其第一组行后,它会再次开始该过程,寻找与逗号之前的模式匹配的行。在以下示例中,gawk 找到位于 chevy 和 ford 之间的三组行。尽管输入的第五行包含 ford,但 gawk 不会选择它,因为它在处理第五行时正在搜索 chevy。
$ gawk '/chevy/ , /ford/' cars chevy malibu 1999 60 3000 ford mustang 1965 45 10000 chevy malibu 2000 50 3500 bmw 325i 1985 115 450 honda accord 2001 30 6000 ford taurus 2004 10 17000 chevy impala 1985 85 1550 ford explor 2003 25 9500
当您编写更长的 gawk 程序时,将程序放在文件中并在命令行上引用该文件会很方便。使用 –f (– –file) 选项,后跟包含 gawk 程序的文件名。
以下 gawk 程序存储在名为 pr_header 的文件中,它有两个动作并使用 BEGIN 模式。 gawk 实用程序在处理数据文件的任何行之前执行与 BEGIN 关联的动作:它显示一个标题。第二个动作, {print}, 没有模式部分,并显示来自输入的所有行。
$ cat pr_header BEGIN {print "Make Model Year Miles Price"} {print} $ gawk -f pr_header cars Make Model Year Miles Price plym fury 1970 73 2500 chevy malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850 ...
下一个示例扩展了与 BEGIN 模式关联的动作。在之前和之后的示例中,标题中的空白由单个 TAB 组成,因此标题与数据列对齐。
$ cat pr_header2 BEGIN { print "Make Model Year Miles Price" print "----------------------------------------" } {print} $ gawk -f pr_header2 cars Make Model Year Miles Price ---------------------------------------- plym fury 1970 73 2500 chevy malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850 ...
length 函数
当您在没有参数的情况下调用 length 函数时,它将返回当前行中的字符数,包括字段分隔符。 $0 变量始终包含当前行的值。在下一个示例中,gawk 将行长度添加到每一行的前面,然后管道将 gawk 的输出发送到 sort(–n 选项指定数字排序)。因此,cars 文件的行按行长度顺序显示。
$ gawk '{print length, $0}' cars | sort -n 21 bmw 325i 1985 115 450 22 plym fury 1970 73 2500 23 volvo s80 1998 102 9850 24 ford explor 2003 25 9500 24 toyota rav4 2002 180 750 25 chevy impala 1985 85 1550 25 chevy malibu 1999 60 3000 25 chevy malibu 2000 50 3500 25 ford taurus 2004 10 17000 25 honda accord 2001 30 6000 26 ford mustang 1965 45 10000 26 ford thundbd 2003 15 10500
此报告的格式取决于 TAB,用于水平对齐。每行开头的三个额外字符会打乱几行的格式;这种情况的补救措施将在稍后介绍。
NR (记录号)
NR 变量包含当前行的记录(行)号。以下模式选择所有包含超过 24 个字符的行。动作显示每个选定行的行号。
$ gawk 'length > 24 {print NR}' cars 2 3 5 6 8 9 11
您可以组合范围运算符 (,) 和 NR 变量,以根据文件中的行号显示一组行。下一个示例显示第 2 行到第 4 行
$ gawk 'NR == 2 , NR == 4' cars chevy malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850
END
END 模式 的工作方式类似于 BEGIN 模式,不同之处在于 gawk 在处理完输入的最后一行之后才采取与此模式关联的动作。以下报告仅在处理完所有输入后才显示信息。在 gawk 完成处理数据文件后,NR 变量会保留其值,因此与 END 模式 关联的动作可以使用它。
$ gawk 'END {print NR, "cars for sale." }' cars 12 cars for sale.
下一个示例使用 if 控制结构来扩展某些第一个字段中使用的缩写。只要 gawk 不更改记录,它就会保持整个记录(包括任何分隔符)不变。一旦它对记录进行了更改,gawk 就会将该记录中的所有分隔符更改为输出字段分隔符的值。默认输出字段分隔符是一个空格。
$ cat separ_demo { if ($1 ~ /ply/) $1 = "plymouth" if ($1 ~ /chev/) $1 = "chevrolet" print } $ gawk -f separ_demo cars plymouth fury 1970 73 2500 chevrolet malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850 ford thundbd 2003 15 10500 chevrolet malibu 2000 50 3500 bmw 325i 1985 115 450 honda accord 2001 30 6000 ford taurus 2004 10 17000 toyota rav4 2002 180 750 chevrolet impala 1985 85 1550 ford explor 2003 25 9500
独立脚本
您可以编写一个脚本,该脚本使用要运行的命令调用 gawk,而不是从命令行使用 –f 选项和要运行的程序的名称来调用 gawk。下一个示例是一个独立的脚本,它运行与前一个示例相同的程序。 #!/bin/gawk –f 命令 (page 280) 直接运行 gawk 实用程序。要执行它,您需要对保存脚本的文件具有读取和执行权限 (page 278)。
$ chmod u+rx separ_demo2 $ cat separ_demo2 #!/bin/gawk -f { if ($1 ~ /ply/) $1 = "plymouth" if ($1 ~ /chev/) $1 = "chevrolet" print } $ ./separ_demo2 cars plymouth fury 1970 73 2500 chevrolet malibu 1999 60 3000 ford mustang 1965 45 10000 ...
OFS 变量
您可以通过为 OFS 变量赋值来更改输出字段分隔符的值。以下示例使用反斜杠转义序列 \t 将 TAB 字符分配给 OFS。此修复改进了报告的外观,但不能正确地对齐列。
$ cat ofs_demo BEGIN {OFS = "\t"} { if ($1 ~ /ply/) $1 = "plymouth" if ($1 ~ /chev/) $1 = "chevrolet" print } $ gawk -f ofs_demo cars plymouth fury 1970 73 2500 chevrolet malibu 1999 60 3000 ford mustang 1965 45 10000 volvo s80 1998 102 9850 ford thundbd 2003 15 10500 chevrolet malibu 2000 50 3500 bmw 325i 1985 115 450 honda accord 2001 30 6000 ford taurus 2004 10 17000 toyota rav4 2002 180 750 chevrolet impala 1985 85 1550 ford explor 2003 25 9500
您可以使用 printf 来优化输出格式。以下示例在两个程序行的末尾使用反斜杠来引用下面的 NEWLINE。您可以使用此技术在一行或多行上继续长行,而不会影响程序的结果。
$ cat printf_demo BEGIN { print " Miles" print "Make Model Year (000) Price" print \ "--------------------------------------------------" } { if ($1 ~ /ply/) $1 = "plymouth" if ($1 ~ /chev/) $1 = "chevrolet" printf "%-10s %-8s %2d %5d $ %8.2f\n",\ $1, $2, $3, $4, $5 } $ gawk -f printf_demo cars Miles Make Model Year (000) Price -------------------------------------------------- plymouth fury 1970 73 $ 2500.00 chevrolet malibu 1999 60 $ 3000.00 ford mustang 1965 45 $ 10000.00 volvo s80 1998 102 $ 9850.00 ford thundbd 2003 15 $ 10500.00 chevrolet malibu 2000 50 $ 3500.00 bmw 325i 1985 115 $ 450.00 honda accord 2001 30 $ 6000.00 ford taurus 2004 10 $ 17000.00 toyota rav4 2002 180 $ 750.00 chevrolet impala 1985 85 $ 1550.00 ford explor 2003 25 $ 9500.00
重定向输出
下一个示例创建两个文件:一个包含包含 chevy 的行,另一个包含包含 ford 的行。
$ cat redirect_out /chevy/ {print > "chevfile"} /ford/ {print > "fordfile"} END {print "done."} $ gawk -f redirect_out cars done. $ cat chevfile chevy malibu 1999 60 3000 chevy malibu 2000 50 3500 chevy impala 1985 85 1550
summary 程序生成有关所有汽车和较新汽车的摘要报告。尽管不是必需的,但程序开始时的初始化代表了良好的编程实践;gawk 会在您使用变量时自动声明和初始化变量。读取所有输入数据后,gawk 会计算并显示平均值。
$ cat summary BEGIN { yearsum = 0 ; costsum = 0 newcostsum = 0 ; newcount = 0 } { yearsum += $3 costsum += $5 } $3 > 2000 {newcostsum += $5 ; newcount ++} END { printf "Average age of cars is %4.1f years\n",\ 2006 - (yearsum/NR) printf "Average cost of cars is $%7.2f\n",\ costsum/NR printf "Average cost of newer cars is $%7.2f\n",\ newcostsum/newcount } $ gawk -f summary cars Average age of cars is 13.1 years Average cost of cars is $6216.67 Average cost of newer cars is $8750.00
以下 gawk 命令显示了 Linux passwd 文件中行的格式,下一个示例使用该文件
$ awk '/mark/ {print}' /etc/passwd mark:x:107:100:ext 112:/home/mark:/bin/tcsh
FS 变量
下一个示例演示了一种查找字段中最大数字的技术。因为它适用于 Linux passwd 文件,该文件使用冒号 (:) 分隔字段,所以该示例在读取任何数据之前更改了输入字段分隔符 (FS)。它读取 passwd 文件并确定下一个可用的用户 ID 号码(字段 3)。在此程序中,数字不必在 passwd 文件中按顺序排列。
模式 ($3 > saveit) 导致 gawk 选择包含大于它处理过的任何先前用户 ID 号码的用户 ID 号码的记录。每次选择记录时,gawk 都会将新的用户 ID 号码的值分配给 saveit 变量。然后 gawk 使用 saveit 的新值来测试所有后续记录的用户 ID。最后,gawk 将 1 添加到 saveit 的值并显示结果。
$ cat find_uid BEGIN {FS = ":" saveit = 0} $3 > saveit {saveit = $3} END {print "Next available UID is " saveit + 1} $ gawk -f find_uid /etc/passwd Next available UID is 1092
下一个示例基于 cars 文件生成另一个报告。此报告使用嵌套的 if...else 控制结构来根据 price 字段的内容替换值。该程序没有模式部分;它处理每个记录。
$ cat price_range { if ($5 <= 5000) $5 = "inexpensive" else if (5000 < $5 && $5 < 10000) $5 = "please ask" else if (10000 <= $5) $5 = "expensive" # printf "%-10s %-8s %2d %5d %-12s\n",\ $1, $2, $3, $4, $5 } $ gawk -f price_range cars plym fury 1970 73 inexpensive chevy malibu 1999 60 inexpensive ford mustang 1965 45 expensive volvo s80 1998 102 please ask ford thundbd 2003 15 expensive chevy malibu 2000 50 inexpensive bmw 325i 1985 115 inexpensive honda accord 2001 30 please ask ford taurus 2004 10 expensive toyota rav4 2002 180 inexpensive chevy impala 1985 85 inexpensive ford explor 2003 25 please ask
接下来,manuf 关联数组使用 cars 文件中每个记录的第一个字段的内容作为索引。该数组由元素 manuf[plym]、manuf[chevy]、manuf[ford] 等组成。每个新元素在创建时初始化为 0(零)。 ++ 运算符递增它后面的变量。
for 结构
END 模式后面的动作是一个 for 结构,它循环遍历关联数组的元素。管道通过 sort 发送输出,以生成汽车的字母列表和库存数量。因为它是一个 shell 脚本而不是 gawk 程序文件,所以您必须对 manuf 文件具有读取和执行权限才能将其作为命令执行。
$ cat manuf gawk ' {manuf[$1]++} END {for (name in manuf) print name, manuf[name]} ' cars | sort $ ./manuf bmw 1 chevy 3 ford 4 honda 1 plym 1 toyota 1 volvo 1
下一个程序 manuf.sh 是一个更通用的 shell 脚本,其中包含错误检查。此脚本列出并计算文件中列的内容,列号和文件名都在命令行上指定。
第一个动作(以 {count 开头的那个)在 gawk 程序的中间使用 shell 变量 $1 来指定数组索引。由于单引号配对的方式,看起来位于单引号之内的 $1 实际上并没有被引用:gawk 程序中的两个带引号的字符串包围着 $1,但不包含它。因为 $1 没有被引用,并且这是一个 shell 脚本,shell 会将第一个命令行参数的值替换 $1 (page 441)。因此,$1 在 gawk 命令被调用之前就被解释了。前导美元符号(该行第一个单引号之前的那个)导致 gawk 将 shell 替换的内容解释为字段号。
$ cat manuf.sh if [ $# != 2 ] then echo "Usage: manuf.sh field file" exit 1 fi gawk < $2 ' {count[$'$1']++} END {for (item in count) printf "%-20s%-20s\n",\ item, count[item]}' | sort $ ./manuf.sh Usage: manuf.sh field file $ ./manuf.sh 1 cars bmw 1 chevy 3 ford 4 honda 1 plym 1 toyota 1 volvo 1 $ ./manuf.sh 3 cars 1965 1 1970 1 1985 2 1998 1 1999 1 2000 1 2001 1 2002 1 2003 2 2004 1
一种避免使用引号的复杂方式,允许在 gawk 程序中进行参数扩展,是在命令行中使用 –v 选项将字段号作为变量传递给 gawk。这种改变使其他人更容易阅读和调试脚本。您调用 manuf2.sh 脚本的方式与调用 manuf.sh 相同。
$ cat manuf2.sh if [ $# != 2 ] then echo "Usage: manuf.sh field file" exit 1 fi gawk -v "field=$1" < $2 ' {count[$field]++} END {for (item in count) printf "%-20s%-20s\n",\ item, count[item]}' | sort
word_usage 脚本显示您在命令行上指定的文件中的单词使用列表。tr 实用程序 (page 864) 从标准输入列出单词,每行一个。sort 实用程序对文件进行排序,将最常用的单词放在最前面。该脚本按字母顺序对使用次数相同的单词组进行排序。
$ cat word_usage tr -cs 'a-zA-Z' '[\n]' < $1 | gawk ' {count[$1]++} END {for (item in count) printf "%-15s%3s\n", item, count[item]}' | sort +1nr +0f -1 $ ./word_usage textfile the 42 file 29 fsck 27 system 22 you 22 to 21 it 17 SIZE 14 and 13 MODE 13 ...
下面是一个格式不同的类似程序。这种风格模仿了 C 程序,并且对于更复杂的 gawk 程序可能更容易阅读和使用。
$ cat word_count tr -cs 'a-zA-Z' '[\n]' < $1 | gawk ' { count[$1]++ } END { for (item in count) { if (count[item] > 4) { printf "%-15s%3s\n", item, count[item] } } } ' | sort +1nr +0f -1
tail 实用程序显示最后十行输出,说明使用次数少于五次的单词不会被列出。
$ ./word_count textfile | tail directories 5 if 5 information 5 INODE 5 more 5 no 5 on 5 response 5 this 5 will 5
下一个示例演示了一种在报告中添加日期的方法。gawk 程序的第一个输入行来自 date。该程序将此行读取为记录号 1 (NR = = 1),相应地处理它,并使用与下一个模式 (NR > 1) 关联的动作处理所有后续行。
$ cat report if (test $# = 0) then echo "You must supply a filename." exit 1 fi (date; cat $1) | gawk ' NR == 1 {print "Report for", $1, $2, $3 ", " $6} NR > 1 {print $5 "\t" $1}' $ ./report cars Report for Mon Jan 31, 2010 2500 plym 3000 chevy 10000 ford 9850 volvo 10500 ford 3500 chevy 450 bmw 6000 honda 17000 ford 750 toyota 1550 chevy 9500 ford
下一个示例对您在命令行上指定的文件中的每一列求和;它从 numbers 文件中获取输入。该程序执行错误检查,报告并丢弃包含非数字条目的行。它使用 next 命令(带有注释 skip bad records)来跳过当前记录的其余命令(如果该记录包含非数字条目)。在程序结束时,gawk 显示文件的总计。
$ cat numbers 10 20 30.3 40.5 20 30 45.7 66.1 30 xyz 50 70 40 75 107.2 55.6 50 20 30.3 40.5 60 30 45.O 66.1 70 1134.7 50 70 80 75 107.2 55.6 90 176 30.3 40.5 100 1027.45 45.7 66.1 110 123 50 57a.5 120 75 107.2 55.6 $ cat tally gawk ' BEGIN { ORS = "" } NR == 1 { # first record only nfields = NF # set nfields to number of } # fields in the record (NF) { if ($0 ~ /[^0-9. \t]/) # check each record to see if it contains { # any characters that are not numbers, print "\nRecord " NR " skipped:\n\t" # periods, spaces, or TABs print $0 "\n" next # skip bad records } else { for (count = 1; count <= nfields; count++) # for good records loop through fields { printf "%10.2f", $count > "tally.out" sum[count] += $count gtotal += $count } print "\n" > "tally.out" } } END { # after processing last record for (count = 1; count <= nfields; count++) # print summary { print " -------" > "tally.out" } print "\n" > "tally.out" for (count = 1; count <= nfields; count++) { printf "%10.2f", sum[count] > "tally.out" } print "\n\n Grand Total " gtotal "\n" > "tally.out" } ' < numbers $ ./tally Record 3 skipped: 30 xyz 50 70 Record 6 skipped: 60 30 45.O 66.1 Record 11 skipped: 110 123 50 57a.5 $ cat tally.out 10.00 20.00 30.30 40.50 20.00 30.00 45.70 66.10 40.00 75.00 107.20 55.60 50.00 20.00 30.30 40.50 70.00 1134.70 50.00 70.00 80.00 75.00 107.20 55.60 90.00 176.00 30.30 40.50 100.00 1027.45 45.70 66.10 120.00 75.00 107.20 55.60 ------- ------- ------- ------- 580.00 2633.15 553.90 490.50 Grand Total 4257.55
下一个示例读取 passwd 文件,列出没有密码的用户和具有重复用户 ID 号的用户。(pwck 实用程序 [仅限 Linux] 执行类似的检查。)由于 Mac OS X 使用 Open Directory 而不是 passwd 文件,因此此示例在 OS X 下不起作用。
$ cat /etc/passwd bill::102:100:ext 123:/home/bill:/bin/bash roy:x:104:100:ext 475:/home/roy:/bin/bash tom:x:105:100:ext 476:/home/tom:/bin/bash lynn:x:166:100:ext 500:/home/lynn:/bin/bash mark:x:107:100:ext 112:/home/mark:/bin/bash sales:x:108:100:ext 102:/m/market:/bin/bash anne:x:109:100:ext 355:/home/anne:/bin/bash toni::164:100:ext 357:/home/toni:/bin/bash ginny:x:115:100:ext 109:/home/ginny:/bin/bash chuck:x:116:100:ext 146:/home/chuck:/bin/bash neil:x:164:100:ext 159:/home/neil:/bin/bash rmi:x:118:100:ext 178:/home/rmi:/bin/bash vern:x:119:100:ext 201:/home/vern:/bin/bash bob:x:120:100:ext 227:/home/bob:/bin/bash janet:x:122:100:ext 229:/home/janet:/bin/bash maggie:x:124:100:ext 244:/home/maggie:/bin/bash dan::126:100::/home/dan:/bin/bash dave:x:108:100:ext 427:/home/dave:/bin/bash mary:x:129:100:ext 303:/home/mary:/bin/bash $ cat passwd_check gawk < /etc/passwd ' BEGIN { uid[void] = "" # tell gawk that uid is an array } { # no pattern indicates process all records dup = 0 # initialize duplicate flag split($0, field, ":") # split into fields delimited by ":" if (field[2] == "") # check for null password field { if (field[5] == "") # check for null info field { print field[1] " has no password." } else { print field[1] " ("field[5]") has no password." } } for (name in uid) # loop through uid array { if (uid[name] == field[3]) # check for second use of UID { print field[1] " has the same UID as " name " : UID = " uid[name] dup = 1 # set duplicate flag } } if (!dup) # same as if (dup == 0) # assign UID and login name to uid array { uid[field[1]] = field[3] } }' $ ./passwd_check bill (ext 123) has no password. toni (ext 357) has no password. neil has the same UID as toni : UID = 164 dan has no password. dave has the same UID as sales : UID = 108
下一个示例显示了一个完整的交互式 shell 脚本,它使用 gawk 根据价格范围生成 cars 文件的报告。
$ cat list_cars trap 'rm -f $$.tem > /dev/null;echo $0 aborted.;exit 1' 1 2 15 echo -n "Price range (for example, 5000 7500):" read lowrange hirange echo ' Miles Make Model Year (000) Price --------------------------------------------------' > $$.tem gawk < cars ' $5 >= '$lowrange' && $5 <= '$hirange' { if ($1 ~ /ply/) $1 = "plymouth" if ($1 ~ /chev/) $1 = "chevrolet" printf "%-10s %-8s %2d %5d $ %8.2f\n", $1, $2, $3, $4, $5 }' | sort -n +5 >> $$.tem cat $$.tem rm $$.tem $ ./list_cars Price range (for example, 5000 7500):3000 8000 Miles Make Model Year (000) Price -------------------------------------------------- chevrolet malibu 1999 60 $ 3000.00 chevrolet malibu 2000 50 $ 3500.00 honda accord 2001 30 $ 6000.00 $ ./list_cars Price range (for example, 5000 7500):0 2000 Miles Make Model Year (000) Price -------------------------------------------------- bmw 325i 1985 115 $ 450.00 toyota rav4 2002 180 $ 750.00 chevrolet impala 1985 85 $ 1550.00 $ ./list_cars Price range (for example, 5000 7500):15000 100000 Miles Make Model Year (000) Price -------------------------------------------------- ford taurus 2004 10 $ 17000.00
可选
高级 gawk 编程
本节讨论 AWK 的一些高级功能。它涵盖了如何使用 getline 语句控制输入,如何使用协进程在 gawk 和后台运行的程序之间交换信息,以及如何使用协进程通过网络交换数据。协进程仅在 gawk 下可用;在 awk 和 mawk 下不可用。
getline:控制输入与其他输入方法相比,使用 getline 语句可以更好地控制 gawk 读取的数据。当您提供变量名作为 getline 的参数时,getline 将数据读入该变量。g1 程序的 BEGIN 块使用 getline 从标准输入中读取一行到变量 aa 中。
$ cat g1 BEGIN { getline aa print aa } $ echo aaaa | gawk -f g1 aaaa接下来的几个示例使用 alpha 文件。
$ cat alpha aaaaaaaaa bbbbbbbbb ccccccccc ddddddddd即使 g1 被赋予多行输入,它也只处理第一行。
$ gawk -f g1 < alpha aaaaaaaaa当 getline 没有给出参数时,它将输入读取到 $0 中并修改字段变量 ($1, $2, . . .)。
$ gawk 'BEGIN {getline;print $1}' < alpha aaaaaaaaag2 程序在 BEGIN 块中使用 while 循环来循环遍历标准输入中的行。getline 语句将每一行读入 holdme,并且 print 输出 holdme 的每个值。
$ cat g2 BEGIN { while (getline holdme) print holdme } $ gawk -f g2 < alpha aaaaaaaaa bbbbbbbbb ccccccccc dddddddddg3 程序演示了当 gawk 的正文中有语句(而不仅仅是一个 BEGIN 块)时,它会自动将每一行输入读取到 $0 中。该程序输出记录号 (NR)、字符串 $0: 和每行输入的 $0 值(当前记录)。
$ cat g3 {print NR, "$0:", $0} $ gawk -f g3 < alpha 1 $0: aaaaaaaaa 2 $0: bbbbbbbbb 3 $0: ccccccccc 4 $0: ddddddddd接下来,g4 演示了 getline 独立于 gawk 的自动读取和 $0 工作。当 getline 将数据读取到变量中时,它既不修改 $0 也不修改当前记录中的任何字段 ($1, $2, . . .)。g4 中的第一个语句与 g3 中的语句相同,输出 gawk 自动读取的行。getline 语句将下一行输入读取到名为 aa 的变量中。第三个语句输出记录号、字符串 aa: 和 aa 的值。g4 的输出表明 getline 独立于 gawk 的自动读取来处理记录。
$ cat g4 { print NR, "$0:", $0 getline aa print NR, "aa:", aa } $ gawk -f g4 < alpha 1 $0: aaaaaaaaa 2 aa: bbbbbbbbb 3 $0: ccccccccc 4 aa: dddddddddg5 程序输出除以字母 b 开头的行之外的每一行输入。第一个 print 语句输出 gawk 自动读取的每一行。接下来,/^b/ 模式 选择所有以 b 开头的行进行特殊处理。动作 使用 getline 将下一行输入读取到变量 hold 中,输出字符串 skip this line: 后跟 hold 的值,并输出 $1 的值。$1 保存着 gawk 自动读取的记录的第一个字段的值,而不是 getline 读取的记录。最后一个语句显示一个字符串和 NR 的值,当前记录号。即使 getline 在将数据读取到变量中时不会更改 $0,gawk 也会递增 NR。
$ cat g5 # print all lines except those read with getline {print "line #", NR, $0} # if line begins with "b" process it specially /^b/ { # use getline to read the next line into variable named hold getline hold # print value of hold print "skip this line:", hold # $0 is not affected when getline reads data into a variable # $1 still holds previous value print "previous line began with:", $1 } { print ">>>> finished processing line #", NR print "" }$ gawk -f g5 < alpha line # 1 aaaaaaaaa >>>> finished processing line # 1 line # 2 bbbbbbbbb skip this line: ccccccccc previous line began with: bbbbbbbbb >>>> finished processing line # 3 line # 4 ddddddddd >>>> finished processing line # 4
© Copyright 2010 Mark G. Sobell. 保留所有权利。