AWK 教程
最后修改于 2023 年 10 月 18 日
这是 AWK 教程。它涵盖了 AWK 工具的基础知识。
AWK
AWK 是一种面向模式的扫描和处理语言。AWK 程序由一组针对文本数据流要执行的动作组成。AWK 广泛使用正则表达式。它是大多数类 Unix 操作系统的标准功能。
AWK 于 1977 年在贝尔实验室创建。它的名字来源于其作者的姓氏——Alfred Aho、Peter Weinberger 和 Brian Kernighan。
AWK 有两个主要的实现。传统的 Unix AWK 和较新的 GAWK。GAWK 是 GNU 项目对 AWK 编程语言的实现。GAWK 对原始 AWK 有一些扩展。
AWK 程序
AWK 程序由一系列模式-动作语句和可选的函数定义组成。它处理文本文件。AWK 是一种面向行的语言。它将文件分成称为记录的行。每行被分解成一系列字段。字段通过特殊变量访问:$1 读取第一个字段,$2 读取第二个字段,依此类推。$0 变量引用整个记录。
AWK 程序的结构具有以下形式
pattern { action }
模式是对每个记录进行的测试。如果满足条件,则执行动作。模式或动作都可以省略,但不能同时省略。默认模式匹配每一行,默认动作是打印记录。
awk -f program-file [file-list] awk program [file-list]
AWK 程序可以通过两种基本方式运行:a) 程序从一个单独的文件中读取;程序名称跟在 -f
选项后面,b) 程序在命令行中指定,并用引号括起来。
AWK 单行命令
AWK 单行命令是从命令行运行的简单的一次性程序。让我们看以下文本文件
storeroom tree cup store book cloud existence ministerial falcon town sky top bookworm bookcase war
我们想打印 words.txt
文件中所有长度超过五个字符的单词。
$ awk 'length($1) > 5 {print $0}' words.txt storeroom existence ministerial falcon bookworm bookcase
AWK 程序放在两个单引号字符之间。第一个是模式;我们指定记录的长度大于五。length
函数返回字符串的长度。$1
变量引用记录的第一个字段;在我们的例子中,每条记录只有一个字段。动作放在花括号之间。
$ awk 'length($1) > 5' words.txt storeroom existence ministerial falcon bookworm bookcase
正如我们之前所说,动作可以省略。在这种情况下,会执行默认动作——打印整个记录。
$ awk 'length($1) == 3' words.txt cup sky top war
我们打印所有三个字符的单词。
$ awk '!(length($1) == 3)' words.txt storeroom tree store book cloud existence ministerial falcon town bookworm bookcase
使用 !
运算符,我们可以否定条件;我们打印所有不包含三个字符的行。
$ awk '(length($1) == 3) || (length($1) == 4)' words.txt tree cup book town sky top war
我们使用 ||
运算符组合了两个条件。
$ awk 'length($1) > 0 {print $1, "has", length($1), "chars"}' words.txt storeroom has 9 chars tree has 4 chars cup has 3 chars store has 5 chars book has 4 chars cloud has 5 chars existence has 9 chars ministerial has 11 chars falcon has 6 chars town has 4 chars sky has 3 chars top has 3 chars bookworm has 8 chars bookcase has 8 chars war has 3 chars
此 AWK 命令打印每个单词的长度。如果用逗号分隔 print
参数,AWK 会添加一个空格字符。
$ grep book words.txt book bookworm bookcase $ grep book words.txt -n 5:book 13:bookworm 14:bookcase
grep
命令用于在文件中查找文本模式。
$ awk '/book/ {print}' words.txt book bookworm bookcase $ awk '/book/ {print NR ":" $0}' words.txt 5:book 13:bookworm 14:bookcase
这些是上面 grep 命令的 AWK 等价物。NR
变量给出正在处理的记录/行的总数。
接下来我们对数字应用条件。
Peter 89 Lucia 95 Thomas 76 Marta 67 Joe 92 Alex 78 Sophia 90 Alfred 65 Kate 46
我们有一个包含学生分数的ファイル。
$ awk '$2 >= 90 { print $0 }' scores.txt Lucia 95 Joe 92 Sophia 90
我们打印所有分数在 90+ 的学生。
$ awk '$2 >= 90 { print }' scores.txt Lucia 95 Joe 92 Sophia 90
如果省略 print
函数的参数,则假定为 $0
。
$ awk '$2 >= 90' scores.txt Lucia 95 Joe 92 Sophia 90
缺失的 { action }
表示打印匹配的行。
$ awk '{ if ($2 >= 90) print }' scores.txt Lucia 95 Joe 92 Sophia 90
除了模式,我们也可以在动作中使用 if 条件。
$ awk '{sum += $2} END { printf("The average score is %.2f\n", sum/NR) }' scores.txt The average score is 77.56
此命令计算平均分数。在动作块中,我们计算分数的总和。在 END
块中,我们打印平均分数。我们使用内置的 printf
函数格式化输出。%.2f
是一个格式说明符;每个说明符都以 %
字符开头。.2
是精度——小数点后的位数。f
期望一个浮点值。\n
不是说明符的一部分;它是一个换行符。在字符串显示在终端后,它会打印一个换行符。
AWK 与管道协同工作
AWK 可以通过管道接收输入并将输出发送到其他命令。
$ echo -e "1 2 3 5\n2 2 3 8" | awk '{print $(NF)}' 5 8
在这种情况下,AWK 接收 echo
命令的输出。它打印最后一列的值。
$ awk -F: '$7 ~ /bash/ {print $1}' /etc/passwd | wc -l 3
在这里,AWK 程序通过管道将数据发送到 wc
命令。在 AWK 程序中,我们找出使用 bash 的用户。他们的名字被传递给 wc
命令进行计数。在我们的例子中,有三个用户使用 bash。
AWK 字段
AWK 逐行读取文件。每一行或记录都可以分成字段。FS
变量存储字段分隔符,默认为空格。
$ ls -l total 132 drwxr-xr-x 2 jano7 jano7 512 Feb 11 16:02 data -rw-r--r-- 1 jano7 jano7 110211 Oct 12 2019 sid.jpg -rw-r--r-- 1 jano7 jano7 5 Jul 22 20:21 some.txt -rw-r--r-- 1 jano7 jano7 226 Apr 23 16:56 thermopylae.txt -rw-r--r-- 1 jano7 jano7 365 Aug 4 10:22 users.txt -rw-r--r-- 1 jano7 jano7 24 Jul 21 21:03 words.txt -rw-r--r-- 1 jano7 jano7 30 Jul 22 21:20 words2.txt
我们在当前工作目录中有这些文件。
$ ls -l | awk '{print $6 " " $9}' Feb data Oct sid.jpg Jul some.txt Apr thermopylae.txt Aug users.txt Jul words.txt Jul words2.txt
我们将 ls
命令的输出重定向到 AWK。我们打印输出的第六列和第九列。
John Doe, gardener, London, M, 11/23/1982 Jane Doe, teacher, London, F, 10/12/1988 Peter Smith, programmer, New York, M, 9/18/2000 Joe Brown, driver, Portland, M, 1/1/1976 Jack Smith, physician, Manchester, M, 2/27/1983 Lucy Black, accountant, Birmingham, F, 5/5/1998 Martin Porto, actor, Los Angeles, M, 4/30/1967 Sofia Harris, interpreter, Budapest, F, 8/18/1993
在 users.txt
文件中,我们有一些用户。字段现在用逗号分隔。
$ awk -F, '{print $1 " is a(n)" $2}' users.txt John Doe is a(n) gardener Jane Doe is a(n) teacher Peter Smith is a(n) programmer Joe Brown is a(n) driver Jack Smith is a(n) physician Lucy Black is a(n) accountant Martin Porto is a(n) actor Sofia Harris is a(n) interpreter
我们打印文件的第一列和第二列。我们使用 -F
选项指定字段分隔符。
$ awk 'BEGIN {FS=","} {print $3}' users.txt London London New York Portland Manchester Birmingham Los Angeles Budapest
字段分隔符也可以在程序中设置。我们在 BEGIN
块中将 FS
变量设置为逗号,BEGIN
块在程序执行开始时执行一次。
$ awk 'BEGIN {FS=","} {print $3}' users.txt | uniq London New York Portland Manchester Birmingham Los Angeles Budapest
我们将输出传递给 uniq
命令以获取唯一值。
$ awk -F, '$4 ~ "F" {print $1}' users.txt Jane Doe Lucy Black Sofia Harris
我们打印所有女性。我们使用 ~
运算符匹配模式。
$ awk '{print "The", NR". record has", length($0), "characters"}' users.txt The 1. record has 41 characters The 2. record has 40 characters The 3. record has 47 characters The 4. record has 40 characters The 5. record has 47 characters The 6. record has 47 characters The 7. record has 46 characters The 8. record has 49 characters
此命令打印每个记录的字符数。$0
代表整行。
$ awk -F, '{print $NF, $(NF-1)}' users.txt 11/23/1982 M 10/12/1988 F 9/18/2000 M 1/1/1976 M 2/27/1983 M 5/5/1998 F 4/30/1967 M 8/18/1993 F
$NF
是最后一个字段,$(NF-1)
是倒数第二个字段。
$ awk -F, '{ if ($4 ~ "M") {m++} else {f++} } END {printf "users: %d\nmales: %d\nfemales: %d\n", m+f, m, f}' users.txt users: 8 males: 5 females: 3
此命令打印用户、男性和女性的数量。当命令变得过于复杂时,最好将其放入文件中。
{ if ($4 ~ "M") { m++ } else { f++ } } END { printf "users: %d\nmales: %d\nfemales: %d\n", m+f, m, f }
由 {}
分隔的第一个块针对文件的每一行执行。我们计算第四个字段中包含 M 和不包含 M 的所有记录。数字存储在 m
和 f
变量中。END
块在程序结束时执行一次。在那里我们打印用户、男性和女性的数量。printf
函数允许我们创建格式化的字符串。
$ awk -F, -f males_females.awk users.txt users: 8 males: 5 females: 3
AWK 从文件读取程序,后面跟 -f
选项。
AWK 正则表达式
正则表达式经常应用于 AWK 字段。~
是正则表达式匹配运算符。它检查字符串是否与提供的正则表达式匹配。
$ awk '$1 ~ /^[b,c]/ {print $1}' words.txt cup book cloud bookworm bookcase
在此命令中,我们打印所有以 b 或 c 字符开头的单词。正则表达式放在两个斜杠字符之间。
$ awk '$1 ~ /[e,n]$/ {print $1}' words.txt tree store existence falcon town bookcase
此命令打印所有以 e 或 n 结尾的单词。
$ awk '$1 ~ /\<...\>/ {print $1}' words.txt cup sky top war
该命令打印所有三个字符的单词。句点(.)代表任何字符,而 \<
和 \>
字符是单词边界。
$ awk '$1 ~ /\<...\>/ || $1 ~ /\<....\>/ {print $1}' words.txt tree cup book town sky top war
我们使用或 (||) 运算符组合了两个条件。AWK 命令打印所有包含三个或四个字符的单词。
$ awk '$1 ~ /store|room|book/' words.txt storeroom store book bookworm bookcase
使用交替运算符 (|),我们打印包含指定单词之一的字段。
$ awk '$1 ~ /^book(worm|case)?$/' words.txt book bookworm bookcase
通过 应用子模式,我们打印包含 book、bookwor 或 bookcase 的字段。
?
表示子模式可能存在也可能不存在。
match
是一个内置的字符串操作函数。它测试给定字符串是否包含正则表达式模式。第一个参数是字符串,第二个是正则表达式模式。它类似于 ~
运算符。
$ awk 'match($0, /^[c,b]/)' words.txt brown craftsmanship book beautiful computer
程序打印以 c 或 b 开头的行。正则表达式放在两个斜杠字符之间。
match
函数设置 RSTART
变量;它是匹配模式开始的索引。
$ awk 'match($0, /i/) {print $0 " has i character at " RSTART}' words.txt craftsmanship has i character at 12 beautiful has i character at 6 existence has i character at 3 ministerial has i character at 2
程序打印包含 i 字符的单词。此外,它还打印该字符的第一次出现。
AWK 内置变量
AWK 提供重要的内置变量。
变量名 | 描述 |
---|---|
FS | 字段分隔符(默认为空格) |
NF | 当前记录中的字段数 |
NR | 当前记录/行号 |
$0 | 整行 |
$n | 第 n 个字段 |
FNR | 当前文件中的当前记录号 |
RS | 输入记录分隔符(默认为换行符) |
OFS | 输出字段分隔符(默认为空格) |
ORS | 输出记录分隔符(默认为换行符) |
OFMT | 数字的输出格式(默认为 %.6g) |
SUBSEP | 分隔多个下标(默认为 034) |
ARGC | 参数计数 |
ARGV | 参数数组 |
FILENAME | 当前输入文件的名称 |
RSTART | 匹配模式开始的索引 |
RLENGTH | match 函数匹配的字符串长度 |
CONVFMT | 转换数字时使用的转换格式(默认为 %.6g) |
该表列出了常见的 AWK 变量。
$ awk 'NR % 2 == 0 {print}' words.txt tree store cloud ministerial town top bookcase
上面的程序打印 words.txt
文件中的每第二条记录。对 NR
变量进行模除可以得到偶数行。
假设我们想打印文件的行号。
$ awk '{print NR, $0}' words.txt 1 storeroom 2 tree 3 cup 4 store 5 book 6 cloud 7 existence 8 ministerial 9 falcon 10 town 11 sky 12 top 13 bookworm 14 bookcase 15 war
再次,我们使用 NR
变量。我们跳过模式,因此动作对每一行都执行。$0
变量引用整个记录。
$ echo -e "cup\nbill\ncoin" > words1.txt $ echo -e "cloud\nbreath\nrank" > words2.txt
我们创建两个文本文件,其中包含一些单词。
$ awk '{ print $1, "is at line", FNR, "in", FILENAME }' words1.txt words2.txt cup is at line 1 in words1.txt bill is at line 2 in words1.txt coin is at line 3 in words1.txt cloud is at line 1 in words2.txt breath is at line 2 in words2.txt rank is at line 3 in words2.txt
我们打印每个单词的位置;我们包含行号和文件名。NR
和 FNR
变量之间的区别在于,前者计算所有文件中的行数,而后者始终计算当前文件中的行数。
对于下面的例子,我们有这个 C 源文件。
1 #include <stdio.h> 2 3 int main(void) { 4 5 char *countries[5] = { "Germany", "Slovakia", "Poland", 6 "China", "Hungary" }; 7 8 size_t len = sizeof(countries) / sizeof(*countries); 9 10 for (size_t i=0; i < len; i++) { 11 12 printf("%s\n", countries[i]); 13 } 14 }
我们有一个带行号的源文件。我们的任务是删除文本中的数字。
$ awk '{print substr($0, 4)}' source.c #include <stdio.h> int main(void) { char *countries[5] = { "Germany", "Slovakia", "Poland", "China", "Hungary" }; size_t len = sizeof(countries) / sizeof(*countries); for (size_t i=0; i < len; i++) { printf("%s\n", countries[i]); } }
我们使用 substr
函数。它从给定字符串打印一个子字符串。我们将该函数应用于每一行,跳过前三个字符。换句话说,我们从第四个字符开始打印每一条记录直到其末尾。
$ awk '{print substr($0, 4) >> "source2.c"}' source.c
我们将输出重定向到一个新文件。
NF
是当前记录中的字段数。
2 3 1 34 21 12 43 21 11 2 11 33 12 43 72 91 90 32 14 34 87 22 12 75 2 42 13 75 23 1 42 41 94 4 32 2 1 6 2 1 3 1 4 53 13 52 84 14 14 63 3 2 5 76 31 45
我们有一个值文件。
$ awk 'NF == 6' values.txt 2 3 1 34 21 12 43 72 91 90 32 14 3 2 5 76 31 45
我们打印包含六个字段的记录。
$ awk '{print "line", NR, "has", NF, "values"}' values.txt line 1 has 6 values line 2 has 7 values line 3 has 6 values line 4 has 8 values line 5 has 8 values line 6 has 8 values line 7 has 7 values line 8 has 6 values
此命令打印每行值的数量。
{ for (i = 1; i<=NF; i++) { sum += $i } print "line", NR, "sum:", sum sum = 0 }
该程序计算每行的值之和。
for (i = 1; i<=NF; i++) { sum += $i }
这是一个经典的 for 循环。我们遍历记录中的每个字段并将值添加到 sum
变量。+=
是一个复合加法运算符。
$ awk -f calc_sum.awk values.txt line 1 sum: 73 line 2 sum: 133 line 3 sum: 342 line 4 sum: 287 line 5 sum: 312 line 6 sum: 20 line 7 sum: 293 line 8 sum: 162
下面是使用 split
函数的替代解决方案。
{ split($0, vals) for (idx in vals) { sum += vals[idx] } print "line", NR, "sum:", sum sum = 0 }
该程序计算每行的值之和。
split($0, vals)
split
函数将给定字符串分割成一个数组;记录元素的默认分隔符是 FS
。
for (idx in vals) { sum += vals[idx] }
我们遍历数组并计算总和。在每次循环中,idx
变量被设置为数组的当前索引。
$ awk -f calc_sum2.awk values.txt line 1 sum: 73 line 2 sum: 133 line 3 sum: 342 line 4 sum: 287 line 5 sum: 312 line 6 sum: 20 line 7 sum: 293 line 8 sum: 162
BEGIN 和 END 块
BEGIN
和 END
是在读取所有记录之前和之后执行的块。这两个关键字后面跟着花括号,我们在其中指定要执行的语句。
$ awk 'BEGIN { print "Unix time: ", systime()}' Unix time: 1628156179
BEGIN
块在处理第一行输入之前执行。我们打印 Unix 时间,利用 systime
函数。该函数是 gawk 的扩展函数。
$ awk 'BEGIN { print "Today is", strftime("%Y-%m-%d") }' Today is 2021-08-05
程序打印当前日期。strftime
是 GAWK 的扩展。
$ echo "1,2,3,4,5" | awk '{ split($0,a,",");for (idx in a) {sum+=a[idx]} } END {print sum}' 15
程序使用 split
函数将行分割成数字数组。我们遍历数组元素并计算它们的总和。在 END
块中,我们打印总和。
The Battle of Thermopylae was fought between an alliance of Greek city-states, led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the course of three days, during the second Persian invasion of Greece.
我们想计算文件中行数、单词数和字符数。
$ wc thermopylae.txt 4 38 226 thermopylae.txt
要计算文件中行数、单词数和字符数,我们有 wc
命令。
{ words += NF chars += length + 1 # include newline character } END { print NR, words, chars }
程序的第一部分针对文件的每一行执行。END
块在程序末尾运行。
$ awk -f count_words.awk thermopylae.txt 4 38 226
$ cat words.txt brown tree craftsmanship book beautiful existence ministerial computer town $ cat words2.txt pleasant curly storm hering immune
我们想知道这两行的行数。
$ awk 'END {print NR}' words.txt words2.txt 14
我们将两个文件传递给 AWK 程序。AWK 按顺序处理命令行接收的文件名。END
关键字后面的块在程序结束时执行;我们打印 NR
变量,它保存最后处理行的行号。
$ awk 'BEGIN {srand()} {lines[NR] = $0} END { r=int(rand()*NR + 1); print lines[r]}' words.txt tree
上面的程序从 words.txt
文件中打印一行随机行。srand
函数初始化随机数生成器。该函数只需执行一次。在程序的主体部分,我们将当前记录存储在 lines
数组中。最后,我们计算一个介于 1 和 NR
之间的随机数,并从数组结构中打印随机选择的行。
AWK 使用单词词典
在下面的示例中,我们创建了几个处理英语词典的 AWK 程序。在 Unix 系统上,词典位于 /usr/share/dict/words
文件中。
$ awk 'length($1) == 10 { n++ } END {print n}' /usr/share/dict/words 30882
此命令打印给定词典中长度为十个字符的单词的数量。在动作块中,我们为每次匹配增加 n
变量。在 END
块中,我们打印最终数字。
$ awk '$1 ~ /^w/ && length($1) == 4 { n++; if (n<15) {print} else {exit} }' /usr/share/dict/words waag waar wabe wace wack wade wadi waeg waer waff waft wage waif waik
此命令打印前十五个以 'w' 开头且长度为四个字母的单词。exit
语句终止 AWK 程序。
$ awk '$1 ~ /^w.*r$/ { n++; if (n<15) {print} } END {print n}' /usr/share/dict/words waar wabber wabster wacker wadder waddler wader wadmaker wadsetter waer wafer waferer wafermaker wafter 417
该命令打印前十五个以 'w' 开头且以 'r' 结尾的单词。最后,它打印文件中此类单词的总数。
回文 是一个单词、数字、短语或任何其他字符序列,它正着读和反着读都一样,例如 madam 或 racecar。
{ for (i=length($0); i!=0; i--) { r = r substr($0, i, 1) } if (length($0) > 1 && $0 == r) { print n++ } r = "" } END { printf "There are %d palindromes\n", n }
该程序查找所有回文。算法是原始单词必须等于反转的单词。
for (i=length($0); i!=0; i--) { r = r substr($0, i, 1) }
使用 for 循环,我们反转给定的字符串。substr
函数返回一个子字符串;第一个参数是字符串,第二个是开始位置,最后一个是子字符串的长度。要在 AWK 中连接字符串,我们只需用空格字符分隔它们。
if (length($0) > 1 && $0 == r) { print n++ }
单词的长度必须大于 1;我们不将单个字母计为回文。如果反转的单词等于原始单词,我们打印它并增加 n
变量。
r = ""
我们重置 r
变量。
END { printf "There are %d palindromes\n", n }
最后,我们打印文件中回文的数量。
{ if (length($0) == 1) {next} rev = reverse($0) if ($0 == rev) { print n++ } } END { printf "There are %d palindromes\n", n } function reverse(word) { r = "" for (i=length(word); i!=0; i--) { r = r substr(word, i, 1) } return r }
为了提高程序的可读性,我们创建了一个自定义的 reverse
函数。
AWK ARGC 和 ARGV 变量
接下来,我们处理 ARGC
和 ARGV
变量。
$ awk 'BEGIN { print ARGC, ARGV[0], ARGV[1]}' words.txt 2 awk words.txt
程序打印 AWK 程序的参数数量和前两个参数。ARGC
是命令行参数的数量;在我们的例子中,有两条参数,包括 AWK 本身。ARGV
是命令行参数的数组。数组的索引从 0 到 ARGC
- 1。
FS
是输入字段分隔符,默认为空格。NF
是当前输入记录中的字段数。
对于下面的程序,我们使用这个文件
$ cat values 2, 53, 4, 16, 4, 23, 2, 7, 88 4, 5, 16, 42, 3, 7, 8, 39, 21 23, 43, 67, 12, 11, 33, 3, 6
我们有三行逗号分隔的值。
BEGIN { FS="," max = 0 min = 10**10 sum = 0 avg = 0 } { for (i=1; i<=NF; i++) { sum += $i if (max < $i) { max = $i } if (min > $i) { min = $i } printf("%d ", $i) } } END { avg = sum / NF printf("\n") printf("Min: %d, Max: %d, Sum: %d, Average: %d\n", min, max, sum, avg) }
程序从提供的值中计算基本统计信息。
FS=","
文件中的值由逗号分隔;因此,我们将 FS
变量设置为逗号。
max = 0 min = 10**10 sum = 0 avg = 0
我们为最大值、最小值、总和和平均值定义了默认值。AWK 变量是动态的;它们的值是浮点数或字符串,或者两者兼有,具体取决于它们的用法。
{ for (i=1; i<=NF; i++) { sum += $i if (max < $i) { max = $i } if (min > $i) { min = $i } printf("%d ", $i) } }
在脚本的主体部分,我们遍历每一行并计算值的最大值、最小值和总和。NF
用于确定每行值的数量。
END { avg = sum / NF printf("\n") printf("Min: %d, Max: %d, Sum: %d, Average: %d\n", min, max, sum, avg) }
在脚本的结尾部分,我们计算平均值并将计算结果打印到控制台。
$ awk -f stats.awk values 2 53 4 16 4 23 2 7 88 4 5 16 42 3 7 8 39 21 23 43 67 12 11 33 3 6 Min: 2, Max: 88, Sum: 542, Average: 67
FS
变量可以通过 -F
标志作为命令行选项指定。
$ awk -F: '{print $1, $7}' /etc/passwd | head -7 root /bin/bash daemon /usr/sbin/nologin bin /usr/sbin/nologin sys /usr/sbin/nologin sync /bin/sync games /usr/sbin/nologin man /usr/sbin/nologin
该示例打印系统 /etc/passwd
文件中的第一个字段(用户名)和第七个字段(用户的 shell)。head
命令用于只打印前七行。/etc/passwd
文件中的数据由冒号分隔。所以冒号被传递给 -F
选项。
RS
是输入记录分隔符,默认为换行符。
$ echo "Jane 17#Tom 23#Mark 34" | awk 'BEGIN {RS="#"} {print $1, "is", $2, "years old"}' Jane is 17 years old Tom is 23 years old Mark is 34 years old
在示例中,我们有由 # 字符分隔的相关数据。RS
用于剥离它们。AWK 可以从其他命令(如 echo
)接收输入。
AWK GET 请求
AWK 可以发出 HTTP 请求。我们使用 getline
函数和 /inet/tcp/0/
文件。
BEGIN { site = "webcode.me" server = "/inet/tcp/0/" site "/80" print "GET / HTTP/1.0" |& server print "Host: " site |& server print "\r\n\r\n" |& server while ((server |& getline line) > 0 ) { content = content line "\n" } close(server) print content }
该程序向 webcode.me 页面发出 GET 请求并读取其响应。
print "GET / HTTP/1.0" |& server
|&
运算符启动一个协进程,允许双向通信。
while ((server |& getline line) > 0 ) { content = content line "\n" }
使用 getline
函数,我们从服务器读取响应。
将变量传递给 AWK
AWK 有 -v
选项,用于为变量赋值。
$ awk -v today=$(date +%Y-%m-%d) 'BEGIN { print "Today is", today }' Today is 2021-08-05
我们将 date
命令的输出传递给 today
变量,然后可以在 AWK 程序中访问该变量。
The Battle of Thermopylae was fought between an alliance of Greek city-states, led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the course of three days, during the second Persian invasion of Greece.
{ for (i=1; i<=NF; i++) { field = $i if (field ~ word) { c = index($0, field) print NR "," c, $0 next } } }
该示例模拟了 grep
工具。它查找提供的单词并打印其行及其起始索引。(程序只查找单词的第一次出现。)word
变量通过 -v
选项传递给程序。
$ awk -v word=the -f mygrep.awk thermopylae.txt 2,37 led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the 3,30 course of three days, during the second Persian invasion of Greece.
我们已在 thermopylae.txt
文件中查找了“the”单词。
词频
接下来,我们计算圣经的词频。
$ wget https://raw.githubusercontent.com/janbodnar/data/main/the-king-james-bible.txt
我们下载了英王詹姆斯圣经。
$ file the-king-james-bible.txt the-king-james-bible.txt: UTF-8 Unicode (with BOM) text
当我们检查文本时,我们可以看到它是带有字节顺序标记的 UTF-8 Unicode 文本。BOM 必须在 AWK 程序中考虑。
{ if (NR == 1) { sub(/^\xef\xbb\xbf/,"") } gsub(/[,;!()*:?.]*/, "") for (i = 1; i <= NF; i++) { if ($i ~ /^[0-9]/) { continue } w = $i words[w]++ } } END { for (idx in words) { print idx, words[idx] } }
我们计算一个单词在书中出现的次数。
if (NR == 1) { sub(/^\xef\xbb\xbf/,"") }
从第一行,我们删除 BOM 字符。如果我们不删除 BOM,第一个单词(本例中的 The)就会包含它,因此会被识别为一个唯一的单词。
gsub(/[,;!()*:?.]*/, "")
从当前记录中,我们删除标点符号,如逗号和冒号。否则,诸如 the 和 the, 之类的文本将被视为两个不同的单词。
gsub
函数将给定的正则表达式全局替换为指定的字符串;由于字符串为空,这意味着它们被删除。如果未指定应进行替换的字符串,则假定为 $0
。
for (i = 1; i <= NF; i++) { if ($i ~ /^[0-9]/) { continue } w = $i words[w]++ }
在 for 循环中,我们遍历当前行的字段。圣经文本前面有章节;这些我们不希望包含。所以如果第一个字段以数字开头,我们就用 continue
关键字跳过当前循环。words
是一个单词数组。每个索引是文本中的一个单词。与索引对应的键是频率。每次遇到一个单词时,它的值就会增加。
END { for (idx in words) { print idx, words[idx] } }
最后,我们遍历单词并打印它们的索引(单词)和值(频率)。
$ awk -f word_freq.awk the-king-james-bible.txt > bible_words.txt
我们运行程序并将输出重定向到文件。
$ sort -nr -k 2 bible_words.txt | head the 62103 and 38848 of 34478 to 13400 And 12846 that 12576 in 12331 shall 9760 he 9665 unto 8942
我们对数据进行排序并打印出现频率最高的前十个单词。
PROCINFO
是一个特殊的内置数组,可以影响 AWK 程序。例如,它可以决定数组的遍历方式。它是 GAWK 的扩展。
{ if (NR == 1) { sub(/^\xef\xbb\xbf/,"") } gsub(/[,;!()*:?.]*/, "") for (i = 1; i <= NF; i++) { if ($i ~ /[0-9]/) { continue } w = $i words[w]++ } } END { PROCINFO["sorted_in"] = "@val_num_desc" for (idx in words) { print idx, words[idx] } }
通过 PROCINFO["sorted_in"] = "@val_num_desc"
,我们通过按降序比较值来遍历数组。
$ awk -f freq_top.awk the-king-james-bible.txt | head the 62103 and 38848 of 34478 to 13400 And 12846 that 12576 in 12331 shall 9760 he 9665 unto 8942
拼写检查
我们创建一个用于拼写检查的 AWK 程序。
BEGIN { count = 0 i = 0 while (getline myword <"/usr/share/dict/words") { dict[i] = myword i++ } } { for (i=1; i<=NF; i++) { field = $i if (match(field, /[[:punct:]]$/)) { field = substr(field, 0, RSTART-1) } mywords[count] = field count++ } } END { for (w_i in mywords) { for (w_j in dict) { if (mywords[w_i] == dict[w_j] || tolower(mywords[w_i]) == dict[w_j]) { delete mywords[w_i] } } } for (w_i in mywords) { if (mywords[w_i] != "") { print mywords[w_i] } } }
该脚本将提供的文本文件的单词与词典进行比较。在标准的 /usr/share/dict/words
路径下,我们可以找到一个英语词典;每个单词占一行。
BEGIN { count = 0 i = 0 while (getline myword <"/usr/share/dict/words") { dict[i] = myword i++ } }
在 BEGIN
块中,我们将词典中的单词读入 dict
数组。getline
命令从给定的文件名读取一条记录;记录存储在 $0
变量中。
{ for (i=1; i<=NF; i++) { field = $i if (match(field, /[[:punct:]]$/)) { field = substr(field, 0, RSTART-1) } mywords[count] = field count++ } }
在程序的主体部分,我们将要进行拼写检查的文件的单词放入 mywords
数组。我们删除单词末尾的任何标点符号(如逗号或点)。
END { for (w_i in mywords) { for (w_j in dict) { if (mywords[w_i] == dict[w_j] || tolower(mywords[w_i]) == dict[w_j]) { delete mywords[w_i] } } } ... }
我们将 mywords
数组中的单词与 dict
数组进行比较。如果单词在词典中,则使用 delete
命令将其删除。以句子开头的单词以大写字母开头;因此,我们还使用 tolower
函数检查小写形式。
for (w_i in mywords) { if (mywords[w_i] != "") { print mywords[w_i] } }
剩余的单词在词典中未找到;它们被打印到控制台。
$ awk -f spellcheck.awk text consciosness finaly
我们在一个文本文件上运行了该程序;我们找到了两个拼写错误的单词。请注意,该程序需要一些时间才能完成。
石头剪刀布
石头剪刀布是一种流行的手部游戏,每个玩家同时用伸出的手形成三种形状之一。我们在 AWK 中创建了这个游戏。
# This program creates a rock-paper-scissors game. BEGIN { srand() opts[1] = "rock" opts[2] = "paper" opts[3] = "scissors" do { print "1 - rock" print "2 - paper" print "3 - scissors" print "9 - end game" ret = getline < "-" if (ret == 0 || ret == -1) { exit } val = $0 if (val == 9) { exit } else if (val != 1 && val != 2 && val != 3) { print "Invalid option" continue } else { play_game(val) } } while (1) } function play_game(val) { r = int(rand()*3) + 1 print "I have " opts[r] " you have " opts[val] if (val == r) { print "Tie, next throw" return } if (val == 1 && r == 2) { print "Paper covers rock, you loose" } else if (val == 2 && r == 1) { print "Paper covers rock, you win" } else if (val == 2 && r == 3) { print "Scissors cut paper, you loose" } else if (val == 3 && r == 2) { print "Scissors cut paper, you win" } else if (val == 3 && r == 1) { print "Rock blunts scissors, you loose" } else if (val == 1 && r == 3) { print "Rock blunts scissors, you win" } }
我们与计算机玩游戏,计算机随机选择其选项。
srand()
我们使用 srand
函数初始化随机数生成器。
opts[1] = "rock" opts[2] = "paper" opts[3] = "scissors"
三个选项存储在 opts
数组中。
do { print "1 - rock" print "2 - paper" print "3 - scissors" print "9 - end game" ...
游戏的循环由 do-while
循环控制。首先,选项会打印到终端。
ret = getline < "-" if (ret == 0 || ret == -1) { exit } val = $0
我们使用 getline
命令从命令行读取一个值,即我们的选择;该值存储在 val
变量中。
if (val == 9) { exit } else if (val != 1 && val != 2 && val != 3) { print "Invalid option" continue } else { play_game(val) }
如果我们选择选项 9,我们将退出程序。如果值超出打印的菜单选项,我们会打印一条错误消息并使用 continue
命令开始一个新的循环。如果我们正确选择了三个选项之一,我们就会调用 play_game
函数。
r = int(rand()*3) + 1
使用 rand
函数选择一个介于 1 和 3 之间的随机值。这是计算机的选择。
if (val == r) { print "Tie, next throw" return }
如果两个玩家选择相同的选项,则平局。我们从函数返回,并开始一个新的循环。
if (val == 1 && r == 2) { print "Paper covers rock, you loose" } else if (val == 2 && r == 1) { ...
我们比较玩家选择的值并将结果打印到控制台。
$ awk -f rock_scissors_paper.awk 1 - rock 2 - paper 3 - scissors 9 - end game 1 I have scissors you have rock Rock blunts scissors, you win 1 - rock 2 - paper 3 - scissors 9 - end game 3 I have paper you have scissors Scissors cut paper, you win 1 - rock 2 - paper 3 - scissors 9 - end game
游戏的一个示例运行。
标记关键字
在下面的示例中,我们在源文件中标记 Java 关键字。
# the program adds tags around Java keywords # it works on keywords that are separate words BEGIN { # load java keywords i = 0 while (getline kwd <"javakeywords2") { keywords[i] = kwd i++ } } { mtch = 0 ln = "" space = "" # calculate the beginning space if (match($0, /[^[:space:]]/)) { if (RSTART > 1) { space = sprintf("%*s", RSTART, "") } } # add the space to the line ln = ln space for (i=1; i <= NF; i++) { field = $i # go through keywords for (w_i in keywords) { kwd = keywords[w_i] # check if a field is a keyword if (field == kwd) { mtch = 1 } } # add tags to the line if (mtch == 1) { ln = ln "<kwd>" field "</kwd> " } else { ln = ln field " " } mtch = 0 } print ln }
该程序在它识别的每个关键字周围添加 <kwd> 和 </kwd> 标签。这是一个基本示例;它适用于独立的关键字。它不处理更复杂的结构。
# load java keywords i = 0 while (getline kwd <"javakeywords2") { keywords[i] = kwd i++ }
我们从文件中加载 Java 关键字;每个关键字占一行。关键字存储在 keywords
数组中。
# calculate the beginning space if (match($0, /[^[:space:]]/)) { if (RSTART > 1) { space = sprintf("%*s", RSTART, "") } }
使用正则表达式,我们计算行开头的空格(如果有)。space
是一个字符串变量,等于当前行中空格的宽度。计算空格是为了保持程序的缩进。
# add the space to the line ln = ln space
空格被添加到 ln
变量中。在 AWK 中,我们使用空格来添加字符串。
for (i=1; i <= NF; i++) { field = $i ... }
我们遍历当前行的字段;正在检查的字段存储在 field
变量中。
# go through keywords for (w_i in keywords) { kwd = keywords[w_i] # check if a field is a keyword if (field == kwd) { mtch = 1 } }
在 for 循环中,我们遍历 Java 关键字并检查字段是否为 Java 关键字。
# add tags to the line if (mtch == 1) { ln = ln "<kwd>" field "</kwd> " } else { ln = ln field " " }
如果是一个关键字,我们将在关键字周围附加标签;否则,我们只将字段附加到行。
print ln
构造好的行被打印到控制台。
$ awk -f markkeywords2.awk program.java <kwd>package</kwd> com.zetcode; <kwd>class</kwd> Test { <kwd>int</kwd> x = 1; <kwd>public</kwd> <kwd>void</kwd> exec1() { System.out.println(this.x); System.out.println(x); } <kwd>public</kwd> <kwd>void</kwd> exec2() { <kwd>int</kwd> z = 5; System.out.println(x); System.out.println(z); } } <kwd>public</kwd> <kwd>class</kwd> MethodScope { <kwd>public</kwd> <kwd>static</kwd> <kwd>void</kwd> main(String[] args) { Test ts = <kwd>new</kwd> Test(); ts.exec1(); ts.exec2(); } }
在一个小型 Java 程序上运行的示例。
这是 AWK 教程。