awk 工具
awk 既是工具又是编程语言,一直以来都以过于复杂和难以使用而闻名。本文将展示它的实用性,而不会纠缠于其复杂性。
自 UNIX 商业化以来,脚本语言(如 UNIX shell)和专用工具(如 awk 和 sed)一直是 UNIX 环境的标准组成部分。在 1982 年,“真正的程序员”会使用 C 语言完成所有任务。像 sed 和 awk 这样的工具被视为缓慢、庞大的程序,会“占用”大量 CPU 资源。即使是执行结构化数据处理和报表生成任务的应用程序,也是使用快速的编译型语言(如 C 语言)实现的。
我撰写本文的部分动机来自于观察到,即使在今天,大多数系统管理员和开发人员要么不了解,要么害怕像 awk 和 sed 这样的实用工具。因此,本应自动化的任务仍然是手动执行(或根本不执行),或者使用较差的工具来代替。
诚然,awk 和 sed 都是相当特殊的工具/语言。两者都识别传统的 UNIX “正则表达式”——功能强大,但学习起来并不容易。这两种工具似乎提供了太多的功能——通常提供多种方法来执行相同的任务。因此,掌握 awk 和 sed 的所有功能并自信地应用它们可能需要一段时间——或者看起来是这样。尽管第一印象如此,但一旦您了解了它们的总体实用性并熟悉了它们最有用功能的一个子集,您就可以快速有效地应用这些工具。我的目的是为您提供足够的信息和示例代码,以便您快速入门 awk。您可以在四月份 Hans de Vreught 的文章 “Take Command: Good Ol' sed” 中阅读关于 sed 的内容。
sed 和 awk 是我使用过的最高效的两个工具。我非常依赖它们来实现各种各样的任务,而使用其他工具/语言来实现这些任务将花费相当长的时间。
我假设您听说过或使用过 Linux 的一些更重要的子系统,并且您了解如何使用 shell 命令行(如文件 I/O 和管道)的基本功能。熟悉标准编辑器(如 vi)和正则表达式的工作知识也很有用。许多 Linux 命令,包括 grep、awk 和 sed,都接受正则表达式作为其调用的一部分,因此您至少应该学习一些基础知识。
我对 awk 工具的介绍仅限于入门基础知识。awk(gawk 和 nawk)提供了许多高级功能,但本文不涉及。
这个工具名称背后的含义不是很有趣,但我将解释一下,以解开其相当不寻常名称的谜团。awk 以其最初的开发者 Aho、Weinberger 和 Kernighan 的名字命名。awk 脚本可以轻松地在所有 UNIX/Linux 版本之间移植。
awk 通常用于重新处理结构化文本数据。它可以很容易地用作命令行过滤器序列的一部分,因为默认情况下,它从标准输入流 (stdin) 接收输入,并将输出写入标准输出流 (stdout)。在一些最有效的应用中,awk 与 sed 结合使用——互补彼此的优势。
以下 shell 命令扫描名为 oldfile 的文件的内容,将所有出现的单词 “UNIX” 更改为 “Linux”,并将结果文本写入名为 newfile 的文件。
$ awk '{gsub(/UNIX/, "Linux"); print}' oldfile \>\ newfile
显然,awk 不会更改原始文件的内容。也就是说,它的行为就像一个流编辑器应该做的那样——被动地将新内容写入输出流。这个例子几乎没有展示任何有用的东西,但它确实表明简单的任务可以简单地实现。虽然 awk 通常是从父 shell 脚本中调用的,涵盖更广泛的范围,但它也可以(并且经常是)直接从命令行使用,以执行像刚才展示的这样简单的任务。
虽然 awk 已被用于执行各种任务,但它最适合用于解析和操作文本数据以及生成格式化报表。awk 的一个典型(且切实的)示例应用是需要检查冗长的系统日志文件并将其汇总为格式化报表的应用。考虑由 sendmail 守护进程或 uucp 程序生成的日志文件。这些文件通常冗长、乏味,并且通常对系统管理员的眼睛不利。可以使用 awk 脚本来解析每个条目,生成一组类别计数,并标记那些代表可疑活动的条目。
awk 最显著的特征是
它将其输入视为一组记录和字段。
它提供的编程结构与 C 语言相似(但不完全相同)。
它提供内置函数和变量。
它的变量是无类型的。
它通过正则表达式执行模式匹配。
awk 脚本可以非常富有表现力,并且通常有几页长。awk 语言提供了任何高级编程语言中期望的典型编程结构。它被描述为 C 语言的解释版本,但尽管存在相似之处,但 awk 在语义和语法上都与 C 语言不同。大量的默认行为、宽松的数据类型以及内置函数和变量使 awk 比 C 更适合快速原型开发任务。
至少可以使用两种不同的方法来调用 awk。第一种方法包括在命令行内联 awk 脚本。第二种方法允许程序员将 awk 脚本保存到文件中,并在命令行中引用它。
查看以下两种调用样式,以典型的手册页表示法格式化。
awk '{ awk -Fc -f script_file [data-file-list ...]
请注意,data-file-list 始终是可选的,因为默认情况下 awk 从标准输入读取数据。我几乎总是使用第二种调用方法,因为我的大多数 awk 脚本都超过 10 行。作为一般规则,如果您的 awk 脚本具有任何重要大小,最好将其维护在单独的文件中。这是一种更有组织的方式来维护源代码,并允许进行单独的版本控制和可读的注释语句。-F 选项控制输入字段分隔符字符,我将在后面详细介绍。以下都是在 shell 提示符下调用 awk 的有效示例
$ ls -l | awk -f
$ awk -f
$ awk -F: '{ print $2 }'
$ awk {'print'} input_file
正如您将通过示例看到的那样,awk 编程是一个覆盖默认操作级别的过程。上面的最后一个示例可能是调用 awk 的最简单示例;它将给定输入文件中的每一行打印到标准输出。如果您彻底理解了 awk 的行为,那么语言语法的复杂性就不会显得那么大了。为了提供平滑的介绍,我将避免利用正则表达式的示例(请参阅“关于正则表达式的说明”)。awk 提供了一个非常明确且有用的过程模型。程序员可以定义在执行任何数据处理之前、在处理每个输入记录期间以及在处理完所有输入数据之后按顺序发生的操作组。
考虑到这些组,任何 awk 脚本的基本语法格式如下
BEGIN { } { } END { }
BEGIN 部分中的代码在 awk 检查任何输入数据之前执行。此部分可用于初始化用户定义的变量或更改内置变量的值。如果您的脚本正在生成格式化报表,您可能希望在此部分中打印标题。END 部分中的代码在 awk 处理完所有输入数据后执行。此部分显然适用于打印报表尾部或根据输入数据计算的摘要。END 和 BEGIN 部分在 awk 脚本中都是可选的。中间部分是 awk 脚本的隐式主输入循环。此部分必须至少包含一个显式操作。该操作可以像无条件打印语句一样简单。每次在输入数据集遇到记录时,都会执行此部分中的代码。默认情况下,记录分隔符是换行符。因此,默认情况下,记录是单行文本。程序员可以重新定义记录分隔符的默认值。
在以下每个示例中,都将假定以下输入数据文本。数据的内容有点傻,但很适合作为练习。您可以想象它代表农产品库存;每行定义一个农产品类别、一个特定项目和一个项目计数。
fruit: oranges 10 fruit: peaches 11 fruit: plums 11 vegetable: cucumbers 8 vegetable: carrots fruit: tomatoes 2
我们将从非常简单的内容开始,并快速深入到一些不平凡的内容。请注意,我习惯于始终定义所有三个部分,即使可选部分被存根化。这是一个很好的可视化占位符,并提醒程序员整个过程模型,即使某些部分当前没有用处。请注意,每个示例都可以折叠成更短的脚本,而不会损失任何功能。我在这里的目的是通过这几个示例尽可能多地演示 awk 功能。
查看列表 1 中的示例脚本,并尝试将其与输出关联起来
fruit: oranges 10 fruit: peaches 11 fruit: plums 11 fruit: tomatoes 2
默认情况下,输入记录是以换行符结尾的文本部分,因此如果输入包含六行,则由 # (1) 注释标记的隐式主循环将执行六次。awk 源代码注释用 # 字符指定——解释器忽略从 # 到行尾的字符(与 UNIX shell 的注释样式相同)。内置变量 $0 始终包含当前的整个记录值(请参阅下面的内置变量表)。标记 (1) 下方的行检查当前输入记录是否为空行。如果是,则 awk 继续读取下一个输入记录。记录中的每个字段都分配给一个有序变量——$1 到 $N,其中 N 等于当前记录中的字段数。什么决定了一个字段?好吧,默认字段分隔符是任何“空白”——空格或制表符。字段分隔符字符可以重新定义。# (2) 注释下方的行将在第一个字段设置为 fruit: 时打印出整个记录。因此,当查看脚本 1 生成的输出时,将显示所有类型为 fruit 的行。
查看列表 2 中的示例脚本,并尝试将其与下面的输出关联起来。唯一明显的增强是在末尾的数据摘要——说明了有多少单位是 fruit 类型。
fruit: oranges 10 fruit: peaches 11 fruit: plums 11 fruit: tomatoes 2 4 out of 5 entries were of type fruit:.
这次,我们使用了 awk 脚本的两个可选的 BEGIN 和 END 部分。以 # (1) 注释开头的语句组初始化了一些程序员定义的变量:FCOUNT、COUNT 和 TYPE——分别表示遇到的 fruit: 记录数、记录总数和农产品类别名称。请注意,以 # (3) 开头的行无条件地递增记录计数器(另请注意,语法是从 C 语言借用的)。以 # (4) 注释开头的代码部分现在引用 TYPE 变量而不是文字字符串,并递增 FCOUNT 变量。下一段代码使用了 printf 内置函数(工作方式与 C 库 printf 完全相同,但在语法上略有不同)来打印子计数和总计数。
查看列表 3 中的示例脚本,并尝试将其与输出关联起来。请注意,显示的唯一记录是那些标记为错误和指示供应短缺的记录。输出末尾的摘要现在包含其他信息。列表 3 的输出
Parsing inventory file "input_data" Bad data encountered: vegetable: carrots Short on tomatoes: 2 left 4 out of 5 entries were of type Fruit. 1 out of 5 entries were of type Vegetable. 0 out of 5 entries were of type Other. 1 out of 5 entries were flagged as bad data. 1 out of 5 entries were flagged in short supply
在第三个示例中,我们进一步使用了两个可选的 BEGIN 和 END 部分。再一次,BEGIN 部分初始化了一些程序员定义的变量。它还打印出一个标题,指示输入文件的名称(引用了内置变量 FILENAME)。请注意以 # (3) 注释开头的新代码部分。NF 变量是一个内置变量,始终包含当前记录中包含的字段数。由于空白仍然是我们的字段分隔符,我们始终期望有三个字段。此代码部分标记并显示被视为坏数据的记录。此外,还会递增维护错误数的计数器。由于被视为无效的记录是无用的,因此程序继续处理下一个输入记录。以 # (5) 注释开头的代码部分已更改为维护基于农产品类别类型的其他计数。
现在,假设系统管理员被要求确定某些 shell 解释器的使用比例,选择标准 Bourne Shell、Korn Shell 和 C Shell。该脚本将提供按总计数和百分比的使用情况细分,并标记登录 shell 不适用或未分配给系统用户的情况。查看列表 4 中的脚本——它满足我们的要求。将代码与列表 5 中的输出关联起来。
列表 4 脚本中第一个值得注意的地方是对内置变量 FS 的赋值——输入字段分隔符。/etc/passwd 文件中的条目由冒号分隔的字段组成。字段 7 指示在登录时代表该用户运行的程序(shell)。字段 7 为空的条目将被打印出来,然后打印摘要报告。
到目前为止,我们已经通过几个小的代码示例回顾了 awk 的行为。演示的功能提供了一个工作基础。您已经看到了 awk 进程的执行流程。您已经看到了内置变量和用户定义的变量被操作。您已经看到了一些内置的 awk 函数被应用。与任何高级语言一样,您可以使用 awk 进行非常有创意的操作。一旦您感到舒适,您就会希望将其用于更复杂的用途。如今,大多数 Linux 系统都提供了 nawk(new awk,新 awk)的功能,它是 20 世纪 80 年代后期开发的。nawk 和 GNU 的 gawk 使在 awk 脚本中执行以下操作成为可能
包含程序员定义的函数。
执行外部程序并处理结果。
更轻松地操作命令行参数。
管理多个 I/O 流。
作为参考,表 1 和表 2 定义了最常见的内置变量和函数。另请注意,以下运算符在 awk 中与在 C 语言中具有相同的含义(请参阅 awk 手册页)
* / % + - = ++ -- += -= *= /= %=
允许快速开发的脚本语言和专用工具在很长一段时间内被广泛接受。awk 和 sed 都应该在任何 Linux 开发人员和管理员的工作台上占有一席之地。这两种工具都是任何 Linux 平台的标准组成部分。awk 和 sed 可以一起用于实现几乎任何文本过滤应用程序——例如对数据流和文件执行重复编辑以及生成格式化报表。
关于 awk 和 sed 最新的参考书是 O'Reilly 出版的 awk and sed,作者是 Dale Dougherty 和 Arnold Robbins。另请参阅 Arnold Robbins 的 Effective AWK Programming (SSC)。要获取 Linux 系统上即时的在线概要,请使用 man 命令,如下所示
我希望这里提供的信息对您有所帮助,并鼓励您开始或扩展您对这些工具的使用。如果您利用 awk 和 sed 提供的功能,您肯定会节省开发时间和金钱。那些知道如何快速应用锋利的工具来解决看似复杂问题的人,在我们这个领域会得到丰厚的回报。
