AWK 作者: truesnow 发布于: 5个月前 收录于: Linux 命令 354 0 摘要:AWK 教程 [TOC] > 原文:[AWK](http://linuxcommand.org/lc3_adv_awk.php) shell 最伟大的地方在于它支持其他编程语言,例如 `sed` 和 `bc`。现在,我们将学习 `awk`。 # 历史 AWK 由贝尔实验室的三位科学家在 1970 年代研发,其名字为三人姓氏的缩写。1985 年改进了一个版本,称为 `nawk`。就是我们今天使用的版本,但是通常我们称之为 `awk`。 # 可用性 有两个开源版本的 awk 常被使用,一个是 `mawk`(Mike's awk),另一个是 `gawk`(GNU awk)。两个都是在 1985 年的 `nawk` 基础上添加了一些扩展。本课程使用任一版本都行,因为我们主要讲的是 `nawk` 的功能。在大部分发行版中,都将 `awk` 软链接到 `mawk` 或 `gawk`。 # 那么,awk 有什么好处? awk 设计的目的是创建 **过滤器**,程序接收标准输入,转换数据,将数据输出到标准输出。awk 尤其擅长处理 **列数据**。和 `sed` 一样,很多 `awk` 程序只有一行。 最近几年,awk 变得不流行了,被更新的解释语言如 Perl 和 Python 取代了,但是 awk 仍然有其优势: - 容易学习。语言不复杂,语法和 C 类似,对以后学习其他语言和工具有帮助。 - 很擅长解决特定类型的问题。 # awk 如何工作 awk 程序由一系列的一个或多个 **模式**(pattern) 和 **动作**(action) 对组成。 awk 每次读取一条记录。默认情况下,一条记录为以换行符结束的一行。每当读取一条记录,awk 自动将其拆分为 **域**(fields)。默认情况下,域以空格为分隔符。每个域都会赋值给一个变量,并给定一个数字名称。`$1` 是第一个域,`$2` 是第二个域,以此类推。`$0` 表示整条记录。另外,名为 `NF` 的变量的值为记录中检测到的域的总数量。 模式/动作对每条记录进行测试并执行相应的动作。如果模式为 true,动作就会执行。当模式列表执行完毕,awk 程序读取下一条记录并重复处理。 一个简单示例,对 `ls` 命令进行过滤: ```sh ls -l /usr/bin | awk '{print $0}' ``` awk 程序由 `awk` 命令和单引号组成。单引号很重要,因为我们不想 shell 尝试识别扩展符。因为 awk 语法与 shell 完全无关。例如,`$0` 代表 awk 从标准输入读取到的整个记录。在 awk 中,`$` 表示「域」,而不是 shell 中的变量扩展。 示例中只有一个动作,没有模式。这是允许的,这表示每条记录都与模式匹配。以上命令像 `cat` 命令一样简单地输出每一行输入。 如果我们观察下 `ls -l` 命令的输出,我们可以看到其由 9 个域组成,通过一个或多个空格符分隔: ``` -rwxr-xr-x 1 root root 265 Apr 17 2012 zxpdf ``` 让我们加入一个模式,以使其输出超过 9 个域的行: ```sh ls -l /usr/bin | awk 'NF > 9 {print $0}' ``` 现在我们可以看到 */usr/bin* 目录下的符号链接列表,因为这些目录列表包含超过 9 个域。此模式也会匹配到文件名中含有空格符的文件,因为此类文件包含超过 9 个域。 # 特殊模式 awk 中的模式可以有多种形式。例如上面示例中的条件表达式。有两个特殊的模式称为 `BEGIN` 和 `END`。`BEGIN` 表达式在读取第一条记录之前将其对应的动作执行。这对初始化变量,或在输出前打印页眉很有用。同样地,`END` 模式在读取最后一条记录后执行对应的动作。这在处理完输入后输出总结时很有用。 以一个例子来说明。假设目录下没有一个文件名中含空格符的文件,我们可以用以下脚本来列出符号链接: ```sh #!/bin/bash # Print a directory report ls -l /usr/bin | awk ' BEGIN { print "Directory Report" print "================" } NF > 9 { print $9, "is a symbolic link to", $NF } END { print "=============" print "End Of Report" } ' ``` 如示例中所示,动作可以分多行写,但是 `{` 必须跟在模式后面,与模式同一行。注意第二个模式中 `$NF` 表示第 NF 个域的值,而不是 NF 本身的值。 # 调用方式 运行 awk 程序的方法有很多种。前面的示例 awk 是嵌入在 shell 中执行的,awk 程序在单引号中。第二种方式是将 awk 脚本放到其自己的文件中,并使用 awk 程序调用,例如: ```sh awk -f program_file ``` 最后,我们可以使用 shebang 让 awk 脚本成为独立的程序,就像 shell 脚本一样: ```sh #!/usr/bin/awk -f # Print a directory report BEGIN { print "Directory Report" print "================" } NF > 9 { print $9, "is a symbolic link to", $NF } END { print "=============" print "End Of Report" } ``` # 语言 下面介绍 awk 程序的功能和语法。 ## 程序格式 awk 程序的格式规则很简单。 - 动作由一个或多个由中括号(`{}`)包裹的声明组成,起始中括号 `{` 需和模式在同一行。 - 空白行会被忽略。 - 注释以井号(`#`)开头,并且可以出现在任意行的末尾。 - 声明如果很长,可以使用反斜杠(`\`)写成多行。 - 变量使用逗号(`,`)分隔。 示例: ```sh BEGIN { # 动作的起始中括号必须和模式在同一行 # 空白行会被忽略 # 反斜杠可以用于分隔很长的行 print \ $1, # 参数列表必须以逗号分隔 $2, # 注释可以出现在任意行的末尾 $3 # 多条声明使用分号分隔时可以出现在同一行 print "String 1"; print "String 2" } # 动作的关闭中括号 ``` ## 模式 以下是 awk 中最常见的模式类型: ### BEGIN 和 END 之前提到过,`BEGIN` 和 `END` 模式分别在读取第一条记录之前执行动作和在读取最后一条记录之后执行动作。 ### 关系表达式 关系表达式用于测试值。例如,测试相等: ``` $1 == "Fedora" ``` 或者关系: ``` $3 >= 50 ``` 也可以执行算术: ``` $1 * $2 < 100 ``` ### /正则表达式/ awk 支持扩展正则表达式,就像 `egrep` 支持的那样。模式使用正则表达式可以有两种方式。第一种,对整条记录应用由斜杠包裹的正则表达式。如果需要进行内部控制,我们可以给表达式提供正则表达式进行匹配,语法如下: ``` expression ~ /regexp/ ``` 例如,如果我们只想对记录的第三个域匹配,我们可以: ``` $3 ~ /^[567]/ ``` 这里,我们可以认为 `~` 表示「匹配」或「包含」,因此我们可以将上面读为「第三个域匹配正则表达式」`^[567]`」。 ### 逻辑运算符模式 逻辑运算符 `||` 和 `&&` 分别表示「或」和「与」。例如,想测试一条记录的第一个域其值大于 100,且最后一个域为单词 Debit,可以写成: ``` $1 > 100 && $NF == "Debit" ``` ### ! 模式 非模式表示模式的否定,可以选定所有不满足模式条件的记录。 ### 范围模式:模式, 模式 两个模式中间有逗号分隔的模式叫 **范围模式**。表示从第一行匹配到第一个模式的记录开始,继续匹配余下所有在模式一和模式二范围之间的记录。 示例: ``` $1 == "0050", $1 == "0100" ``` 假设有一个文件 *test.txt* 内容如下: ``` 0008 field field field 0003 field field field 0001 field field field 0002 field field field 0003 field field field 0001 field field field ``` 对其执行以下命令: ```sh cat test.txt | awk '$1 == "0001", $2 == "0003" {print $0}' ``` 得到结果: ``` 0001 field field field 0002 field field field 0003 field field field 0001 field field field ``` # 域和记录 awk 默认使用换行符为记录分隔符,使用空格作为域分隔符,这是可以调整的。例如,*/etc/passwrd* 文件是使用冒号(`:`)作为域分隔符的。awk 有一个名为 `FS`(field seperator)的内置变量,定义了记录中的域分隔符。以下程序可以打印系统中的用户 ID 和用户名: ``` BEGIN { FS = ":" } { print $1, $5 } ``` `FS` 变量可以是正则表达式,用此方法分隔域很强大。 awk 内置变量 `RS`(record separator)定义了记录分隔符。一些常见的文件通常一条记录里会包含一个或多个空行。awk 中可以通过以下特殊的设置实现: ``` RS = "" ``` 注意,当如此设置之后,换行符,除了其他特殊字符之外,总会视作域分隔符,无论 `FS` 变量如何设置。通常这种情况下,我们希望每一行都是一个域,所以可以设置如下: ``` BEGIN { FS = "\n"; RS = "" } ``` # 变量和数据类型 awk 根据上下文将数据视为字符串或数字。awk 总是将数字看作是字符串,除非指定其为 `numeric`。 我们可以通过在字符串上进行算术运算来使其强制转换为数字。最简单的方法就是与 0 相加: ``` n = 105 + 0 ``` 同理,我们可以通过与空字符串拼接,将数字转换为字符串: ``` s = 105 "" ``` awk 中使用空格作为字符串连接符 - 一个不常见的语言功能。 awk 不需要事先声明变量。awk 的变量命名规则同 shell 一致。变量名由字母、数字和下划线组成。和 shell 一样,第一个字符不能是字符串。变量名是大小写敏感的。 ## 内置变量 前面已经提到过一些 awk 内置变量。以下是最有用的一些: ### FS - Field seperator 域分隔符 该变量包含一个正则表达式来将一条记录分隔为域。默认为空格。 `FS` 的值也可以通过 `-F` 选项进行设置。例如,我们可以从 */etc/passwd* 文件快速解析用户名和 UID 字段如下: ```sh awk -F: '{print $1, $3}' /etc/passwd ``` ### NF - 域数量 每当一条记录被读取时,该变量值就会更新。我们可以通过 `$NF` 来访问最后一个域。 ### NR - 记录数 每当读取一条记录该变量值加一,因此它包含了输入流的总记录数。使用该变量,我们可以模拟 `wc -l` 命令如下: ``` awk 'END {print NR}' ``` 或者对文件中的行编号: ``` awk '{print NR, $0}' ``` ### OFS - Output Field Seperator 输出域分隔符 该字符串用于打印输出时分隔域。默认为一个空格。在格式化数据时设置该变量很方便。例如,我们可以通过设置 `OFS` 为 `,` 将一个值表转换为一个 CSV(逗号分隔的值)文件。为了演示,以下程序读取你的目录列表并将其输出为 CSV 流: ```sh ls -l | awk 'BEGIN {OFS = ","} NF == 9 {print $1,$2,$3,$4,$5,$6,$7,$8,$9}' ``` 我们将模式设置为只匹配有 9 个域的输入行。这将排除链接符合其他奇怪的文件名,避免其被处理。 每行的结果输出都将组装成如下格式: ``` -rwxr-xr-x,1,root,root,100984,Jan,11,2015,a2p ``` 如果我们省略设置 `OFS`,`print` 声明将使用默认值(一个空格): ```sh ls -l | awk 'NF == 9 {print $1,$2,$3,$4,$5,$6,$7,$8,$9}' ``` 此时输出将组装成类似: ``` -rwxr-xr-x 1 root root 100984 Jan 11 2015 a2p ``` ### ORS - Output Record Seperator 输出记录分隔符 该字符串用于打印输出时分隔记录。默认值为一个换行符。我们可以使用通过将该参数设置为两个换行符来使文件变为双倍行距: ``` ls -l | awk 'BEGIN {ORS = "\n\n"} {print}' ``` ### RS - Record Seperator 记录分隔符 当读取输入时,awk 将此字符串视作一个记录的结尾标志。默认值是一个换行符。 ### FILENAME - 文件名 如果 awk 从命令行中指定的文件中读取输入,那么该变量值为该文件的文件名。 ### FNR - File Record Number 文件记录数 如果从命令行中指定的文件名读取输入,awk 将设置该变量的值为该文件的记录数。 ## 数组 awk 支持一位数组。其值既可以是数字也可以是字符串。数组索引也可以是数字或字符串(关联数组)。 对数组元素赋值语法如下: ``` a[1] = 5 # Numeric index a["five"] = 5 # String index ``` 虽然 awk 像 bash 一样支持一维数组,但是它也提供了类多维数组的机制。格式如下: ``` a[j,k] = "foo" ``` `mawk` 和 `gawk` 实现了多维数组,参考它们的文档。 删除数组及数组元素: ``` delete a[i] # delete a single element delete a # delete array ``` # 算术和逻辑表达式 awk 支持完整的算术和逻辑操作符: ## 操作符 ``` 赋值 = += -= *= /= %= ^= ++ -- 关系 < > <= >= == != 算术 + - * / % ^ 匹配 ~ !~ 数组 in 逻辑 && || ``` 这些操作符和 shell 中的一样,然而,和 shell 只能做整数运算不一样的是,awk 是浮点运算。 算术和逻辑表达式在模式和动作中都可以使用。以下是统计有 9 个域的行数的示例: ```sh ls -l /usr/bin | awk 'NF == 9 {count++} END {print count}' ``` 统计列表中文件的大小示例: ```sh ls -l /usr/bin | awk 'NF >=9 {total += $5} END {print total}' ``` 计算文件平均大小示例: ```sh ls -l /usr/bin | awk 'NF >=9 {c++; t += $5} END {print t / c}' ``` # 流控制 单条语句: ``` a = a + 1 ``` 多条语句: ``` {a = a + 1; b = b * a} ``` ## if ( expression ) 声明和 if ( expression ) 声明 else 声明 `if/then/else` 结构示例: ```sh awk 'BEGIN {if (1) print "true"; else print "false"}' awk 'BEGIN {if (0) print "true"; else print "false"}' ``` 以下示例实现分页生成原始报告的功能: ```sh ls -l /usr/bin | awk ' BEGIN { line_count = 0 page_length = 60 } { line_count++ if (line_count < page_length) print else { print "\f" $0 line_count = 0 } } ' ``` 更简单的实现: ```sh ls -l /usr/bin | awk ' BEGIN { page_length = 60 } { if (NR % page_length) print else print "\f" $0 } ' ``` 我们通过对 NR 与每页行数取余的方式进行处理,每隔 60 行添加一个分页符。 awk 支持元素是否在数组中的表达式: ``` (var in array) ``` var 是索引值,array 是数组变量。该表达式检查索引 var 是否在指定数组 array 中。此种检查方式可以避免无意间创建数组元素: ``` if (array[var] != "") ``` 像上面这样会创建一个 var 为索引的数组元素,因为 awk 在变量被使用的时候创建。使用 `(var in array)` 这种格式就不会有变量创建。 多维数组的语法: ``` ((var1,var2) in array) ``` ## for ( expression ; expression ; expression ) 声明 示例:反序输出域 ```sh ls -l | awk '{s = ""; for (i = NF; i > 0; i--) s = s $i OFS; print s}' ``` ## for ( var in array ) 声明 遍历数组索引示例: ```sh awk 'BEGIN {for (i=0; i<10; i++) a[i]="foo"; for (i in a) print i}' ``` 注意数组索引的是变化无序的。 删除数组中所有元素示例: ``` for (i in a) delete a[i] ``` ## while ( expression ) 声明和 do statement while ( expression ) 倒序输出域的 while 实现示例: ```sh ls -l | awk '{ s = "" i = NF while (i > 0) { s = s $i OFS i-- } print s }' ``` ## break、continue、next ## exit 表达式 和 shell 一样,退出 awk,并设置一个退出状态。 # 正则表达式 正则表达式经常使用在模式中,也可以使用在内置变量如 `FS` 和 `RS` 中。 以下示例将会得到目录列表中各文件类型的数量(同时使用了关联数组): ``` ls -l /usr/bin | awk ' $1 ~ /^-/ {t["Regular Files"]++} $1 ~ /^d/ {t["Directories"]++} $1 ~ /^l/ {t["Symbolic Links"]++} END {for (i in t) print i ":\t" t[i]} ' ``` # 输出函数 ## print expr1, expr2, expr3,... `print` 的参数为逗号分隔的表达式列表。 逗号很重要,awk 输出时会使用输出域分隔符 `OFS` 分隔输出项。如果省略了逗号,那么 awk 会将这些参数作为字符串拼接输出。 ## printf(format, expr1, expr2, expr3,...) 在 awk 中,`printf` 像 shell 内置命令一样。基于一个**格式字符串**对参数列表进行格式化。以下示例输出文件列表及其大小(以 KB 为单位): ```sh ls -l /usr/bin | awk '{printf("%-30s%8.2fK\n", $9, $5 / 1024)}' ``` # 写入到文件和管道 除了将结果输出到标准输出,也可以将输出发送给文件或管道: ```sh ls -l /usr/bin | awk ' $1 ~ /^-/ {print $0 > "regfiles.txt"} $1 ~ /^d/ {print $0 > "directories.txt"} $1 ~ /^l/ {print $0 > "symlinks.txt"} ' ``` 上面的程序将普通文件、目录和符号链接输入到不同的文件中。 awk 也提供了 `>>` 符追加内容到文件。awk 在命令执行时会打开文件,`>` 会使 awk 打开文件并将内容清空到长度为 0。 awk 也支持将输出发送到管道。以下程序读取目录到一个数组中,并将数组输出: ```sh ls -l /usr/bin | awk ' $1 ~ /^-/ {a[$9] = $5} END {for (i in a) {print a[i] "\t" i} } ' ``` 运行此程序会发现,其输出是无序的。要纠正这一点,我们可以将其输出通过管道传给 `sort`: ```sh ls -l /usr/bin | awk ' $1 ~ /^-/ {a[$9] = $5} END {for (i in a) {print a[i] "\t" i | "sort -nr"} } ' ``` # 读取数据 awk 经常从标准输入读取并解析数据。然而,我们也可以从命令行中指定输入文件: ```sh awk 'program' file... ``` 例如,使用 awk 来模拟 `cat` 的功能: ```sh awk '{print $0}' file1 file2 file3 ``` 或者 `wc` 的功能: ```sh awk '{chars += length($0); words += NF} END {print NR, words, chars + NR}' file1 ``` 以上示例中,使用了 `length` 来获取字符串中的字符数。`chars + NR` 是因为 `length` 命令没有将换行符计算为字符数,是为了保持结果和 `wc` 一致进行的处理。 如果我们想将一个文件的内容插入到另一个文件,我们可以使用 awk 的 `getline` 函数。以下示例给一个文件插入页头和页尾: ```sh awk ' BEGIN { while (getline <"header.txt" > 0) { print $0 } } {print} END { while (getline <"footer.txt" > 0) { print $0 } } ' < body.txt > finished_file.txt ``` `getline` 的使用方法很灵活,有以下几种方式: ## getline 最基础的形式,`getline` 从当前输入流中读取下一条记录。`$0`、`NF`、`NR` 和 `FNR` 的值被设置。 ## getline var 从当前输入流中读取下一条记录并将其内容赋值给变量 `var`。`var`、`NR` 和 `FNR` 的值被设置。 ## geline 0) ``` `getline` 从 *header.txt* 中读取,`>0` 表示读取正确。`getline` 会返回一个值,正数表示成功,0 表示 EOF(end of file,文件结尾),负数表示文件相关的错误,比如文件未找到。如果我们不检查返回值,可能会导致无限循环。 ## getline var random_table.dat ``` 会产生一个有 100 行,5 列的随机整数表。 # 将文件转换为 CSV 格式 awk 的众多能力中的一个就是文件格式转换。下面我们将上述示例的结果转换为 CSV(Comma Seperated Values,逗号分隔的值)文件。 ```sh awk 'BEGIN {OFS=","} {print $1,$2,$3,$4,$5}' random_table.dat ``` 这是很简单的转换。我们只要设置输出域分隔符(OFS),然后输出每个域即可。写一个 CSV 文件很简单,但是读一个 CSV 文件就很难了。在某些情况下,应用写 CSV 文件(包括很多流行的电子表格程序)会创建类似以下的数据行: ``` word1, "word2a, word2b", word3 ``` 注意第二个域中的逗号。这种情况下,简单的 awk 解决方案(FS=",")就行不通了。可以解析这种文件,但是不简洁(gawk 中有一个扩展可以解决此问题)。最好是避免读取此类文件。 # 将文件转换为 TSV 格式 另一种和 CSV 一样常见的是 TSV(Tab Seperated Value)文件。此格式文件使用 tab 符作为域分隔符: ```sh awk 'BEGIN {OFS="\t"} {print $1,$2,$3,$4,$5}' random_table.dat ``` 使用 TSV 格式可以避免 CSV 格式引号中的逗号问题。 # 打印每一行的和 ```sh awk ' { t = $1 + $2 + $3 + $4 + $5 printf("%s = %6d\n", $0, t) } ' random_table.dat ``` # 打印每一列的和 ```sh awk ' { for (i = 1; i <= 5; i++) { t[i] += $i } print } END { print " ===" for (i = 1; i <= 5; i++) { printf(" %7d", t[i]) } printf("\n", "") } ' random_table.dat ``` # 打印第一列的最小值和最大值 ```sh awk ' BEGIN {min = 99999} $1 > max {max = $1} $1 < min {min = $1} END {print "Max =", max, "Min =", min} ' random_table.dat ``` # 最后一个示例:按扩展名统计各类文件数量 我们创建一个程序,处理一个路径名列表并将解析每个文件的扩展名,得到每个扩展名的数量: ```sh # file_types.awk - sorted list of file name extensions and counts BEGIN {FS = "."} {types[$NF]++} END { for (i in types) { printf("%6d %s\n", types[i], i) | "sort -nr" } } ``` 要找到家目录中最多的 10 个扩展名,我们可以这样使用该程序: ```sh find ~ -name "*.*" | awk -f file_types.awk | head ``` # 总结 为 awk 作者在 Unix 早期给我们提供了这么优雅的工具致敬。直到今天仍然强大有用。本文只是简要阐述了 awk 的功能,要想更加深入的了解,参考各 awk 实现的文档。同时,在网上搜索「AWK one-liners」(一行 awk)可以看到许多聪明的 awk 把戏。 # 深入阅读 - nawk 文档:[nawk(1) - Linux man page](http://linux.die.net/man/1/nawk) - Eric Pement 总结的一行 awk:[HANDY ONE-LINE SCRIPTS FOR AWK](http://www.pement.org/awk/awk1line.txt) - gawk 官方文档:[Gawk: Effective AWK Programming](https://www.gnu.org/software/gawk/manual/) - 有用的 awk 使用提示:[10 Awk Tips, Tricks and Pitfalls](https://catonmat.net/ten-awk-tips-tricks-and-pitfalls)
所有评论(0)
暂无评论~_~