十二、shell编程之awk

12.1 什么是awk

虽然sed编辑器是非常方便自动修改文本文件的工具,但其也有自身的限制。通常你需要一个用来处理文件中的数据的更高级工具,它能提供一个类编程环境来修改和重新组织文件中的数据。这正是awk能够做到的。

awk程序是Unix中的原始awk程序的GNU版本。 awk程序让流编辑迈上了一个新的台阶,它提供了一种编程语言而不只是编辑器命令。在awk编程语言中,你可以做下面的事情:

**·**定义变量来保存数据;

**·**使用算术和字符串操作符来处理数据;

**·**使用结构化编程概念(比如if-then语句和循环)来为数据处理增加处理逻辑;

**·**通过提取数据文件中的数据元素,将其重新排列或格式化,生成格式化报告。

awk程序的报告生成能力通常用来从大文本文件中提取数据元素,并将它们格式化成可读的报告。其中最完美的例子是格式化日志文件。在日志文件中找出错误行会很难, awk程序可以让你从日志文件中过滤出需要的数据元素,然后你可以将其格式化,使得重要的数据更易于阅读。

12.2 awk命令格式

awk程序的基本格式如下:

复制代码
awk options program file  #选项 程序 文件

命令行选项提供了一个简单的途径来定制awk程序中的功能。我们会在探索awk时进一步了解这些选项。

awk的强大之处在于程序脚本。可以写脚本来读取文本行的数据,然后处理并显示数据,创建任何类型的输出报告。

12.3 awk的基本使用

12.3.1 从命令行读取程序脚本

awk程序脚本用一对花括号来定义。你必须将脚本命令放到两个花括号( {}) 中。如果你错误地使用了圆括号来包含awk脚本,就会得到一条类似于下面的错误提示。

复制代码
[root@kittod ~]# awk '(print "Hello World!"}'
awk: cmd. line:1: (print "Hello World!"}
awk: cmd. line:1: ^ syntax error

由于awk命令行假定脚本是单个文本字符串,你还必须将脚本放到单引号中。下面的例子在命令行上指定了一个简单的awk程序脚本:

复制代码
[root@kittod ~]# awk '{print "Hello World!"}'

这个程序脚本定义了一个命令: print命令。这个命令名副其实:它会将文本打印到STDOUT。如果尝试运行这个命令,你可能会有些失望,因为什么都不会发生。原因在于没有在命令行上指定文件名,所以awk程序会从STDIN接收数据。在运行这个程序时,它会一直等待从STDIN输入的文本。

如果你输入一行文本并按下回车键, awk会对这行文本运行一遍程序脚本。跟sed编辑器一样, awk程序会针对数据流中的每行文本执行程序脚本。由于程序脚本被设为显示一行固定的文本字符串,因此不管你在数据流中输入什么文本,都会得到同样的文本输出。

复制代码
[root@kittod ~]# awk '{print "Hello World!"}'
This is a test
Hello World!
haha
Hello World!
^C

12.3.2 使用数据字段变量

awk的主要特性之一是其处理文本文件中数据的能力。它会自动给一行中的每个数据元素分配一个变量。默认情况下, awk会将如下变量分配给它在文本行中发现的数据字段:

·$0代表整个文本行;

·$1代表文本行中的第1个数据字段;

·$2代表文本行中的第2个数据字段;

·$n代表文本行中的第n个数据字段。

在文本行中,每个数据字段都是通过字段分隔符划分的。 awk在读取一行文本时,会用预定义的字段分隔符划分每个数据字段。 awk中默认的字段分隔符是任意的空白字符(例如空格或制表符)。

在下面的例子中, awk程序读取文本文件,只显示第1个数据字段的值。

复制代码
[root@kittod ~]# cat data2.txt
One line of test text.
Two lines of test text.
Three lines of test text.
[root@kittod ~]# awk '{print $1}' data2.txt
One
Two
Three

该程序用$1字段变量来仅显示每行文本的第1个数据字段。如果你要读取采用了其他字段分隔符的文件,可以用-F选项指定。

复制代码
[root@kittod ~]# awk -F: '{print $1}' /etc/passwd | head -3
root
bin
daemon

这个简短的程序显示了系统中密码文件的第1个数据字段。由于/etc/passwd文件用冒号来分隔数字字段,因而如果要划分开每个数据元素,则必须在awk选项中将冒号指定为字段分隔符。

