awk命令
如果要格式化一个报文或从一个大的文本文件中抽取数据包,可以使用awk
命令。它在文本浏览和数据的熟练使用上性能优异。
1. 调用awk
有三种方式调用awk
,第一种是命令行方式:
$ awk [-F field-separator] 'commands' input-file(s)
这里,commands
是真正的awk
命令。[-F 域分隔符]
是可选的,因为awk
使用空格作为缺省的域分隔符,因此如果要浏览域间有空格的文本,不必指定这个选项,但如果浏览如password文件,此文件各域以冒号作为分隔符,则必须明确指明,awk -F: 'commands' input-file
。
第二种方法是将所有的awk
命令插入一个文件,并使awk
程序可以执行,然后用awk
命令解释器作为脚本的首行,以便通过键入脚本名称来调用它。
第三种方式是将所有的awk
命令插入一个单独文件,然后调用:awk -f awk-script-file input-file(s)
。-f
指明在文件awk-script-file
中的awk
脚本。
2. awk脚本
如果设置了-F
选项,则awk
每次读一条记录或一行,并使用指定的分隔符分隔指定域;如果未设置-F
选项,awk
假定空格为域分隔符,并保持这个设置直到发现一新行。当新行出现时,awk
命令知道已经读完整条记录,然后在下一个记录启动读命令,这个读进程将持续到文件尾或文件不再存在。
2.1 模式和动作
任何awk
语句都由模式和动作组成。在一个awk
脚本中可能有很多语句。模式部分决定动作语句何时触发时间。处理即对数据的操作。如果省略模式部分,动作将时刻保持执行状态。
模式可以是任何条件语句或复合语句或正则表达式。模式包括两个字段,BEGIN和END。使用BEGIN语句设置计数和打印头,BEGIN语句使用在任何文本浏览动作之前,之后文本浏览动作依据输入文件开始执行。END语句用来在awk
完成文本浏览动作后打印输出文件总数和结尾状态标志。如果不特别指明模式,awk
总是匹配或打印行数。
实际动作在大括号{}
内指明,动作大多数用来打印,但是还有更长的代码诸如if
和循环语句及循环退出结构。如果不指明采取动作,awk
将打印出所有浏览出来的记录。
2.2 域和记录
awk
执行时,其浏览域标记为$1, $2, ..., $n
。这种方法称为域标识。使用$1,$3
表示参照第1和第3域,这里用逗号作分隔。如果希望打印所有域,可使用$0
。awk
浏览时,到达以新行,即假定到达包含域的记录末尾,然后执行新纪录的下一行的读动作,并重新设置域分隔。为了打印一个域或所有域,使用print
命令,这是一个awk
动作(动作语法用圆括号括起来)。
保存awk
输出的话,可以使用重定向输出:
$ awk '{print $0}' input.txt >output.txt
这种方法不会将结果显示在屏幕上,并且会重写硬盘上同名文件。当然还可以使用tee
命令,在输出到屏幕同时输出到文件:
$ awk '{print $0}' input.txt | tee output.txt
awk
的输入:
$ name.awk input.txt
# 重定向方法
$ name.awk < input.txt
# 管道方法
$ input.txt | name.awk
打印:
# 单独打印记录
$ awk '{print $1,$4}' input.txt
# 打印报告头
$ awk 'BEGIN {print "Headname1 Headname2\n----------------"} {print $1"\t"$4}' input.txt
# 打印信息尾
$ awk 'BEGIN {print "Headname\n--------------"} {print $1} END {"end-of-report"}' ipt.txt
错误信息,遇到awk
错误时,大概有几个常见的错误:
- 确保整个
awk
命令用单引号括起来 - 确保命令内所有引号成对出现
- 确保用花括号括起来动作语句,用圆括号括起条件语句
- 可能忘记使用花括号等等
正则表达式:正则表达式需要用斜线括起来,比如/Hello/
可以查出单词Hello的情况。
元字符:\ ^ $ . [] | () * + ?
。+
匹配一个或多个字符,?
匹配模式出现频率,这两个只适用于awk
而不适用于grep
和sed
。
条件操作符:<、 <=、 ==、 !=、 >、 >=、 ~、 !~
。~
匹配正则表达式,!~
不匹配正则表达式。
为使一域号匹配正则表达式,使用~
后紧跟正则表达式,也可以用if语句:
$ awk '{if($4~/Hello/) print $0}' ipt.txt #如果域4包含Hello,打印匹配行记录
$ awk '$0 ~/Hello/' ipt.txt #如果记录包含模式Hello,打印,匹配记录找到时,不特别声明则缺省打印整条
如果{if($3~/32/) print $0}
会匹配到诸如3212、32、3211等,想精确匹配到32则:
$ awk '$3=="48" {print $0}' ipt.txt
不匹配时:
$ awk '$0 !~/Hello/' ipt.txt #查找不包含模式Hello,缺省情况下将打印所有匹配记录,不必加动作部分
其它小于大于等:
$ awk '{if ($6 > $7) print $1}' ipt.txt
$ awk '{if ($5 <= $3) print $0}' ipt.txt
逻辑符号&& AND
、|| OR
、!
:
$ awk '{if ($1=="name1" && $2=="name2") print $0}' ipt.txt
2.3 内置变量
awk
有许多内置变量用来设置环境信息,这些变量可以被改变。下表给出了最常使用的一些变量及含义:
命令 | 含义 |
---|---|
ARGC |
命令行参数个数 |
ARGV |
命令行参数排列 |
ENVIRON |
支持队列中系统环境变量的使用 |
FNR |
浏览文件的记录数 |
FS |
设置输入域分隔符,等价于命令行-F 选项,如设置, ,FS="," |
NF |
浏览记录的域个数 |
NR |
已读的记录数 |
OFS |
输出域分隔符,缺省为空格,如果设置为# 可以OFS="#" |
ORS |
输出记录分隔符,缺省为新行\n |
RS |
控制记录分隔符,缺省为新行\n |
FILENAME |
awk 浏览的文件名 |
如使用NR
将打印输入文件的记录个数:
$ awk 'END {print NR}' ipt.txt #可以在末尾输出已读的记录数
在下面例子,使用NF
变量显示每一条读记录中有多少个域,NR
显示已读记录数,并在END部分打印输入文件名:
$ awk '{print NF,NR,$0} END{print FILENAME}' ipt.txt
还可以使用NF
显示目录或者文件名:
$ echo $PWD | awk -F/ '{print $NF}'
假设$PWD
返回usr/local/etc
,则将其作为awk
输入,-F/
指定域分隔符分隔出3个域,则NF
值是3,$3
则指定第3个域内容,即etc
。
$ echo "/usr/local/etc/hello.txt" | awk -F/ '{print $NF}' #输出 hello.txt
2.4 awk操作符
在awk
中使用操作符,基本表达式可以划分为数字型、字符串型、变量型、域及数组元素。
设置输入域到域变量名:在awk
中设置有意义的域名是一种好的习惯,在进行域名匹配或关系操作时更容易理解。一般的变量名设置方式位name=$n
,这里name
为调用的域变量名,n
为实际域号。多个设置之间用分号隔开。
$ awk '{name=$1;age=$4; if(age ~/42/) print name" is age:"age}' ipt.txt
域值比较操作:有两种方法测试一数值域是否小于另一数值域:1)在BEGIN中给变量名赋值。2)在关系操作中使用实际数值。
通常在BEGIN部分赋值是很有益的,可以在awk
表达式进行改动时减少麻烦。使用关系操作必须用圆括号括起来。
$ awk '(if($6 < 27) print $0)' ipt.txt
$ awk 'BEGIN {BASELINE="27 "}{if($6 < BASELINE) print $0}' ipt.txt #两者作用相同
其中用引号将27括起来是可选的,效果一样。
修改数值域取值:在awk
中修改任何域时,重要的一点要记住实际输入文件是不可修改的,修改的只是保存在缓存里的awk
副本。awk
会在变量NR
和NF
中反映出修改痕迹。
为修改数值域,简单的给域标识重新赋值,如$1=$1+5
,会将$1
的值加5,当然在实施修改前首先要匹配域名。
$ awk '{if($1=="Hello") $6=$6-1; print $1,%6}' ipt.txt
修改文本域:修改文本域即对其重新赋值。需要做的是赋给一个新的字符串。记住字符串需要使用双引号,并用圆括号括起来整个语法。
$ awk '{if($1=="hello") ($1="world"); print $1}' ipt.txt
只显示修改记录:如果一个文件记录数很多,打印全部只为了查看修改部分显然不合理。在模式后面使用花括号将只打印修改部分:
$ awk '{if($1=="hello"){$1="world";print $1}}' ipt.txt
创建新的输出域:在awk
中处理数据时,基于各域进行计算时创建新域是一种好的习惯。创建新域要通过其它域赋予新域标识符。如创建一个基于其它域的加法新域{$4=$2+$3}
,这里假定记录包含3个域,则域4位新建域,保存域2和域3相加结果。
$ awk '{if($6<$7) {$8=$7-$6; print $1,$8}}' ipt.txt #$8是新域
或者可以直接使用更具有意义的变量名:
$ awk 'if($6<$7){diff=$7-$6; print $1,diff}' ipt.txt
增加列值:为增加列数或进行运行结果统计,使用符号+=
。例如将$1
加入变量total
:total+=$1
。列值增加很有用,比如许多文件都要求统计总数:
$ awk '(total+=$6); END{print "total: " total}' ipt.txt
如果不想打印所有记录,只打印累加结果:
$ awk '{(total+$6)}; END{print "total: "total}' ipt.txt
文件长度相加:在目录中查看文件时,如果想快速查看所有文件的长度及其总和,但要排除子目录,使用ls -l
命令,然后管道输出到awk
,awk
首先使用正则剔除首字母d
的记录,然后将文件长度列相加,输出到每一文件长度及在END部分输出所有文件的长度。
$ ls -l | awk '/^[^d]/ {print $9"\t"$5} {tol+=$5} END{print "total KB: "tol}'
2.5 内置的字符串函数
字符串函数 | 含义 |
---|---|
gsub(r,s) |
在整个$0 中用s 替代r |
gsub(r,s,t) |
在整个t 中用s 替代r |
index(s,t) |
返回s 中字符串t 的第一位置 |
length(s) |
返回s 长度 |
match(s,r) |
测试s 是否包含匹配r 的字符串 |
split(s,a,fs) |
在fs 上将s 分成序列a |
sprint(fmt,exp) |
返回经fmt 格式化后的exp |
sub(r,s) |
$0 中最左边最长的子串被替换为s |
substr(s,p) |
返回字符串s 中从p 开始的后缀部分 |
substr(s,p,n) |
返回字符串s 中从p 开始长度为n 的后缀部分 |
gsub
函数有点类似sed
的查找和替换,允许替换一个字符串或字符为另一个字符串或字符,并以正则表达式形式执行。第一个函数作用域$0
,第二个函数允许指定目标,然而未指定目标,缺省为$0
。
index(s,t)
函数返回目标字符串s
中查找字符串t
的首位置。split
使用域分隔符fs
将字符串s
划分为指定序列a
。sub(r,s)
将用s
替代$0
中最左边最长的子串,该子串被r
匹配。
# 1.gsub 使用正则表达式,/目标模式/
$ awk 'gsub(/48311/,48322) {print $0}' ipt.txt
# 2.index 必须用双引号将字符串括起来
$ awk 'BEGIN{print index("Lulu", "lu")}' ipt.txt
# 3.length
$ awk '$1=="Hello" {print length($1)" "$1}' ipt.txt
# 4.match 测试字符串是否包含查找字符的一部分,可对查找部分使用正则,返回值为成功出现的字符排列数
$ awk 'BEGIN {print match("ANCD",/d/)}' # 无,返回0
# 5.split
$ awk 'BEGIN {print split("123#456#789", myarray, "#")}' #返回3
# 6.sub
$ awk '$1=="Hello" sub(/26/, "29", $0)' ipt.txt
# 7.substr
$ awk '$1=="Hello" {print substr($1,1,5)}' ipt.txt
2.6 字符串屏蔽序列
使用字符串或者正则表达式时,有时需要在输出中加入一新行或查询一元字符。
打印一新行时(\n
),给出其屏蔽序列,以不失其特殊含义,用法在字符串前加入反斜线。
如果使用正则表达式,查询花括号{}
,在字符前加反斜线,如/\{/
,将在awk
中失去其特殊含义。
$ awk 'BEGIN {print "\n\May\tDay\n\nMay \104\141\171"}'
\ddd
表示八进制ASCII码。
2.7 awk输出函数printf
awk
提供函数printf
,拥有集中不同的格式化输出功能。例如按列输出、左对齐或右对齐等。
每一种printf
函数(格式控制字符)都以一个%
符号开始,以一个决定转换的字符结束。转换包含三种修饰符。
printf
修饰符:
修饰符 | 含义 |
---|---|
- |
左对齐 |
Width |
域的步长,用0表示0步长 |
.prec |
最大字符串长度,或小数点右边的位数 |
printf
格式:
格式 | 含义 |
---|---|
%c |
ASCII字符 |
%d |
整数 |
%e |
浮点数,科学计数法 |
%f |
浮点数 |
%g |
awk 决定使用e 或f |
%o |
八进制数 |
%s |
字符串 |
%x |
十六进制数 |
$ echo "65" | awk '{printf "%c\n", $0}' # 输出 A
$ awk 'BEGIN{printf "%c\n", 65}' # 输出 A
$ awk '{printf "%-15s %s\n", $1, $3}' ipt.txt # -表示左对齐 15表示字符长度
向一行awk
命令传值,下面的例子在命令行中设置变量AGE等于10,然后传入awk
中:
$ awk '{if($5 < AGE) print $0}' AGE=10 ipt.txt
2.8 脚本文件
可以将awk
脚本写入一个文件再执行,这样可以保存,不必每次使用都重新输入。
如创建一个student.awk
文件:
!/bin/awk -f
# comment line
# to call: student.awk ipt.txt
#print header first
BEGIN{
print "Student Date Member Grade Age points"
print "Name Joined"
print "===================================="
}
# add the points
(tol+=$6)
# print the total and average point
END{
print "Club student total points: " tol
print "Average points: " tol/NR
}
第一行!/bin/awk -f
非常重要,没有它脚本将不能执行。
在脚本文件中设置FS
分隔符时,需要在BEGIN
部分,不然awk
将会发生混淆,不知道域分隔符是什么。
在脚本传值类似:
$ awk script_file var=value input_file
2.9 awk数组
前面说到split
返回数组myarray
下标数,实际上myarra
为:myarray[1]、myarray[2]、myarray[3]
。
数组使用前不必定义,也不必指定数组元素个数,经常使用循环来访问数组:
for (element in array) print array[element]