ZetCode

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 单行命令是从命令行运行的简单的一次性程序。让我们看以下文本文件

words.txt
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 变量给出正在处理的记录/行的总数。


接下来我们对数字应用条件。

scores.txt
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。我们打印输出的第六列和第九列。

users.txt
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

此命令打印用户、男性和女性的数量。当命令变得过于复杂时,最好将其放入文件中。

males_females.awk
{
    if ($4 ~ "M") {

        m++
    } else {

        f++
    }
}

END {
    printf "users: %d\nmales: %d\nfemales: %d\n", m+f, m, f
}

{} 分隔的第一个块针对文件的每一行执行。我们计算第四个字段中包含 M 和不包含 M 的所有记录。数字存储在 mf 变量中。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匹配模式开始的索引
RLENGTHmatch 函数匹配的字符串长度
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

我们打印每个单词的位置;我们包含行号和文件名。NRFNR 变量之间的区别在于,前者计算所有文件中的行数,而后者始终计算当前文件中的行数。


对于下面的例子,我们有这个 C 源文件。

source.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 是当前记录中的字段数。

values.txt
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

此命令打印每行值的数量。

calc_sum.awk
{
    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 函数的替代解决方案。

calc_sum2.awk
{
    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 块

BEGINEND 是在读取所有记录之前和之后执行的块。这两个关键字后面跟着花括号,我们在其中指定要执行的语句。

$ 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 块中,我们打印总和。

thermopylae.txt
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 命令。

count_words.txt
{
    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。

palindromes.awk
{
    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
}

最后,我们打印文件中回文的数量。

palindromes2.awk
{
    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 变量

接下来,我们处理 ARGCARGV 变量。

$ 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

我们有三行逗号分隔的值。

stats.awk
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/ 文件。

get_page.awk
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 程序中访问该变量。

thermopylae.txt
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.
mygrep.awk
{
    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 程序中考虑。

word_freq.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 的扩展。

freq_top.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 {

    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 程序。

spellcheck.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 中创建了这个游戏。

rock_scissors_paper.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 关键字。

mark_keywords.awk
# 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 教程。

作者

我叫 Jan Bodnar,我是一名热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直撰写编程文章。到目前为止,我已撰写了 1400 多篇文章和 8 本电子书。我在教授编程方面有十多年的经验。