12.3.3 在脚本中使用多个命令

如果一种编程语言只能执行一条命令,那么它不会有太大用处。 awk编程语言允许你将多条命令组合成一个正常的程序。要在命令行上的程序脚本中使用多条命令,只要在命令之间放个分号即可。

复制代码
[root@kittod ~]# echo "My name is Rich" | awk '{$4="Charistine"; print $0}'
My name is Charistine

第一条命令会给字段变量$4赋值。第二条命令会打印整个数据字段。注意, awk程序在输出中已经将原文本中的第四个数据字段替换成了新值。

也可以用次提示符一次一行地输入程序脚本命令。

复制代码
[root@kittod ~]# awk '{
> $4="Christine"
> print $0}'
My name is Rich
My name is Christine

在你用了表示起始的单引号后, bash shell会使用次提示符来提示你输入更多数据。你可以每次在每行加一条命令,直到输入了结尾的单引号。因为没有在命令行中指定文件名, awk程序会从STDIN中获得数据。当运行这个程序的时候,它会等着读取来自STDIN的文本。要退出程序,只需按下Ctrl+D组合键来表明数据结束。

12.3.4 从文件中读取程序

跟sed编辑器一样, awk编辑器允许将程序存储到文件中,然后再在命令行中引用。

复制代码
[root@kittod ~]# cat script2.awk
{print $1 "'s home directory is " $6}
[root@kittod ~]# awk -F: -f script2.awk /etc/passwd | head -3
root's home directory is /root
bin's home directory is /bin
daemon's home directory is /sbin

-f:从指定文件读取程序

script2.awk程序脚本会再次使用print命令打印/etc/passwd文件的主目录数据字段(字段变量6),以及userid数据字段(字段变量1)。

可以在程序文件中指定多条命令。要这么做的话,只要一条命令放一行即可,不需要用分号。

复制代码
[root@kittod ~]# cat script3.awk
{
text = "'s home directory is "
print $1 text $6
}

[root@kittod ~]# awk -F: -f script3.awk /etc/passwd | head -3
root's home directory is /root
bin's home directory is /bin
daemon's home directory is /sbin

script3.awk程序脚本定义了一个变量来保存print命令中用到的文本字符串。注意, awk程序在引用变量值时并未像shell脚本一样使用美元符。

12.3.5 在处理数据前运行脚本

awk还允许指定程序脚本何时运行。默认情况下, awk会从输入中读取一行文本,然后针对该行的数据执行程序脚本。有时可能需要在处理数据前运行脚本,比如为报告创建标题。BEGIN关键字就是用来做这个的。它会强制awk在读取数据前执行BEGIN关键字后指定的程序脚本。

复制代码
[root@kittod ~]# awk 'BEGIN {print "Hello World!"}'
Hello World!

这次print命令会在读取数据前显示文本。但在它显示了文本后,它会快速退出,不等待任何数据。如果想使用正常的程序脚本中处理数据,必须用另一个脚本区域来定义程序。

复制代码
[root@kittod ~]# cat data3.txt
Line 1
Line 2
Line 3

[root@kittod ~]# awk 'BEGIN {print "The data3 File Contents:"}
> {print $0}' data3.txt
The data3 File Contents:
Line 1
Line 2
Line 3

在awk执行了BEGIN脚本后,它会用第二段脚本来处理文件数据。这么做时要小心,两段脚本仍然被认为是awk命令行中的一个文本字符串。你需要相应地加上单引号。

12.3.6 在处理数据后运行脚本

与BEGIN关键字类似, END关键字允许你指定一个程序脚本, awk会在读完数据后执行它。

复制代码
[root@kittod ~]# awk 'BEGIN {print "The data3 File Contents:"}
> {print $0}
> END {print "End of File"}' data3.txt
The data3 File Contents:
Line 1
Line 2
Line 3
End of File

当awk程序打印完文件内容后,它会执行END脚本中的命令。这是在处理完所有正常数据后给报告添加页脚的最佳方法。

可以将所有这些内容放到一起组成一个漂亮的小程序脚本文件,用它从一个简单的数据文件中创建一份完整的报告。

复制代码
[root@kittod ~]# cat script4.awk
BEGIN {
print "The latest list of users and shells"
print " UserID \t Shell"
print "--------\t ---------"
FS=":"
}

{
print $1 "       \t " $7
}

END {
print "This concludes the listing"
}

