1. gawk概述

在《sed编辑器基本用法》一文中我们已经介绍了Linux世界中使用最广泛的两个命令行编辑器之一——sed,今天我们来介绍另外一个:gawk。gawk是Unix中原始awk程序的GNU版本,比sed功能更加强大,因为它提供了一个类编程环境,允许我们修改和重新组织文件中的数据。gawk的特点如下:

  • 定义变量来保存数据;
  • 使用算术和字符运算符来处理数据;
  • 使用结构化编程的概念,比如if-else语句和循环,来为数据处理增加逻辑;
  • 提取数据文件中的数据元素并将它们按另一顺序或格式重新放置,从而生成格式化的报告。

2. gawk的命令格式及选项

gawk程序的基本格式如下:gawk options program file

其支持的选项如下表:

选项 描述
-F fs 指定分隔行中数据字段的文件分隔符
-f file 指定读取程序的文件名
-v var=value 定义gawk程序中的一个变量及其默认值
-mf N 指定要处理的数据文件中的最大字段数
-mr N 指定数据文件中的最大数据行数
-W keyword 指定gawk的兼容模式或者警告等级。用help选项来列出所有可用的关键字。

3. 使用gawk

我们可以在命令行或者shell脚本中使用gawk。

3.1 从命令行上读取程序脚本

gawk程序脚本是由一对花括号定义的,你必须将脚本命令放在两个括号之间。由于gawk行假定脚本是单个文本字符串,所以还必须用单引号将脚本命令圈起来:

linux-osud:~/temp # echo "just a test" | gawk '{print $1}'
just

这个命令会显示输入流中每行的第一个数据字段。

3.2 在程序脚本中使用多个命令

gawk允许我们将多条命令合成一个普通程序,只要使用分号将多个命令分隔起来就行:

linux-osud:~/temp # echo "My name is Allan ." | gawk '{$4="Alne"; print $0}'
My name is Alne .

3.3 从文件中读取程序

跟sed编辑器一样,gawk允许我们将程序存储在文件中,然后在命令行上引用它们:

linux-osud:~/temp # gawk -F: -f script2 /etc/passwd
Batch jobs daemon's userid is 25
User for Avahi's userid is 486
User for Avahi IPv4LL's userid is 499
bin's userid is 1
user for colord's userid is 490
Daemon's userid is 2
dnsmasq's userid is 494
...

3.4 在处理数据前运行脚本

gawk允许我们指定程序脚本命令何时允许。默认情况下,gawk从输入读入一行文本,然后执行针对文本行中的数据执行脚本。有时,我们可能需要在处理数据之前(比如创建报告的标题)允许一些命令。为了做到这点,我们可以使用BEGIN关键字。它会强制先执行BEGIN关键字后面指定的程序脚本,然后再读取数据:

linux-osud:~/temp # gawk 'BEGIN {print "This is a test report"}'
This is a test report

我们可以在BEGIN块中放置任何类型的gawk命令,比如给变量赋默认值等。

3.5 在处理数据后运行脚本

和BEGIN关键字相反,END关键字允许我们指定一个程序脚本,在gawk读取数据之后执行:

linux-osud:~/temp # gawk 'BEGIN {print "Hello World !"} {print $0} END {print "Bye bye"}'
Hello World !
This is a test
This is a test
This is another test
This is another test
Bye bye

gawk会先执行BEGIN块中的代码,然后处理输入流中的数据(使用Ctrl+D可以结束输入),最后执行END块中的代码。

4. gawk变量

注意:跟shell变量不同,引用gawk变量时,变量名前不加美元符。

4.1 内建变量

gawk使用内建变量来在程序数据中引用特定功能。gawk将数据定义成数据行和数据字段:数据行是一行数据(默认用换行符分隔),而数据字段是行中一个单独的数据元素(默认用空白符比如空格或制表符分隔)。下面介绍gawk的数据字段和数据行变量:

变量 说明
$0 整个数据行
$1 数据行中第一个数据字段
$2 数据行中第二个数据字段
$n 数据行中第n个字段
FIELDWIDTHS 由空格分隔开的定义了每个字段确切宽度的一列数字,一旦设置该变量,gawk会忽略FS变量
FS 输入字段分隔符
RS 输入数据行分隔符,默认为换行符
OFS 输出字段分隔符,默认为一个空格
ORS 输出数据行分隔符,默认为换行符

看下面有些例子:

# 默认OFS为一个空格
linux-osud:~/temp # gawk 'BEGIN{FS=","} {print $1,$2,$3}' data1
data11 data12 data13
data21 data22 data23
data31 data32 data33

# 更改OFS
linux-osud:~/temp # gawk 'BEGIN{FS=","; OFS="-"} {print $1,$2,$3}' data1
data11-data12-data13
data21-data22-data23
data31-data32-data33

# 使用FIELDWIDTHS变量来读取数据行,而不是用FS来划分字段
linux-osud:~/temp # gawk 'BEGIN{FIELDWIDTHS="3 5 2 5"} {print $1,$2,$3,$4}' data1b
100 5.324 75 96.37
115 -2.34 91 94.00
058 10.12 98 100.1

除了字段和数据行分隔符变量,gawk还提供了一些其他内建变量来帮助我们了解数据中正在做什么以及从shell环境中提取信息,这些变量列举如下:

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

看下面几个例子:

# ARGC和ARGV
linux-osud:~ # gawk 'BEGIN{print ARGC,ARGV[0],ARGV[1]}' data1
2 gawk data1

可以看到gawk不把单引号中的程序脚本认为是命令行参数。

linux-osud:~ # gawk '
> BEGIN {
> print ENVIRON["HOME"]
> print ENVIRON["PATH"]
> }'
/root
/sbin:/usr/sbin:/usr/local/sbin:/root/bin:/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games

ENVIRON使用关联数组来提取shell变量,关联数组用文本作为数组的索引值,而不是shell中数组那样使用数值。

linux-osud:~ # gawk 'BEGIN {FS=":"; OFS=":"} {print $1, $NF}' /etc/passwd
at:/bin/bash
avahi:/bin/false
avahi-autoipd:/bin/false
bin:/bin/bash
colord:/sbin/nologin
daemon:/bin/bash
dnsmasq:/bin/false
ftp:/bin/bash

利用NF定位到最后一个数据字段。

linux-osud:~/temp # cat data1
data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35

# FNR
linux-osud:~/temp # gawk '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

# NR
linux-osud:~/temp # gawk '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和NR类似,但从例子可以看出区别:如果只使用一个数据文件作为输入,那么FNR和NR的值将会相同。如果使用多个数据文件作为输入,那么FNR的值会在处理每个数据文件时被重置,而NR的值则会继续计数,直到处理完整个文件。

4.2 自定义变量

4.2.1 在脚本中给变量赋值

在gawk中给变量赋值类似于在shell脚本中给变量赋值——使用赋值语句:

linux-osud:~/temp # gawk  '
> BEGIN {
> testing="This is a test"
> print testing
> }'
This is a test

给变量赋值后,就可以在gawk脚本中任何地方使用该变量了(未赋值的变量默认为空字符串)。

4.2.2  在命令行上给变量赋值

我们也可以用gawk命令行为gawk程序变量赋值。这允许我们在普通代码外设置值,即使修改这个值。比如:

linux-osud:~/temp # echo "One,Two,Three,Four,Five" | gawk -f script1 n=2 
Two
linux-osud:~/temp # echo "One,Two,Three,Four,Five" | gawk -f script1 n=3 
Three

5. gawk数组

gawk的数组为关联数组。关联数组与数字数组不同之处在于它的索引值可以是任意文本字符串,每个索引字符串都必须是唯一的,并且唯一的标识赋给它的数据元素。这有点类似于其他编程语言的哈希表的概念。

  • 定义数组变量:var[index] = element
  • 遍历数组变量:for (var in array) { statements }
  • 删除数组变量:delete array[index]

