Shell学习笔记(11)——gawk进阶

在gawk中使用变量

gawk支持两种变量,第一,内建变量;第二,自定义变量。gawk有一些内建变量,这些变量存放用来处理数据文件中的数据字段和记录的信息。用户也可以在gawk程序里创建自己的变量。

内建变量

字段和记录分隔符变量

前面笔记提到过gawk中的一种内建变量类型是数据字段变量。数据字段变量允许用户使用美元符号($)和字段在该记录中的位置值来引用记录对应的字段。因此,要引用记录中的第一个数据字段,就用变量$1;要引用第二个字段,就用$2,依次类推。数据字段是由字段分隔符来划定的。默认情况下,字段分隔符是一个空白字符,也就是空格符或者制表符。也可以在命令行下使用命令行参数-F或者在gawk程序中使用特殊的内建变量FS来更改字段分隔符。内建变量FS是一组内建变量中的一个,这组变量用于控制gawk如何处理输入输出数据中的字段和记录下表列出了这些内建变量。

变量 描述
FIELDWIDTHS 由空格分隔的一列数字,定义了每个数据字段确切宽度
FS 输入字段分隔符
RS 输入记录分隔符
OFS 输出字段分隔符
ORS 输出记录分隔符

变量FSOFS定义了gawk如何处理数据流中的数据字段,变量FS来定义记录中的字段分隔符。变量OFS具备相同的功能,只不过是用在print命令的输出上。默认情况下,gawk将OFS设成一个空格,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","}{print $1, $2,$3}' data1.txt
Data11 data12 data13
data21 data22 data23
data31 data32 data33

在这个案例中,print命令会自动将OFS变量的值放置在输出中的每个字段间。通过设置OFS变量,可以在输出中使用任意字符串来分隔字段,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=",";OFS="-"}{print $1,$2,$3}' data1.txt
Data11-data12-data13
data21-data22-data23
data31-data32-data33
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=",";OFS="--"}{print $1,$2,$3}' data1.txt
Data11--data12--data13
data21--data22--data23
data31--data32--data33
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=",";OFS="<-->"}{print $1,$2,$3}' data1.txt
Data11<-->data12<-->data13
data21<-->data22<-->data23
data31<-->data32<-->data33

FIELDWIDTHS变量允许你不依靠字段分隔符来读取记录,从字面上很好理解,间隔宽度。在一些应用程序中,数据并没有使用字段分隔符,而是被放置在了记录中的特定列。这种情况下,必须设定FIELDWIDTHS变量来匹配数据在记录中的位置。一旦设置了FIELDWIDTH变量,gawk就会忽略FS变量,并根据提供的字段宽度来计算字段。下面是个采用字段宽度而非字段分隔符的例子,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_gawk$ cat data1b.txt
1005.3247596.37
115-2.349194.00
05810.1298100.1
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FIELDWIDTHS="3 5 2 5 "}{print $1, $2, $3, $4}' data1b.txt
100 5.324 75 96.37
115 -2.34 91 94.00
058 10.12 98 100.1

FIELDWIDTHS变量定义了四个字段,即3 5 2 5 ,gawk依此来解析数据记录。每个记录中的数字串会根据已定义好的字段长度来分割。一旦设定了FIELDWIDTHS变量的值,就不能再改变了,这种方法并不适用于长的字段。

变量RSORS定义了gawk程序如何处理数据流中的字段。默认情况下,gawk将RSORS设为换行符。默认的RS值表明,输入数据流中的每行新文本就是一条新纪录。有时,你会在数据流中碰到占据多行的字段。典型的例子是包含地址和电话号码的数据,其中地址和电话号码各占一行,如下所示:

1
2
3
4
Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234

如果用默认的FSRS变量值来读取这组数据,gawk就会把每行作为一条单独的记录来读取,并将记录中的空格当作字段分隔符。这可不是你希望看到的。要解决这个问题,只需把FS变量设置成换行符。这就表明数据流中的每行都是一个单独的字段,每行上的所有数据都属于同一个字段。此时要判断一个新的数据行从何开始。对于这一问题,可以把RS变量设置成空字符串,然后在数据记录间留一个空白行。gawk会把每个空白行当作一个记录分隔符,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
biotest@ubuntu:~/advance_gawk$ cat data2.txt
Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234
Frank Williams
456 Oak Street
Indianapolis, IN 46201
(317)555-9876
Haley Snell
4231 Elm Street
Detroit, MI 48201
(313)555-4938
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS="\n";RS=""} {print $1,$4}' data2.txt
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938