这个脚本用BEGIN脚本来为报告创建标题。它还定义了一个叫作FS的特殊变量。这是定义字段
分隔符的另一种方法。这样你就不用依靠脚本用户在命令行选项中定义字段分隔符了。

[root@kittod ~]# awk -f script4.awk /etc/passwd
The latest list of users and shells
 UserID Shell
-------- ---------
root 	/bin/bash
bin 	/sbin/nologin
daemon 		/sbin/nologin
....
sshd 		/sbin/nologin
tcpdump 	/sbin/nologin
redhat 		/bin/bash
This concludes the listing

与预想的一样, BEGIN脚本创建了标题,程序脚本处理特定数据文件( /etc/passwd)中的信息, END脚本生成页脚。

12.4 awk高级用法

12.4.1 使用变量

所有编程语言共有的一个重要特性是使用变量来存取值。awk编程语言支持两种不同类型的变量:

**·**内建变量

**·**自定义变量

awk有一些内建变量。这些变量存放用来处理数据文件中的数据字段和记录的信息。你也可以在awk程序里创建你自己的变量。下面将带你了解如何在awk程序里使用变量。

12.4.1.1 内建变量

awk程序使用内建变量来引用程序数据里的一些特殊功能。

1、字段和记录分隔符变量

之前内容介绍了awk中的一种内建变量类型------数据字段变量。数据字段变量允许使用美元符号( )和字段在该记录中的位置值来引用记录对应的字段。因此,要引用记录中的第一个数据字段,就用变量1;要引用第二个字段,就用$2,依次类推。

数据字段是由字段分隔符来划定的。默认情况下,字段分隔符是一个空白字符,也就是空格符或者制表符。之前讲了如何在命令行下使用命令行参数-F或者在awk程序中使用特殊的内建变量FS来更改字段分隔符。

内建变量FS是一组内建变量中的一个,这组变量用于控制awk如何处理输入输出数据中的字段和记录。下表列出了这些内建变量。

变量FS和OFS定义了awk如何处理数据流中的数据字段。之前知道了如何使用变量FS来定义记录中的字段分隔符。变量OFS具备相同的功能,只不过是用在print命令的输出上。

默认情况下, awk将OFS设成一个空格,所以如果你用命令:

复制代码
print $1,$2,$3
会看到如下输出:
field1 field2 field3

在下面的例子里,你能看到这点。

复制代码
[root@kittod ~]# cat data1
data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
[root@kittod ~]# awk 'BEGIN{FS=","} {print $1,$2,$3}' data1
data11 data12 data13
data21 data22 data23
data31 data32 data33

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

复制代码
[root@kittod ~]# awk 'BEGIN{FS=","; OFS="-"}{print $1,$2,$3}' data1
data11-data12-data13
data21-data22-data23
data31-data32-data33

[root@kittod ~]# awk 'BEGIN{FS=","; OFS="<-->"}{print $1,$2,$3}' data1
data11<-->data12<-->data13
data21<-->data22<-->data23
data31<-->data32<-->data33

FIELDWIDTHS变量允许你不依靠字段分隔符来读取记录。在一些应用程序中,数据并没有使用字段分隔符,而是被放置在了记录中的特定列。这种情况下,必须设定FIELDWIDTHS变量来匹配数据在记录中的位置。

一旦设置了FIELDWIDTH变量, awk就会忽略FS变量,并根据提供的字段宽度来计算字段。

下面是个采用字段宽度而非字段分隔符的例子。

复制代码
[root@kittod ~]# awk 'BEGIN{FIELDWIDTHS="3 5 2 5"}{print $1,$2,$3,$4}' data1b #字段长度3 5 2 5 
100 5.324 75 96.37
115 -2.34 91 94.00
058 10.12 98 100.1

FIELDWIDTHS变量定义了四个字段, awk依此来解析数据记录。每个记录中的数字串会根据已定义好的字段长度来分割。

一定要记住,一旦设定了FIELDWIDTHS变量的值,就不能再改变了。这种方法并不适用于变长的字段。

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

复制代码
Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234

如果你用默认的FS和RS变量值来读取这组数据, awk就会把每行作为一条单独的记录来读取,并将记录中的空格当作字段分隔符。这可不是你希望看到的。

要解决这个问题,只需把FS变量设置成换行符。这就表明数据流中的每行都是一个单独的字段,每行上的所有数据都属于同一个字段。但现在令你头疼的是无从判断一个新的数据行从何开始。