看下面的例子:

# 定义数组变量
linux-osud:~/temp # gawk 'BEGIN{capital["China"] = "Beijing"; print capital["China"]}'
Beijing
linux-osud:~/temp # gawk 'BEGIN{
> var[1] = 34
> var[2] = 3
> total = var[1] + var[2]
> print total
> }'
37

# 遍历数组变量
linux-osud:~/temp # gawk 'BEGIN{
> var["a"] = 1
> var["b"] = 2
> var["g"] = 3
> var["m"] = 4
> for (test in var)
> {
>     print "Index:"test," - Value:",var[test]
> }
> }'
Index:m  - Value: 4
Index:a  - Value: 1
Index:b  - Value: 2
Index:g  - Value: 3

# 删除数组变量
linux-osud:~/temp # gawk 'BEGIN{
> var["a"] = 1
> var["g"] = 2
> for (test in var)
> {
>     print "Index:",test," - Value:",var[test]
> }
> delete var["g"]
> for (test in var)
> {
>     print "Index:",test," - Value:",var[test]
> }
> }'
Index: a  - Value: 1
Index: g  - Value: 2
Index: a  - Value: 1

6. 使用模式

gawk支持几种类型的匹配模式来过滤数据行,与sed编辑器大同小异。BEGIN和END关键字就是两个特殊的模式,下面再介绍几种模式。

6.1 正则表达式

我们可以使用基本正则表达式(BRE)或扩展正则表达式(ERE)来过滤程序脚本要作用在数据流的哪些行上。使用正则表达式时,正则表达式必须出现在它控制的程序代码的左花括号之前:

linux-osud:~/temp # cat data1
This is a test.
This is a trial.
This is an example.
linux-osud:~/temp # gawk 'BEGIN{FS=","} /test/{print $1}' data1
This is a test.

关于BRE和ERE两种表达式会在后面的博客中介绍。

6.2 匹配操作符

匹配操作符(matching operator)允许我们将正则表达式限定在数据行中的特定数据字段上。匹配操作符是波浪线(~)。我们要一起指定匹配操作符、数据字段变量以及要匹配的正则表达式,比如:$1 ~ /^data/

$1代表数据行中的第一个数据字段。上面这个表达式会过滤出第一个字段以文本data开头的所有数据行。

linux-osud:~/temp # cat data1
data11,data12,data13,data14,data15
data21,data22,data23,data24,data25
data31,data32,data33,data34,data35
linux-osud:~/temp # gawk 'BEGIN{FS=","} $2 ~ /^data2/ {print $0}' data1
data21,data22,data23,data24,data25
linux-osud:~/temp # gawk -F: '$1 ~ /allan/ {print $1,$NF}' /etc/passwd
allan /bin/bash
linux-osud:~/temp

也可以使用!符号来排除正则表达式的匹配:$1 !~ /expression/

linux-osud:~/temp # gawk -F: '$1 !~ /allan/ {print $1,$NF}' /etc/passwd
at /bin/bash
avahi /bin/false
avahi-autoipd /bin/false
bin /bin/bash
colord /sbin/nologin
daemon /bin/bash
dnsmasq /bin/false
ftp /bin/bash
games /bin/bash
gdm /bin/false
lp /bin/bash
mail /bin/false
man /bin/bash
messagebus /bin/false
news /bin/bash
nm-openconnect /sbin/nologin
nobody /bin/bash
nscd /sbin/nologin
ntp /bin/false
polkitd /sbin/nologin
postfix /bin/false
pulse /sbin/nologin
root /bin/bash
rpc /sbin/nologin
rtkit /bin/false
srvGeoClue /sbin/nologin
sshd /bin/false
statd /sbin/nologin
tftp /bin/false
uucp /bin/bash
wwwrun /bin/false

6.3 数学表达式

除了正则表达式,我们还可以在模式匹配中使用数学表达式:

linux-osud:~/temp # gawk -F: '$4 == 0 {print $1}' /etc/passwd
root

比如我们可以用上面的语句找出所有属于root用户组的用户。

我们可以使用任意的普通数学比较表达式:

  • x == y
  • x <= y
  • x < y
  • x >= y
  • x > y

注意:也可以对文本数据使用表达式,但必须小心:

linux-osud:~/temp # gawk -F, '$1=="data" {print $1}' data1
linux-osud:~/temp # gawk -F, '$1=="data11" {print $1}' data1
data11
linux-osud:~/temp # gawk -F, '$1>="data11" {print $1}' data1
data11
data21
data31
linux-osud:~/temp # gawk -F, '$1>="aata11" {print $1}' data1
data11
data21
data31
linux-osud:~/temp # gawk -F, '$1>="eata11" {print $1}' data1

可以看到,对于文本,比较的时候是使用ASCII值进行比较的。

7. 结构化命令

gawk程序支持如下结构化命令:

if-then-else语句:

if (condition) statement1; else statement2

while语句:

while (condition)
{
statements
}

do-while语句:

do {
statements
} while (condition)

for语句:

for(variable assignment; condition; iteration process)

下面看一些例子:

linux-osud:~/temp # cat data4
10
5
13
50
34

# if
linux-osud:~/temp # gawk '{if ($1 > 20) print $1}' data4
50
34
linux-osud:~/temp # gawk '{
> if ($1 > 20)
> {
>     x = $1 * 2
>     print x
> }
> }' data4
100
68

# if else
linux-osud:~/temp # gawk '{
> if ($1 > 20)
> {
>     x = $1 * 2
>     print x
> } else
> {
>     x = $1 / 2
>     print x
> }}' data4
5
2.5
6.5
100
68

# 放在一行时,需要使用分号
linux-osud:~/temp # gawk '{if ($1 > 20) print $1*2; else print $1/2}' data4
5
2.5
6.5
100
68

linux-osud:~/temp # cat data5
130 120 135
160 113 140
145 170 215

# while
linux-osud:~/temp # gawk '{
> 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

# break、continue
linux-osud:~/temp # 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
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

# do while
linux-osud:~/temp # gawk '{
> total = 0
> i = 1
> do
> {
>     total += $i
>     i++
> } while (total < 150)
> print total }' data5
250
160
315

# for
linux-osud:~/temp # gawk '{
> 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

8. 格式化打印printf

虽然我们可以使用print打印,但是print的输出格式比较单一。如果需要比较精细的控制输出格式,可以使用printf。printf的使用与C语言中的使用基本相同,所以此处不详细介绍,仅列举几个例子:

linux-osud:~/temp # gawk 'BEGIN{
> x = 10 * 100
>  printf "The answer is:%en", x
> }'
The answer is:1.000000e+03

linux-osud:~/temp # gawk 'BEGIN{FS=","} {printf "%s ", $1} END {printf "n"}' data1
data11 data21 data31 

linux-osud:~/temp # gawk '{
> total = 0
> for ( i = 1; i < 4; i++ )
>     total += $i
> avg = total / 3
> printf "Average: %5.1fn", avg
> }' data5
Average: 128.3
Average: 137.7
Average: 176.7
linux-osud:~/tem

控制符说明:

控制字母 说明
c 将一个整数作为ASCII字符显示
d 显示一个整数值
i 显示一个整数值
e 科学计数法显示
f 显示一个浮点数
g 用科学计数法或者浮点数中的较短者显示
o 显示八进制值
s 显示文本字符串
x 显示十六进制值
X 显示十六进制值,但是用大写字母

除了控制字母,还有三个修饰符:

  • width:指定输出字段最小宽度的数字值,如果输出短于这个值,则会右对齐,并使用空格填充。如果输出长度超过这个值,则覆盖width
  • prec:指定浮点数中小数点后面位数的个数,或者文本字符串中显示的最大字符数
  • -:采用左对齐而不是右对齐

9. 内建函数