现在gawk把文件中的每行都当成一个字段,把空白行当作记录分隔符。

数据变量

gawk还有其他一些内建变量来帮助你了解数据发生了什 么变化,并提取shell环境的信息。下表列出了gawk中的其他内建变量。

变量 描述
ARGC 当前命令行参数个数
ARGIND 当前文件在 ARGV 中的位置
ARGV 包含命令行参数的数组
CONVFMT 数字的转换格式(参见 printf 语句),默认值为
ENVIRON 当前shell环境变量及其值组成的关联数组
ERRNO 当读取或关闭输入文件发生错误时的系统错误号
FILENAME 用作gawk输入数据的数据文件的文件名
FNR 当前数据文件中的数据行数
IGNORECASE 设成非零值时,忽略 gawk 命令中出现的字符串的字符大小写
NF 数据文件中的字段总数
NR 已处理的输入记录数
OFMT 数字的输出格式,默认值为 %.6 g
RLENGTH 由 match 函数所匹配的子字符串的长度
RSTART 由 match 函数所匹配的子字符串的起始位置

ARGC和ARGV变量允许从shell中获得命令行参数的总数以及它们的值,需要注意的是,gawk并不会将程序脚本当成命令行参数的一部分,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{print ARGC,ARGV[0]}' data1.txt
2 gawk
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{print ARGC,ARGV[1]}' data1.txt
2 data1.txt

结果显示:ARGC的结果是2,表明命令行上有两个参数,这包括gawk命令和data1参数(记住,程序脚本并不算参数)。ARGV是包含命令行参数的数组,这个数组从索引0开始,代表的是命令,从结果可以看出,ARGV[0]是gawk,ARGV[1]的结果是data1,这是gawk命令后的第一个命令行参数。从代码可以看出,在脚本中引用gawk变量时,变量名前不加美元符。

ENVIRON使用关联数组来提取shell环境变量。关联数组用文本作为数组的索引值,而不是数值。数组索引中的文本是shell环境变量名,而数组的值则是shell环境变量的值,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/advance_gawk$ gawk '
> BEGIN{
> print ENVIRON["HOME"]
> print ENVIRON["PATH"]
> }'
/home/biotest
/home/biotest/miniconda2/bin:/home/miniconda2/bin:/home/bio/miniconda2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/biotest/home/test:/home/biotest/home/biotest:/home/biotest:/home/biotest/

ENVIRON["HOME"]变量从shell中提取了HOME环境变量的值。ENVIRON["PATH"]提取了PATH环境变量的值。可以用这种方法来从shell中提取任何环境变量的值,以供gawk程序使用。当要在gawk程序中跟踪数据字段和记录时,就需要变量FNRNFNR,有时用户如果不知道记录中有多少个数据字段。NF变量可以让用户在不知道具体位置的情况下指定记录中的最后一个数据字段,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=":";OFS=":"}{print $1,$NF}' /etc/passwd
root:/bin/bash
daemon:/usr/sbin/nologin
bin:/usr/sbin/nologin
sys:/usr/sbin/nologin
sync:/bin/sync
...
usbmux:/bin/false
biotest:/bin/bash
userid:
501:
502:
503:
511:
512:
513:
test001:

NF变量含有数据文件中最后一个数据字段的数字值。可以在它前面加个美元符将其用作字段变量。

FNRNR变量虽然类似,但又略有不同。FNR变量含有当前数据文件中已处理过的记录数,NR变量则含有已处理过的记录总数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","}{print $1, "FNR="FNR}' data1.txt data1.txt
Data11 FNR=1
data21 FNR=2
data31 FNR=3
Data11 FNR=1
data21 FNR=2
data31 FNR=3

在这个例子中,gawk程序的命令行定义了两个输入文件(两次指定的是同样的输入文件)。这个脚本会打印第一个数据字段的值和FNR变量的当前值。注意,当gawk程序处理第二个数据文件时,FNR值被设回了1。接着看NR变量的案例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_gawk$ gawk '
> BEGIN {FS=","}
> {print $1, "FNR="FNR,"NR="NR}
> END{print "There were",NR,"records processed"}' data1.txt data1.txt
Data11 FNR=1 NR=1
data21 FNR=2 NR=2
data31 FNR=3 NR=3
Data11 FNR=1 NR=4
data21 FNR=2 NR=5
data31 FNR=3 NR=6
There were 6 records processed

FNR变量的值在gawk处理第二个数据文件时被重置了,而NR变量则在处理第二个数据文件时继续计数。结果就是:如果只使用一个数据文件作为输入,FNR和NR的值是相同的;如果使用多个数据文件作为输入,FNR的值会在处理每个数据文件时被重置,而NR的值则会继续计数直到处理完所有的数据文件。

