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中国 荣誉推出
为了简化 Java 的繁琐，我制作了一个 Groovy 工具来分析我的音乐目录。最近，我一直在研究 Groovy 是如何简化略微繁琐的 Java 的。在这篇文章中，我开始了一个简短的系列，通过创建一个分析我的音乐目录的工具来演示 Groovy 脚本。在本文中，我将演示 groovy.File 类如何扩展和精简 java.File 并简化其使用。这为查看音乐文件夹的内容提供了一个框架，以确保预期的内容（例如，cover.jpg 文件）就位。我使用 JAudiotagger 库 来分析音乐文件的标签。安装 Java 和 GroovyGroovy 基于 Java，需要安装 Java。 Java 和 Groovy 的最新和稳定的版本可能都在你的 Linux 发行版的仓库中。 Groovy 也可以直接从 Apache Foundation 网站 安装。对于 Linux 用户来说，一个不错的选择是 SDKMan，它可用于获取 Java、Groovy 和许多其他相关工具的多个版本。对于本文，我使用以下 SDK 版本：Java：版本 11.0.12-open 的 OpenJDK 11Groovy：版本 3.0.8音乐元数据最近，我重整了我的音乐消费方式。我决定使用优秀的开源 Cantata 音乐播放器，它是开源 MPD 音乐播放器 的一个前端。我所有的电脑的音乐都存储在 /var/lib/mpd/music 目录下。在该音乐目录下有艺术家子目录，在每个艺术家子目录下有专辑子目录，包含音乐文件、cover.jpg，偶尔还有 PDF 格式的内页说明。我绝大部分的音乐文件都是 FLAC 格式的，有一些是 MP3 格式，可能还有一小部分是 OGG 格式。我选择 JAudiotagger 库的一个原因是它可以透明地处理不同的标签格式。当然，JAudiotagger 是开源的！那么查看音频标签有什么意义呢？以我的经验，音频标签的管理极差。（提到音频标签，）我的脑海中浮现出“粗心”这个词。这是标签本身真正存在的问题，也可能是出于我自己的学究倾向。无论如何，这是一个可以通过使用 Groovy 和 JAudiotagger 解决的重要问题。不过，它不仅适用于音乐收藏。许多其他现实世界的问题也适用，如需要下沉到文件系统中的目录树来处理在那里找到的内容。使用 Groovy 脚本这是此任务所需的基本代码。我在脚本中加入了注释，这些注释反映了我通常留给自己的（相对简写的）“注释提醒”：// 定义音乐库目录 def musicLibraryDirName = '/var/lib/mpd/music' // 输出 CSV 文件标题行 println "artistDir|albumDir|contentFile" // 迭代音乐库目录中的每个目录 // 这一层应该是艺术家目录 new File(musicLibraryDirName).eachDir { artistDir -> // 迭代艺术家目录中的每个目录 // 这一层应该是专辑目录 artistDir.eachDir { albumDir -> // 迭代专辑目录中的每个目录 // 这里应该是内容 // 或相关内容（如 `cover.jpg`，PDF 格式的内页说明） albumDir.eachFile { contentFile -> println "$artistDir.name|$albumDir.name|$contentFile.name" } } } 如上所述，我使用 groovy.File 在目录树中移动。具体来说：第 7 行创建一个新的 groovy.File 对象并在其上调用 groovy.File.eachDir()，第 7 行的 { 和第 18 行的结尾的 } 之间的代码是传给 eachDir() 的 groovy.Colsue 参数。这意味着 eachDir() 为目录中找到的每个子目录执行该代码。这类似于 Java lambda（也称为“匿名函数”）。 Groovy 闭包不会像 lambda 那样限制对调用环境的访问（在最新版本的 Groovy 中，如果你愿意，也可以使用 Java lambda）。如上所述，音乐库目录中的子目录应该是艺术家目录（例如，“Iron Butterfly” 或 “Giacomo Puccini”），因此 artistDir 是 eachDir() 传递给闭包的参数。第 10 行对每个 artistDir 调用 eachDir()，第 10 行的 { 和第 17 行的 } 之间的代码形成另一个处理 albumDir 的闭包。第 14 行，在每个 albumDir 上调用 eachFile()，第 14 行的 { 和第 16 行的 } 之间的代码形成了处理专辑内容的第三级闭包。在本文的范围内，我对每个文件唯一需要做的就是开始构建信息表，我将其创建为一个以竖线分隔的 CSV 文件，它可以导入 LibreOffice 或 OfficeOnly 或任何其他电子表格。现在，代码输出前三列：艺术家目录名、专辑目录名和内容文件名（同样，第 2 行输出 CSV 标题行）。在我的 Linux 笔记本电脑上运行它会产生以下输出：$ groovy TagAnalyzer.groovy | head artistDir|albumDir|contentFile Habib Koite & Bamada|Afriki|02 - Ntesse.flac Habib Koite & Bamada|Afriki|08 - NTeri.flac Habib Koite & Bamada|Afriki|01 - Namania.flac Habib Koite & Bamada|Afriki|07 - Barra.flac Habib Koite & Bamada|Afriki|playlist.m3u Habib Koite & Bamada|Afriki|04 - Fimani.flac Habib Koite & Bamada|Afriki|10 - Massake.flac Habib Koite & Bamada|Afriki|11 - Titati.flac Habib Koite & Bamada|Afriki|03 – Africa.flac [...] Richard Crandell|Spring Steel|04-Japanese Lullaby [Richard Crandell].flac Richard Crandell|Spring Steel|Spring Steel.pdf Richard Crandell|Spring Steel|03-Zen Dagger [Richard Crandell].flac Richard Crandell|Spring Steel|cover.jpg $ 在性能方面：$ time groovy TagAnalyzer.groovy | wc -l 9870 real 0m1.482s user 0m4.392s sys 0m0.230s $ 又好又快。它在一秒半内处理近 10,000 个文件！对我来说足够快。可观的性能、紧凑且可读的代码，还有什么不喜欢的？在我的下一篇文章中，我会打开 JAudiotagger 并查看每个文件中的标签。via: https://opensource.com/article/22/8/groovy-script-java-music作者：Chris Hermansen 选题：lkxed 译者：geekpi 校对：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中国 荣誉推出
学习如何使用 apt 命令在基于 Debian 的 Linux 发行版上安装软件包，然后下载我们的速查表，让正确的命令触手可及。包管理器 可帮助你处理 Linux 系统的计算机上软件的更新、卸载、故障排除等问题。Seth Kenlon 写了 使用 dnf 进行 Linux 包管理 一文，介绍了如何使用 dnf 这款命令行包管理工具，在 RHEL、CentOS、Fedora、Mageia、OpenMandriva 等 Linux 发行版中安装软件。Debian 和基于 Debian 的发行版（例如 MX Linux、Deepin、Ubuntu）以及基于 Ubuntu 的发行版（例如 Linux Mint 和 Pop!\_OS）都有 apt，这是一个“相似但不同”的工具。在本文中，我将按照 Seth 的示例（但使用 apt）向你展示如何使用它。在一开始，我想先提一下四个跟 apt 相关的软件安装工具：Synaptic 是为 apt 服务的一个基于 GTK+ 的图形用户界面（GUI）的前端工具。Aptitude 是为 apt 服务的一个基于 Ncurses 的全屏命令行前端工具。apt 的前身有 apt-get、apt-cache 等工具。Dpkg 是在 apt 包管理器背后处理繁杂事务的”幕后工作者“。还有其他的包管理系统，例如 Flatpak 和 Snap，你可能会在 Debian 和基于 Debian 的系统上遇到它们，但我不打算在这里讨论。还有一些应用程序“商店”，例如 GNOME “软件”，与 apt 和其他打包技术重叠；我也不打算在这里讨论它们。最后，还有其他 Linux 发行版，例如 Arch 和 Gentoo 既不使用 dnf 也不使用 apt，我也不打算在这里讨论它们！上面我讲了这么多我不想提及的内容，你可能怀疑 apt 到底还能处理多少软件。这么说吧，在我的 Ubuntu 20.04 上，apt 可以让我使用 69,371 个软件包，从 0ad（一款古代战争题材的即时战略游戏）到 zzuf（一个透明的应用程序模糊测试工具），一点也不差。使用 apt 搜索软件使用 apt 软件包管理器的第一步是找到感兴趣的软件包。Seth 的 dnf 文章以 Cockpit 服务器管理应用程序为例。用 apt 我会输入如下命令：$ apt search cockpit Sorting... Done Full Text Search... Done 389-ds/hirsute,hirsute 1.4.4.11-1 all 389 Directory Server suite - metapackage cockpit/hirsute,hirsute 238-1 all Web Console for Linux servers ... $ 上面的第二个包就是你要的那个（以 cockpit/hirsute 开头的那一行）。如果你决定要安装它，输入：$ sudo apt install cockpit apt 将负责安装 Cockpit 以及使其工作所需的所有部件或 依赖。有时我们不太确定这是我们所需要的。了解更多的信息可能有助于你决定是否真的要安装此应用程序。包元数据要了解有关软件包的更多信息，使用 apt show 命令：$ apt show cockpit Package: cockpit Version: 238-1 Priority: optional Section: universe/admin Origin: Ubuntu Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Original-Maintainer: Utopia Maintenance Team <pkg-utopia-maintainers@lists.alioth.debian.org> Bugs: https://bugs.launchpad.net/ubuntu/+filebug Installed-Size: 88.1 kB Depends: cockpit-bridge (>= 238-1), cockpit-ws (>= 238-1), cockpit-system (>= 238-1) Recommends: cockpit-storaged (>= 238-1), cockpit-networkmanager (>= 238-1), cockpit-packagekit (>= 238-1) Suggests: cockpit-doc (>= 238-1), cockpit-pcp (>= 238-1), cockpit-machines (>= 238-1), xdg-utils Homepage: https://cockpit-project.org/ Download-Size: 21.3 kB APT-Sources: http://ca.archive.ubuntu.com/ubuntu hirsute/universe amd64 Packages Description: Web Console for Linux servers The Cockpit Web Console enables users to administer GNU/Linux servers using a web browser. . It offers network configuration, log inspection, diagnostic reports, SELinux troubleshooting, interactive command-line sessions, and more. $ 特别要注意的是 Description 字段，它会告诉你更多关于应用程序的信息。Depends 字段说明还必须安装什么，而 Recommends 则显示建议安装的其他（如果有的话）合作组件。Homepage 字段会提供一个网址，通过它你可以了解更多。哪个包提供的这个文件？有时你并不知道包名，但你知道包里一定包含着的某个文件。Seth 以 qmake-qt5 程序作为示例。使用 apt search 找不到它：$ apt search qmake-qt5 Sorting... Done Full Text Search... Done $ 但是，另一个有关联的命令 apt-file 可以用来探索包内部：$ apt-file search qmake-qt5 qt5-qmake-bin: /usr/share/man/man1/qmake-qt5.1.gz $ 这时会显示一个 qmake-qt5 的手册页。它是一个名为 qt5-qmake-bin 的包的一部分。注意，此包名称颠倒了字符串 qmake 和 qt5 的顺序。包里包含哪些文件？方便的 apt-file 命令会列出给定的包中包含哪些文件。例如：$ apt-file list cockpit cockpit: /usr/share/doc/cockpit/TODO.Debian cockpit: /usr/share/doc/cockpit/changelog.Debian.gz cockpit: /usr/share/doc/cockpit/copyright cockpit: /usr/share/man/man1/cockpit.1.gz cockpit: /usr/share/metainfo/cockpit.appdata.xml cockpit: /usr/share/pixmaps/cockpit.png $ 注意，这与 apt show 命令提供的信息不同，后者列出了包的依赖（其他必须安装的包）。移除一个应用程序你还可以使用 apt 移除软件包。例如，要移除apt-file 应用程序：$ sudo apt purge apt-file 注意必须由超级用户运行 apt 才能安装或移除应用程序。移除一个包并不会自动移除 apt 在此过程中安装的所有依赖项。不过，一点点的工作就很容易去除这些残留：$ sudo apt autoremove 认识一下 apt正如 Seth 所写的，“你对包管理器的工作方式了解得越多，在需要安装和查询应用程序时就会越容易。”即便你不是 apt 的重度使用者，当你需要在命令行中安装或删除软件包时（例如，在一台远程服务器上或遵循某些热心肠发布的操作指南时），掌握一些 apt 的知识也会很有用。在某些软件创作者仅提供了一个裸 .pkg 文件的情况下，可能还需要了解一些关于 dpkg 的知识（如上所述）。我发现 Synaptic 包管理器在我的桌面上是一个非常有用的工具，但出于各种目的，我也在少数维护的服务器上使用着 apt。下载我们的 apt 速查表 习惯该命令并尝试一些新技巧。一旦你这样做了，你可能会发现很难再使用其他任何东西。apt 速查表via: https://opensource.com/article/21/6/apt-linux作者：Chris Hermansen 选题：lujun9972 译者：hanszhao80 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
事实上，有无穷无尽的方法来为开源做贡献，其中一个简单的方法就是回答我们的投票问题。你是如何参与开源贡献的呢？我们组织了一个投票，结果如下：提交错误报告 - 67 票（35%）解答用户的问题 - 39 票（20%）写作（指南、故事、文档等） - 73 票（38%）其他 - 12 票（6%）我的第一次开源贡献可以追溯到 20 世纪 80 年代中期，当时我们的机构第一次连上了 UseNet，在那里我们发现了贡献代码，以及在其开发和支持过程中和别人分享的机会。在今天，我们有无尽的贡献开源的机会。无论是贡献代码，还是制作一个视频教程，都是贡献的一种途径。不过，我将直接跳过整个贡献代码的部分。诚然，我们中有许多写代码但不认为自己是开发者的人，他们也可以 贡献代码。但是，我更想提醒大家，还存在很多 非代码形式可以贡献开源。接下来，我会谈到其中的三种。提交错误报告有一种重要而具体的贡献形式，它可以被描述为“不要畏惧 提交一个像样的错误报告”以及 与此相关的所有后果。有时，要 提交一个像样的错误报告 是很有挑战性的。比如说：某些错误可能很难记录或描述。当计算机启动时，屏幕上可能会出现又长又复杂的信息，其中包含各种不能理解的代码。或者屏幕上可能显示有一些“异常行为”，但是却没有提供具体的错误信息。某些错误可能很难重现。它可能只发生在某些特定的硬件/软件配置上，或者它可能很少被触发，或者错误的产生场景不明确。某些错误可能与一个非常特殊的开发环境配置有关，但是这个配置庞杂混乱，无法分享，需要先耗费大量精力创建一个精简后的例子才行。当向发行版报告一个错误时，维护者可能会建议将该错误提交给上游，这有时会需要付出大量的工作，因为发行版所提供的版本不是上游社区感兴趣的主要版本。（当发行版提供的版本落后于官方支持的发布和开发版本时，就会有这种情况发生）。尽管如此，我还是鼓励那些潜在的错误报告者（包括我）继续努力，并尝试让错误得到完整的记录和确认。但如何开始呢？你可以使用你最喜欢的搜索工具寻找类似的错误报告，看看它们是如何描述的，它们被归档在哪里，等等。你也可以留意你使用的发行版（例如，Fedora、openSUSE、Ubuntu）或软件包（LibreOffice、Mozilla）的错误报告页面，它们定义了正式的报告机制，你可以按步骤为他们报告相关错误。解答用户的问题我潜伏在各种邮件列表和 论坛 里，偶尔也会冒个泡，例如 Ubuntu 质量控制团队 和 论坛、LinuxQuestions.org，以及 ALSA 用户的邮件列表 等。在这里，我的贡献可能与错误报告的关系不大，更多的是记录复杂的用例。不过，看到有人热心帮助他人，解决他人在某个问题上的遇到的麻烦，对每个人来说，这都是无疑一种很棒的体验。从事开源相关的写作最后，另一个我非常喜欢贡献的领域是 撰写 关于使用开源软件的文章。无论是使用指南，还是对某一特定问题的不同解决方案进行比较评估，或者只是笼统地探索一个感兴趣的领域（就我而言，是使用开源音乐播放软件来享受音乐）。一个类似的选择是制作一个教学视频。你很容易就可以做到边演示一些复杂的桌面操作（比如用 GIMP 创建一个绚丽的标志），边 录制桌面。而那些精通两种或多种语言的人，也可以考虑将现有的使用指南或视频翻译成另一种语言。（LCTT 译注：读了这篇文章，你是不是想要马上投身于开源贡献呢？那么请考虑加入“Linux 中国翻译组（LCTT）”吧！我们有能帮助你快速上手翻译的 维基，有热心友爱的 QQ 群，你甚至还能够在我们的官网上获得属于自己的译者专页……心动了吗？那就立刻行动起来吧！阅读 维基 以了解如何加入我们。）via: https://opensource.com/article/19/4/contribute-without-code作者：Chris Hermansen 选题：lkxed 译者：lkxed 校对：校对者ID本文由 LCTT 原创编译，Linux中国 荣誉推出
首先在 Java 中创建初始化一个整数列表，然后在 Groovy 中做同样的事。我非常喜欢 Groovy 编程语言。我喜欢它是因为我喜欢 Java，尽管 Java 有时候感觉很笨拙。正因为我是那么喜欢 Java，其他运行在 JVM 上语言都不能吸引我。比方说 Kotlin、Scala 还有 Clojure 语言，它们感觉上就和 Java 不一样，因为它们对于什么是好的编程语言的理解不同。Groovy 和它们都不一样，在我看来，Groovy 是一个完美的选项，特别是对于一部分程序员来说，他们喜欢 Java，但是又需要一个更灵活、更紧凑，并且有时候更直接的语言。 列表 List 这种数据结构是一个很好的例子，它可以容纳一个无序的列表，列表中的元素可以是数字、字符串或者对象，程序员可以用某种方式高效地遍历这些元素，特别是对于编写和维护脚本的人来说，“高效”的关键就是要有简洁清晰的表达，而不需要一大堆“仪式”，把代码的意图都变模糊了。安装 Java 和 GroovyGroovy 是基于 Java 的，因此需要同时安装一个 Java 才行。你的 Linux 发行版的仓库中可能有最近的比较好的 Java 版本。或者，你也可以在根据 这些指导 来安装 Groovy。对于 Linux 用户来说，SDKMan 是一个不错的代替选项，你可以使用它来获取多个 Java 和 Groovy 版本，以及许多其他的相关工具。在这篇文章中，我使用的 SDK 发行版是：Java: OpenJDK 11 的 11.0.12-open 版本Groovy: 3.0.8 版本言归正传Java 中有很多方法可以实例化并初始化列表，从它最初被引入的时候就有了（我记得是在 Java 1.5 的时候，但请不要引用我的话）。在这些方法里，有两个有趣的方法，它们涉及到了 java.util.Arrays 和 java.util.List 这两个类。使用 java.util.Arrays 类java.util.Arrays 类定义了一个 asList() 静态方法，它可以被用来创建一个基于数组的列表，因此大小是不可变的，尽管其中的元素是可以被修改的。下面是它的使用方式：var a1 = Arrays.asList(1,2,3,4,5,6,7,8,9,10); // immutable list of mutable elements System.out.println("a1 = " + a1); System.out.println("a1 is an instance of " + a1.getClass()); // output is // a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // a1 is an instance of class java.util.Arrays$ArrayList a1.set(0,0); // succeeds System.out.println("a1 = " + a1); // output is // a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10] a1.add(11); // fails producing // Exception in thread "main" java.lang.UnsupportedOperationException System.out.println("a1 = " + a1); // not reached 使用 java.util.List 类java.util.List 类定义了一个 of() 静态方法，它可以被用来创建一个不可变的列表，其中的元素是否可变要取决于它们本身是否支持修改。下面是它的使用方式：var a2 = List.of(1,2,3,4,5,6,7,8,9,10); System.out.println("a2 = " + a2); System.out.println("a2 is an instance of " + a2.getClass()); // output is // a2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // a2 is an instance of class java.util.ImmutableCollections$ListN a2.set(0,0); // fails producing // Exception in thread "main" java.lang.UnsupportedOperationException System.out.println("a2 = " + a2); // not reached a2.add(11); // also fails for same reason if above two lines commented out System.out.println("a2 = " + a2); // not reached 因此，我可以使用 Arrays.asList()，也可以使用 List.of() 方法，前提是如果我想要的是一个大小不能改变、且不关心元素是否可变的列表。如果我想要初始化一个可变的列表，我更倾向于把这些不可变的列表作为参数传给一个列表构造器，就像下面这样：var a1 = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9,10)); System.out.println("a1 = " + a1); System.out.println("a1 is an instance of " + a1.getClass()); // output is // a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // a1 is an instance of class java.util.ArrayList a1.set(0,0); System.out.println("a1 = " + a1); //output is // a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10] a1.add(11); System.out.println("a1 = " + a1); // output is // a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 注意，这个 Arrays.asList() 方法是用来初始化这个新的 ArrayList<Integer>() 的，也就是说，它为这个传进来的列表创建了一个可变的拷贝。现在，或许只有我这么想，但是这种方式确实看起来需要理解很多关于 java.util.Arrays 和 java.util.List 类的细节才行，而我只是想要创建并初始化一个数字列表而已（尽管真正使用到的语句并没有太多“仪式”）。下面是真正用到的那行代码，仅供参考：var a1 = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9,10)); Groovy 是怎么做的下面来看看在 Groovy 中如何实现上述需求：def a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] println "a1 = $a1" println "a1 is an instance of ${a1.getClass()}" // output is // a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // a1 is an instance of class java.util.ArrayList a1[0] = 0 println "a1 = $a1" // output is // a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10] a1 << 11 println "a1 = $a1" // output is // a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 我们一眼就能发现，Groovy 使用了 def 关键字而不是 var 关键字。我还发现了，仅仅是把一系列的类型（在这个例子里是整数）放进括号里，我就得到了一个创建好的列表。此外，这样创建出来的列表完全就是我想要的：一个可变的 ArrayList 实例。现在，或许再一次只有我这么想，但是上面的代码看起来要简单多得多 —— 不用记住 .of() 和 .asList() 返回的是“ 半不变 semi-mutable ”的结果，也不用为它们做一些补偿。另外一个好处是，我现在可以使用括号和下标来引用列表中的某个特定元素，而不用这个叫 set() 方法。另外，这个跟在列表后面的 << 操作符也很方便，我再也不用调用 add() 方法来添加元素啦。还有，你注意到代码中没有分号了吗？没错，在 Groovy 里，句末的分号并不是必须的。最后，我们来看看字符串插值，只要在字符串里用 $变量 或者 ${表达式} 就可以实现了哦！在 Groovy 世界中还藏着许多“有待发掘”的东西。上面的列表定义其实是一个动态类型（Groovy 中默认）和 Java 中的静态类型的对比。在上面的 Groovy 代码定义的那一行，变量 a1 的类型是在运行的时候，根据等号右边的表达式的计算结果推断出来的。现在我们都知道，动态语言可以给我们带来强大的功能，有了强大的功能，我们有了很多机会去尝试不同的东西。对于那些不喜欢动态类型的程序员来说，Groovy 也支持静态类型。Groovy 相关资源Apache Groovy 网站上有非常多的文档。另一个很棒的 Groovy 资源是 Mr. Haki。学习 Groovy 还有一个很棒的原因，那就是可以接着学习 Grails，后者是一个优秀的、高效率的全栈 Web 框架，基于许多优秀组件构建而成，比如有 Hibernate、Spring Boot 和 Micronaut 等。via: https://opensource.com/article/22/1/creating-lists-groovy-java作者：Chris Hermansen 选题：lujun9972 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Java 和 Groovy 中的 映射 map 都是非常通用的，它允许 关键字 key 和 值 value 为任意类型，只要继承了 Object 类即可。我最近在探索 Java 与 Groovy 在 创建并初始化 列表 List 和 在运行时构建 列表 List 方面的一些差异。我观察到，就实现这些功能而言，Groovy 的简洁和 Java 的繁复形成了鲜明对比。在这篇文章中，我将实现在 Java 和 Groovy 中创建并初始化 映射 Map 。映射为开发支持根据 关键字 key 检索的结构提供了可能，如果找到了这样一个关键字，它就会返回对应的 值 value 。今天，很多编程语言都实现了映射，其中包括 Java 和 Groovy，也包括了 Python（它将映射称为 字典 dict ）、Perl、awk 以及许多其他语言。另一个经常被用来描述映射的术语是 关联数组 associative array ，你可以在 这篇维基百科文章 中了解更多。Java 和 Groovy 中的映射都是非常通用的，它允许关键字和值为任意类型，只要继承了 Object 类即可。安装 Java 和 GroovyGroovy 基于 Java，因此你需要先安装 Java。你的 Linux 发行版的仓库中可能有最近的比较好的 Java 和 Groovy 版本。或者，你也可以在根据上面链接中的指示来安装 Groovy。对于 Linux 用户来说，SDKMan 是一个不错的代替选项，你可以使用它来获取多个 Java 和 Groovy 版本，以及许多其他的相关工具。在这篇文章中，我使用的 SDK 发行版是：Java: version 11.0.12-open of OpenJDK 11;Groovy: version 3.0.8.言归正传Java 提供了非常多的方式来实例化和初始化映射，并且从 Java 9 之后，添加了一些新的方式。其中最明显的方式就是使用 java.util.Map.of() 这个静态方法，下面介绍如何使用它：var m1 = Map.of( "AF", "Afghanistan", "AX", "Åland Islands", "AL", "Albania", "DZ", "Algeria", "AS", "American Samoa", "AD", "Andorra", "AO", "Angola", "AI", "Anguilla", "AQ", "Antarctica"); System.out.println("m1 = " + m1); System.out.println("m1 is an instance of " + m1.getClass()); 事实证明，在此种情况下，Map.of() 有两个重要的限制。其一，这样创建出来的映射实例是 不可变的 immutable 。其二，你最多只能提供 20 个参数，用来表示 10 个 键值对 key-value pair 。你可以尝试着添加第 10 对和第 11 对，比方说 "AG", "Antigua and Barbuda" 和 "AR", "Argentina"，然后观察会发生什么。你将发现 Java 编译器尝试寻找一个支持 11 个键值对的 Map.of() 方法而遭遇失败。快速查看 java.util.Map 类的文档，你就会找到上述第二个限制的原因，以及解决这个难题的一种方式：var m2 = Map.ofEntries( Map.entry("AF", "Afghanistan"), Map.entry("AX", "Åland Islands"), Map.entry("AL", "Albania"), Map.entry("DZ", "Algeria"), Map.entry("AS", "American Samoa"), Map.entry("AD", "Andorra"), Map.entry("AO", "Angola"), Map.entry("AI", "Anguilla"), Map.entry("AQ", "Antarctica"), Map.entry("AG", "Antigua and Barbuda"), Map.entry("AR", "Argentina"), Map.entry("AM", "Armenia"), Map.entry("AW", "Aruba"), Map.entry("AU", "Australia"), Map.entry("AT", "Austria"), Map.entry("AZ", "Azerbaijan"), Map.entry("BS", "Bahamas"), Map.entry("BH", "Bahrain"), Map.entry("BD", "Bangladesh"), Map.entry("BB", "Barbados") ); System.out.println("m2 = " + m2); System.out.println("m2 is an instance of " + m2.getClass()); 这就是一个比较好的解决方式，前提是我不在随后的代码里改变使用 Map.ofEntries() 创建并初始化的映射内容。注意，我在上面使用了 Map.ofEntries() 来代替 Map.of()。然而，假设我想要创建并初始化一个非空的映射，随后往这个映射中添加数据，我需要这样做：var m3 = new HashMap<String,String>(Map.ofEntries( Map.entry("AF", "Afghanistan"), Map.entry("AX", "Åland Islands"), Map.entry("AL", "Albania"), Map.entry("DZ", "Algeria"), Map.entry("AS", "American Samoa"), Map.entry("AD", "Andorra"), Map.entry("AO", "Angola"), Map.entry("AI", "Anguilla"), Map.entry("AQ", "Antarctica"), Map.entry("AG", "Antigua and Barbuda"), Map.entry("AR", "Argentina"), Map.entry("AM", "Armenia"), Map.entry("AW", "Aruba"), Map.entry("AU", "Australia"), Map.entry("AT", "Austria"), Map.entry("AZ", "Azerbaijan"), Map.entry("BS", "Bahamas"), Map.entry("BH", "Bahrain"), Map.entry("BD", "Bangladesh"), Map.entry("BB", "Barbados") )); System.out.println("m3 = " + m3); System.out.println("m3 is an instance of " + m3.getClass()); m3.put("BY", "Belarus"); System.out.println("BY: " + m3.get("BY")); 这里，我把使用 Map.ofEntries() 创建出来的不可变映射作为 HashMap 的一个构造参数，以此创建了该映射的一个 可变副本 mutable copy ，之后我就可以修改它 —— 比如使用 put() 方法。让我们来看看上述过程如何用 Groovy 来实现：def m1 = [ "AF": "Afghanistan", "AX": "Åland Islands", "AL": "Albania", "DZ": "Algeria", "AS": "American Samoa", "AD": "Andorra", "AO": "Angola", "AI": "Anguilla", "AQ": "Antarctica", "AG": "Antigua and Barbuda", "AR": "Argentina", "AM": "Armenia", "AW": "Aruba", "AU": "Australia", "AT": "Austria", "AZ": "Azerbaijan", "BS": "Bahamas", "BH": "Bahrain", "BD": "Bangladesh", "BB": "Barbados"] println "m1 = $m1" println "m1 is an instance of ${m1.getClass()}" m1["BY"] = "Belarus" println "m1 = $m1" 只看一眼，你就会发现 Groovy 使用了 def 关键字而不是 var —— 尽管在 最近模型 late-model 的 Groovy（version 3+）中，使用 var 关键字也是可行的。 你还会发现，你是通过在括号里添加了一个键值对列表来创建一个映射的。不仅如此，这样创建的列表对象还非常有用，这里有几个原因。其一，它是可变的；其二，它是一个 LinkedHashMap 的实例，内部维持了数据的插入顺序。所以，当你运行 Java 版本的代码并打印出变量 m3，你会看到：m3 = {BB=Barbados, BD=Bangladesh, AD=Andorra, AF=Afghanistan, AG=Antigua and Barbuda, BH=Bahrain, AI=Anguilla, AL=Albania, AM=Armenia, AO=Angola, AQ=Antarctica, BS=Bahamas, AR=Argentina, AS=American Samoa, AT=Austria, AU=Australia, DZ=Algeria, AW=Aruba, AX=Åland Islands, AZ=Azerbaijan} 而当你运行 Groovy 版本的代码，你会看到：m1 = [AF:Afghanistan, AX:Åland Islands, AL:Albania, DZ:Algeria, AS:American Samoa, AD:Andorra, AO:Angola, AI:Anguilla, AQ:Antarctica, AG:Antigua and Barbuda, AR:Argentina, AM:Armenia, AW:Aruba, AU:Australia, AT:Austria, AZ:Azerbaijan, BS:Bahamas, BH:Bahrain, BD:Bangladesh, BB:Barbados] 再一次，你将看到 Groovy 是如何简化事情的。这样的语法非常直观，有点像 Python 里的字典，并且，即使你有一个超过 10 个键值对的初始列表，你也不需要去记住各种必要的别扭方式。注意我们使用的表达式：m1[“BY”] = “Belarus” 而在 Java 中，你需要这样做：m1.put(“BY”, “Belarus”) 还有，这个映射默认是可变的，这么做的利弊很难评判，还是得取决于你的需求是什么。我个人觉得，Java 在这种情况下的 “默认不可变” 机制，最让我困扰的地方是，它没有一个类似于 Map.mutableOfMutableEntries() 的方法。这迫使一些刚学会如何声明和初始化一个映射的程序员，不得不转念去思考该如何把他们手中不可变的映射，转换为可变的。同时我也想问，创建一个不可变的对象然后再舍弃它，这样真的好吗？另一个值得考虑的事情是，Groovy 使用方括号代替 Java 中的 put() 和 get() 方法来进行关键字查找。因此你可以这样写：m1[“ZZ”] = m1[“BY”] 而不需要这样写：m1.put(“ZZ”,m1.get(“BY”)) 有时候，就像使用某个类的实例变量一样来使用映射中的关键字和值是一个好办法。设想你现在有一堆想要设置的属性，在 Groovy 中，它们看起来就像下面这样：def properties = [ verbose: true, debug: false, logging: false] 然后，你可以改变其中的某个属性，就像下面这样：properties.verbose = false 之所以这样能工作，是因为，只要关键字符合特定的规则，你就可以省略引号，然后直接用点操作符来代替方括号。尽管这个功能非常有用，也非常好用，它也同时也意味着，如果你要把一个变量作为一个映射的关键字来使用，你就必须把这个变量包裹在圆括号里，就像下面这样：def myMap = [(k1): v1, (k2): v2] 是时候告诉勤奋的读者 Groovy 是一门为编写脚本而量身定制的语言了。映射通常是脚本中的关键元素，它为脚本提供了 查找表 lookup table ，并且通常起到了作为内存数据库的作用。我在这里使用的例子是 ISO 3166 规定的两个字母的国家代码和国家名称。对在世界上各个国家的互联网使用者来说，这些代码是很熟悉的。此外，假设我们要编写一个从日志文件中查找互联网主机名，并借此来了解用户的地理位置分布的脚本工具，那么这些代码会是十分有用的部分。Groovy 相关资源Apache Groovy 网站 上有非常多的文档。另一个很棒的 Groovy 资源是 Mr. Haki。Baeldung 网站 提供了大量 Java 和 Groovy 的有用教程。学习 Groovy 还有一个很棒的原因，那就是可以接着学习 Grails，后者是一个优秀的、高效率的全栈 Web 框架。它基于许多优秀组件构建而成，比如有 Hibernate、Spring Boot 和 Micronaut 等。via: https://opensource.com/article/22/3/maps-groovy-vs-java作者：Chris Hermansen 选题：lujun9972 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
无论你喜欢哪个发行版和包管理器，都可以都很容易地在你的 Linux 系统上安装 Java。把 Java 安装到你的 Linux 桌面上有多种方法。一个明显的方式是使用你的 Linux 发行版中提供的软件包。请注意，这并不适合所有人。例如，有些用户可能需要一个非常具体的 Java 版本。在你开始之前，你必须确定你需要“哪种 Java”。你是否只需要运行一些 .class 文件或 .jar 文件？还是你正在编写一些需要编译的代码？就我而言，我运行的大部分 Java 都是我自己（至少部分）编写的 Java，所以安装完整的 Java 开发工具包（或称 JDK）总是有意义的，它包含了 Java 编译器、库和一些非常有用的工具。当然，在这里，我们更倾向于使用开源的 JDK，称为 OpenJDK。由于我主要在 Ubuntu Linux 发行版上工作，我的软件包管理器是 apt。我可以用 apt 来查找哪些 OpenJDK 包是可用的：apt list OpenJDK\* 这个命令的输出看起来像这样：Listing... Done openjdk-11-dbg/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-dbg/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-demo/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-demo/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-doc/hirsute-updates,hirsute-updates,hirsute-security,hirsute-security 11.0.11+9-0ubuntu2 all openjdk-11-jdk-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-jdk-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-jdk/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-jdk/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-jre-dcevm/hirsute 11.0.10+1-1 amd64 openjdk-11-jre-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-jre-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-jre-zero/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-jre-zero/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-jre/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64 openjdk-11-jre/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386 openjdk-11-source/hirsute-updates,hirsute-updates,hirsute-security,hirsute-security 11.0.11+9-0ubuntu2 all openjdk-15-dbg/hirsute 15.0.3+3-1 amd64 openjdk-15-dbg/hirsute 15.0.3+3-1 i386 openjdk-15-demo/hirsute 15.0.3+3-1 amd64 ... openjdk-8-jre/hirsute-updates,hirsute-security 8u292-b10-0ubuntu1 i386 openjdk-8-source/hirsute-updates,hirsute-updates,hirsute-security,hirsute-security 8u292-b10-0ubuntu1 all 我在上面用 ... 省略了不少行。事实证明，即使限制在 OpenJDK 中，我仍然有很多选择：不同的架构（在我的例子中，i386 还是 amd64）。不同的 Java 版本（就我而言，有 8、11、15、16、17 等）。纯粹的 OpenJDK 或无头版本。Java 运行时环境（JRE）。用于调试、演示，以及是否包含源代码等。同样，在我的情况中，我主要对纯粹的普通 OpenJDK 感兴趣。假设我想为我的 amd64 架构安装 Java 11 版本的普通 OpenJDK，我可以输入：sudo apt install -a=amd64 openjdk-11-jdk 几分钟后，我就可以编译、运行、调试和打包我的 Java 代码了。注意，很有可能需要同时安装多个版本的 Java，有时甚至是必要的。在 Ubuntu 中，有一个有用的工具，叫做 update-java-alternatives，它可以 显示并配置在使用哪个 Java 环境。那些使用不同 Linux 发行版的人，一般来说，可以采取类似的方法。其他的几个发行版（如 Debian 和 Mint）也使用 apt ，尽管可用的软件包可能不同。发行版可能使用不同的软件包管理器。例如， Fedora 安装 Java 的文档页面 显示了如何使用 Fedora dnf 包管理器来处理安装。首先，为了显示可用的版本，输入：dnf search openjdk 接下来，要安装完整的开发 x86\_64 架构版本，请输入：sudo dnf install java-11-openjdk-devel.x86_64 同样地，Fedora 提供了 alternatives 工具来 显示和配置 Java 环境。再比如，很棒的 Arch Linux 维基 显示对应的软件包是 jdk11-openjdk。该维基还解释了许多在 Arch 中使用 Java 的其他重要细节，比如使用 archlinux-java 工具来显示安装了哪些 Java 环境或选择一个不同的默认环境。Arch 使用一个叫 pacman 的包管理器，它也有文档 在 Arch Linux 维基上。不管你喜欢哪个发行版和软件包管理器，在你的 Linux 系统上获得 Java 是很容易的。当然，在安装之前，要考虑版本和功能。还要记住，在同一台电脑上有管理两个或多个 Java 版本的方法。我的大多数例子都使用了 apt，但也要记得可以选择使用 dnf。via: https://opensource.com/article/21/9/install-java-linux-repositories作者：Chris Hermansen 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
外部库填补了 Java 核心库中的一些功能空白。Java 自带有一组核心库，其中包含了定义常用数据类型和相关行为的库（例如 String 和 Date）、与主机操作系统交互的实用程序（例如 System 和 File），以及一些用来管理安全性、处理网络通信、创建或解析 XML的有用的子系统。鉴于核心库的丰富性，程序员通常很容易在其中找到有用的组件，以减少需要编写的代码量。即便如此，核心库仍有一些功能上的不足，因此发现这些不足的程序员们还额外创建了很多有趣的 Java 库。例如，Apache Commons“是一个专注于可重用 Java 组件所有方面的 Apache 项目”，提供了大约 43 个开源库的集合（截至撰写本文时），涵盖了 Java 核心库之外的一系列功能 （例如 geometry 或 statistics），并增强或替换了 Java 核心库中的原有功能（例如 math 或 numbers）。另一种常见的 Java 库类型是系统组件的接口（例如数据库系统接口），本文会着眼于使用此类接口连接到 PostgreSQL 数据库，并得到一些有趣的信息。首先，我们来回顾一下库的重要部分。什么是库？ 库 library 里自然包含的是一些有用的代码。但为了发挥用处，代码需要以特定方式进行组织，特定的方式使 Java 程序员可以访问其中组件来解决手头问题。可以说，一个库最重要的部分是它的应用程序编程接口（API）文档。这种文档很多人都熟悉，通常是由 Javadoc 生成的。Javadoc 读取代码中的结构化注释并以 HTML 格式输出文档，通常 API 的 包 package 在页面左上角的面板中显示， 类 class 在左下角显示，同时右侧会有库、包或类级别的详细文档（具体取决于在主面板中选择的内容）。例如，Apache Commons Math 的顶级 API 文档 如下所示：单击主面板中的包会显示该包中定义的 Java 类和接口。例如，org.apache.commons.math4.analysis.solvers 显示了诸如 BisectionSolver 这样的类，该类用于使用二分算法查找单变量实函数的零点。单击 BisectionSolver 链接会列出 BisectionSolver 类的所有方法。这类文档可用作参考文档，不适合作为学习如何使用库的教程。比如，如果你知道什么是单变量实函数并查看包 org.apache.commons.math4.analysis.function，就可以试着使用该包来组合函数定义，然后使用 org.apache.commons.math4.analysis.solvers 包来查找刚刚创建的函数的零点。但如果你不知道，就可能需要更多学习向的文档，也许甚至是一个实际例子，来读懂参考文档。这种文档结构还有助于阐明 包 package （相关 Java 类和接口定义的集合）的含义，并显示特定库中捆绑了哪些包。这种库的代码通常是在 .jar 文件) 中，它基本上是由 Java 的 jar 命令创建的 .zip 文件，其中还包含一些其他有用的信息。.jar 文件通常被创建为构建过程的端点，该构建过程编译了所定义包中的所有 .java 文件。要访问外部库提供的功能，有两个主要步骤：确保通过类路径（或者命令行中的 -cp 参数或者 CLASSPATH 环境变量），库可用于 Java 编译步骤（javac）和执行步骤（java）。使用恰当的 import 语句访问程序源代码中的包和类。其余的步骤就与使用 String 等 Java核心类相同，使用库提供的类和接口定义来编写代码。很简单对吧？不过也没那么简单。首先，你需要了解库组件的预期使用模式，然后才能编写代码。示例：连接 PostgreSQL 数据库在数据库系统中访问数据的典型使用步骤是：访问正在使用的特定数据库软件代码。连接到数据库服务器。构建查询字符串。执行查询字符串。针对返回的结果，做需要的处理。断开与数据库服务器的连接。所有这些面向程序员的部分由接口包 java.sql 提供，它独立于数据库，定义了核心客户端 Java 数据库连接（JDBC）API。java.sql 包是 Java 核心库的一部分，因此无需提供 .jar 文件即可编译。但每个数据库提供者都会创建自己的 java.sql 接口实现（例如 Connection 接口），并且必须在运行步骤中提供这些实现。接下来我们使用 PostgreSQL，看看这一过程是如何进行的。访问特定数据库的代码以下代码使用 Java 类加载器（Class.forName() 调用）将 PostgreSQL 驱动程序代码加载到正在执行的虚拟机中：import java.sql.*; public class Test1 { public static void main(String args[]) { // Load the driver (jar file must be on class path) [1] try { Class.forName("org.postgresql.Driver"); System.out.println("driver loaded"); } catch (Exception e1) { System.err.println("couldn't find driver"); System.err.println(e1); System.exit(1); } // If we get here all is OK System.out.println("done."); } } 因为类加载器可能失败，失败时会抛出异常，所以将对 Class.forName() 的调用放在 try-catch 代码块中。如果你使用 javac 编译上面的代码，然后用 java 运行，会报异常：me@mymachine:~/Test$ javac Test1.java me@mymachine:~/Test$ java Test1 couldn't find driver java.lang.ClassNotFoundException: org.postgresql.Driver me@mymachine:~/Test$ 类加载器要求类路径中有包含 PostgreSQL JDBC 驱动程序实现的 .jar 文件：me@mymachine:~/Test$ java -cp ~/src/postgresql-42.2.5.jar:. Test1 driver loaded done. me@mymachine:~/Test$ 连接到数据库服务器以下代码实现了加载 JDBC 驱动程序和创建到 PostgreSQL 数据库的连接：import java.sql.*; public class Test2 { public static void main(String args[]) { // Load the driver (jar file must be on class path) [1] try { Class.forName("org.postgresql.Driver"); System.out.println("driver loaded"); } catch (Exception e1) { System.err.println("couldn't find driver"); System.err.println(e1); System.exit(1); } // Set up connection properties [2] java.util.Properties props = new java.util.Properties(); props.setProperty("user","me"); props.setProperty("password","mypassword"); String database = "jdbc:postgresql://myhost.org:5432/test"; // Open the connection to the database [3] try (Connection conn = DriverManager.getConnection(database, props)) { System.out.println("connection created"); } catch (Exception e2) { System.err.println("sql operations failed"); System.err.println(e2); System.exit(2); } System.out.println("connection closed"); // If we get here all is OK System.out.println("done."); } } 编译并运行上述代码:me@mymachine:~/Test$ javac Test2.java me@mymachine:~/Test$ java -cp ~/src/postgresql-42.2.5.jar:. Test2 driver loaded connection created connection closed done. me@mymachine:~/Test$ 关于上述的一些注意事项：注释 [2] 后面的代码使用系统属性来设置连接参数（在本例中参数为 PostgreSQL 用户名和密码）。代码也可以从 Java 命令行获取这些参数并将所有参数作为参数包传递，同时还有一些其他 Driver.getConnection() 选项可用于单独传递参数。JDBC 需要一个用于定义数据库的 URL，它在上述代码中被声明为 String database 并与连接参数一起传递给 Driver.getConnection() 方法。代码使用 try-with-resources 语句，它会在 try-catch 块中的代码完成后自动关闭连接。Stack Overflow 上对这种方法进行了长期的讨论。try-with-resources 语句提供对 Connection 实例的访问，并可以在其中执行 SQL 语句；所有错误都会被同一个 catch 语句捕获。用数据库的连接处理一些有趣的事情日常工作中，我经常需要知道为给定的数据库服务器实例定义了哪些用户，这里我使用这个 简便的 SQL 来获取所有用户的列表：import java.sql.*; public class Test3 { public static void main(String args[]) { // Load the driver (jar file must be on class path) [1] try { Class.forName("org.postgresql.Driver"); System.out.println("driver loaded"); } catch (Exception e1) { System.err.println("couldn't find driver"); System.err.println(e1); System.exit(1); } // Set up connection properties [2] java.util.Properties props = new java.util.Properties(); props.setProperty("user","me"); props.setProperty("password","mypassword"); String database = "jdbc:postgresql://myhost.org:5432/test"; // Open the connection to the database [3] try (Connection conn = DriverManager.getConnection(database, props)) { System.out.println("connection created"); // Create the SQL command string [4] String qs = "SELECT " + " u.usename AS \"User name\", " + " u.usesysid AS \"User ID\", " + " CASE " + " WHEN u.usesuper AND u.usecreatedb THEN " + " CAST('superuser, create database' AS pg_catalog.text) " + " WHEN u.usesuper THEN " + " CAST('superuser' AS pg_catalog.text) " + " WHEN u.usecreatedb THEN " + " CAST('create database' AS pg_catalog.text) " + " ELSE " + " CAST('' AS pg_catalog.text) " + " END AS \"Attributes\" " + "FROM pg_catalog.pg_user u " + "ORDER BY 1"; // Use the connection to create a statement, execute it, // analyze the results and close the result set [5] Statement stat = conn.createStatement(); ResultSet rs = stat.executeQuery(qs); System.out.println("User name;User ID;Attributes"); while (rs.next()) { System.out.println(rs.getString("User name") + ";" + rs.getLong("User ID") + ";" + rs.getString("Attributes")); } rs.close(); stat.close(); } catch (Exception e2) { System.err.println("connecting failed"); System.err.println(e2); System.exit(1); } System.out.println("connection closed"); // If we get here all is OK System.out.println("done."); } } 在上述代码中，一旦有了 Connection 实例，它就会定义一个查询字符串（上面的注释 [4]），创建一个 Statement 实例并用其来执行查询字符串，然后将其结果放入一个 ResultSet 实例。程序可以遍历该 ResultSet 实例来分析返回的结果，并以关闭 ResultSet 和 Statement 实例结束（上面的注释 [5]）。编译和执行程序会产生以下输出：me@mymachine:~/Test$ javac Test3.java me@mymachine:~/Test$ java -cp ~/src/postgresql-42.2.5.jar:. Test3 driver loaded connection created User name;User ID;Attributes fwa;16395;superuser vax;197772; mbe;290995; aca;169248; connection closed done. me@mymachine:~/Test$ 这是在一个简单的 Java 应用程序中使用 PostgreSQL JDBC 库的（非常简单的）示例。要注意的是，由于 java.sql 库的设计方式，它不需要在代码中使用像 import org.postgresql.jdbc.*; 这样的 Java 导入语句，而是使用 Java 类加载器在运行时引入 PostgreSQL 代码的方式，也正因此无需在代码编译时指定类路径。via: https://opensource.com/article/20/2/external-libraries-java作者：Chris Hermansen 选题：lujun9972 译者：unigeorge 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
我们在 Linux 的第一次尝试只是一个 apt-get 的距离。我在 Linux 的第一次尝试是那种“或许我应该试一试”的情况。那是 1990 年代，我在一些软盘上找到了用某种打包方式打包的红帽发行版，我为家里的笔记本电脑买了第二个硬盘，然后开始安装它。这是一件有趣的实验，但是我记得当时家人还没有准备好在电脑上使用 Linux。转眼到了 2005 年，我最终放弃了这种做法，买了一台可爱的东芝笔记本电脑，来运行 Windows XP。在工作中，我有一台有点年头的 SUN SPARCStation 5，并且我不太喜欢当时整个 Solaris 的发展方向（基于 Motif 的桌面）。我真的想要用 GIMP 来完成一些这样或那样的项目，但是在 Solaris 上安装 GNOME 1.x（也许是 1.4？）的曲折旅程是很有挑战性的。所以，我实际上是在考虑跳槽到 Windows XP。但是在我的家用机上用了几个月之后，我发现我更不喜欢在 Solaris 上运行 GNOME，所以我安装了 Ubuntu Hoary Hedgehog 5.04，随后在我的笔记本电脑上安装了 Breezy Badger 5.10。这太棒了，那台拥有 3.2GHz 奔腾处理器、2GB 内存和 100GB 的硬盘的机器就在我的 SPARCStation 5 旁边运行。突然之间，我不再用拼凑起来的 Solaris 安装包来试图去让东西运行起来，而只是用 apt-get 就可以了。并且这个时机也很好。我家庭和我从 2006 年 8 月到 2007 年 7 月居住在法国格勒诺布尔，当时我的妻子在休假。因为有了运行 Linux 的东芝笔记本，我可以带着我的工作一起走。那个时候我在几个大的项目上做了大量的 GIS 数据处理，我发现我可以在 PostGIS / PostgreSQL 上做同样的事情，比我们在加拿大家中使用的昂贵得多的商业 GIS 软件要快得多。大家都很开心，尤其是我。这一路上发生的有趣的事情是，我们把另外两台电脑带到了法国 —— 我妻子的类似的东芝电脑（运行 XP，对她来说很好用）和我们孩子最近新买的东芝牌笔记本电脑，也运行 XP。也就在圣诞节过后，他们有一些朋友过来，无意中在他们的电脑上安装了一个讨厌的、无法清除的病毒。经过几个小时甚至几天后，我的一个孩子问我：“爸爸，我们就不能安装和你电脑上一样的东西吗？”然后，三个新的 Linux 用户就这样产生了。我的儿子，29 岁了，依然是一个快乐的 Linux 用户，我猜他有第四或第五台 Linux 笔记本电脑了，最后几台都是由 System 76 提供的。我的一个女儿三年前开始读法学院时被迫转换为 Windows，因为她所在的学校有一个强制性的测试框架，只能在 Windows 上运行，而且据称会检测虚拟机之类的东西（请不要让我开始骂人）。而我的另一个女儿被她的公司为她买的 Macbook Air 诱惑了。哦，好吧，不可能全都赢了吧！via: https://opensource.com/article/21/5/my-linux-story作者：Chris Hermansen 选题：lujun9972 译者：shiboi77 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出