Shell 使用awk进行高级文本处理,awk
命令可以处理数据流。它支持关联数组、递归函数、条件语句等功能。
预备知识
awk
脚本的结构如下:
awk 'BEGIN{ print "start" } pattern { commands } END{ print "end" }' file
awk
命令也可以从stdin
中读取输入。
awk
脚本通常由3部分组成:BEGIN
、END
和带模式匹配选项的公共语句块(common statement block)。这3个部分都是可选的,可以不用出现在脚本中。
awk
以逐行的形式处理文件。BEGIN
之后的命令会先于公共语句块执行。对于匹配PATTERN
的行,awk
会对其执行PATTERN
之后的命令。最后,在处理完整个文件之后,awk
会执行END
之后的命令。
实战演练
简单的awk
脚本可以放在单引号或双引号中:
awk 'BEGIN { statements } { statements } END { end statements }'
或者
awk "BEGIN { statements } { statements } END { end statements }"
下面的命令会输出文件行数:
$ awk 'BEGIN { i=0 } { i++ } END { print i}' filename
或者
$ awk "BEGIN { i=0 } { i++ } END { print i }" filename
工作原理
awk
命令的工作方式如下。
(1) 首先执行BEGIN { commands }
语句块中的语句。
(2) 接着从文件或stdin
中读取一行,如果能够匹配pattern
,则执行随后的commands
语句块。重复这个过程,直到文件全部被读取完毕。
(3) 当读至输入流末尾时,执行END { commands }
语句块。
BEGIN
语句块在awk
开始从输入流中读取行之前被执行。这是一个可选的语句块,诸如变量初始化、打印输出表格的表头等语句通常都可以放在BEGIN
语句块中。
END
语句块和BEGIN
语句块类似。它在awk
读取完输入流中所有的行之后被执行。像打印所有行的分析结果这种常见任务都是在END
语句块中实现的。
最重要的部分就是和pattern
关联的语句块。这个语句块同样是可选的。如果不提供,则默认执行{ print }
,即打印所读取到的每一行。awk
对于读取到的每一行都会执行该语句块。这就像一个用来读取行的while
循环,在循环体中提供了相应的语句。
每读取一行,awk
就会检查该行是否匹配指定的模式。模式本身可以是正则表达式、条件语句以及行范围等。如果当前行匹配该模式,则执行{ }
中的语句。模式是可选的。如果没有提供模式,那么awk
就认为所有的行都是匹配的:
$ echo -e "line1\nline2" | awk 'BEGIN { print "Start" } { print } \
END { print "End" } '
Start
line1
line2
End
当使用不带参数的print
时,它会打印出当前行。print
能够接受参数。这些参数以逗号分隔,在打印参数时则以空格作为参数之间的分隔符。在awk
的print
语句中,双引号被当作拼接操作符(concatenation operator)使用。例如:
$ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; \
print var1,var2,var3 ; }'
该命令输出如下:
v1 v2 v3
echo
命令向标准输出写入一行,因此awk
的 { }
语句块中的语句只被执行一次。如果awk
的输入中包含多行,那么 { }
语句块中的语句也就会被执行相应的次数。拼接的使用方法如下:
$ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; \
print var1 "-" var2 "-" var3 ; }'
该命令输出如下:
v1-v2-v3
{ }
就像一个循环体,对文件中的每一行进行迭代。
我们通常将变量初始化语句(如
var=0;
)放入BEGIN
语句块中。在END{}
语句块中,往往会放入用于打印结果的语句。
补充内容
awk
命令与诸如grep
、find
和tr
这类命令不同,它功能众多,而且拥有很多能够更改命令行为的选项。awk
命令是一个解释器,它能够解释并执行程序,和shell一样,它也包括了一些特殊变量。
- 特殊变量
以下是awk
可以使用的一些特殊变量。
NR
:表示记录编号,当awk
将行作为记录时,该变量相当于当前行号。NF
:表示字段数量,在处理当前记录时,相当于字段数量。默认的字段分隔符是空格。$0
:该变量包含当前记录的文本内容。$1
:该变量包含第一个字段的文本内容。$2
:该变量包含第二个字段的文本内容。
例如:
$ echo -e "line1 f2 f3\nline2 f4 f5\nline3 f6 f7" | \
awk '{
print "Line no:"NR",No of fields:"NF, "$0="$0,
"$1="$1,"$2="$2,"$3="$3
}'
Line no:1,No of fields:3 $0=line1 f2 f3 $1=line1 $2=f2 $3=f3
Line no:2,No of fields:3 $0=line2 f4 f5 $1=line2 $2=f4 $3=f5
Line no:3,No of fields:3 $0=line3 f6 f7 $1=line3 $2=f6 $3=f7
我们可以用print $NF
打印一行中最后一个字段,用 $(NF-1)
打印倒数第二个字段,其他字段以此类推。awk
也支持printf()
函数,其语法和C语言中的同名函数一样。下面的命令会打印出每一行的第二和第三个字段:
$awk '{ print $3, $2 }' file
我们可以使用NR统计文件的行数:
$ awk 'END{ print NR }' file
这里只用到了END
语句块。每读入一行,awk
都会更新NR
。当到达文件末尾时,NR
中的值就是最后一行的行号。你可以将每一行中第一个字段的值按照下面的方法累加:
$ seq 5 | awk 'BEGIN { sum=0; print "Summation:" }
{ print $1"+"; sum+=$1 } END { print "=="; print sum }'
Summation:
1+
2+
3+
4+
5+
==
15
- 将外部变量值传递给
awk
借助选项-v
,我们可以将外部值(并非来自stdin
)传递给awk
:
$ VAR=10000
$ echo | awk -v VARIABLE=$VAR '{ print VARIABLE }'
10000
还有另一种灵活的方法可以将多个外部变量传递给awk
。例如:
$ var1="Variable1" ; var2="Variable2"
$ echo | awk '{ print v1,v2 }' v1=$var1 v2=$var2
Variable1 Variable2
当输入来自于文件而非标准输入时,使用下列命令:
$ awk '{ print v1,v2 }' v1=$var1 v2=$var2 filename
在上面的方法中,变量以键-值对的形式给出,使用空格分隔(v1=var1 v2=var2
),作为awk
的命令行参数紧随在BEGIN
、{}
和END
语句块之后。
- 用
getline
读取行
awk
默认读取文件中的所有行。如果只想读取某一行,可以使用getline
函数。它可以用于在BEGIN
语句块中读取文件的头部信息,然后在主语句块中处理余下的实际数据。该函数的语法为:getline var
。变量var
中包含了特定行。如果调用时不带参数,我们可以用 $0
、$1
和$2
访问文本行的内容。例如:
$ seq 5 | awk 'BEGIN { getline; print "Read ahead first line", $0 }
{ print $0 }'
Read ahead first line 1
2
3
4
5
- 使用过滤模式对
awk
处理的行进行过滤
我们可以为需要处理的行指定一些条件:
$ awk 'NR < 5' # 行号小于5的行
$ awk 'NR==1,NR==4' # 行号在1到5之间的行
$ awk '/linux/' # 包含模式为linux的行(可以用正则表达式来指定模式)
$ awk '!/linux/' # 不包含模式为linux的行
- 设置字段分隔符
默认的字段分隔符是空格。我们也可以用选项-F
指定不同的分隔符:
$ awk -F: '{ print $NF }' /etc/passwd
或者
awk 'BEGIN { FS=":" } { print $NF }' /etc/passwd
在BEGIN
语句块中可以用OFS="delimiter"
设置输出字段分隔符。
- 从
awk
中读取命令输出
awk
可以调用命令并读取输出。把命令放入引号中,然后利用管道将命令输出传入getline
:
"command" | getline output ;
下面的代码从/etc/passwd文件中读入一行,然后显示出用户登录名及其主目录。在BEGIN
语句块中将字段分隔符设置为:
,在主语句块中调用了grep
。
$ awk 'BEGIN {FS=":"} { "grep root /etc/passwd" | getline; \
print $1,$6 }'
root /root
awk
的关联数组
除了数字和字符串类型的变量,awk
还支持关联数组。关联数组是一种使用字符串作为索引的数组。你可以通过中括号中索引的形式来分辨出关联数组:
arrayName[index]
就像用户定义的简单变量一样,你也可以使用等号为数组元素赋值:
myarray[index]=value
- 在
awk
中使用循环
在awk
中可以使用for
循环,其格式与C语言中的差不多:
for(i=0;i<10;i++) { print $i ; }
另外awk
还支持列表形式的for
循环,也可以显示出数组的内容:
for(i in array) { print array[i]; }
下面的例子展示了如何将收集到的数据存入数组并显示出来。这个脚本从/etc/password中读取文本行,以:
作为分隔符将行分割成字段,然后创建一个关联数组,数组的索引是登录ID,对应的值是用户名:
$ awk 'BEGIN {FS=":"} {nam[$1]=$5} END {for {i in nam} \
{print i,nam[i]}}' /etc/passwd
root root
ftp FTP User
userj Joe User
awk
内建的字符串处理函数
awk
有很多内建的字符串处理函数。
length(string)
:返回字符串string
的长度。index(string, search_string)
:返回search_string
在字符串string
中出现的位置。split(string, array, delimiter)
:以delimiter
作为分隔符,分割字符串string
,将生成的字符串存入数组array
。substr(string, start-position, end-position)
:返回字符串string
中以start-position
和end-position
作为起止位置的子串。sub(regex, replacement_str, string)
:将正则表达式regex
匹配到的第一处内容替换成replacment_str
。gsub(regex, replacement_str, string)
:和sub()
类似。不过该函数会替换正则表达式regex
匹配到的所有内容。match(regex, string)
:检查正则表达式regex
是否能够在字符串string
中找到匹配。如果能够找到,返回非0值;否则,返回0。match()
有两个相关的特殊变量,分别是RSTART
和RLENGTH
。变量RSTART
包含了匹配内容的起始位置,而变量RLENGTH
包含了匹配内容的长度。