由于gawk脚本通常会比shell脚本中的其他部分还要大一些,因此用户可以shell的多行特性直接在命令行上运行了gawk脚本。在shell脚本中使用gawk时,应该将不同的gawk命令放到不同的行,这样会比较容易阅读和理解,不要在shell脚本中将所有的命令都塞到同一行。如果要在不同的shell脚本中用到同样的gawk脚本,可以将这段gawk脚本放到一个单独的文件中,并用-f参数来在shell脚本中引用即可。

自定义变量

在脚本中给变量赋值

在gawk程序中给变量赋值跟在shell脚本中赋值类似,都用赋值语句,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/advance_gawk$ gawk '
> BEGIN{
> testing="This is a test"
> print testing
> }'
This is a test

print语句的输出是testing变量的当前值。跟shell脚本变量一样,gawk变量可以保存数值或文本值,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/advance_gawk$ gawk '
> BEGIN{
> testing="This is a test"
> print testing
> testing=45
> print testing
> }'
This is a test
45

在这个案例中,testing变量开始是文本,后来是数字,赋值语句可以包含数学公式,如下所示:

1
2
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{x=4;x=x*2+3;print x}'
11

从结果可以看出,gawk包含了用来处理数字值的标准算数操作符,其中包括求余符号(%)和幂运算符号(^或**)。

在命令行上给变量赋值

gawk命令行可以给程序中的变量赋值,这样用户就能在正常的代码之外赋值,即时改变变量的值,在下面的例子中,有入眠使用命令行变量来显示文件中特定数据字段,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_gawk$ cat script1.sh
BEGIN{FS=","}
{print $n}
biotest@ubuntu:~/advance_gawk$ gawk -f script1.sh n=2 data1.txt
data12
data22
data32
biotest@ubuntu:~/advance_gawk$ gawk -f script1.sh n=3 data1.txt
data13
data23
data33

第一个例子显示了文件的第二个数据字段(n=2),第二个例子显示了第三个数据字段(n=3),只要在命令行上设置n变量的值就行。这个特性可以让用户在不改变脚本代码的情况下就能够改变脚本的行为,使用命令行参数来定义变量值会有一个问题,就是当设置了变量后,这个值在代码的BEGIN部分不可用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/advance_gawk$ cat script2.sh
BEGIN{print "The starting value is",n; FS=","}
{print $n}
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk -f script2.sh n=3 data1.txt
The starting value is
data13
data23
data33

如果要解决这个问题,可以用-v命令行参数,此参数允许你在BEGIN代码之前设定变量。在命令行上,
-v命令行参数必须放在脚本代码之前,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_gawk$ gawk -v n=3 -f script2.sh data1.txt
The starting value is 3
data13
data23
data33

从结果中可以看出,现在在BEGIN代码部分中的变量n的值已经是命令行上设定的那个值了。

处理数组

为了在单个变量中存储多个值,许多编程语言都提供数组。gawk编程语言使用关联数组提供数组功能。关联数组跟数字数组不同之处在于,关联数据的索引值可以是任意文本字符串(我觉得跟Python中的字典类似)。你不需要用连续的数字来标识数组中的数据元素。相反,关联数组用各种字符串来引用值。每个索引字符串都必须能够唯一地标识出赋给它的数据元素。

定义数组变量

可以用标准赋值语句来定义数组变量。数组变量赋值的格式为var[index]=element其中var是变量名,index是关联数组的索引值,element是数据元素值,在引用数组变量时,必须包含索引值来提取相应的数据元素值,下面是一些gawk中数组变量的例子,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
> capital["Zhengzhou"]="Henan"
> print capital["Zhengzhou"]
> }'
Henan

代码解释:与var[index]=element对应的是:capital是一个变量名,Zhengzhou是一个索引值,Henan是数据元素值。

在引用数组变量时,会得到数据元素的值。数据元素值是数字值时也一样,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
> var[1]=34
> var[2]=3
> total=var[1]+var[2]
> print total
> }'
37

遍历数组变量

关联数组变量的问题在于你可能无法知晓索引值是什么。跟使用连续数字作为索引值的数字数组不同,关联数组的索引可以是任何东西。如果要在gawk中遍历一个关联数组,可以用for语句的一种特殊形式,格式如下所示:

1
2
3
4
for (var in array)
{
statements
}

