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]