对于这一问题,可以把RS变量设置成空字符串,然后在数据记录间留一个空白行。 awk会把每个空白行当作一个记录分隔符。

下面的例子使用了这种方法。

复制代码
[root@kittod ~]# cat data2
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

[root@kittod ~]# awk 'BEGIN{FS="\n";RS=""} {print $1,$4}' data2
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938

2、数据变量

除了字段和记录分隔符变量外, awk还提供了其他一些内建变量来帮助你了解数据发生了什么变化,并提取shell环境的信息。下表列出了awk中的其他内建变量。

你应该能从上面的列表中认出一些shell脚本编程中的变量。 ARGC和ARGV变量允许从shell中获得命令行参数的总数以及它们的值。但这可能有点麻烦,因为awk并不会将程序脚本当成命令行参数的一部分

复制代码
[root@kittod ~]# awk 'BEGIN{print ARGC,ARGV[1]}' data1
2 data1

ARGC变量表明命令行上有两个参数。这包括awk命令和data1参数(记住,程序脚本并不算参数)。 ARGV数组从索引0开始,代表的是命令。第一个数组值是awk命令后的第一个命令行参数。

注意:跟shell变量不同,在脚本中引用awk变量时,变量名前不加美元符。

ENVIRON变量看起来可能有点陌生。它使用关联数组来提取shell环境变量。关联数组用文本作为数组的索引值,而不是数值。

数组索引中的文本是shell环境变量名,而数组的值则是shell环境变量的值。下面有个例子。

复制代码
[root@kittod ~]# awk '
> BEGIN{
> print ENVIRON["HOME"]
> print ENVIRON["PATH"]
> }'
/root
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

ENVIRON["HOME"]变量从shell中提取了HOME环境变量的值。类似地,ENVIRON["PATH"]提取了PATH环境变量的值。可以用这种方法来从shell中提取任何环境变量的值,以供awk程序使用。

当要在awk程序中跟踪数据字段和记录时,变量FNR、 NF和NR用起来就非常方便。有时你并不知道记录中到底有多少个数据字段。NF变量可以让你在不知道具体位置的情况下指定记录中的最后一个数据字段。

复制代码
[root@kittod ~]# awk 'BEGIN{FS=":"; OFS=":"} {print $1,$NF}' /etc/passwd
root:/bin/bash
bin:/sbin/nologin
daemon:/sbin/nologin

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

FNR和NR变量虽然类似,但又略有不同。FNR变量含有当前数据文件中已处理过的记录数(这是第几行)

NR变量则含有已处理过的记录总数(已经处理的总的行数)。让我们看几个例子来了解一下这个差别。

复制代码
[root@kittod ~]# awk 'BEGIN{FS=","}{print $1,"FNR="FNR}' data1  data1
data11 FNR=1
data21 FNR=2
data31 FNR=3
data11 FNR=1
data21 FNR=2
data31 FNR=3

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

现在,让我们加上NR变量看看会输出什么。

复制代码
[root@kittod ~]# awk '
BEGIN {FS=","}
{print $1,"FNR="FNR,"NR="NR}
END{print "There were",NR,"records processed"}' data1 data1 
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变量的值在awk处理第二个数据文件时被重置了,而NR变量则在处理第二个数据文件时继续计数。结果就是:如果只使用一个数据文件作为输入, FNR和NR的值是相同的;如果使用多个数据文件作为输入, FNR的值会在处理每个数据文件时被重置,而NR的值则会继续计数直到处理完所有的数据文件。

说明 在使用awk时你可能会注意到, awk脚本通常会比shell脚本中的其他部分还要大一些。为了简单起见,在例子中,我们利用shell的多行特性直接在命令行上运行了awk脚本。在shell脚本中使用awk时,应该将不同的awk命令放到不同的行,这样会比较容易阅读和理解,不要在shell脚本中将所有的命令都塞到同一行。还有,如果发现在不同的shell脚本中用到了同样的awk脚本,记着将这段awk脚本放到一个单独的文件中,并用-f参数来在shell脚本中引用它。

12.4.1.2 自定义变量

跟其他典型的编程语言一样, awk允许你定义自己的变量在程序代码中使用。 awk自定义变量名可以是任意数目的字母、数字和下划线,但不能以数字开头。重要的是,要记住awk变量名区分大小写

1、在脚本中给变量赋值