这个for语句会在每次循环时将关联数组array的下一个索引值赋给变量var,然后执行一遍statements。这个变量中存储的是索引值而不是数组元素值,可以将这个变量用作数组的索引,轻松地取出数据元素值,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
> var["a"]=1
> var["g"]=2
> var["m"]=3
> var["u"]=4
> for (test in var)
> {
> print "Index:",test," - Value:",var[test]
> }
> }'
Index: u - Value: 4
Index: m - Value: 3
Index: a - Value: 1
Index: g - Value: 2

从结果可以发现,索引值返回的顺序并没有按照变量的变量进行返回,不过它们都能够指向对应的数据元素值,这一点需要注意。

删除数组变量

从关联数组中删除数组索引的命令为delete array[index],删除命令会从数组中删除关联索引值和相关的数据元素值,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
> var["a"]=1
> var["g"]=2
> for (test in var)
> {
> print "Index:",test,"-Value:",var[test]
> }
> delete var["g"]
> print "---"
> for (test in var)
> {
> print "Index:",test,"-Value:",var[test]
> }
> }'
Index: a -Value: 1
Index: g -Value: 2
---
Index: a -Value: 1

一旦从关联数组中删除了索引值,你就没法再用它来提取元素值。

使用模式

gawk程序支持多种类型的匹配模式来过滤数据记录,BEGIN和END关键字是用来在读取数据流之前或之后执行命令的特殊模式。类似地,用户可以创建其他模式在数据流中出现匹配数据时执行一些命令。

正则表达式

在使用正则表达式时,正则表达式必须出现在它要控制的程序脚本的左花括号前,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","} /11/{print $1}' data1.txt
Data11

正则表达式/11/匹配了数据字段中含有字符串11的记录。gawk程序会用正则表达式对记录中所有的数据字段进行匹配,包括字段分隔符,如下所示:

1
2
3
4
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","}/,d/{print $1}' data1.txt
Data11
data21
data31

这个例子使用正则表达式匹配了用作字段分隔符的逗号(/,)。但这可能会造成如下问题:当试图匹配某个数据字段中的特定数据时,这些数据又出现在其他数据字段中。如果需要用正则表达式匹配某个特定的数据实例,应该使用匹配操作符。

匹配操作符

匹配操作符(matching operator)允许将正则表达式限定在记录中的特定数据字段。匹配操作符是波浪线(~)。可以指定匹配操作符、数据字段变量以及要匹配的正则表达式。例如$1~/^data/中,$1变量代表记录中的第1个数据字段,这个表达式会过滤出第1个字段以文本data开头的所有记录,下面是gawk使用匹配操作符的例子,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","} $2~/data2/{print $0}' data1.txt
data21,data22,data23,data24,data25

匹配操作符会用正则表达式/^data2/来比较第二个数据字段,该正则表达式指明字符串要以文本data2开头,常常利用gawk的这个特性来搜索相应的数据元素,如下所示:

1
2
biotest@ubuntu:~/advance_gawk$ gawk -F: '$1~/biotest/{print $1,$NF}' /etc/passwd
biotest /bin/bash

这段代码会在第一个数据字段中查找文本biotest。如果在记录中找到了这个模式,它会打印该记录的第一个和最后一个数据字段值。也可以用!符号来排除正则表达式的匹配,即$1 !~/expression/,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/advance_gawk$ gawk -F: '$1 !~ /biotest/{print $1, $NF}' /etc/passwd
root /bin/bash
daemon /usr/sbin/nologin
bin /usr/sbin/nologin
sys /usr/sbin/nologin
sync /bin/sync
...

在这段代码中,gawk程序脚本会打印/etc/passwd文件中与用户ID biotest不匹配的用户ID和登录shell。

数学表达式

除了正则表达式,也可以在匹配模式中用数学表达式。这个功能在匹配数据字段中的数字值时非常方便。举个例子,如果你想显示所有属于root用户组(组ID为0)的系统用户,可以用以下的脚本:

1
2
biotest@ubuntu:~/advance_gawk$ gawk -F: '$4 ==0{print $1}' /etc/passwd
root

这段脚本会查看第四个数据字段含有值 0 的记录。在这个Linux系统中,有1个用户账户属于root用户组。

可以使用任何常见的数学比较表达式,如下所示:

1
2
3
4
5
x == y :值x等于y。
x <= y :值x小于等于y。
x < y :值x小于y。
x >= y :值x大于等于y。
x > y :值x大于y。

也可以对文本数据使用表达式,但这一点与正则表达式不同,gawk的表达式必须完全匹配,数据必须跟模式严格匹配,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk -F, '$1=="data"{print $1}' data1.txt
biotest@ubuntu:~/advance_gawk$ gawk -F, '$1=="Data11"{print $1}' data1.txt
Data11

