awk 和 Groovy 相辅相成，可以创建强大、有用的脚本。最近我写了一个使用 Groovy 脚本来清理我的音乐文件中的标签的系列。我开发了一个 框架，可以识别我的音乐目录的结构，并使用它来遍历音乐文件。在该系列的最后一篇文章中，我从框架中分离出一个实用类，我的脚本可以用它来处理文件。这个独立的框架让我想起了很多 awk 的工作方式。对于那些不熟悉 awk 的人来说，你学习下这本电子书：《awk 实用指南》我从 1984 年开始大量使用 awk，当时我们的小公司买了第一台“真正的”计算机，它运行的是 System V Unix。对我来说，awk 是非常完美的：它有 关联内存 associative memory ——将数组视为由字符串而不是数字来索引的。它内置了正则表达式，似乎专为处理数据而生，尤其是在处理数据列时，而且结构紧凑，易于学习。最后，它非常适合在 Unix 工作流使用，从标准输入或文件中读取数据并写入到输出，数据不需要经过其他的转换就出现在了输入流中。说 awk 是我日常计算工具箱中的一个重要部分一点也不为过。然而，在我使用 awk 的过程中，有几件事让我感到不满意。可能主要的问题是 awk 善于处理以分隔字段呈现的数据，但很奇怪它不善于处理 CSV 文件，因为 CSV 文件的字段被引号包围时可以嵌入逗号分隔符。另外，自 awk 发明以来，正则表达式已经有了很大的发展，我们需要记住两套正则表达式的语法规则，而这并不利于编写无 bug 的代码。一套这样的规则已经很糟糕了。由于 awk 是一门简洁的语言，因此它缺少很多我认为有用的东西，比如更丰富的基础类型、结构体、switch 语句等等。相比之下，Groovy 拥有这些能力：可以使用 OpenCSV 库，它很擅长处理 CSV 文件、Java 正则表达式和强大的匹配运算符、丰富的基础类型、类、switch 语句等等。Groovy 所缺乏的是简单的面向管道的概念，即把要处理数据作为一个传入的流，以及把处理过的数据作为一个传出的流。但我的音乐目录处理框架让我想到，也许我可以创建一个 Groovy 版本的 awk “引擎”。这就是我写这篇文章的目的。安装 Java 和 GroovyGroovy 是基于 Java 的，需要先安装 Java。最新的、合适的 Java 和 Groovy 版本可能都在你的 Linux 发行版的软件库中。Groovy 也可以按照 Groovy 主页 上的说明进行安装。对于 Linux 用户来说，一个不错的选择是 SDKMan，它可以用来获得多个版本的 Java、Groovy 和其他许多相关工具。在这篇文章中，我使用的是 SDK 的版本：Java：OpenJDK 11 的 11.0.12 的开源版本Groovy：3.0.8使用 Groovy 创建 awk这里的基本想法是将打开一个或多个文件进行处理、将每行分割成字段、以及提供对数据流的访问等复杂情况封装在三个部分：在处理数据之前在处理每行数据时在处理完所有数据之后我并不打算用 Groovy 来取代 awk。相反，我只是在努力实现我的典型用例，那就是：使用一个脚本文件而不是在命令行写代码处理一个或多个输入文件设置默认的分隔符为 |，并基于这个分隔符分割所有行使用 OpenCSV 完成分割工作（awk 做不到）框架类下面是用 Groovy 类实现的 “awk 引擎”：@Grab('com.opencsv:opencsv:5.6') import com.opencsv.CSVReader public class AwkEngine { // With admiration and respect for // Alfred Aho // Peter Weinberger // Brian Kernighan // Thank you for the enormous value // brought my job by the awk // programming language Closure onBegin Closure onEachLine Closure onEnd private String fieldSeparator private boolean isFirstLineHeader private ArrayList<String> fileNameList public AwkEngine(args) { this.fileNameList = args this.fieldSeparator = "|" this.isFirstLineHeader = false } public AwkEngine(args, fieldSeparator) { this.fileNameList = args this.fieldSeparator = fieldSeparator this.isFirstLineHeader = false } public AwkEngine(args, fieldSeparator, isFirstLineHeader) { this.fileNameList = args this.fieldSeparator = fieldSeparator this.isFirstLineHeader = isFirstLineHeader } public void go() { this.onBegin() int recordNumber = 0 fileNameList.each { fileName -> int fileRecordNumber = 0 new File(fileName).withReader { reader -> def csvReader = new CSVReader(reader, this.fieldSeparator.charAt(0)) if (isFirstLineHeader) { def csvFieldNames = csvReader.readNext() as ArrayList<String> csvReader.each { fieldsByNumber -> def fieldsByName = csvFieldNames. withIndex(). collectEntries { name, index -> [name, fieldsByNumber[index]] } this.onEachLine(fieldsByName, recordNumber, fileName, fileRecordNumber) recordNumber++ fileRecordNumber++ } } else { csvReader.each { fieldsByNumber -> this.onEachLine(fieldsByNumber, recordNumber, fileName, fileRecordNumber) recordNumber++ fileRecordNumber++ } } } } this.onEnd() } }虽然这看起来是相当多的代码，但许多行是因为太长换行了（例如，通常你会合并第 38 行和第 39 行，第 41 行和第 42 行，等等）。让我们逐行看一下。第 1 行使用 @Grab 注解从 Maven Central 获取 OpenCSV 库的 5.6 本周。不需要 XML。第 2 行我引入了 OpenCSV 的 CSVReader 类第 3 行，像 Java 一样，我声明了一个 public 实用类 AwkEngine。第 11-13 行定义了脚本所使用的 Groovy 闭包实例，作为该类的钩子。像任何 Groovy 类一样，它们“默认是 public”，但 Groovy 将这些字段创建为 private，并对其进行外部引用（使用 Groovy 提供的 getter 和 setter 方法）。我将在下面的示例脚本中进一步解释这个问题。第 14-16 行声明了 private 字段 —— 字段分隔符，一个指示文件第一行是否为标题的标志，以及一个文件名的列表。第 17-31 行定义了三个构造函数。第一个接收命令行参数。第二个接收字段的分隔符。第三个接收指示第一行是否为标题的标志。第 31-67 行定义了引擎本身，即 go() 方法。第 33 行调用了 onBegin() 闭包（等同于 awk 的 BEGIN {} 语句）。第 34 行初始化流的 recordNumber（等同于 awk 的 NR 变量）为 0（注意我这里是从 00 而不是 1 开始的）。第 35-65 行使用 each {} 来循环处理列表中的文件。第 36 行初始化文件的 fileRecordNumber（等同于 awk 的 FNR 变量）为 0（从 0 而不是 1 开始）。第 37-64 行获取一个文件对应的 Reader 实例并处理它。第 38-39 行获取一个 CSVReader 实例。第 40 行检测第一行是否为标题。如果第一行是标题，那么在 41-42 行会从第一行获取字段的标题名字列表。第 43-54 行处理其他的行。第 44-48 行把字段的值复制到 name:value 的映射中。第 49-51 行调用 onEachLine() 闭包（等同于 awk 程序 BEGIN {} 和 END {} 之间的部分，不同的是，这里不能输入执行条件），传入的参数是 name:value 映射、处理过的总行数、文件名和该文件处理过的行数。第 52-53 行是处理过的总行数和该文件处理过的行数的自增。如果第一行不是标题：第 56-62 行处理每一行。第 57-59 调用 onEachLine() 闭包，传入的参数是字段值的数组、处理过的总行数、文件名和该文件处理过的行数。第 60-61 行是处理过的总行数和该文件处理过的行数的自增。第 66 行调用 onEnd() 闭包（等同于 awk 的 END {}）。这就是该框架的内容。现在你可以编译它：$ groovyc AwkEngine.groovy 一点注释：如果传入的参数不是一个文件，编译就会失败，并出现标准的 Groovy 堆栈跟踪，看起来像这样：Caught: java.io.FileNotFoundException: not-a-file (No such file or directory) java.io.FileNotFoundException: not-a-file (No such file or directory) at AwkEngine$_go_closure1.doCall(AwkEngine.groovy:46) OpenCSV 可能会返回 String[] 值，不像 Groovy 中的 List 值那样方便（例如，数组没有 each {}）。第 41-42 行将标题字段值数组转换为 list，因此第 57 行的 fieldsByNumber 可能也应该转换为 list。在脚本中使用这个框架下面是一个使用 AwkEngine 来处理 /etc/group 之类由冒号分隔并没有标题的文件的简单脚本：def ae = new AwkEngine(args, ':') int lineCount = 0 ae.onBegin = { println “in begin” } ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber -> if (lineCount < 10) println “fileName $fileName fields $fields” lineCount++ } ae.onEnd = { println “in end” println “$lineCount line(s) read” } ae.go() 第 1 行 调用的有两个参数的构造函数，传入了参数列表，并定义冒号为分隔符。第 2 行定义一个脚本级的变量 lineCount，用来记录处理过的行数（注意，Groovy 闭包不要求定义在外部的变量为 final）。第 3-5 行定义 onBegin() 闭包，在标准输出中打印出 “in begin” 字符串。第 6-10 行定义 onEachLine() 闭包，打印文件名和前 10 行字段，无论是否为前 10 行，处理过的总行数 lineCount 都会自增。第 11-14 行定义 onEnd() 闭包，打印 “in end” 字符串和处理过的总行数。第 15 行运行脚本，使用 AwkEngine。像下面一样运行一下脚本：$ groovy Test1Awk.groovy /etc/group in begin fileName /etc/group fields [root, x, 0, ] fileName /etc/group fields [daemon, x, 1, ] fileName /etc/group fields [bin, x, 2, ] fileName /etc/group fields [sys, x, 3, ] fileName /etc/group fields [adm, x, 4, syslog,clh] fileName /etc/group fields [tty, x, 5, ] fileName /etc/group fields [disk, x, 6, ] fileName /etc/group fields [lp, x, 7, ] fileName /etc/group fields [mail, x, 8, ] fileName /etc/group fields [news, x, 9, ] in end 78 line(s) read $ 当然，编译框架类生成的 .class 文件需要在 classpath 中，这样才能正常运行。通常你可以用 jar 把这些 class 文件打包起来。我非常喜欢 Groovy 对行为委托的支持，这在其他语言中需要各种诡异的手段。许多年来，Java 需要匿名类和相当多的额外代码。Lambda 已经在很大程度上解决了这个问题，但它们仍然不能引用其范围之外的非 final 变量。下面是另一个更有趣的脚本，它很容易让人想起我对 awk 的典型使用方式：def ae = new AwkEngine(args, ';', true) ae.onBegin = { // nothing to do here } def regionCount = [:] ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber -> regionCount[fields.REGION] = (regionCount.containsKey(fields.REGION) ? regionCount[fields.REGION] : 0) + (fields.PERSONAS as Integer) } ae.onEnd = { regionCount.each { region, population -> println “Region $region population $population” } } ae.go() 第 1 行调用了三个函数的构造方法，true 表示这是“真正的 CSV” 文件，第一行为标题。由于它是西班牙语的文件，因此它的逗号表示数字的点，标准的分隔符是分号。第 2-4 行定义 onBegin() 闭包，这里什么也不做。第 5 行定义一个（空的）LinkedHashmap，键是 String 类型，值是 Integer 类型。数据文件来自于智利最近的人口普查，你要在这个脚本中计算出智利每个地区的人口数量。第 6-11 行处理文件中的行（加上标题一共有 180,500 行）—— 请注意在这个案例中，由于你定义 第 1 行为 CSV 列的标题，因此 fields 参数会成为 LinkedHashMap<String,String> 实例。第 7-10 行是 regionCount 映射计数增加，键是 REGION 字段的值，值是 PERSONAS 字段的值 —— 请注意，与 awk 不同，在 Groovy 中你不能在赋值操作的右边使用一个不存在的映射而期望得到空值或零值。第 12-16 行，打印每个地区的人口数量。第 17 行运行脚本，调用 AwkEngine 。像下面一样运行一下脚本：$ groovy Test2Awk.groovy ~/Downloads/Censo2017/ManzanaEntidad_CSV/Censo*csv Region 1 population 330558 Region 2 population 607534 Region 3 population 286168 Region 4 population 757586 Region 5 population 1815902 Region 6 population 914555 Region 7 population 1044950 Region 8 population 1556805 Region 16 population 480609 Region 9 population 957224 Region 10 population 828708 Region 11 population 103158 Region 12 population 166533 Region 13 population 7112808 Region 14 population 384837 Region 15 population 226068 $ 以上为全部内容。对于那些喜欢 awk 但又希望得到更多的东西的人，我希望你能喜欢这种 Groovy 的方法。via: https://opensource.com/article/22/9/awk-groovy作者：Chris Hermansen 选题：lkxed 译者：lxbwolf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
80 高龄的计算机科学家曾为 “Unix” 命名，如今为 AWK 代码添加了 Unicode 支持
布莱恩·克尼汉在 80 岁的时候还在积极为他的原始项目 AWK 增加代码贡献。这真是鼓舞人心！ 布莱恩·克尼汉 Brian Kernighan 因其与 Unix 的创造者 肯·汤普森 Ken Thompson 和 丹尼斯·里奇 Dennis Ritchie 一起的工作而广为人知。他对 Unix 的发展做出了重大贡献。不仅如此，布莱恩·克尼汉还提出了 “Unix” 这个名字，并创造了 “Hello, world” 作为程序的测试短语。他也是《C 编程语言》一书的共同作者（另一位是丹尼斯·里奇）。因此，可以说他是你所知道的关于 Unix、Linux、BSD 和 C 编程语言的演变的重要组成部分。而且，作为一位如今已 80 岁的老人家，他似乎投入了一些时间来为 AWK（一种他在上世纪 70 年代共同创造的脚本语言）增加了一个新的功能。? 这真是妙极了，对吗？而且，听起真是鼓舞人心！注：AWK 仍然是一个处理文本和提取数据的强大工具，忠实于它的最初目的。如果你感到好奇，你可以在 freeCodeCamp 上了解更多关于它的信息。为 AWK 添加 Unicode 支持最近，The Register 通过一篇发表在 YouTube 上的近期采访，发现了这个功能的增加。从技术上讲，这项贡献早在几个月前就有了，但现在它才得到人们的关注。当然，这个功能的增加对很多人来说可能不是什么大事。但是，它背后的努力，以及谁贡献了它，就有了天壤之别。此外，有趣的是，他并不完全了解 Git 的工作原理。所以，考虑到这一点，我认为他在这里的提交做得相当好。在这个提交 “附上 BWK 的邮件 - onetrueawk/awk@9ebe940” 中，他提到：一旦我搞清楚了（并做了一些检查，我将尝试提交一个拉取请求。我希望我更了解 git，但尽管有你的帮助，我仍然没能正确地理解，所以这可能需要一段时间。如果你对 Unix 的原始创造者和贡献者以及一路走来的许多重要创新有好奇心，我建议你观看上面链接的采访。你也可以在 普林斯顿大学网站 上查看他的更多工作和最近的书籍。? 那么，你对这位 80 岁的 Unix 传奇人物的代码贡献有何看法？你有什么特别佩服他的地方吗？请在下面的评论中分享你的想法。via: https://news.itsfoss.com/unix-awk-unicode/作者：Ankush Das 选题：lkxed 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
编程语言往往具有许多共同特征。学习一门新语言的好方法是去写一个熟悉的程序。在本文中，我将会使用 Awk 编写一个“猜数字”程序来展示熟悉的概念。当你学习一门新的编程语言时，最好把重点放在大多数编程语言都有的共同点上:变量 —— 存储信息的地方表达式 —— 计算的方法语句 —— 在程序中表示状态变化的方法这些概念是大多是编程语言的基础。一旦你理解了这些概念，你就可以开始把其他的弄清楚。例如，大多数语言都有由其设计所支持的“处理方式”，这些方式在不同语言之间可能有很大的不同。这些方法包括模块化（将相关功能分组在一起）、声明式与命令式、面向对象、低级与高级语法特性等等。许多程序员比较熟悉的是编程“仪式”，即，在处理问题之前设置场景所需花费的工作。据说 Java 编程语言有一个源于其设计的重要仪式要求，就是所有代码都在一个类中定义。但从根本上讲，编程语言通常有相似之处。一旦你掌握了一种编程语言，就可以从学习另一种语言的基本知识开始，品味这种新语言的不同之处。一个好方法是创建一组基本的测试程序。有了这些，就可以从这些相似之处开始学习。你可以选择创建的一个测试程序是“猜数字”程序。电脑从 1 到 100 之间选择一个数字，让你猜这个数字。程序一直循环，直到你猜对为止。“猜数字”程序练习了编程语言中的几个概念:变量输入输出条件判断循环这是学习一门新的编程语言的一个很好的实践实验。注：本文改编自 Moshe Zadka 在 Julia 中使用这种方法和 Jim Hall在 Bash 中使用这种方法的文章。在 awk 程序中猜数让我们编写一个实现“猜数字”游戏的 Awk 程序。Awk 是动态类型的，这是一种面向数据转换的脚本语言，并且对交互使用有着令人惊讶的良好支持。Awk 出现于 20 世纪 70 年代，最初是 Unix 操作系统的一部分。如果你不了解 Awk，但是喜欢电子表格，这就是一个你可以 去学习 Awk 的信号！您可以通过编写一个“猜数字”游戏版本来开始对 Awk 的探索。以下是我的实现（带有行号，以便我们可以查看一些特定功能）： 1 BEGIN { 2 srand(42) 3 randomNumber = int(rand() * 100) + 1 4 print "random number is",randomNumber 5 printf "guess a number between 1 and 100\n" 6 } 7 { 8 guess = int($0) 9 if (guess < randomNumber) { 10 printf "too low, try again:" 11 } else if (guess > randomNumber) { 12 printf "too high, try again:" 13 } else { 14 printf "that's right\n" 15 exit 16 } 17 } 我们可以立即看到 Awk 控制结构与 C 或 Java 的相似之处，但与 Python 不同。 在像 if-then-else、while 这样的语句中，then、else 和 while 部分接受一个语句或一组被 { 和 } 包围的语句。然而，Awk 有一个很大的区别需要从一开始就了解：根据设计，Awk 是围绕数据管道构建的。这是什么意思呢？大多数 Awk 程序都是一些代码片段，它们接收一行输入，对数据做一些处理，然后将其写入输出。认识到这种转换管道的需要，Awk 默认情况下提供了所有的转换管道。让我们通过关于上面程序的一个基本问题来探索：“从控制台读取数据”的结构在哪里？答案是——“内置的”。特别的，第 7-17 行告诉 Awk 如何处理被读取的每一行。在这种情况下，很容易看到第 1-6 行是在读取任何内容之前被执行的。更具体地说，第 1 行上的 BEGIN 关键字是一种“模式”，在本例中，它指示 Awk 在读取任何数据之前，应该先执行 { ... } 中 BEGIN 后面的内容。另一个类似的关键字 END，在这个程序中没有被使用，它指示 Awk 在读取完所有内容后要做什么。回到第 7-17 行，我们看到它们创建了一个类似代码块 { ... } 的片段，但前面没有关键字。因为在 { 之前没有任何东西可以让 Awk 匹配，所以它将把这一行用于接收每一行输入。每一行的输入都将由用户输入作为猜测。让我们看看正在执行的代码。首先，是在读取任何输入之前发生的序言部分。在第 2 行，我们用数字 42 初始化随机数生成器（如果不提供参数，则使用系统时钟）。为什么要用 42？当然要选 42！#The_Hitchhiker's_Guide_to_the_Galaxy) 第 3 行计算 1 到 100 之间的随机数，第 4 行输出该随机数以供调试使用。第 5 行邀请用户猜一个数字。注意这一行使用的是 printf，而不是 print。和 C 语言一样，printf 的第一个参数是一个用于格式化输出的模板。既然用户知道程序需要输入，她就可以在控制台上键入猜测。如前所述，Awk 将这种猜测提供给第 7-17 行的代码。第 18 行将输入记录转换为整数；$0 表示整个输入记录，而 $1 表示输入记录的第一个字段，$2 表示第二个字段，以此类推。是的，Awk 使用预定义的分隔符（默认为空格）将输入行分割为组成字段。第 9-15 行将猜测结果与随机数进行比较，打印适当的响应。如果猜对了，第 15 行就会从输入行处理管道中提前退出。就这么简单！考虑到 Awk 程序不同寻常的结构，代码片段会对特定的输入行配置做出反应，并处理数据，让我们看看另一种结构，看看过滤部分是如何工作的： 1 BEGIN { 2 srand(42) 3 randomNumber = int(rand() * 100) + 1 4 print "random number is",randomNumber 5 printf "guess a number between 1 and 100\n" 6 } 7 int($0) < randomNumber { 8 printf "too low, try again: " 9 } 10 int($0) > randomNumber { 11 printf "too high, try again: " 12 } 13 int($0) == randomNumber { 14 printf "that's right\n" 15 exit 16 } 第 1–6 行代码没有改变。但是现在我们看到第 7-9 行是当输入整数值小于随机数时执行的代码，第 10-12 行是当输入整数值大于随机数时执行的代码，第 13-16 行是两者相等时执行的代码。这看起来“很酷但很奇怪” —— 例如，为什么我们会重复计算 int($0)？可以肯定的是，用这种方法来解决问题会很奇怪。但这些模式确实是分离条件处理的非常好的方式，因为它们可以使用正则表达式或 Awk 支持的任何其他结构。为了完整起见，我们可以使用这些模式将普通的计算与只适用于特定环境的计算分离开来。下面是第三个版本： 1 BEGIN { 2 srand(42) 3 randomNumber = int(rand() * 100) + 1 4 print "random number is",randomNumber 5 printf "guess a number between 1 and 100\n" 6 } 7 { 8 guess = int($0) 9 } 10 guess < randomNumber { 11 printf "too low, try again: " 12 } 13 guess > randomNumber { 14 printf "too high, try again: " 15 } 16 guess == randomNumber { 17 printf "that's right\n" 18 exit 19 } 认识到这一点，无论输入的是什么值，都需要将其转换为整数，因此我们创建了第 7-9 行来完成这一任务。现在第 10-12、13-15 和 16-19 行这三组代码，都是指已经定义好的变量 guess，而不是每次都对输入行进行转换。让我们回到我们想要学习的东西列表:变量 —— 是的，Awk 有这些；我们可以推断出，输入数据以字符串形式输入，但在需要时可以转换为数值输入 —— Awk 只是通过它的“数据转换管道”的方式发送输入来读取数据输出 —— 我们已经使用了 Awk 的 print 和 printf 函数来将内容写入输出条件判断 —— 我们已经学习了 Awk 的 if-then-else 和对应特定输入行配置的输入过滤器循环 —— 嗯，想象一下！我们在这里不需要循环，这还是多亏了 Awk 采用的“数据转换管道”方法；循环“就这么发生了”。注意，用户可以通过向 Awk 发送一个文件结束信号（当使用 Linux 终端窗口时可通过快捷键 CTRL-D）来提前退出管道。不需要循环来处理输入的重要性是非常值得的。Awk 能够长期保持存在的一个原因是 Awk 程序是紧凑的，而它们紧凑的一个原因是不需要从控制台或文件中读取的那些格式代码。让我们运行下面这个程序：$ awk -f guess.awk random number is 25 guess a number between 1 and 100: 50 too high, try again: 30 too high, try again: 10 too low, try again: 25 that's right $ 我们没有涉及的一件事是注释。Awk 注释以 # 开头，以行尾结束。总结Awk 非常强大，这种“猜数字”游戏是入门的好方法。但这不应该是你探索 Awk 的终点。你可以看看 Awk 和 Gawk（GNU Awk）的历史，Gawk 是 Awk 的扩展版本，如果你在电脑上运行 Linux，可能会有这个。或者，从它的原始开发者那里阅读关于 最初版本 的各种信息。你还可以 下载我们的备忘单 来帮你记录下你所学的一切。Awk 备忘单via: https://opensource.com/article/21/1/learn-awk作者：Chris Hermansen 选题：lujun9972 译者：FYJNEVERFOLLOWS 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
编写一个 awk 脚本来找到一组单词中出现次数最多（和最少）的单词。近一段时间，我开始编写一个小游戏，在这个小游戏里，玩家使用一个个字母块来组成单词。编写这个游戏之前，我需要先知道常见英文单词中每个字母的使用频率，这样一来，我就可以找到一组更有用的字母块。字母频次统计在很多地方都有相关讨论，包括在 维基百科 上，但我还是想要自己来实现。Linux 系统在 /usr/share/dict/words 文件中提供了一个单词列表，所以我已经有了一个现成的单词列表。然而，尽管这个 words 文件包含了很多我想要的单词，却也包含了一些我不想要的。我想要的单词首先不能是复合词（即不包含连接符和空格的单词），也不能是专有名词（即不包含大写字母单词）。为了得到这个结果，我可以运行 grep 命令来取出只由小写字母组成的行：$ grep '^[a-z]*$' /usr/share/dict/words 这个正则表达式的作用是让 grep 去匹配仅包含小写字母的行。表达式中的字符 ^ 和 $ 分别代表了这一行的开始和结束。[a-z] 分组仅匹配从 “a” 到 “z” 的小写字母。下面是一个输出示例：$ grep '^[a-z]*$' /usr/share/dict/words | head a aa aaa aah aahed aahing aahs aal aalii aaliis 没错，这些都是合法的单词。比如，“aahed” 是 “aah” 的过去式，表示在放松时的感叹，而 “aalii” 是一种浓密的热带灌木。现在我只需要编写一个 gawk 脚本来统计出单词中各个字母出现的次数，然后打印出每个字母的相对频率。字母计数一种使用 gawk 来统计字母个数的方式是，遍历每行输入中的每一个字符，然后对 “a” 到 “z” 之间的每个字母进行计数。substr 函数会返回一个给定长度的子串，它可以只包含一个字符，也可以是更长的字符串。比如，下面的示例代码能够取到输入中的每一个字符 c：{ len = length($0); for (i = 1; i <= len; i++) { c = substr($0, i, 1); } } 如果使用一个全局字符串变量 LETTERS 来存储字母表，我就可以借助 index 函数来找到某个字符在字母表中的位置。我将扩展 gawk 代码示例，让它在输入数据中只取范围在 “a” 到 “z” 的字母：BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" } { len = length($0); for (i = 1; i <= len; i++) { c = substr($0, i, 1); ltr = index(LETTERS, c); } } 需要注意的是，index 函数将返回字母在 LETTERS 字符串中首次出现的位置，第一个位置返回 1，如果没有找到则返回 0。如果我有一个大小为 26 的数组，我就可以利用这个数组来统计每个字母出现的次数。我将在下面的示例代码中添加这个功能，每当一个字母出现在输入中，我就让它对应的数组元素值增加 1（使用 ++）：BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" } { len = length($0); for (i = 1; i <= len; i++) { c = substr($0, i, 1); ltr = index(LETTERS, c); if (ltr > 0) { ++count[ltr]; } } } 打印相对频率当 gawk 脚本统计完所有的字母后，我希望它能输出每个字母的频率。毕竟，我对输入中各个字母的个数没有兴趣，我更关心它们的 相对频率。我将先统计字母 “a” 的个数，然后把它和剩余 “b” 到 “z” 字母的个数比较：END { min = count[1]; for (ltr = 2; ltr <= 26; ltr++) { if (count[ltr] < min) { min = count[ltr]; } } } 在循环的最后，变量 min 会等于最少的出现次数，我可以把它为基准，为字母的个数设定一个参照值，然后计算打印出每个字母的相对频率。比如，如果出现次数最少的字母是 “q”，那么 min 就会等于 “q” 的出现次数。接下来，我会遍历每个字母，打印出它和它的相对频率。我通过把每个字母的个数都除以 min 的方式来计算出它的相对频率，这意味着出现次数最少的字母的相对频率是 1。如果另一个字母出现的次数恰好是最少次数的两倍，那么这个字母的相对频率就是 2。我只关心整数，所以 2.1 和 2.9 对我来说是一样的（都是 2）。END { min = count[1]; for (ltr = 2; ltr <= 26; ltr++) { if (count[ltr] < min) { min = count[ltr]; } } for (ltr = 1; ltr <= 26; ltr++) { print substr(LETTERS, ltr, 1), int(count[ltr] / min); } } 最后的完整程序现在，我已经有了一个能够统计输入中各个字母的相对频率的 gawk 脚本：#!/usr/bin/gawk -f # 只统计 a-z 的字符，忽略 A-Z 和其他的字符 BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" } { len = length($0); for (i = 1; i <= len; i++) { c = substr($0, i, 1); ltr = index(LETTERS, c); if (ltr < 0) { ++count[ltr]; } } } # 打印每个字符的相对频率 END { min = count[1]; for (ltr = 2; ltr <= 26; ltr++) { if (count[ltr] < min) { min = count[ltr]; } } for (ltr = 1; ltr <= 26; ltr++) { print substr(LETTERS, ltr, 1), int(count[ltr] / min); } } 我将把这段程序保存到名为 letter-freq.awk 的文件中，这样一来，我就可以在命令行中更方便地使用它。如果你愿意的话，你也可以使用 chmod +x 命令把这个文件设为可独立执行。第一行中的 #!/usr/bin/gawk -f 表示 Linux 会使用 /usr/bin/gawk 把这个文件当作一个脚本来运行。由于 gawk 命令行使用 -f 来指定它要运行的脚本文件名，你需要在末尾加上 -f。如此一来，当你在 shell 中执行 letter-freq.awk，它会被解释为 /usr/bin/gawk -f letter-freq.awk。接下来我将用几个简单的输入来测试这个脚本。比如，如果我给我的 gawk 脚本输入整个字母表，每个字母的相对频率都应该是 1：$ echo abcdefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk a 1 b 1 c 1 d 1 e 1 f 1 g 1 h 1 i 1 j 1 k 1 l 1 m 1 n 1 o 1 p 1 q 1 r 1 s 1 t 1 u 1 v 1 w 1 x 1 y 1 z 1 还是使用上述例子，只不过这次我在输入中添加了一个字母 “e”，此时的输出结果中，“e” 的相对频率会是 2，而其他字母的相对频率仍然会是 1：$ echo abcdeefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk a 1 b 1 c 1 d 1 e 2 f 1 g 1 h 1 i 1 j 1 k 1 l 1 m 1 n 1 o 1 p 1 q 1 r 1 s 1 t 1 u 1 v 1 w 1 x 1 y 1 z 1 现在我可以跨出最大的一步了！我将使用 grep 命令和 /usr/share/dict/words 文件，统计所有仅由小写字母组成的单词中，各个字母的相对使用频率：$ grep '^[a-z]*$' /usr/share/dict/words | gawk -f letter-freq.awk a 53 b 12 c 28 d 21 e 72 f 7 g 15 h 17 i 58 j 1 k 5 l 36 m 19 n 47 o 47 p 21 q 1 r 46 s 48 t 44 u 25 v 6 w 4 x 1 y 13 z 2 在 /usr/share/dict/words 文件的所有小写单词中，字母 “j”、“q” 和 “x” 出现的相对频率最低，字母 “z” 也使用得很少。不出意料，字母 “e” 是使用频率最高的。via: https://opensource.com/article/21/4/gawk-letter-game作者：Jim Hall 选题：lujun9972 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
下载我们的电子书，学习如何更好地使用 awk。在众多 Linux 命令中，sed、awk 和 grep 恐怕是其中最经典的三个命令了。它们引人注目或许是由于名字发音与众不同，也可能是它们无处不在，甚至是因为它们存在已久，但无论如何，如果要问哪些命令很有 Linux 风格，这三个命令是当之无愧的。其中 sed 和 grep 已经有很多简洁的标准用法了，但 awk 的使用难度却相对突出。在日常使用中，通过 sed 实现字符串替换、通过 grep 实现过滤，这些都是司空见惯的操作了，但 awk 命令相对来说是用得比较少的。在我看来，可能的原因是大多数人都只使用 sed 或者 grep 的一些变化实现某些功能，例如：$ sed -e 's/foo/bar/g' file.txt $ grep foo file.txt 因此，尽管你可能会觉得 sed 和 grep 使用起来更加顺手，但实际上它们还有更多更强大的作用没有发挥出来。当然，我们没有必要在这两个命令上钻研得很深入，但我有时会好奇自己“学习”命令的方式。很多时候我会记住一整串命令“咒语”，而不会去了解其中的运作过程，这就让我产生了一种很熟悉命令的错觉，我可以随口说出某个命令的好几个选项参数，但这些参数具体有什么作用，以及它们的相关语法，我都并不明确。这大概就是很多人对 awk 缺乏了解的原因了。为使用而学习 awkawk 并不深奥。它是一种相对基础的编程语言，因此你可以把它当成一门新的编程语言来学习：使用一些基本命令来熟悉语法、了解语言中的关键字并实现更复杂的功能，然后再多加练习就可以了。awk 是如何解析输入内容的awk 的本质是将输入的内容看作是一个数组。当 awk 扫描一个文本文件时，会把每一行作为一条 记录 record ，每一条记录中又分割为多个 字段 field 。awk 记录了各条记录各个字段的信息，并通过内置变量 NR（记录数） 和 NF（字段数） 来调用相关信息。例如一下这个命令可以查看文件的行数：$ awk 'END { print NR;}' example.txt 36 从上面的命令可以看出 awk 的基本语法，无论是一个单行命令还是一整个脚本，语法都是这样的：模式或关键字 { 操作 } 在上面的例子中，END 是一个关键字而不是模式，与此类似的另一个关键字是 BEGIN。使用 BEGIN 或 END 可以让 awk 在解析内容前或解析内容后执行大括号中指定的操作。你可以使用 模式 pattern 作为过滤器或限定符，这样 awk 只会对匹配模式的对应记录执行指定的操作。以下这个例子就是使用 awk 实现 grep 命令在文件中查找“Linux”字符串的功能：$ awk '/Linux/ { print $0; }' os.txt OS: CentOS Linux (10.1.1.8) OS: CentOS Linux (10.1.1.9) OS: Red Hat Enterprise Linux (RHEL) (10.1.1.11) OS: Elementary Linux (10.1.2.4) OS: Elementary Linux (10.1.2.5) OS: Elementary Linux (10.1.2.6) awk 会将文件中的每一行作为一条记录，将一条记录中的每个单词作为一个字段，默认情况下会以空格作为 字段分隔符 field separator （FS）切割出记录中的字段。如果想要使用其它内容作为分隔符，可以使用 --field-separator 选项指定分隔符：$ awk --field-separator ':' '/Linux/ { print $2; }' os.txt CentOS Linux (10.1.1.8) CentOS Linux (10.1.1.9) Red Hat Enterprise Linux (RHEL) (10.1.1.11) Elementary Linux (10.1.2.4) Elementary Linux (10.1.2.5) Elementary Linux (10.1.2.6) 在上面的例子中，可以看到在 awk 处理后每一行的行首都有一个空格，那是因为在源文件中每个冒号（:）后面都带有一个空格。和 cut 有所不同的是，awk 可以指定一个字符串作为分隔符，就像这样：$ awk --field-separator ': ' '/Linux/ { print $2; }' os.txt CentOS Linux (10.1.1.8) CentOS Linux (10.1.1.9) Red Hat Enterprise Linux (RHEL) (10.1.1.11) Elementary Linux (10.1.2.4) Elementary Linux (10.1.2.5) Elementary Linux (10.1.2.6) awk 中的函数可以通过这样的语法在 awk 中自定义函数：函数名称(参数) { 操作 } 函数的好处在于只需要编写一次就可以多次复用，因此函数在脚本中起到的作用会比在构造单行命令时大。同时 awk 自身也带有很多预定义的函数，并且工作原理和其它编程语言或电子表格一样。你只需要了解函数需要接受什么参数，就可以放心使用了。awk 中提供了数学运算和字符串处理的相关函数。数学运算函数通常比较简单，传入一个数字，它就会传出一个结果：$ awk 'BEGIN { print sqrt(1764); }' 42 而字符串处理函数则稍微复杂一点，但 GNU awk 手册中也有充足的文档。例如 split() 函数需要传入一个待分割的单一字段、一个用于存放分割结果的数组，以及用于分割的 定界符 delimiter 。例如前面示例中的输出内容，每条记录的末尾都包含了一个 IP 地址。由于变量 NF 代表的是每条记录的字段数量，刚好对应的是每条记录中最后一个字段的序号，因此可以通过引用 NF 将每条记录的最后一个字段传入 split() 函数：$ awk --field-separator ': ' '/Linux/ { split($NF, IP, "."); print "subnet: " IP[3]; }' os.txt subnet: 1 subnet: 1 subnet: 1 subnet: 2 subnet: 2 subnet: 2 还有更多的函数，没有理由将自己限制在每个 awk 代码块中。你可以在终端中使用 awk 构建复杂的管道，也可以编写 awk 脚本来定义和使用你自己的函数。下载电子书使用 awk 本身就是一个学习 awk 的过程，即使某些操作使用 sed、grep、cut、tr 命令已经完全足够了，也可以尝试使用 awk 来实现。只要熟悉了 awk，就可以在 Bash 中自定义一些 awk 函数，进而解析复杂的数据。下载我们的这本电子书（需注册）学习并开始使用 awk 吧！via: https://opensource.com/article/20/9/awk-ebook作者：Seth Kenlon 选题：lujun9972 译者：HankChow 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
通过两个简单的教程来提高你的 awk 技能
超越单行的 awk 脚本，学习如何做邮件合并和字数统计。awk 是 Unix 和 Linux 用户工具箱中最古老的工具之一。awk 由 Alfred Aho、Peter Weinberger 和 Brian Kernighan（即工具名称中的 A、W 和 K）在 20 世纪 70 年代创建，用于复杂的文本流处理。它是流编辑器 sed 的配套工具，后者是为逐行处理文本文件而设计的。awk 支持更复杂的结构化程序，是一门完整的编程语言。本文将介绍如何使用 awk 完成更多结构化的复杂任务，包括一个简单的邮件合并程序。awk 的程序结构awk 脚本是由 {}（大括号）包围的功能块组成，其中有两个特殊的功能块，BEGIN 和 END，它们在处理第一行输入流之前和最后一行处理之后执行。在这两者之间，块的格式为：模式 { 动作语句 } 当输入缓冲区中的行与模式匹配时，每个块都会执行。如果没有包含模式，则函数块在输入流的每一行都会执行。另外，以下语法可以用于在 awk 中定义可以从任何块中调用的函数。function 函数名(参数列表) { 语句 } 这种模式匹配块和函数的组合允许开发者结构化的 awk 程序，以便重用和提高可读性。awk 如何处理文本流awk 每次从输入文件或流中一行一行地读取文本，并使用字段分隔符将其解析成若干字段。在 awk 的术语中，当前的缓冲区是一个记录。有一些特殊的变量会影响 awk 读取和处理文件的方式：FS（ 字段分隔符 field separator ）。默认情况下，这是任何空格字符（空格或制表符）。RS（ 记录分隔符 record separator ）。默认情况下是一个新行（n）。NF（ 字段数 number of fields ）。当 awk 解析一行时，这个变量被设置为被解析出字段数。$0: 当前记录。$1、$2、$3 等：当前记录的第一、第二、第三等字段。NR（ 记录数 number of records ）。迄今已被 awk 脚本解析的记录数。影响 awk 行为的变量还有很多，但知道这些已经足够开始了。单行 awk 脚本对于一个如此强大的工具来说，有趣的是，awk 的大部分用法都是基本的单行脚本。也许最常见的 awk 程序是打印 CSV 文件、日志文件等输入行中的选定字段。例如，下面的单行脚本从 /etc/passwd 中打印出一个用户名列表：awk -F":" '{print $1 }' /etc/passwd 如上所述，$1 是当前记录中的第一个字段。-F 选项将 FS 变量设置为字符 :。字段分隔符也可以在 BEGIN 函数块中设置：awk 'BEGIN { FS=":" } {print $1 }' /etc/passwd 在下面的例子中，每一个 shell 不是 /sbin/nologin 的用户都可以通过在该块前面加上匹配模式来打印出来：awk 'BEGIN { FS=":" } ! /\/sbin\/nologin/ {print $1 }' /etc/passwd awk 进阶：邮件合并现在你已经掌握了一些基础知识，尝试用一个更具有结构化的例子来深入了解 awk：创建邮件合并。邮件合并使用两个文件，其中一个文件（在本例中称为 email_template.txt）包含了你要发送的电子邮件的模板：From: Program committee <pc@event.org> To: {firstname} {lastname} <{email}> Subject: Your presentation proposal Dear {firstname}, Thank you for your presentation proposal: {title} We are pleased to inform you that your proposal has been successful! We will contact you shortly with further information about the event schedule. Thank you, The Program Committee 而另一个则是一个 CSV 文件（名为 proposals.csv），里面有你要发送邮件的人：firstname,lastname,email,title Harry,Potter,hpotter@hogwarts.edu,"Defeating your nemesis in 3 easy steps" Jack,Reacher,reacher@covert.mil,"Hand-to-hand combat for beginners" Mickey,Mouse,mmouse@disney.com,"Surviving public speaking with a squeaky voice" Santa,Claus,sclaus@northpole.org,"Efficient list-making" 你要读取 CSV 文件，替换第一个文件中的相关字段（跳过第一行），然后把结果写到一个叫 acceptanceN.txt 的文件中，每解析一行就递增文件名中的 N。把 awk 程序写在一个叫 mail_merge.awk 的文件中。在 awk 脚本中的语句用 ; 分隔。第一个任务是设置字段分隔符变量和其他几个脚本需要的变量。你还需要读取并丢弃 CSV 中的第一行，否则会创建一个以 Dear firstname 开头的文件。要做到这一点，请使用特殊函数 getline，并在读取后将记录计数器重置为 0。BEGIN { FS=","; template="email_template.txt"; output="acceptance"; getline; NR=0; } 主要功能非常简单：每处理一行，就为各种字段设置一个变量 —— firstname、lastname、email 和 title。模板文件被逐行读取，并使用函数 sub 将任何出现的特殊字符序列替换为相关变量的值。然后将该行以及所做的任何替换输出到输出文件中。由于每行都要处理模板文件和不同的输出文件，所以在处理下一条记录之前，需要清理和关闭这些文件的文件句柄。{ # 从输入文件中读取关联字段 firstname=$1; lastname=$2; email=$3; title=$4; # 设置输出文件名 outfile=(output NR ".txt"); # 从模板中读取一行，替换特定字段， # 并打印结果到输出文件。 while ( (getline ln < template) > 0 ) { sub(/{firstname}/,firstname,ln); sub(/{lastname}/,lastname,ln); sub(/{email}/,email,ln); sub(/{title}/,title,ln); print(ln) > outfile; } # 关闭模板和输出文件，继续下一条记录 close(outfile); close(template); } 你已经完成了! 在命令行上运行该脚本：awk -f mail_merge.awk proposals.csv 或awk -f mail_merge.awk < proposals.csv 你会在当前目录下发现生成的文本文件。awk 进阶：字频计数awk 中最强大的功能之一是关联数组，在大多数编程语言中，数组条目通常由数字索引，但在 awk 中，数组由一个键字符串进行引用。你可以从上一节的文件 proposals.txt 中存储一个条目。例如，在一个单一的关联数组中，像这样： proposer["firstname"]=$1; proposer["lastname"]=$2; proposer["email"]=$3; proposer["title"]=$4; 这使得文本处理变得非常容易。一个使用了这个概念的简单的程序就是词频计数器。你可以解析一个文件，在每一行中分解出单词（忽略标点符号），对行中的每个单词进行递增计数器，然后输出文本中出现的前 20 个单词。首先，在一个名为 wordcount.awk 的文件中，将字段分隔符设置为包含空格和标点符号的正则表达式：BEGIN { # ignore 1 or more consecutive occurrences of the characters # in the character group below FS="[ .,:;()<>{}@!\"'\t]+"; } 接下来，主循环函数将遍历每个字段，忽略任何空字段（如果行末有标点符号，则会出现这种情况），并递增行中单词数：{ for (i = 1; i <= NF; i++) { if ($i != "") { words[$i]++; } } } 最后，处理完文本后，使用 END 函数打印数组的内容，然后利用 awk 的能力，将输出的内容用管道输入 shell 命令，进行数字排序，并打印出 20 个最常出现的单词。END { sort_head = "sort -k2 -nr | head -n 20"; for (word in words) { printf "%s\t%d\n", word, words[word] | sort_head; } close (sort_head); } 在这篇文章的早期草稿上运行这个脚本，会产生这样的输出：[dneary@dhcp-49-32.bos.redhat.com]$ awk -f wordcount.awk < awk_article.txt the 79 awk 41 a 39 and 33 of 32 in 27 to 26 is 25 line 23 for 23 will 22 file 21 we 16 We 15 with 12 which 12 by 12 this 11 output 11 function 11 下一步是什么？如果你想了解更多关于 awk 编程的知识，我强烈推荐 Dale Dougherty 和 Arnold Robbins 所著的《Sed 和 awk》这本书。awk 编程进阶的关键之一是掌握“扩展正则表达式”。awk 为你可能已经熟悉的 sed 正则表达式语法提供了几个强大的补充。另一个学习 awk 的好资源是 GNU awk 用户指南。它有一个完整的 awk 内置函数库的参考资料，以及很多简单和复杂的 awk 脚本的例子。via: https://opensource.com/article/19/10/advanced-awk作者：Dave Neary 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
帮助你排序文本文件的 Awk 命令行或脚本
Awk 是一个强大的工具，可以执行某些可能由其它常见实用程序（包括 sort）来完成的任务。Awk 是个普遍存在的 Unix 命令，用于扫描和处理包含可预测模式的文本。但是，由于它具有函数功能，因此也可以合理地称之为编程语言。令人困惑的是，有不止一个 awk。（或者，如果你认为只有一个，那么其它几个就是克隆。）有 awk（由Aho、Weinberger 和 Kernighan 编写的原始程序），然后有 nawk 、mawk 和 GNU 版本的 gawk。GNU 版本的 awk 是该实用程序的一个高度可移植的自由软件版本，具有几个独特的功能，因此本文是关于 GNU awk 的。虽然它的正式名称是 gawk，但在 GNU+Linux 系统上，它的别名是 awk，并用作该命令的默认版本。 在其他没有带有 GNU awk 的系统上，你必须先安装它并将其称为 gawk，而不是 awk。本文互换使用术语 awk 和 gawk。awk 既是命令语言又是编程语言，这使其成为一个强大的工具，可以处理原本留给 sort、cut、uniq 和其他常见实用程序的任务。幸运的是，开源中有很多冗余空间，因此，如果你面临是否使用 awk 的问题，答案可能是肯定的“随便”。awk 的灵活之美在于，如果你已经确定使用 awk 来完成一项任务，那么无论接下来发生什么，你都可以继续使用 awk。这包括对数据排序而不是按交付给你的顺序的永恒需求。样本数据集在探索 awk 的排序方法之前，请生成要使用的样本数据集。保持简单，这样你就不会为极端情况和意想不到的复杂性所困扰。这是本文使用的样本集：Aptenodytes;forsteri;Miller,JF;1778;Emperor Pygoscelis;papua;Wagler;1832;Gentoo Eudyptula;minor;Bonaparte;1867;Little Blue Spheniscus;demersus;Brisson;1760;African Megadyptes;antipodes;Milne-Edwards;1880;Yellow-eyed Eudyptes;chrysocome;Viellot;1816;Sothern Rockhopper Torvaldis;linux;Ewing,L;1996;Tux这是一个很小的数据集，但它提供了多种数据类型：属名和种名，彼此相关但又是分开的姓，有时是以逗号开头的首字母缩写代表日期的整数任意术语所有字段均以分号分隔根据你的教育背景，你可能会认为这是二维数组或表格，或者只是行分隔的数据集合。你如何看待它只是你的问题，而 awk 只认识文本。由你决定告诉 awk 你想如何解析它。只想排序如果你只想按特定的可定义字段（例如电子表格中的“单元格”）对文本数据集进行排序，则可以使用 sort 命令。字段和记录无论输入的格式如何，都必须在其中找到模式才可以专注于对你重要的数据部分。在此示例中，数据由两个因素定界：行和字段。每行都代表一个新的记录，就如你在电子表格或数据库转储中看到的一样。在每一行中，都有用分号（;）分隔的不同的字段（将其视为电子表格中的单元格）。awk 一次只处理一条记录，因此，当你在构造发给 awk 的这指令时，你可以只关注一行记录。写下你想对一行数据执行的操作，然后在下一行进行测试（无论是心理上还是用 awk 进行测试），然后再进行其它的一些测试。最后，你要对你的 awk 脚本要处理的数据做好假设，以便可以按你要的数据结构提供给你数据。在这个例子中，很容易看到每个字段都用分号隔开。为简单起见，假设你要按每行的第一字段对列表进行排序。在进行排序之前，你必须能够让 awk 只关注在每行的第一个字段上，因此这是第一步。终端中 awk 命令的语法为 awk，后跟相关选项，最后是要处理的数据文件。$ awk --field-separator=";" '{print $1;}' penguins.list Aptenodytes Pygoscelis Eudyptula Spheniscus Megadyptes Eudyptes Torvaldis因为字段分隔符是对 Bash shell 具有特殊含义的字符，所以必须将分号括在引号中或在其前面加上反斜杠。此命令仅用于证明你可以专注于特定字段。你可以使用另一个字段的编号尝试相同的命令，以查看数据的另一个“列”的内容：$ awk --field-separator=";" '{print $3;}' penguins.list Miller,JF Wagler Bonaparte Brisson Milne-Edwards Viellot Ewing,L我们尚未进行任何排序，但这是良好的基础。脚本编程awk 不仅仅是命令，它是一种具有索引、数组和函数的编程语言。这很重要，因为这意味着你可以获取要排序的字段列表，将列表存储在内存中，进行处理，然后打印结果数据。对于诸如此类的一系列复杂操作，在文本文件中进行操作会更容易，因此请创建一个名为 sort.awk 的新文件并输入以下文本：#!/bin/gawk -f BEGIN { FS=";"; }这会将该文件建立为 awk 脚本，该脚本中包含执行的行。BEGIN 语句是 awk 提供的特殊设置功能，用于只需要执行一次的任务。定义内置变量 FS，它代表 字段分隔符 field separator ，并且与你在 awk 命令中使用 --field-separator 设置的值相同，它只需执行一次，因此它包含在 BEGIN 语句中。awk 中的数组你已经知道如何通过使用 $ 符号和字段编号来收集特定字段的值，但是在这种情况下，你需要将其存储在数组中而不是将其打印到终端。这是通过 awk 数组完成的。awk 数组的重要之处在于它包含键和值。 想象一下有关本文的内容；它看起来像这样：author:"seth",title:"How to sort with awk",length:1200。诸如作者、标题和长度之类的元素是键，跟着的内容为值。在排序的上下文中这样做的好处是，你可以将任何字段分配为键，将任何记录分配为值，然后使用内置的 awk 函数 asorti()（按索引排序）按键进行排序。现在，随便假设你只想按第二个字段排序。没有被特殊关键字 BEGIN 或 END 引起来的 awk 语句是在每个记录都要执行的循环。这是脚本的一部分，该脚本扫描数据中的模式并进行相应的处理。每次 awk 将注意力转移到一条记录上时，都会执行 {} 中的语句（除非以 BEGIN 或 END 开头）。要将键和值添加到数组，请创建一个包含数组的变量（在本示例脚本中，我将其称为 ARRAY，虽然不是很原汁原味，但很清楚），然后在方括号中分配给它键，用等号（=）连接值。{ # dump each field into an array ARRAY[$2] = $R; }在此语句中，第二个字段的内容（$2）用作关键字，而当前记录（$R）用作值。asorti() 函数除了数组之外，awk 还具有一些基本函数，你可以将它们用作常见任务的快速简便的解决方案。GNU awk中引入的函数之一 asorti() 提供了按键（索引）或值对数组进行排序的功能。你只能在对数组进行填充后对其进行排序，这意味着此操作不能对每个新记录都触发，而只能在脚本的最后阶段进行。为此，awk 提供了特殊的 END 关键字。与 BEGIN 相反，END 语句仅在扫描了所有记录之后才触发一次。将这些添加到你的脚本：END { asorti(ARRAY,SARRAY); # get length j = length(SARRAY); for (i = 1; i <= j; i++) { printf("%s %s\n", SARRAY[i],ARRAY[SARRAY[i]]) } }asorti() 函数获取 ARRAY 的内容，按索引对其进行排序，然后将结果放入名为 SARRAY 的新数组（我在本文中发明的任意名称，表示“排序的 ARRAY”）。接下来，将变量 j（另一个任意名称）分配给 length() 函数的结果，该函数计算 SARRAY 中的项数。最后，使用 for 循环使用 printf() 函数遍历 SARRAY 中的每一项，以打印每个键，然后在 ARRAY 中打印该键的相应值。运行该脚本要运行你的 awk 脚本，先使其可执行：$ chmod +x sorter.awk然后针对 penguin.list 示例数据运行它：$ ./sorter.awk penguins.list antipodes Megadyptes;antipodes;Milne-Edwards;1880;Yellow-eyed chrysocome Eudyptes;chrysocome;Viellot;1816;Sothern Rockhopper demersus Spheniscus;demersus;Brisson;1760;African forsteri Aptenodytes;forsteri;Miller,JF;1778;Emperor linux Torvaldis;linux;Ewing,L;1996;Tux minor Eudyptula;minor;Bonaparte;1867;Little Blue papua Pygoscelis;papua;Wagler;1832;Gentoo如你所见，数据按第二个字段排序。这有点限制。最好可以在运行时灵活选择要用作排序键的字段，以便可以在任何数据集上使用此脚本并获得有意义的结果。添加命令选项你可以通过在脚本中使用字面值 var 将命令变量添加到 awk 脚本中。更改脚本，以使迭代子句在创建数组时使用 var：{ # dump each field into an array ARRAY[$var] = $R; }尝试运行该脚本，以便在执行脚本时使用 -v var 选项将其按第三字段排序：$ ./sorter.awk -v var=3 penguins.list Bonaparte Eudyptula;minor;Bonaparte;1867;Little Blue Brisson Spheniscus;demersus;Brisson;1760;African Ewing,L Torvaldis;linux;Ewing,L;1996;Tux Miller,JF Aptenodytes;forsteri;Miller,JF;1778;Emperor Milne-Edwards Megadyptes;antipodes;Milne-Edwards;1880;Yellow-eyed Viellot Eudyptes;chrysocome;Viellot;1816;Sothern Rockhopper Wagler Pygoscelis;papua;Wagler;1832;Gentoo修正本文演示了如何在纯 GNU awk 中对数据进行排序。你可以对脚本进行改进，以便对你有用，花一些时间在gawk 的手册页上研究 awk 函数并自定义脚本以获得更好的输出。这是到目前为止的完整脚本：#!/usr/bin/awk -f # GPLv3 appears here # usage: ./sorter.awk -v var=NUM FILE BEGIN { FS=";"; } { # dump each field into an array ARRAY[$var] = $R; } END { asorti(ARRAY,SARRAY); # get length j = length(SARRAY); for (i = 1; i <= j; i++) { printf("%s %s\n", SARRAY[i],ARRAY[SARRAY[i]]) } }via: https://opensource.com/article/19/11/how-sort-awk作者：Seth Kenlon 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
学习怎样使用 awk 的 !visited[$0]++ 在不重新排序或改变原排列顺序的前提下删掉重复的行。假设你有一个文本文件，你需要删掉所有重复的行。TL;DR要保持原来的排列顺序删掉重复行，使用：awk '!visited[$0]++' your_file > deduplicated_file工作原理这个脚本维护一个关联数组，索引（键）为文件中去重后的行，每个索引对应的值为该行出现的次数。对于文件的每一行，如果这行（之前）出现的次数为 0，则值加 1，并打印这行，否则值加 1，不打印这行。我之前不熟悉 awk，我想弄清楚这么短小的一个脚本是怎么实现的。我调研了下，下面是调研心得：这个 awk “脚本” !visited[$0]++ 对输入文件的每一行都执行。visited[] 是一个关联数组（又名映射）类型的变量。awk 会在第一次执行时初始化它，因此我们不需要初始化。$0 变量的值是当前正在被处理的行的内容。visited[$0] 通过与 $0（正在被处理的行）相等的键来访问该映射中的值，即出现次数（我们在下面设置的）。! 对表示出现次数的值取反：在 awk 中，任意非零的数或任意非空的字符串的值是 true。变量默认的初始值为空字符串，如果被转换为数字，则为 0。也就是说：如果 visited[$0] 的值是一个比 0 大的数，取反后被解析成 false。如果 visited[$0] 的值为等于 0 的数字或空字符串，取反后被解析成 true 。++ 表示变量 visited[$0] 的值加 1。如果该值为空，awk 自动把它转换为 0（数字） 后加 1。注意：加 1 操作是在我们取到了变量的值之后执行的。总的来说，整个表达式的意思是：true：如果表示出现次数为 0 或空字符串false：如果出现的次数大于 0awk 由 模式或表达式和一个与之关联的动作 组成：<模式/表达式> { <动作> }如果匹配到了模式，就会执行后面的动作。如果省略动作，awk 默认会打印（print）输入。省略动作等价于 {print $0}。我们的脚本由一个 awk 表达式语句组成，省略了动作。因此这样写：awk '!visited[$0]++' your_file > deduplicated_file等于这样写：awk '!visited[$0]++ { print $0 }' your_file > deduplicated_file对于文件的每一行，如果表达式匹配到了，这行内容被打印到输出。否则，不执行动作，不打印任何东西。为什么不用 uniq 命令？uniq 命令仅能对相邻的行去重。这是一个示例：$ cat test.txt A A A B B B A A C C C B B A $ uniq < test.txt A B A C B A其他方法使用 sort 命令我们也可以用下面的 sort 命令来去除重复的行，但是原来的行顺序没有被保留。sort -u your_file > sorted_deduplicated_file使用 cat + sort + cut上面的方法会产出一个去重的文件，各行是基于内容进行排序的。通过管道连接命令可以解决这个问题。cat -n your_file | sort -uk2 | sort -nk1 | cut -f2-工作原理假设我们有下面一个文件：abc ghi abc def xyz def ghi klmcat -n test.txt 在每行前面显示序号：1 abc 2 ghi 3 abc 4 def 5 xyz 6 def 7 ghi 8 klmsort -uk2 基于第二列（k2 选项）进行排序，对于第二列相同的值只保留一次（u 选项）：1 abc 4 def 2 ghi 8 klm 5 xyzsort -nk1 基于第一列排序（k1 选项），把列的值作为数字来处理（-n 选项）：1 abc 2 ghi 4 def 5 xyz 8 klm最后，cut -f2- 从第二列开始打印每一行，直到最后的内容（-f2- 选项：留意 - 后缀，它表示这行后面的内容都包含在内）。abc ghi def xyz klm参考GNU awk 用户手册awk 中的数组Awk — 真值Awk 表达式Unix 怎么删除文件中重复的行？不用排序去掉重复的行（去重）[‘!a[$0]++’ 工作原理](https://unix.stackexchange.com/questions/159695/how-does-awk-a0-work/159734#159734)以上为全文。via: https://opensource.com/article/19/10/remove-duplicate-lines-files-awk作者：Lazarus Lazaridis 选题：lujun9972 译者：lxbwolf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
awk 命令不仅提供了简单的输入字符串筛选功能，还包含提取数据列、打印简单文本、筛选内容——甚至做一些数学计算。如果你仅使用 awk 选取一行中的特定文本，那么你可能错过了它的很多功能。在这篇文章中，我们会来看看使用 awk 可以帮你做一些其他的什么事情，并提供一些例子。提取数据列awk 所提供的最简单与最常用的功能便是从文件或管道传输的数据中选取特定的内容。默认使用空格当做分隔符，这非常简单。$ echo one two three four five | awk ‘{print $4}’ four $ who | awk ‘{print $1}’ jdoe fhenry空格指的是一系列的 space 或 tab 字符。在下面所展示的命令里，awk 从提供的数据中筛选第一和第四项。awk 命令也可以通过在其后增加文件名参数的方式从文本文件中获取数据。$ awk '{print $1,$5,$NF}' HelenKellerQuote The beautiful heart.（LCTT 译注：“The best and most beautiful things in the world can not be seen or even touched , they must be felt with heart.” ——海伦凯勒）在这个例子中，awk 挑选了一行中的第一个、第五个和最后一个字段。命令中的 $NF 指定选取每行的最后一个字段。这是因为 NF 代表一行中的 字段数量 Number of Field ，也就是 23，而 $NF 就代表着那个字段的值，也就是heart。最后的句号也包含进去了，因为它是最后一个字符串的一部分。字段能以任何有用的形式打印。在这个例子中，我们将字段以日期的格式进行打印输出。$ date | awk '{print $4,$3,$2}' 2019 Nov 22如果你省略了 awk 命令中字段指示符之间的逗号，输出将会挤成一个字符串。$ date | awk '{print $4 $3 $2}' 2019Nov21如果你将通常使用的逗号替换为连字符，awk 就会尝试将两个字段的值相减——或许这并不是你想要的。它不会将连字符插入到输出结果中。相反地，它对输出做了一些数学计算。$ date | awk '{print $4-$3-$2}' 1997在这个例子中，它将年 “2019” 和日期 “22” 相减，并忽略了中间的 “Nov”。如果你想要空格之外的字符作为输出分隔符，你可以通过 OFS（ 输出分隔符 output field separator ）指定分隔符，就像这样：$ date | awk '{OFS="-"; print $4,$3,$2}' 2019-Nov-22打印简单文本你也可以使用 awk 简单地显示一些文本。当然了，比起 awk 你可能更想使用 echo 命令。但换句话说，作为 awk 脚本的一部分，打印某些相关性文本将会非常实用。这里有一个没什么用的例子：$ awk 'BEGIN {print "Hello, World" }' Hello, World下面的例子更加合理，添加一行文本标签来更好的辨识数据。$ who | awk 'BEGIN {print "Current logins:"} {print $1}' Current logins: shs nemo指定字段分隔符不是所有的输入都以空格作为分隔符的。如果你的文本通过其它的字符作为分隔符（例如：逗号、冒号、分号），你可以通过 -F 选项（输入分隔符）告诉 awk：$ cat testfile a:b:c,d:e $ awk -F : '{print $2,$3}' testfile b c,d下面是一个更加有用的例子——从冒号分隔的 /etc/passwd 文件中获取数据：$ awk -F: '{print $1}' /etc/passwd | head -11 root daemon bin sys sync games man lp mail news uucp筛选内容你也可以使用 awk 命令评估字段。例如你仅仅想列出 /etc/passwd 中的用户账号，就可以对第三个字段做一些筛选。下面的例子中我们只关注大于等于 1000 的 UID：$ awk -F":" ' $3 >= 1000 ' /etc/passwd nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin shs:x:1000:1000:Sandra Henry-Stocker,,,:/home/shs:/bin/bash nemo:x:1001:1001:Nemo,,,:/home/nemo:/usr/bin/zsh dory:x:1002:1002:Dory,,,:/home/dory:/bin/bash ...如果你想为输出增加标题，可以添加 BEGIN 从句：$ awk -F":" 'BEGIN {print "user accounts:"} $3 >= 1000 ' /etc/passwd user accounts: nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin shs:x:1000:1000:Sandra Henry-Stocker,,,:/home/shs:/bin/bash nemo:x:1001:1001:Nemo,,,:/home/nemo:/usr/bin/zsh dory:x:1002:1002:Dory,,,:/home/dory:/bin/bash如果你想要不止一行的标题，你可以通过 "\n" 分隔输出：$ awk -F":" 'BEGIN {print "user accounts\n============="} $3 >= 1000 ' /etc/passwd user accounts ============= nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin shs:x:1000:1000:Sandra Henry-Stocker,,,:/home/shs:/bin/bash nemo:x:1001:1001:Nemo,,,:/home/nemo:/usr/bin/zsh dory:x:1002:1002:Dory,,,:/home/dory:/bin/bash在 awk 中进行数学计算awk 提供了惊人的数学计算能力，并且可以开平方，算 log，算 tan 等等。这里有一对例子：$ awk 'BEGIN {print sqrt(2019)}' 44.9333 $ awk 'BEGIN {print log(2019)}' 7.61036想要详细了解 awk 的数学计算能力，可以看《使用 awk 进行数学计算》这篇文章。awk 脚本你也可以使用 awk 写一套单独的脚本。下面的例子模仿了之前写过的一个，不过还计算了系统里账户的数量。#!/usr/bin/awk -f # 这一行是注释 BEGIN { printf "%s\n","User accounts:" print "==============" FS=":" n=0 } # 现在开始遍历数据 { if ($3 >= 1000) { print $1 n ++ } } END { print "==============" print n " accounts" }注意 BEGIN 那一节是如何提供标题、指定字段分隔符和初始化计数器的，它仅在脚本初始化时期执行。这个脚本也包含 END 节，它仅在中间所有命令处理完成之后运行，显示了所有中间小节所筛选数据的最终行数（第三个字段大于等于 1000）。作为一个长存于 Unix 之上的命令，awk 依旧提供着非常有用的服务，这也是我几十年前爱上 Unix 的原因之一。via: https://www.networkworld.com/article/3454979/the-many-faces-of-awk.html作者：Sandra Henry-Stocker 选题：lujun9972 译者：LuuMing 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
将一个 awk 脚本移植到 Python 主要在于代码风格而不是转译。脚本是解决问题的有效方法，而 awk 是编写脚本的出色语言。它特别擅长于简单的文本处理，它可以带你完成配置文件的某些复杂重写或目录中文件名的重新格式化。何时从 awk 转向 Python但是在某些方面，awk 的限制开始显现出来。它没有将文件分解为模块的真正概念，它缺乏质量错误报告，并且缺少了现在被认为是编程语言工作原理的其他内容。当编程语言的这些丰富功能有助于维护关键脚本时，移植将是一个不错的选择。我最喜欢的完美移植 awk 的现代编程语言是 Python。在将 awk 脚本移植到 Python 之前，通常值得考虑一下其原始使用场景。例如，由于 awk 的局限性，通常从 Bash 脚本调用 awk 代码，其中包括一些对 sed、sort 之类的其它命令行常见工具的调用。 最好将所有内容转换为一个一致的 Python 程序。有时，脚本会做出过于宽泛的假设，例如，即使实际上只运行一个文件，该代码也可能允许任意数量的文件。在仔细考虑了上下文并确定了要用 Python 替代的东西之后，该编写代码了。标准 awk 到 Python 功能以下 Python 功能是有用的，需要记住：with open(some_file_name) as fpin: for line in fpin: pass # do something with line此代码将逐行循环遍历文件并处理这些行。如果要访问行号（相当于 awk 的 NR），则可以使用以下代码：with open(some_file_name) as fpin: for nr, line in enumerate(fpin): pass # do something with line在 Python 中实现多文件的 awk 式行为如果你需要能够遍历任意数量的文件同时保持行数的持续计数（类似 awk 的 FNR），则此循环可以做到这一点：def awk_like_lines(list_of_file_names): def _all_lines(): for filename in list_of_file_names: with open(filename) as fpin: yield from fpin yield from enumerate(_all_lines())此语法使用 Python 的生成器和 yield from 来构建迭代器，该迭代器将遍历所有行并保持一个持久计数。如果你需要同时使用 FNR 和 NR，这是一个更复杂的循环：def awk_like_lines(list_of_file_names): def _all_lines(): for filename in list_of_file_names: with open(filename) as fpin: yield from enumerate(fpin) for nr, (fnr, line) in _all_lines: yield nr, fnr, line更复杂的 FNR、NR 和行数的 awk 行为如果 FNR、NR 和行数这三个你全都需要，仍然会有一些问题。如果确实如此，则使用三元组（其中两个项目是数字）会导致混淆。命名参数可使该代码更易于阅读，因此最好使用 dataclass：import dataclass @dataclass.dataclass(frozen=True) class AwkLikeLine: content: str fnr: int nr: int def awk_like_lines(list_of_file_names): def _all_lines(): for filename in list_of_file_names: with open(filename) as fpin: yield from enumerate(fpin) for nr, (fnr, line) in _all_lines: yield AwkLikeLine(nr=nr, fnr=fnr, line=line)你可能想知道，为什么不一直用这种方法呢？使用其它方式的的原因是总用这种方法太复杂了。如果你的目标是把一个通用库更容易地从 awk 移植到 Python，请考虑这样做。但是编写一个可以使你确切地了解特定情况所需的循环的方法通常更容易实现，也更容易理解（因而易于维护）。理解 awk 字段一旦有了与一行相对应的字符串，如果要转换 awk 程序，则通常需要将其分解为字段。Python 有几种方法可以做到这一点。这将把行按任意数量的连续空格拆分，返回一个字符串列表：line.split()如果需要另一个字段分隔符，比如以 : 分隔行，则需要 rstrip 方法来删除最后一个换行符：line.rstrip("\n").split(":")完成以下操作后，列表 parts 将存有分解的字符串：parts = line.rstrip("\n").split(":")这种拆分非常适合用来处理参数，但是我们处于偏差一个的错误场景中。现在 parts[0] 将对应于 awk 的 $1，parts[1] 将对应于 awk 的 $2，依此类推。之所以偏差一个，是因为 awk 计数“字段”从 1 开始，而 Python 从 0 开始计数。在 awk 中，$0 是整个行 —— 等同于 line.rstrip("\n")，而 awk 的 NF（字段数）更容易以 len(parts) 的形式得到。移植 awk 字段到 Python例如，让我们将这个单行代码“如何使用 awk 从文件中删除重复行”转换为 Python。awk 中的原始代码是：awk '!visited[$0]++' your_file > deduplicated_file“真实的” Python 转换将是：import collections import sys visited = collections.defaultdict(int) for line in open("your_file"): did_visit = visited[line] visited[line] += 1 if not did_visit: sys.stdout.write(line)但是，Python 比 awk 具有更多的数据结构。与其计数访问次数（除了知道是否看到一行，我们不使用它），为什么不记录访问的行呢？import sys visited = set() for line in open("your_file"): if line in visited: continue visited.add(line) sys.stdout.write(line)编写 Python 化的 awk 代码Python 社区提倡编写 Python 化的代码，这意味着它要遵循公认的代码风格。更加 Python 化的方法将区分唯一性和输入/输出的关注点。此更改将使对代码进行单元测试更加容易：def unique_generator(things): visited = set() for thing in things: if thing in visited: continue visited.add(things) yield thing import sys for line in unique_generator(open("your_file")): sys.stdout.write(line)将所有逻辑置于输入/输出代码之外，可以更好地分离问题，并提高代码的可用性和可测试性。结论：Python 可能是一个不错的选择将 awk 脚本移植到 Python 时，通常是在考虑适当的 Python 代码风格时重新实现核心需求，而不是按条件/操作进行笨拙的音译。考虑原始上下文并产生高质量的 Python 解决方案。虽然有时候使用 awk 的 Bash 单行代码可以完成这项工作，但 Python 编码是通往更易于维护的代码的途径。另外，如果你正在编写 awk 脚本，我相信您也可以学习 Python！如果你有任何疑问，请告诉我。via: https://opensource.com/article/19/11/awk-to-python作者：Moshe Zadka 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出