在awk程序中给变量赋值跟在shell脚本中赋值类似,都用赋值语句。

复制代码
[root@kittod ~]# awk '
> BEGIN{
> testing="This is a test"
> print testing
> }'
This is a test

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

复制代码
[root@kittod ~]# awk '
> BEGIN{
> testing="This is a test"
> print testing
> testing=45
> print testing
> }'
This is a test
45

在这个例子中, testing变量的值从文本值变成了数值。

赋值语句还可以包含数学算式来处理数字值。

复制代码
[root@kittod ~]# awk 'BEGIN{x=4; x=x*2+3;print x}'
11

如你在这个例子中看到的, awk编程语言包含了用来处理数字值的标准算数操作符。其中包括求余符号( %)和幂运算符号( ^或**)。

2、在命令行上给变量赋值

也可以用awk命令行来给程序中的变量赋值。这允许你在正常的代码之外赋值,即时改变变量的值。下面的例子使用命令行变量来显示文件中特定数据字段。

复制代码
[root@kittod ~]# cat script1
BEGIN{FS=","}
{print $n}
[root@kittod ~]# awk -f script1 n=2 data1
data12
data22
data32
[root@kittod ~]# awk -f script1 n=3 data1
data13
data23
data33

这个特性可以让你在不改变脚本代码的情况下就能够改变脚本的行为。第一个例子显示了文件的第二个数据字段,第二个例子显示了第三个数据字段,只要在命令行上设置n变量的值就行。

使用命令行参数来定义变量值会有一个问题。在你设置了变量后,这个值在代码的BEGIN部分不可用。

复制代码
[root@kittod ~]# cat script2
BEGIN{print "The starting value is",n;FS=","}
{print $n}
[root@kittod ~]# awk -f script2 n=3 data1
The starting value is
data13
data23
data33

可以用-v命令行参数来解决这个问题。它允许你在BEGIN代码之前设定变量。在命令行上,-v命令行参数必须放在脚本代码之前。

复制代码
[root@kittod ~]# awk -v n=3 -f script2 data1
The starting value is 3
data13
data23
data33

12.4.2 处理数组

为了在单个变量中存储多个值,许多编程语言都提供数组。 awk编程语言使用关联数组提供数组功能。

关联数组跟数字数组不同之处在于它的索引值可以是任意文本字符串。你不需要用连续的数字来标识数组中的数据元素。相反,关联数组用各种字符串来引用值。每个索引字符串都必须能够唯一地标识出赋给它的数据元素。

12.4.2.1 定义数组变量

可以用标准赋值语句来定义数组变量。数组变量赋值的格式如下:

复制代码
var[index] = element

其中var是变量名, index是关联数组的索引值, element是数据元素值。下面是一些awk中数组变量的例子。

复制代码
capital["Illinois"] = "Springfield"
capital["Indiana"] = "Indianapolis"
capital["Ohio"] = "Columbus"

在引用数组变量时,必须包含索引值来提取相应的数据元素值。

复制代码
[root@kittod ~]# awk 'BEGIN{
> capital["Illinois"] = "Springfield"
> print capital["Illinois"]
> }'
Springfield

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

复制代码
[root@kittod ~]# awk 'BEGIN{
> var[1] = 34
> var[2] = 3
> total = var[1] + var[2]
> print total
> }'
37

正如你在该例子中看到的,可以像使用awk程序中的其他变量一样使用数组变量。

12.4.2.2 遍历数组变量

关联数组变量的问题在于你可能无法知晓索引值是什么。跟使用连续数字作为索引值的数字数组不同,关联数组的索引可以是任何东西。

如果要在awk中遍历一个关联数组,可以用for语句的一种特殊形式。

复制代码
for (var in array)
{
statements
}

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

复制代码
[root@kittod ~]# awk '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

注意,索引值不会按任何特定顺序返回,但它们都能够指向对应的数据元素值 。明白这点很重要,因为你不能指望着返回的值都是有固定的顺序,只能保证索引值和数据值是对应的。

12.4.2.3 删除数组变量

从关联数组中删除数组索引要用一个特殊的命令。

复制代码
delete array[index]

删除命令会从数组中删除关联索引值和相关的数据元素值。

复制代码
[root@kittod ~]# awk '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

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

12.4.3 使用模式

awk程序支持多种类型的匹配模式来过滤数据记录,这一点跟sed编辑器大同小异。BEGIN和END关键字是用来在读取数据流之前或之后执行命令的特殊模式。类似地,你可以创建其他模式在数据流中出现匹配数据时执行一些命