第一个测试没有匹配任何记录,因为第一个数据字段的值不在任何记录中。第二个测试用值Data11匹配了一条记录。

结构化命令

gawk编程语言支持常见的结构化编程命令。

if语句

gawk编程语言支持标准的if-then-else格式的if语句。必须为if语句定义一个求值的条件,并将其用圆括号括起来。如果条件求值为TRUE,紧跟在if语句后的语句会执行。如果条件求值为FALSE,这条语句就会被跳过,格式如下:

1
2
3
4
if (conditon)
statement1
# 或者放到一行上
if (condition) statement1

下面的例子就说明了这种用法:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_gawk$ cat data4.txt10
5
13
50
34
biotest@ubuntu:~/advance_gawk$ gawk '{if($1 > 20) print $1}' data4.txt
50
34

如果需要在if语句中执行多条语句,就必须用花括号将它们括起来,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/advance_gawk$ gawk '{
> if($1>20)
> {
> x=$1*2
> print x
> }
> }' data4.txt
100
68

gawk的if语句也支持else子句,允许在if语句条件不成立的情况下执行一条或多条语句,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
biotest@ubuntu:~/advance_gawk$ gawk '{
> if ($1>20)
> {
> x=$1*2
> print x
> } else
> {
> x=$1/2
> print x
> }}' data4.txt
5
2.5
6.5
100
68

可以在单行上使用else子句,但必须在if语句部分之后使用分号,格式为if (condition) statement1; else statement2,案例如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/advance_gawk$ gawk '{if ($1>20) print $1*2;else print $1/2}' data4.txt
5
2.5
6.5
100
68

while 语句

whlie语句的基本格式为:

1
2
3
4
while (conditoin)
{
statements
}

while循环可以遍历一组数据,并检查迭代的结束条件。如果在计算中必须使用每条记录中的多个数据值,就能够使用到while语句,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
biotest@ubuntu:~/advance_gawk$ cat data5.txt
130 120 135
160 113 140
145 170 215
biotest@ubuntu:~/advance_gawk$ gawk '{
> total=0
> i=1
> while(i<4)
> {
> total+=$i
> i++
> }
> avg=total/3
> print "Average:",avg
> }' data5.txt
Average: 128.333
Average: 137.667
Average: 176.667

while语句会遍历记录中的数据字段,将每个值都加到total变量上,并将计数器变量i增值。当计数器值等于4时,while的条件变成了FALSE,循环结束,然后执行脚本中的下一条语句。这条语句会计算并打印出平均值。这个过程会在数据文件中的每条记录上不断重复。

gawk编程语言支持在while循环中使用break语句和continue语句,允许你从循环中跳出,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
biotest@ubuntu:~/advance_gawk$ gawk '{
> total=0
> i=1
> while(i<4)
> {
> total +=$i
> if(i==2)
> break
> i++
> }
> avg=total/2
> print "The average of the first two data elements is:",avg
> }' data5.txt
The average of the first two data elements is: 125
The average of the first two data elements is: 136.5
The average of the first two data elements is: 157.5

break语句用来在i变量的值为2时从while循环中跳出。

do-while 语句

do-while语句会在检查条件语句之前执行命令,下面是do-while语句的格式。

1
2
3
4
5
do
{
statements
} while(condition)
}

这种格式保证了语句会在条件被求值之前至少执行一次,当需要在求值条件前执行语句时,这个特性非常方便,案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_gawk$ gawk '{
> total=0
> i=1
> do
> {
> total+=$i
> i++
> } while (total<150)
> print total}' data5.txt
250
160
315

这个脚本会读取每条记录的数据字段并将它们加在一起,直到累加结果达到150。如果第一个数据字段大于150(就像在第二条记录中看到的那样),则脚本会保证在条件被求值前至少读取第一个数据字段的内容。

for语句

格式为for (variable assignment; condition; iteration process),案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_gawk$ gawk '{
> total=0
> for (i=1;i<4;i++)
> {
> total+=$i
> }
> avg=total/3
> print "Average:",avg
> }' data5.txt
Average: 128.333
Average: 137.667
Average: 176.667

格式化打印

gawk中的格式化打印命令是printf,格式为printf "format string",vaar1,var2...,其中format string是关键部分,它会用文本元素和格式化指定符