9.1 数学函数

函数 说明
atan2(x, y) x/y的反正切,以弧度为单位
cos(x) x的余弦
exp(x) x的指数函数
int(x) x的整数部分,同C的floor函数
log(x) x的自然对数
rand( ) 0到1之间的随机浮点值
sin(x) x的正弦
sqrt(x) x的平方根
srand(x) 随机数种子
位操作函数
and(v1, v2) v1与v2与运算
compl(val) val的补运算
lshift(val, count) 左移
rshift(val, count) 右移
or(v1, v2) 位或
xor(v1, v2) 异或

9.2 字符串函数

函数 说明
asort(s [, d]) 将数组s按数据元素数值排序。索引值会被替换成表示新的排序顺序的连续数字。另外,如果指定了d,则排序后的数组会存储在数组d中
asorti(s [, d]) 将数组s按索引值paixu.shengcheng的数组会将索引值作为数组元素值,用连续数字索引来表明排序顺序。另外,如果指定了d,排序后的数组会存储在数组d中
gensub(r, s, h [, t]) 查找变量$0或目标字符串t(如果提供了的话)来匹配正则表达式r。如果h是一个以g或G开头的字符串,就用s替换掉匹配的文本。如果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中所有的字符转换成大写

有的字符串函数使用比较复杂,有的则比较简单。看几个例子:

linux-osud:~ # gawk 'BEGIN{x = "testing"; print toupper(x); print length(x) }'
TESTING
7

linux-osud:~ # gawk 'BEGIN{
> var["a"] = 1
> var["g"] = 2
> var["m"] = 3
> var["u"] = 4
> asort(var, test)
> for ( i in test )
>     print "Index:", i, " - valu:", test[i]
> }'
Index: 1  - valu: 1
Index: 2  - valu: 2
Index: 3  - valu: 3
Index: 4  - valu: 4

linux-osud:~/temp #  gawk 'BEGIN{ FS=","} {
> split($0, var)
> print var[1], var[5]
>  }' data1
data11 data15
data21 data25
data31 data35

9.3 时间函数

函数 说明
mktime(dataspec) 将一个按YYYY MM DD HH MM SS [DST]格式指定的日期转换成从UTC到现在经历的秒数(称为时间戳)
strftime(format [, timestamp]) 将当前时间的时间戳或timestamp转化成用shell函数格式date( )的格式化日期
systime( ) 返回当前时间时间戳

看一个例子:

linux-osud:~/temp # gawk 'BEGIN {
> date = systime()
> day = strftime("%A, %B %d, %Y", date)
> print day
> }'
Wednesday, May 06, 2015

10. 自定义函数

自定义函数使用function关键字,格式如下:

function name([variables])
{
    statements
}

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

在定义函数时,它必须出现在所有代码块之前(包括BEGIN代码块):

linux-osud:~/temp # cat data2
Riley Mullen
123 Main Street
Chicago, IL 60601
(321)555-1234

Frank Williams
456 Oak Street
Indianapolis In 46201
(317)555-9875

Haley Snell
4231 Elm Street
Detroit. MI 48201
(313)555-4938

linux-osud:~/temp # gawk '
> function myprint()
> {
>     printf "%-16s - %sn", $1, $4
> }
> BEGIN{FS="n"; RS=""}
> {
>     myprint()
> }' data2
Riley Mullen     - (321)555-1234
Frank Williams   - (317)555-9875
Haley Snell      - (313)555-4938

创建函数库:

linux-osud:~/temp # cat funclib 
function myprint()
{
    printf "%-16s - %sn", $1, $4
}

function myrand(limit)
{
    return int(limit * rand())
}

function printthird()
{
    print $3
}
linux-osud:~/temp # cat script4
BEGIN{FS="n"; RS=""}
{
     myprint()
}
linux-osud:~/temp # gawk -f funclib -f script4 data2
Riley Mullen     - (321)555-1234
Frank Williams   - (317)555-9875
Haley Snell      - (313)555-4938