令。

12.4.3.1 正则表达式

前面介绍了如何将正则表达式用作匹配模式。可以用基础正则表达式( BRE)或扩展正则表达式( ERE)来选择程序脚本作用在数据流中的哪些行上。在使用正则表达式时,正则表达式必须出现在它要控制的程序脚本的左花括号前。

复制代码
[root@localhost ~]# awk 'BEGIN{FS=","} /11/{print $1}' data1
data11

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

复制代码
[root@kittod ~]# awk 'BEGIN{FS=","} /,d/{print $1}' data1
data11
data21
data31

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

12.4.3.2 匹配操作符

匹配操作符( matching operator)允许将正则表达式限定在记录中的特定数据字段。匹配操作符是波浪线( ~)。可以指定匹配操作符、数据字段变量以及要匹配的正则表达式。

复制代码
$1 ~ /^data/

$1变量代表记录中的第一个数据字段。这个表达式会过滤出第一个字段以文本data开头的所有记录。下面是在awk程序脚本中使用匹配操作符的例子。

复制代码
[root@kittod ~]# awk 'BEGIN{FS=","} $2 ~ /^data2/{print $0}' data1
data21,data22,data23,data24,data25

匹配操作符会用正则表达式/^data2/来比较第二个数据字段,该正则表达式指明字符串要以文本data2开头。

这可是件强大的工具, awk程序脚本中经常用它在数据文件中搜索特定的数据元素。

复制代码
[root@kittod ~]# awk -F: '$1 ~ /redhat/{print $1,$NF}' /etc/passwd #$NF最后一个字段
redhat /bin/bash

这个例子会在第一个数据字段中查找文本redhat。如果在记录中找到了这个模式,它会打印该记录的第一个和最后一个数据字段值。

也可以用!符号来排除正则表达式的匹配。

复制代码
$1 !~ /expression/

如果记录中没有找到匹配正则表达式的文本,程序脚本就会作用到记录数据。

复制代码
[root@kittod ~]# awk -F: '$1 !~ /redhat/{print $1,$NF}' /etc/passwd
root /bin/bash
bin /sbin/nologin
...

在这个例子中, awk程序脚本会打印/etc/passwd文件中与用户ID redhat不匹配的用户ID和登录shell。

12.4.3.3 数学表达式

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

复制代码
[root@kittod ~]# awk -F: '$4 == 0{print $1}' /etc/passwd
root
sync
shutdown
halt
operator

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

可以使用任何常见的数学比较表达式。

x == y:值x等于y。

x <= y:值x小于等于y。

x < y:值x小于y。

x >= y:值x大于等于y。

x > y:值x大于y。

也可以对文本数据使用表达式,但必须小心。跟正则表达式不同,表达式必须完全匹配。数据必须跟模式严格匹配

复制代码
[root@kittod ~]# awk -F, '$1 == "data"{print $1}' data1
[root@kittod ~]# awk -F, '$1 == "data11"{print $1}' data1
data11

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

12.4.4 结构化命令

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

12.4.4.1 if 语句

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

复制代码
if (condition)
statement1

也可以将它放在一行上,像这样:

复制代码
if (condition) statement1

下面这个简单的例子:

复制代码
[root@kittod ~]# cat data4
10
5
13
50
34

[root@kittod ~]# awk '{if ($1 > 20) print $1}' data4
50
34

并不复杂。如果需要在if语句中执行多条语句,就必须用花括号将它们括起来。

复制代码
[root@kittod ~]# awk '{
> if ($1 > 20)
> {
> x = $1 * 2
> print x
> }
> }' data4
100
68

注意,不能弄混if语句的花括号和用来表示程序脚本开始和结束的花括号。如果弄混了,awk程序能够发现丢失了花括号,并产生一条错误消息。

复制代码
[root@kittod ~]# awk '{
> if ($1 > 20)
> {
> x = $1 * 2
> print x
> }' data4
awk: cmd. line:6: }
awk: cmd. line:6: ^ unexpected newline or end of string

awk的if语句也支持else子句,允许在if语句条件不成立的情况下执行一条或多条语句。这里有个使用else子句的例子

复制代码
[root@kittod ~]# awk '{
> if ($1 > 20)
> {
> x = $1 * 2
> print x
> } else
> {
> x = $1 / 2
    > print x
> }}' data4
5
2.5
6.5
100
68