来具体指定如何呈现格式化输出。格式化指定符是一种特殊的代码,会指明显示什么类型的变量以及如何显示。gawk程序会将每个格式化指定符作为占位符,供命令中的变量使用。第一个格式化指定符对应列出的第一个变量,第二个对应第二个变量,依此类推。格式化指定符的格式为%[modifier] control-letter,其中,control-letter是一个单字符代码,用于指明显示什么类型的数据,而modifier则定义了可选的格式化特性。下列出了可用在格式化指定符中的控制字母(其实很多编程语言的格式化输出大同小异)。

控制字母 描述
c 将一个数作为ASCII字符显示
d 显示一个整数值
i 显示一个整数值(跟 d 一样)
e 用科学计数法显示一个数
f 显示一个浮点值
g 用科学计数法或浮点数显示(选择较短的格式)
o 显示一个八进制值
s 显示一个文本字符串
x 显示一个十六进制值
X 显示一个十六进制值,但用大写字母A~F

因此,如果显示一个字符串变量,可以用格式化指定符%s。如果要显示一个整数值,可以用%d%i%d是十进制数的C风格显示方式)。如果你要用科学计数法显示很大的值,就用%e格式化指定符,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
> x=10*100
> printf "The answer is: %e\n",x
> }'
The answer is: 1.000000e+03

除了控制字母外,还有3种修饰符可以用来进一步控制输出:

  1. width:指定了输出字段最小宽度的数字值。如果输出短于这个值, printf 会将文本右 对齐,并用空格进行填充。如果输出比指定的宽度还要长,则按照实际的长度输出。 
  2. prec:这是一个数字值,指定了浮点数中小数点后面位数,或者文本字符串中显示的最 大字符数。 
  3. - (减号):指明在向格式化空间中放入数据时采用左对齐而不是右对齐。

下面是一个案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
biotest@ubuntu:~/advance_gawk$ cat data2.txt
Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234
Frank Williams
456 Oak Street
Indianapolis, IN 46201
(317)555-9876
Haley Snell
4231 Elm Street
Detroit, MI 48201
(313)555-4938
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS="\n";RS=""}{print $1,$4}' data2.txt
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS="\n";RS=""}{printf "%s %s\n",$1,$4}' data2.txt
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938
# printf会产生跟print命令相同的输出。printf命令用%s格式化指定符来作为这两个字符串值的占位符。

在printf命令的末尾手动添加换行符来生成新行。没添加的话,printf命令会继续在同一行打印后续输出。如果需要用几个单独的printf命令在同一行上打印多个输出,这就会非常有用,如下所示:

1
2
3
4
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","}{printf "%s ",$1}' data1.txt
Data11 data21 data31 biotest@ubuntu:~/advance_gawk$ gawtf "%s ",$1} END{printf "\n"}' data1.txt
Data11 data21 data31
biotest@ubuntu:~/advance_gawk$

从结果可以看出,第一次命令没有加换行符,结果出来后直接就是命令提示符,即biotest@ubuntu:~/advance_gawk$,如果加换行符后,就正常了,再看一个案例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
biotest@ubuntu:~/advance_gawk$ cat data2.txtRiley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234
Frank Williams
456 Oak Street
Indianapolis, IN 46201
(317)555-9876
Haley Snell
4231 Elm Street
Detroit, MI 48201
(313)555-4938
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS="\n";RS=""}{printf "%16s %s\n",$1,$4}' data2.txt
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938

通过添加一个值为16的修饰符,这表示强制第一个字符串的输出宽度为16个字符。默认情况下,printf命令使用右对齐来将数据放到格式化空间中。要改成左对齐,只需给修饰符加一个减号即可,如下所示:

1
2
3
4
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS="\n";RS=""}{printf "%-16s %s\n",$1,$4}' data2.txt
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938

printf命令也能处理浮点值,通过为变量指定一个格式,可以让输出看起来更统一,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
biotest@ubuntu:~/advance_gawk$ cat data5.txt
130 120 135
160 113 140
145 170 215
biotest@ubuntu:~/advance_gawk$ gawk '{
> total=0
> for (i=0;i<4;i++)
> {
> total+=$i
> }
> avg=total/3
> printf "Average: %5.1f\n",avg
> }' data5.txt
Average: 171.7
Average: 191.0
Average: 225.0

可以使用%5.1f格式指定符来强制printf命令输出5位的浮点数,并且将浮点值近似到小数点后一位。

内建函数

gawk内置了一些处理数字、字符串以及时间的函数。

数学函数

如下表所示:

函数 描述
atan2(x, y) x/y的反正切,x和y以弧度为单位
cos(x) x的余弦,x以弧度为单位
exp(x) x的指数函数
int(x) x的整数部分,取靠近零一侧的值,5.6取5,-5.6取-5。
log(x) x的自然对数
rand( ) 比0大比1小的随机浮点值,创建随机数
sin(x) x的正弦,x以弧度为单位
sqrt(x) x的平方根
srand(x) 为计算随机数指定一个种子值

