AWK 现状
AWK 是一门拥有 40 多年历史的文本处理语言。它拥有 POSIX 标准、多个符合标准的实现,并且在 2020 年依然出人意料地保持着活力——无论是简单的文本处理任务,还是处理“大数据”,都仍有用武之地。GNU Awk 5.1 最近的 发布似乎正好提供了一个回顾 AWK 发展现状、了解 GNU Awk 近况以及考察当下 AWK 应用场景的好契机。
这门语言于 1977 年诞生于贝尔实验室。它的名字取自三位原始作者姓名的首字母:Alfred Aho、Peter Weinberger 和 Brian Kernighan。作为一款纯粹的 Unix 工具,AWK 的设计目标是把一件事做好:过滤和转换文本行。它常被用于从日志文件中解析字段、转换其他工具的输出,以及统计单词和字段的出现次数。Aho 曾简洁地 概括过 AWK 的功能:
AWK 逐行读取输入。程序中的每一个模式都会对每一行进行扫描,匹配上的模式所对应的动作就会被执行。
AWK 程序通常是以一行命令的形式直接在命令行上执行。例如,要从某个假想的 Web 服务器日志中计算 GET 请求的平均响应时间,你可以这样输入:
$ awk '/GET/ { total += $6; n++ } END { print total/n }' server.log
0.0186667它的意思是:对于所有匹配正则表达式 /GET/ 的行,累加响应时间(第六个字段,即 $6)并计数;最后打印出响应时间的算术平均值。
AWK 的各个版本
如今常用的 AWK 主要有三个版本,它们都(至少在绝大多数使用场景中已足够接近地)符合 POSIX 标准。第一个是经典 awk,即 Aho、Weinberger 和 Kernighan 在其著作 The AWK Programming Language 中描述的版本。它有时被称为“新 AWK”(nawk)或“唯一真正的 AWK”,如今 托管在 GitHub 上。这是许多基于 BSD 的系统(包括 macOS)预装的版本(不过 macOS 自带的版本已经过时,值得升级)。
第二个是 GNU Awk(gawk),它是目前功能最丰富、维护也最活跃的版本。Gawk 通常预装在 Linux 系统上,往往就是默认的 awk。在 macOS 上可以通过 Homebrew 安装,也有可用的 Windows 二进制包。自 1994 年以来,Arnold Robbins 一直是 gawk 的主要维护者,至今仍在引领这门语言的发展(他也为经典的 awk 版本贡献了许多修复)。Gawk 拥有 许多特性,是 awk 或 POSIX 标准所没有的,包括新增的函数、网络功能、C 扩展 API、分析器和调试器,以及最近新增的命名空间。
第三个常见版本是由 Michael Brennan 编写的 mawk。它是 Ubuntu 和 Debian Linux 上的默认 awk,并且由于其字节码编译器和更节省内存的值表示方式,至今仍是最快的 AWK 实现。(Gawk 自 4.0 起也有了字节码编译器,因此现在速度已非常接近 mawk。)
如果你只是想用 AWK 写些单行命令或做基本的文本处理,上述任何一个版本都足够好用。如果你打算用它编写更大的脚本或程序,Gawk 丰富的功能使其成为更明智的选择。
此外还有其他几个成熟度和维护状况各不相同的 AWK 实现,值得一提的有:用于嵌入式 Linux 环境、针对体积优化的 BusyBox 版本,可在运行时调用 Java 语言特性的 Java 重写版,以及我自己用 Go 编写的符合 POSIX 标准的 GoAWK。三个主要的 AWK 以及 BusyBox 版本都是用 C 语言编写的。
自 4.0 以来 Gawk 的变化
距离 LWN 报道 gawk 4.0 发布已过去近 10 年。人们或许会想说“自 2011 年以来变化很大”,但事实是,AWK 世界的发展相对缓慢。这里我将介绍自 4.0 以来的一些重要特性,更详细的内容可以查阅完整的 4.x 和 5.x 更新日志。Gawk 5.1.0 就在一个多月前的 4 月 14 日发布。
面向用户最大的新特性是在 5.0 中引入的命名空间。大多数现代语言都有某种命名空间的概念,以便在发布大型项目和库时避免命名冲突。Gawk 5.0 以向后兼容的方式加入了命名空间,让开发者可以创建库,例如下面这个简单的数学库:
# area.awk
@namespace "area"
BEGIN {
pi = 3.14159 # namespaced "constant"
}
function circle(radius) {
return pi*radius*radius
}要引用库中的变量或函数,请使用类似 C++ 的 namespace::name 语法:
$ gawk -f area.awk -e 'BEGIN { print area::pi, area::circle(10) }'
3.14159 314.159Robbins 认为,AWK 缺乏命名空间是它未能发展为大规模编程语言的关键原因之一,而 gawk 5.0 的这一特性或许有助于解决这个问题。Robbins 认为制约 AWK 发展的另一大问题是缺乏良好的 C 扩展接口。Gawk 的动态扩展接口在 4.1 中被彻底重构;现在它拥有明确的 API,可以对现有的 C 和 C++ 库进行封装,从而在 AWK 中轻松调用。
用户手册中示例 C 代码封装的以下代码片段展示了如何用文件名和 stat() 系统调用返回的值来填充 AWK 数组(以字符串为键的哈希表):
/* empty out the array */
clear_array(array);
/* fill in the array */
array_set(array, "name", make_const_string(name, strlen(name), &tmp));
array_set_numeric(array, "dev", sbuf->st_dev);
array_set_numeric(array, "ino", sbuf->st_ino);
array_set_numeric(array, "mode", sbuf->st_mode);另一项变化是在 4.2 版本(并延续到 5.0)中对源码美化器进行了彻底改造。Gawk 的美化器使其可以作为标准化的 AWK 代码格式化工具来使用,类似于 Go 的 go fmt 工具和 Python 的 Black 格式化工具。例如,要美化上面 area.awk 文件的格式:
$ gawk --pretty-print -f area.awk
其输出如下:
@namespace "area"
BEGIN {
pi = 3.14159 # namespaced "constant"
}
function circle(radius)
{
return (pi * radius * radius)
}你可能会对该工具的选择感到疑惑:为什么“BEGIN {”的“{”前没有换行,而 function 却有?(原来 AWK 语法不允许那样做。)为什么函数前要空两行,return 表达式还要加上括号?但至少它保持了一致,或许还能避免代码风格之争。
Gawk 支持有限的运行时类型检查,并在 4.2 中通过新增的 typeof() 函数对其进行了扩展。typeof() 会根据输入类型返回类似“string”、“number”或“array”的字符串常量。例如,这些函数对于递归遍历嵌套数组中每一项的代码就很重要(这是 POSIX AWK 无法做到的)。
在 4.2 中,gawk 还通过 @/foo/ 语法将正则表达式常量作为一等数据类型来支持。此前无法将正则表达式常量存储在变量中;typeof(@/foo/) 会返回字符串“regexp”。在性能方面,gawk 4.2 在 Linux 系统上通过在可用时使用 fwrite_unlocked() 带来了显著提升。由于 gawk 是单线程的,它可以使用非加锁的 stdio 函数,使原始输出速度提升 7-18%——例如在处理大文件时执行 gawk '{ print }'。
《GNU Awk 用户指南》一直是一份详尽的参考资料,但在 4.1 以及 5.x 系列中又得到了大幅更新,包括新增的示例、总结章节和练习,并进行了大量的文字润色。
最后(也是最不重要的一点),4.0 中一个让我觉得有趣的细微变化是对 sub() 和 gsub() 中反斜杠处理的回退。Robbins 写道:
在 sub() 和 gsub() 中对反斜杠的默认处理已回退到 3.1 版本的行为。竟然想着为了符合标准就这样破坏兼容性,实在是愚蠢。
sub 和 gsub 函数是核心的正则表达式替换函数,即使是对反斜杠复杂处理的一个小“修正”也会破坏人们的代码:
在 4.0.0 版本发布时,gawk 维护者将 POSIX 规则设为默认,破坏了十多年的向后兼容性。不用说,这是一个糟糕的主意,于是从 4.0.1 起,gawk 恢复了其原有行为,仅在给出 --posix 选项时才遵循 POSIX 规则。
Robbins 在最初改动时的判断或许有小小的失误,但显然他非常重视向后兼容性。尤其是对于 gawk 这样广受欢迎的工具,有时宁可继续违背规范,也不去改变其一贯的行为。
AWK 是否依然重要?
问 AWK 是否依然重要,有点像问空气是否依然重要:你可能看不见它,但它无处不在。许多 Linux 管理员和 DevOps 工程师用它来转换数据或通过日志文件诊断问题。几乎所有基于 Unix 的机器上都安装了某个版本的 AWK。除了临时性使用,许多大型开源项目也在其构建或文档工具链的某个环节使用 AWK。仅举几例:Linux 内核在 x86 工具链中使用它来检查和重排 objdump 文件,Neovim 用它来生成文档,而 FFmpeg 则将其用于构建和测试。
AWK 构建脚本出人意料地难以被取代,即使有人想淘汰它:2018 年 LWN 报道称,GCC 贡献者想用 Python 取代用于生成选项解析代码的脚本中的 AWK。当时该提议获得了一些支持,但显然没有人自告奋勇去真正完成移植,于是这些 AWK 脚本至今仍在使用。
Robbins 在其 2018 年的论文中主张将 AWK(特指 gawk)用作“系统编程语言”,在这里指的是用于编写更大型的工具和程序的语言。他阐述了自己认为 AWK 未能流行起来的原因,但 Kernighan 却“并不完全认同”缺乏扩展机制是 AWK 未被广泛用于大型程序的主要原因。他认为,这可能是由于缺乏对系统调用等的内置支持。但这些都没有阻止一些人用它构建更大的工具:Robbins 自己的 TexiWeb Jr. 文学编程工具(1300 行 AWK)、Werner Stoop 的 d.awk 文档生成工具(从源码中的 Markdown 注释生成文档,800 行),以及 Translate Shell(一个 6000 行的 AWK 工具,为基于云的翻译 API 提供了相当强大的命令行界面)。
近几年来,一些开发者撰文讲述如何在自己的“大数据”工具箱中使用 AWK,将其作为比 Spark 和 Hadoop 这类笨重的分布式计算系统更简单(有时也更快)的工具。Nick Strayer 撰文讲述了如何使用 AWK 和 R 在多个核心上解析 25 TB 的数据。其他大数据方面的例子还有 Adam Drake 那篇标题颇为诱人的文章《命令行工具比你的 Hadoop 集群快 235 倍》,以及 Brendan O'Connor 的“别再 MAWK AWK——最快、最优雅的大数据处理语言!”
无论是临时的文本处理、构建工具、“系统编程”还是大数据处理——更不用说文本模式第一人称射击游戏——看起来 AWK 在 2020 年依然生机勃勃。
[感谢 Arnold Robbins 审阅本文草稿。]
随机一篇博客
评论
登录后参与讨论