可以在单行上使用else子句,但必须在if语句部分之后使用分号。

复制代码
if (condition) statement1; else statement2

以下是上一个例子的单行格式版本。

复制代码
[root@kittod ~]# awk '{if ($1 > 20) print $1 * 2; else print $1 / 2 }' data4
5
2.5
6.5
100
68

这个格式更紧凑,但也更难理解。

12.4.4.2 while 语句

while语句为awk程序提供了一个基本的循环功能。下面是while语句的格式。

复制代码
while (condition)
{
statements
}

while循环允许遍历一组数据,并检查迭代的结束条件。如果在计算中必须使用每条记录中的多个数据值,这个功能能帮得上忙。

复制代码
[root@kittod ~]# cat data5
130 120 135
160 113 140
145 170 215
[root@kittod ~]# awk '{
> total = 0
> i = 1
> while (i < 4)
> {
> total += $i
> i++
> }
> avg = total / 3
> print "Average:",avg
> }' data5
Average: 128.333
Average: 137.667
Average: 176.667

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

awk编程语言支持在while循环中使用break语句和continue语句,允许你从循环中跳出。

复制代码
[root@kittod ~]# awk '{
> 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
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循环中跳出。

12.4.4.3 do-while 语句

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

复制代码
do
{
statements
} while (condition)

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

复制代码
[root@kittod ~]# awk '{
> total = 0
> i = 1
> do
> {
> total += $i
> i++
> } while (total < 150)
> print total }' data5
250
160
315

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

12.4.4.4 for 语句

for语句是许多编程语言执行循环的常见方法。 awk编程语言支持C风格的for循环。

复制代码
for( variable assignment; condition; iteration process)

将多个功能合并到一个语句有助于简化循环。

复制代码
[root@kittod ~]# awk '{
> total = 0
> for (i = 1; i < 4; i++)
> {
> total += $i
> }
> avg = total / 3
> print "Average:",avg
> }' data5
Average: 128.333
Average: 137.667
Average: 176.667

定义了for循环中的迭代计数器,你就不用担心要像使用while语句一样自己负责给计数器增值了。

12.4.5 内建函数

awk编程语言提供了不少内置函数,可进行一些常见的数学、字符串以及时间函数运算。你可以在awk程序中利用这些函数来减少脚本中的编码工作。

12.4.5.1 数学函数

如果你有过其他语言的编程经验,可能就会很熟悉在代码中使用内建函数来进行一些常见的数学运算。 awk编程语言不会让那些寻求高级数学功能的程序员失望。

虽然数学函数的数量并不多,但awk提供了标准数学运算中要用到的一些基本元素。 int()函数会生成一个值的整数部分,但它并不会四舍五入取近似值。它的做法更像其他编程语言中的floor函数。它会生成该值和0之间最接近该值的整数。

这意味着int()函数在值为5.6时返回5,在值为-5.6时则返回-5。

rand()函数非常适合创建随机数,但你需要用点技巧才能得到有意义的值。 rand()函数会返回一个随机数,但这个随机数只在0和1之间(不包括0或1)。要得到更大的数,就需要放大返回值。

产生较大整数随机数的常见方法是用rand()函数和int()函数创建一个算法。

复制代码
x = int(10 * rand())

这会返回一个0~ 9(包括0和9)的随机整数值。只要为你的程序用上限值替换掉等式中的10就可以了。

在使用一些数学函数时要小心,因为awk语言对于它能够处理的数值有一个限定区间。如果超出了这个区间,就会得到一条错误消息。

复制代码
[root@kittod ~]# awk 'BEGIN{x=exp(100); print x}'
26881171418161356094253400435962903554686976
[root@kittod ~]# awk 'BEGIN{x=exp(1000); print x}'
awk: cmd. line:1: warning: exp: argument 1000 is out of range
inf

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

除了标准数学函数外, awk还支持一些按位操作数据的函数。

 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的按位异或运算。

位操作函数在处理数据中的二进制值时非常有用。

12.4.5.2 字符串函数

awk编程语言还提供了一些可用来处理字符串值的函数,如表所示。

一些字符串函数的作用相对来说显而易见

复制代码
[root@kittod ~]# awk 'BEGIN{x = "testing"; print toupper(x); print
length(x) }'
TESTING
7

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