gawk语言对于它能够处理的数值有一个限定区间,如果超出了这个区间,就会得到一条错误消息,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{x=exp(100);print x}'
26881171418161356094253400435962903554686976
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{x=exp(1000);print x}'
gawk: cmd. line:1: warning: exp: argument 1000 is out of range
inf

第一个例子会计算e的100次幂,虽然数值很大,但尚在系统的区间内。第二个例子尝试计算e的1000次幂,已经超出了系统的数值区间,所以就生成了一条错误消息。

gawk还有一些按位操作数据的函数,如下表所示:

运算 描述
and(v1,v2) 执行值v1和v2的按位与运算。
compl(val) 执行val的补运算。
lshift(val,count) 将值val左移count位
or(v1,v2) 执行值v1和v2的按位或运算
rshift(val,count) 将值val右移count位。
xor(v1,v2) 执行值v1和v2的按位异或运算。

位操作函数通常用于听任二进制值时。

字符串函数

gawk的字符串函数如下表所示:

函数 描述
asort(s[,d]) 将数组s按数据元素值排序。索引值会被替换成表示新的排序顺序的连续数字。另外,如果指定了d,则排序后的数组会存储在数组d中
asorti(s[,d]) 将数组s按索引值排序。生成的数组会将索引值作为数据元素值,用连续数字索引来表明排序顺序。另外如果指定了d,排序后的数组会存储在数组d中
gensub(r,s,h[,t]) 查找变量$0或目标字符串t(如果提供了的话)来匹配正则表达式r。如果h是一个以g或G开头的字符串,就用s替换掉匹配的文本。如果h是一个数字,它表示要替换掉第h处r匹配的地方
gsub(r,s[,t]) 查找变量$0或目标字符串t(如果提供了的话)来匹配正则表达式r。如果找到了,就全部替换成字符串s
index(s,t) 返回字符串t在字符串s中的索引值,如果没找到的话返回0
length([s]) 返回字符串s的长度;如果没有指定的话,返回$0的长度
match(s,r[,a]) 返回字符串s中正则表达式r出现位置的索引。如果指定了数组a,它会存储s中匹配正则表达式的那部分
split(s,a[,r]) 将s用FS字符或正则表达式r(如果指定了的话)分开放到数组a中。返回字段的总数
sprintf(format,variables) 用提供的format和variables返回一个类似于printf输出的字符串
sub(r,s[,t]) 在变量$0或目标字符串t中查找正则表达式r的匹配。如果找到了,就用字符串s替换掉第一处匹配
substr(s,i[,n]) 返回s中从索引值i开始的n个字符组成的子字符串。如果未提供n,则返回s剩下的部分
tolower(s) 将s中的所有字符转换成小写
toupper(s) 将s中的所有字符转换成大写

下面是一个案例:

1
2
3
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{x="testing";print toupper(x);print length(x)}'
TESTING
7

但一些字符串函数的用法相当复杂。asort和asorti函数是新加入的gawk函数,允许你基于数据元素值(asort)或索引值(asorti)对数组变量进行排序。这里有个使用asort的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
var["a"]=11
var["g"]=2
var["m"]=31
var["u"]=14
asort(var,test)
for(i in test)
print "Index:",i," - value:",test[i]
}'
Index: 1 - value: 2
Index: 2 - value: 11
Index: 3 - value: 14
Index: 4 - value: 31
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
var["a"]=11
var["g"]=2
var["m"]=31
var["u"]=14
asorti(var,test)
for(i in test)
print "Index:",i," - value:",test[i]
}'
Index: 1 - value: a
Index: 2 - value: g
Index: 3 - value: m
Index: 4 - value: u

从结果可以看出,新数组test含有排序后的原数组的数据元素,但索引值现在变为表明正确顺序的数字值了。asort是对数组的值进行排序;而asorti是对数组的索引进行排序。

split 函数是将数据字段放到数组中以供进一步处理的好办法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_gawk$ cat data1.txt
Data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{FS=","}{
> split($0,var)
> print var[1],var[5]
> }' data1.txt
Data11 data15
data21 data25
data31 data35

新数组使用连续数字作为数组索引,从含有第一个数据字段的索引值 1 开始。

时间函数

gawk的时间是函数下如下表所示:

函数 描述
mktime(datespec) 将一个按YYYY MM DD HH MM SS [DST]格式指定的日期转换成时间戳值
strftime(format [,timestamp]) 将当前时间的时间戳或timestamp(如果提供了的话)转化格式化日期(采用shell函数date()的格式)
systime( ) 返回当前时间的时间戳

时间函数常用来处理日志文件,而日志文件则常含有需要进行比较的日期。通过将日期的文本表示形式转换成epoch时间(自1970-01-0100:00:00UTC到现在的秒数),可以轻松地比较日期。下面是在gawk程序中使用时间函数的例子。

1
2
3
4
5
6
biotest@ubuntu:~/advance_gawk$ gawk 'BEGIN{
> date=systime()
> day=strftime("%A,%B %d,%Y",date)
> print day
> }'
Tuesday,May 15,2018

该例用systime函数从系统获取当前的epoch时间戳,然后用strftime函数将它转换成用户可读的格式,转换过程中使用了shell命令date的日期格式化字符。

自定义函数

自定义函数需要用到function关键字,如下所示:

1
2
3
4
function name([variables])
{
statements
}

下面看一个简单的函数:

1
2
3
4
function printthird()
{
print $3
}

这个函数会打印记录中的第3个数据字段,函数还可以使用return语句返回值,即return value,值可以是变量,也可以是表达,如下所示:

1
2
3
4
funciotn myrand(limit)
{
return int(limit*rand())
}

可以将函数的返回值赋值给gawk程序中的一个变量,例如x=myrand(100),这个变量包含函数的返回值。

自定义函数使用

在定义函数时,它必须出现在所有代码块之前(包括BEGIN代码块)。这种形式有助于将函数代码与gawk程序的其他部分分开,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_gawk$ gawk '
> function myprint()
> {
> printf "%-16s - %s\n",$1,$4
> }
> BEGIN{FS="\n";RS=""}
> {
> myprint()
> }' data2.txt
Riley Mullen - (312)555-1234
Frank Williams - (317)555-9876
Haley Snell - (313)555-4938

这个函数定义了myprint()函数,它会格式化记录中的第一个和第四个数据字段以供打印输出。gawk程序然后用该函数显示出数据文件中的数据。一旦定义了函数,就能在程序的代码中随意使用。在涉及很大的代码量时,这会省去许多工作。

创建函数库

gawk提供了一种途径来将多个函数 放到一个库文件中,这样能在所有的gawk程序中使用了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat funclib
function myprint()
{
printf "%-16s-%s\n",$1,$4
}
function myrand(limit)
{
return int(limit*rand())
}
function printthird()
{
print $3
}

funclib文件含有三个函数定义。需要使用-f命令行参数来使用它们。但不能将-f命令行参数和内联gawk脚本放到一起使用,不过可以在同一个命令行中使用多个-f参数。因此,要使用库,只要创建一个含有你的gawk程序的文件,然后在命令行上同时指定库文件和程序文件就行了,如下所示:

1
2
3
4
5
cat script4.sh
BEGIN{ FS="\n";RS=""}
{
myprint()
}

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
biotest@ubuntu:~/advance_gawk$ cat data2.txt
Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234
Frank Williams
456 Oak Street
Indianapolis, IN 46201
(317)555-9876
Haley Snell
4231 Elm Street
Detroit, MI 48201
(313)555-4938
biotest@ubuntu:~/advance_gawk$ gawk -f funclib.sh -f script4.sh data2.txt
Riley Mullen -(312)555-1234
Frank Williams -(317)555-9876
Haley Snell -(313)555-4938

实例

现在有一个数据文件,其中包含了两支队伍(每队两名选手)的保龄球比赛 得分情况,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_gawk$ cat scores.txt
Rich Blum, team1, 100,115,95
Barbara Blum, team1, 110, 115,100
Christine Bresnahan, team2, 120, 115,118
Time Bresnahan, team2, 125, 112, 116

每位选手都有三场比赛的成绩,这些成绩都保存在数据文件中,每位选手由位于第二列的队名来标识。下面的脚本对每队的成绩进行了排序,并计算了总分和平均分,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
for team in $(gawk 'BEGIN{FS=","}{print $2}' scores.txt| uniq)
do
gawk -v team=$team 'BEGIN{FS=",";total=0}
{
if($2==team)
{
total+=$3+$4+$5;
}
}
END {
avg=total/6;
print "Total for",team, "is",total,",the average is",avg
}' scores.txt
done

运行结果如下所示:

1
2
3
biotest@ubuntu:~/advance_gawk$ bash bowling.sh
Total for team1 is 635 ,the average is 105.833
Total for team2 is 706 ,the average is 117.667