Gawk 简介
您是否经常对自己说:“我应该编写一个程序来做这件事!” 但随即意识到,您必须编写的不仅仅是解决手头问题的代码?您的程序可能需要从命令行获取数据文件名,打开并读取这些文件,以及为数据存储分配和管理内存。这种编程开销可能需要大量的编写和调试工作。更令人不悦的是,如果您“现在”就需要这个程序,而且可能只使用一两次呢?编写这个程序似乎仍然值得付出所有努力吗?如果您使用的是 C 或 C++ 等更传统的语言,也许不值得。但是,awk 编程语言可能正是编写您需要的程序,同时最大限度地减少编程开销的合适工具。
gawk,功能强大的 awk 编程语言的 GNU 版本,让您专注于编写代码来解决手头的问题,而无需担心实际使程序完成其工作所需的所有开销。gawk 提供了许多旨在帮助您快速编写有用且功能强大的程序的功能。凭借模式匹配、关联数组、自动处理命令行参数文件以及无需变量声明等功能,gawk 能够让您摆脱许多繁琐的细节,这些细节通常会妨碍您完成工作。
gawk 适用于广泛的应用,从简单的一行应用程序到将在常规基础上使用的复杂应用程序。gawk 也是 Perl 的一个更简单、更易于使用的替代方案。虽然 Perl 程序比类似的 gawk 程序运行得更快,但 gawk 的语法和功能(在我看来)更容易阅读,并且往往不会变得那么晦涩难懂。
C 程序员会发现 gawk 的某些部分对他们来说已经非常熟悉。在许多方面,gawk 的语法看起来很像 C 的语法,具有诸如前缀和后缀递增和递减运算符、可嵌套的 if-else 代码块、看起来与 C 中的 for 循环完全相同的 for 循环——甚至熟悉的 { 和 } 定义的代码段。当您考虑到 awk 编程语言的创始人之一 Brian Kernighan 也是 C 语言的创始人之一时,这种与 C 语言的密切相似性也就不足为奇了。
然而,除了语法上的相似性之外,awk 是一种与当今最常用的传统语言截然不同的语言。
在本文中,我将介绍使用 gawk(awk 的 GNU 版本)的更基本的功能。这种语言有很多部分我无法在此涵盖——对于这些部分,您需要查阅文末参考部分列出的来源之一。虽然我将介绍 gawk,但此处讨论的功能应适用于 awk 编程语言的大多数版本。因此,名称 gawk 和 awk 经常互换使用。
为了与无数关于编程语言的作者设定的传统保持一致,这里是用 awk 编写的广受欢迎的“Hello World”程序
BEGIN { print "Hello World" }
在解释如何运行此程序之前,我将介绍 gawk 程序或脚本的工作原理。
gawk 与大多数其他语言的主要区别在于,gawk 是一种模式匹配语言。也就是说,gawk 扫描其输入,查找 gawk 程序中指定的模式,并执行与该模式关联的 gawk 代码块。gawk 程序或脚本由一个或多个程序员希望与每行输入匹配的模式,以及在输入行中找到该模式时要执行的相应操作块(用 { 和 } 括起来)组成。因此,gawk 程序的格式如下
pattern1 { action1 } pattern2 { action2 } . . . patternN { actionN }
这些模式可以由简单表达式、正则表达式、模式组合甚至空模式组成,可以根据需要简单或复杂。要打印文件中包含单词“Linux”的所有行,模式只需定义为 /Linux/,操作块为 {print}。因此,完整的 gawk 程序可以写成
/Linux/ { print }
操作块由一个或多个用 { 和 } 括起来的 gawk 语句组成。在这个简单的示例中,print 语句将打印包含模式“Linux”的每一行上的所有内容。但是,此程序还将匹配诸如“LinuxKernel”之类的单词——模式不必是离散的单词。此外,由于默认情况下模式匹配区分大小写,因此它不会匹配模式“linux”。
如果您需要同时匹配大写和小写,可以更改模式以允许这样做——它只是变得更复杂的模式。如果您希望模式同时匹配“linux”和“Linux”,您可以将模式写为 /[Ll]inux/。在这种情况下,您告诉 gawk 查找以方括号(此处为大写或小写“L”)括起来的任何字符之一开头的字符组,后跟小写字母“inux”。处理大小写敏感性的其他选项是使用内置函数 tolower() 或 toupper() 在模式匹配发生之前更改输入行(或仅行的一部分)的大小写,或者您可以将内置变量 IGNORECASE(在 awk 中,内置变量始终以大写字母书写)设置为程序开始时的任何非零值。
gawk 中的模式可以根据需要简单或复杂,以匹配输入行中的所需项。如果您未指定模式,则将为每一行输入执行操作块。这称为空模式。因此,如果您未在程序中显式放入模式,则 gawk 会将缺少模式视为将匹配输入中所有内容的模式。
或者,如果您指定模式但没有操作,gawk 将为您提供默认操作——即 {print}。因此,上面的简单程序可以重写为 /Linux/,尽管通常最好显式定义操作,因为这会使代码更具可读性。
gawk 还定义了几个不匹配任何输入的特殊模式,最常用的是 BEGIN 和 END。与 BEGIN 关联的操作块将仅在 gawk 开始读取输入文件之前执行一次,并允许您处理可能需要的任何设置和初始化详细信息。END 模式的操作块将在处理完所有输入后执行,并且对于打印程序的任何最终结果非常有用。BEGIN 和 END 模式是可选的——仅在需要时才包含它们。
但是,如果您希望编写一个不接受任何输入的 gawk 脚本——例如,前面显示的广受欢迎的“Hello World”程序——您的 gawk 语句必须包含在 BEGIN 模式的操作块中。否则,gawk 会将它们视为主输入循环块(在下面描述)的一部分,并等待一些输入(或 Control-D)才能打印——这可能不是您在这种情况下想要发生的事情。
在几乎任何编程语言中工作,您都必须编写代码以从命令行获取任何文件的名称,打开这些文件并读取其内容。对于大多数文件访问,gawk 让您完全跳过这些步骤。如果您在命令行上传递一个或多个文件名,在执行 BEGIN 块中的代码(如果存在)后,gawk 将自动从命令行获取名称,打开文件,逐行读取其内容,尝试将您定义的任何模式与这些行匹配,在完成后关闭文件,然后移动到下一个列出的文件。如果输入来自标准输入(即,您正在将另一个程序的输出通过管道传输到您的 gawk 程序),则输入过程同样是透明的。但是,如果您发现需要以某种不同的方式处理此文件输入,gawk 会为您提供执行此操作所需的所有工具。但对于您需要的大多数文件处理,最好让 gawk 的输入循环为您完成工作。
现在我们已经了解了 gawk 程序的工作原理,下一步是了解如何使您的程序运行。在 Linux 上使用 gawk,我们有三种方法可以做到这一点。对于那些真正快速而肮脏的任务,可以在命令行上编写和执行整个 gawk 程序,但这实际上只适用于非常小的程序。使用我们上面的简单示例,我们可以使用以下命令运行它
gawk '/Linux/ {print}' file.txt
从命令行运行 gawk 脚本时,您必须将 awk 语句括在单引号中,并在结束引号后列出任何数据文件。如果您需要在操作块中使用多个 gawk 语句,只需使用分号分隔每个语句即可。例如,如果您想打印包含“Linux”的每一行,并记录有多少输入行包含模式 /Linux/,您可以编写
gawk '/Linux/{ print; count=count+1 } END { print count " lines" }' file.txt
您可以在命令行上列出任意数量的数据文件,gawk 将自动打开并读取它们,查找与定义的模式匹配的任何行。
您还可以使用您喜欢的编辑器编写 gawk 程序,并使用 -f 选项将文件名传递给 gawk,以告诉 gawk 尝试执行该文件的内容。(为方便起见,我喜欢在这些文件上使用扩展名“.awk”,但这并非必要。)因此,如果文件 linux.awk 包含模式-操作块
/Linux/ { print count = count + 1 } END { print count "lines found." }
可以使用以下命令执行它
gawk -f linux.awk file.txt anotherfile.txt
在 Linux(和其他版本的 Unix)下,还有另一种更简单的方法来运行您的 gawk 程序——只需将以下行放在程序顶部
#!/usr/bin/gawk -f
以指示 gawk 解释器的路径。使用 chmod 命令使文件可执行——chmod +x linux.awk。然后我们可以通过键入其名称和任何参数来执行 gawk 程序。(注意:您需要检查系统上 gawk 解释器的实际位置,并将此路径放在第一行。)
gawk 的另一个强大且节省时间的功能是它能够自动将每行输入分隔为字段,每个字段都用数字引用。整行称为 $0,当前行中的每个字段为 $1、$2,依此类推。因此,如果输入行是 This is a line,
$0 = This is a line $1 = This $2 = is $3 = a $4 = line
同样,内置变量 NF(包含当前输入行中的字段数)将设置为 4。如果您尝试引用超出 NF 的字段,则它们的值将为 NULL。另一个内置变量 NR 包含 awk 到目前为止已读取的输入行总数。
作为这些字段用法的示例,如果您需要获取文件的内容并将其打印出来,每行一个单词(如果您想将文件中的每个单词通过管道传输到拼写检查器,这将非常有用),只需运行此脚本
{ for (i=1;i<=NF;i++) print $i }
为了将行分隔为字段,gawk 使用另一个内置变量 FS(代表“字段分隔符”)。FS 的默认值为 " ",因此字段由空格分隔:任意数量的连续空格或制表符。将 FS 设置为任何其他字符意味着字段由 恰好一个 该字符的出现分隔。因此,如果该字符连续出现两次,gawk 将向您显示一个空字段。
为了更好地了解 FS 如何与输入行一起工作,假设我们想打印 /etc/passwd 中列出的所有用户的全名,其中字段由 : 分隔。您需要设置 FS=":"。如果文件 names.awk 包含以下 gawk 语句
{ FS=":" print $5 }
您使用 gawk -f names.awk /etc/passwd 运行它,程序会将每行分隔为字段并打印字段 5,在本例中,字段 5 是用户的全名。但是,行 FS=":" 将为数据文件中的每一行执行——效率不高。如果您要设置 FS,通常最好使用 BEGIN 模式,该模式仅运行一次,并将我们的程序重写为
BEGIN { FS=":" } { print $5 }
现在,行 FS=":" 将仅在 gawk 开始读取文件 /etc/passwd 之前执行一次。
这种将输入行自动拆分为字段的功能可用于使模式更强大,从而允许您将模式匹配限制为单个字段。仍然以 /etc/passwd 为例,如果您想查看 Linux 系统上所有用户的全名(/etc/passwd 的字段 5),他们更喜欢使用 csh 而不是 bash 作为他们选择的 shell(/etc/passwd 的字段 7),您可以运行以下 gawk 程序
# (in awk, anything after the # is a comment) # change the field separator so we can separate # each line of the file /etc/passwd and access # the name and shell fields BEGIN { FS=":" } $7 ~ /csh/ {print $5}
gawk 运算符 ~ 表示“匹配”,因此我们正在测试第七个字段的内容是否与 csh 匹配。如果找到匹配项,则将执行操作块并打印名称。另外,请记住,由于模式匹配子字符串,因此这也将打印 tcsh 用户的姓名。如果特定输入行不包含第七个字段,则没问题——此模式将找不到匹配项。同样,如果第七个字段的内容 不 匹配模式 bash,则模式 $7 !~ /bash/ 将运行其操作块。(与匹配运算符不同,如果 $7 在当前输入行中不存在,则此模式将匹配。回想一下,如果我们尝试访问超出 NF 的字段,则其值将为 NULL,而 NULL 不匹配 /bash/,因此将执行此模式的操作块。)
为了进一步演示字段和模式匹配的强大功能,让我们回到处理模式匹配中大小写敏感性的问题。通过使用内置函数 toupper() 或 tolower(),我们可以更改输入行的全部或选定部分的大小写。假设我们有一个数据文件,其中包含姓名(第一个字段)和电话号码(第二个字段),但有些姓名全部小写,有些姓名全部大写,有些姓名大小写混合。我们可以通过修改模式来简化匹配
toupper($1) ~ /LINUX/ {print $0}
这将导致在 awk 尝试将其与模式匹配之前,字段 1 中的名称转换为大写。输入行的其他部分将不会与模式进行比较。
gawk 语言中的控制语句与 C 语言中的控制语句非常相似,因此 C 程序员更容易编写和理解 gawk。gawk 包含前缀和后缀递增和递减运算符 ++ 和 --,以及看起来与 C 语言中的 if-else 语句非常相似的 if-else 语句。此外,多行代码块在 { 和 } 内分组。甚至 for 循环似乎也是直接从 C 编程书中取出来的。
这允许您“混合和匹配”利用 gawk 模式匹配的代码与使用更传统控制结构的代码,因此,如果模式不足以完成您的任务(或者您不确定如何使用它们来完成您的任务),您也可以使用标准编程技术。本文不介绍使用 gawk 进行传统编程;gawk 信息页(运行 info gawk)对此进行了很好的记录,本文的目标是演示 gawk 的独特功能。
gawk 的另一个节省时间的功能是,无需在使用变量之前声明变量。变量可以是字符串、整数或浮点数,具体取决于分配给它的值。gawk 将自动为您处理转换。因此,诸如 total = 2 + "3" 之类的表达式是有效的,并且将给出预期的结果 5。为了使您的工作更加轻松,gawk 将在首次使用每个变量时对其进行初始化,对于整数,将其设置为 0,对于整数或字符串,分别将其设置为 ""。这消除了对未初始化变量的任何担忧。
gawk 还将变量的易用性扩展到数组。无需在使用数组之前声明数组,甚至无需指定该数组的最大大小。要创建数组,只需使用它,gawk 将为您分配所需的空间。当您向数组添加更多数据时,其大小将自动扩展以容纳它。
但是,gawk 中的数组索引与 C 等语言中的数组索引不同,因为 gawk 索引是关联的,而不是数字的。
在关联数组中,数组索引与分配给它的值关联。这意味着您可以编写诸如 theArray["text"]="this is a line" 之类的表达式。如果您愿意,您仍然可以使用整数作为索引,如 theArray[50] = "some value" 中所示。也可以在同一数组中使用字符串、整数甚至浮点数的混合作为索引,因为 gawk 将所有索引都视为字符串。因此,表达式 theArray[50] = "some value" 等效于 theArray["50"] = "some value"。
为了尽可能轻松地使用数组,awk 为程序员提供了几个强大的数组运算符。例如,要测试值是否存在于数组中,可以使用 in 运算符。例如
if (someValue in theArray) { # action to take if somevalue is in theArray } else { # an alternate action if it is not present }
要在数组中的所有值上执行操作,例如打印其中包含的每个值,可以使用 for 循环的变体,例如
for (i in theArray) print i
gawk 在每次循环遍历时将变量 i 设置为 theArray 中的下一个值,然后打印它。
要从数组中删除值,只需使用 delete 运算符即可。例如,delete theArray["word"] 将从 theArray 中删除 "word"。
使用关联数组,您可以快速构建功能强大的应用程序,而无需担心声明数组、分配内存或在数组中搜索项目的传统开销。大小不是问题——以下 gawk 程序轻松读取并将文件 /usr/dict/words 中的所有 45,101 个单词存储到关联数组中(在本例中,使用当前行的编号作为数组索引)
{ words[NR] = $1 } END { print NR " words read" }
这样的任务在 C 语言中会更加复杂,因为您需要确定如何存储所有单词(声明大小足以容纳所有 45101 个字符串的数组?链表?二叉树?)。您可能会争辩说,使用 C,您可以自由选择一种数据结构,该结构将提供比关联数组更高的内存分配效率和更快的访问速度。虽然这可能是真的,但这并没有说明全部情况——您肯定需要花费一些时间来编写和测试此 C 程序(并且很可能需要更多时间来调试它)。关联数组的强大功能以及 gawk 中内置的简单、透明的内存管理意味着您可以摆脱对这些问题的担忧——只需告诉 gawk 您想要什么,它就会在幕后处理大部分繁重的工作。
似乎不可能同时拥有如此易用性和速度;必须进行权衡。这是 gawk 的一个缺点——运行时性能。但是,这并不是说 gawk 是一种非常慢的语言。由于 gawk 是解释型而不是编译型,因此它无法在执行速度上与编译型语言竞争。(它也比用 Perl 编写的类似程序稍慢。)但是,如果您的主要关注点是尽快编写可工作的程序,您可能不想与 C 或 C++ 语言搏斗一周来完善最有效的算法。通过权衡 C 语言(或其他编译型语言)的速度优势和控制功能以换取易用性,gawk 让您可以快速且相对轻松地完成工作。
但是,如果执行速度至关重要,gawk 是在您开始用 C 语言编码之前实现和测试原型的好工具。当原型完成后,您可能会发现 gawk 版本足够快,可以满足您的需求。
gawk 为程序员提供了简单、有点像 C 语言的语法、自动文件处理、关联数组和强大的模式匹配——这些功能可以帮助您比使用更传统的语言更快地创建程序。gawk 还具有许多其他有用且强大的功能,例如用户定义的函数、递归、许多内置函数、正则表达式、多维数组、使用 printf 和 sprintf 的格式化输出,甚至能够在命令行上设置变量。这些功能超出了本文的范围。毫无疑问,gawk 的解释器将产生比 C 编译器甚至 Perl 解释器运行速度更慢的最终产品。但是,这种较慢的执行速度(当然不慢!)可以通过程序开发和测试的速度和易用性来充分补偿。当您需要一个程序来执行任务并且您现在就需要它时,无论是快速而肮脏的一次性程序还是将被大量使用的程序,gawk 都可能被证明是完成任务的正确语言。
Ian Gordon (iang@hyprotech.com) 是位于加拿大艾伯塔省卡尔加里的 Hyprotech Ltd. 的支持程序员。他在 15 个月前发现了 Linux 的乐趣,这一发现占据了他大部分的空闲时间。