复制代码
[root@kittod ~]# awk 'BEGIN{
> var["a"] = 1
> var["g"] = 2
> var["m"] = 3
> var["u"] = 4
> asort(var,test)
> for (i in test)
> print "Index:",i," - value:",test[i]
> }'
Index: 1 - value: 1
Index: 2 - value: 2
Index: 3 - value: 3
Index: 4 - value: 4

新数组test含有排序后的原数组的数据元素,但索引值现在变为表明正确顺序的数字值了。

split函数是将数据字段放到数组中以供进一步处理的好办法。

复制代码
[root@kittod ~]# awk 'BEGIN{ FS=","}{
> split($0,var)
> print var[1],var[5]
> }' data1
data11 data15
data21 data25
data31 data35

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

12.4.5.3 时间函数

awk编程语言包含一些函数来帮助处理时间值,如表所示。

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

下面是在awk程序中使用时间函数的例子。

复制代码
[root@kittod ~]# awk 'BEGIN{
date = systime()
day = strftime("%A, %B %d, %Y", date)
print day
}'
Monday, May 31, 2021

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

12.4.6 自定义函数

除了awk中的内建函数,还可以在awk程序中创建自定义函数。

12.4.6.1 定义函数

要定义自己的函数,必须用function关键字。

复制代码
function name([variables])
{
statements
}

函数名必须能够唯一标识函数。可以在调用的awk程序中传给这个函数一个或多个变量。

复制代码
function printthird()
{
print $3
}

这个函数会打印记录中的第三个数据字段。

函数还能用return语句返回值:

复制代码
return value

值可以是变量,或者是最终能计算出值的算式:

复制代码
function myrand(limit)
{
return int(limit * rand())
}

你可以将函数的返回值赋给awk程序中的一个变量:

复制代码
x = myrand(100)

这个变量包含函数的返回值。

12.4.6.2 使用自定义函数

在定义函数时,它必须出现在所有代码块之前(包括BEGIN代码块)。乍一看可能有点怪异,但它有助于将函数代码与awk程序的其他部分分开。

复制代码
[root@kittod ~]# awk '
function myprint()
{
printf "%-16s - %s\n", $1,$4
}
BEGIN{FS="\n"; RS=""}
{
myprint()
}' data2
Riley Mullen - (312)555-1234
Frank Williams - (317)555-9876
Haley Snell - (313)555-4938

这个函数定义了myprint()函数,它会格式化记录中的第一个和第四个数据字段以供打印输出。 awk程序然后用该函数显示出数据文件中的数据。

一旦定义了函数,你就能在程序的代码中随意使用。在涉及很大的代码量时,这会省去许多工作。

12.4.6.3 创建函数库

显而易见,每次使用函数都要重写一遍并不美妙。不过, awk提供了一种途径来将多个函数放到一个库文件中,这样你就能在所有的awk程序中使用了。

首先,你需要创建一个存储所有awk函数的文件。

复制代码
[root@kittod ~]# cat funclib
function myprint()
{
 printf "%-16s - %s\n", $1, $4
}
function myrand(limit)
{
 return int(limit * rand())
}
function printthird()
{
 print $3
}
[root@kittod ~]# cat script4
BEGIN{ FS="\n"; RS=""}
{
myprint()
}
[root@kittod ~]# awk -f funclib -f script4 data2
Riley Mullen - (312)555-1234
Frank Williams - (317)555-9876
Haley Snell - (313)555-4938

你要做的是当需要使用库中定义的函数时,将funclib文件加到你的awk命令行上就可以了。

相关推荐
小生不才yz1 分钟前
shell编程 - 数据流指南
linux
lisanmengmeng7 分钟前
添加ceph节点
linux·服务器·ceph
Tinyundg10 分钟前
Linux系统分区
linux·运维·服务器
要做一个小太阳13 分钟前
华为Atlas 900 A3 SuperPoD 超节点网络架构
运维·服务器·网络·华为·架构
江畔何人初18 分钟前
service发现
linux·运维·云原生
life码农24 分钟前
Linux系统清空文件内容的几种方法
linux·运维·chrome
zbguolei29 分钟前
虚拟机安装Ubuntu后无法登录
linux·运维·ubuntu
UP_Continue32 分钟前
Linux--基础IO
linux·运维·服务器
驱动探索者36 分钟前
linux hwspinlock 学习
linux·运维·学习
FJW02081436 分钟前
使用HAProxy实现动静分离
linux·服务器