文章的干货来源于酷壳的**AWK 简明教程以及Linux与unix shell编程指南**。
本文纯属搬砖。
最近经常遇到需要处理大段文本输出的情况,所以想学习一下awk命令,这个命令更像是一个微型语言。
《awk命令简介(二)》着重介绍awk命令的详情,内容主要来自**《Linux与unix shell编程指南》**。
书接上回。
awk 命令详解
调用awk
如果在命令行中使用awk,通常型为:
$ awk -F field-separator 'commands' input-files-F field-separator为可选项,用于指定域分隔符,默认为空格,如果需要处理如','、':'、';'之类的字符做分隔符时,可以设置该值,如使用':'的情形:
$ awk -F: 'commands' input-files也可以将awk命令写在文件中,作为脚本调用,如调用名为awk-script-file的脚本文件:
$ awk -f awk-script-file input-files-f选项用于指定脚本文件名。
awk脚本
模式和动作
awk脚本由各种动作和模式组成。
awk使用-F命令指定的分隔符(不指定时使用空格)分离记录中的域,直到发现新一行。这个动作将一直持续到文件结束。
awk命令可以有许多语句,语句中的模式用于控制动作的触发条件。
模式可以是任何条件语句、复合语句或正则表达式。模式包括了两个特殊字段,BEGIN和END。BEGIN语句使用在awk的任何文本浏览动作之前,之后awk文本浏览动作依据输入文件开始执行,END语句用来用来在awk完成文本浏览动作后打印输出文本总数及结尾状态标识。
动作在{}内声明,通常为打印动作,或是诸如条件语句或循环语句。如果不指名动作,awk将打印所有浏览记录。
记录和域
awk会将读取的一条记录用-F指定的分隔符分为多个域,并依次将域命名为$1, $2, $3等,这些域名的使用类似于语言中变量名的使用。
{print $1, $3}就表示打印第1域和第3域,而$0表示所有的域,所以{print $0}表示打印所有域。
上面的在{}中的print就是一个awk动作。
假设有数据文件grade.txt(此文件会在本文的示例中大量出现),文件分隔符为TAB(即'\t'):
$ cat grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28常用的输入输出
使用命令行时通常我们将结果重定向到文件中:
$ awk -F\t '{print $1, $3}' grade.txt > wow或是使用tee,在将结果输出至标准输出的同时,写入文件:
$ awk -F\t '{print $1, $3}' grade.txt | tee wow如果是脚本,在输入上我们可以使用文件:
$ belt.awk grade.txt或是使用标准输入:
$ belt.awk < grade.txt或是管道:
$ grade.txt | belt.awk打印头尾信息
-
使用
BEGIN打印表头,为了对齐使用了制表符\t:Terminal window $ awk 'BEGIN {print "Name\tBelt\n--------------------"}quote> {print $1"\t"$4}' grade.txtName Belt--------------------M.Tansley GreenJ.Lulu greenP.Bunny YellowJ.Troll Brown-3L.Transly Brown-2 -
使用
END打印表尾:Terminal window $ awk 'BEGIN {print "Name\n----------"} {print $1} END {print "----------\ntotle: " NR}' grade.txtName----------M.TansleyJ.LuluP.BunnyJ.TrollL.Transly----------totle: 5
条件操作
| 操作符 | 描述 |
|---|---|
< | 小于 |
<= | 小于等于 |
== | 等于 |
> | 大于 |
>= | 大于等于 |
~ | 匹配正则表达式 |
!~ | 不匹配正则表达式 |
- 使用正则表达式匹配域时,用
~接/Regular_Express/,如果使用if语句则需要放在()中。示例,为第4域匹配正则表达式,输出匹配的记录:
$ awk '{if($4~/Brown/) print $0}' grade.txtJ.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28上例中不使用if也可以,不指定动作时awk默认输出整条记录:
$ awk '$4~/Brown/' grade.txtJ.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28- 使用正则表达式模糊匹配域:
$ awk '{if($3~/48/) print $0}' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26或
$ awk '$3~/48/ {print $0}' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26- 使用
==精确匹配:
$ awk '$3==48 {print $0}' grade.txtP.Bunny 02/99 48 Yellow 12 35 28或
$ awk '{if($3==48) print $0}' grade.txtP.Bunny 02/99 48 Yellow 12 35 28- 使用
!~的正则表达式:
$ awk '$0 !~ /Brown/' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28配合if使用!~:
$ awk '{if($4!~/Brown/) print $1, $4}' grade.txtM.Tansley GreenJ.Lulu greenP.Bunny Yellow- 比较:
- 小于:
$ awk '{if($6<$7) print $1" try better at next comp"}' grade.txtM.Tansley try better at next compJ.Lulu try better at next comp- 小于等于:
$ awk '{if($6<=$7) print $1}' grade.txtM.TansleyJ.LuluJ.Troll- 大于:
$ awk '{if($6>$7) print $1}' grade.txtP.BunnyL.Transly- 常见的正则表达式功能:
- 匹配大小写,使用
[]匹配字符:
$ awk '/[Gg]reen/' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26- 通配符,
.:
$ awk '$1 ~ /^....a/' grade.txtM.Tansley 05/99 48311 Green 8 40 44L.Transly 05/99 4712 Brown-2 12 30 28- 逻辑或,
|:
$ awk '$4 ~ /(Yellow|Brown)/' grade.txtP.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28- 行首
^:
$ awk '/^J/' grade.txtJ.Lulu 06/99 48317 green 9 24 26J.Troll 07/99 4842 Brown-3 12 26 26- 行尾
$:
$ awk '/28$/' grade.txtP.Bunny 02/99 48 Yellow 12 35 28L.Transly 05/99 4712 Brown-2 12 30 28复合逻辑
-
逻辑与,
&&:Terminal window $ awk '{if ($1=="P.Bunny" && $4=="Yellow") print $0}' grade.txtP.Bunny 02/99 48 Yellow 12 35 28 -
逻辑或,
||:Terminal window $ awk '{if ($4=="Yellow" || $4~/Brown/) print $0}' grade.txtP.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28 -
逻辑否,
!:Terminal window $ awk '$4 != "Brown" {print $1, $4}' grade.txtM.Tansley GreenJ.Lulu greenP.Bunny YellowJ.Troll Brown-3L.Transly Brown-2
内置变量的示例
- 已读取的记录数,
NR:
$ awk '{print} END {print "total: "NR}' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28total: 5- 使用
NR检查保证文件记录数大于0:
$ awk '{if (NR>0 && $4~/Brown/) print $0}' grade.txtJ.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28- 当前记录的域数,
NF:
$ awk '{print NF, NR, $0} END {print FILENAME}' grade.txt7 1 M.Tansley 05/99 48311 Green 8 40 447 2 J.Lulu 06/99 48317 green 9 24 267 3 P.Bunny 02/99 48 Yellow 12 35 287 4 J.Troll 07/99 4842 Brown-3 12 26 267 5 L.Transly 05/99 4712 Brown-2 12 30 28grade.txt- 显示当前目录名:
$ pwd/Users/zealot$ echo $PWD | awk -F/ '{print $NF}'zealot- 显示文件名:
$ echo "/Users/zealot/grade.txt" | awk -F/ '{print $NF}'grade.txtawk操作符
在awk中使用操作符,基本表达式可以划分为数字型、字符串型、变量型、域及数组元素。
| 操作符 | 描述 |
|---|---|
= += *= /= %= ^= | 赋值操作符 |
? | 条件表达操作符 |
&& ! | 与、或、非操作符 |
~ !~ | 匹配操作符,包括匹配和不匹配 |
< <= == != >> | 关系操作符 |
+ - * / % ^ | 算术操作符 |
++ -- | 前缀和后缀 |
-
设置输入域到域变量名:
给特定的数据域设置有意义的域名,可以使得代码更加清楚明了。
一般的变量名设置方式为
name=$n,如在grade.txt的例子中,可以设置name=$1;belts=$4。Terminal window $ awk '{name=$1;belts=$4; if(belts~/Yellow/) print name " is belt " belts}' grade.txtP.Bunny is belt Yellow -
域值的比较操作:
有两种方式测试一数值域是否小于另一数值域:
- 在关系操作中使用实际数值;
- 在
BEGIN中给变量名赋值; 关系操作必须写在圆括号内,如查询比赛中得分在27以下的学生:
$ awk '{if($6<27) print $0}' grade.txtJ.Lulu 06/99 48317 green 9 24 26J.Troll 07/99 4842 Brown-3 12 26 26在BEGIN中给变量名赋值可以使代码更清晰:
$ awk 'BEGIN {BASELINE=27} {if($6<BASELINE) print $0}' grade.txtJ.Lulu 06/99 48317 green 9 24 26J.Troll 07/99 4842 Brown-3 12 26 26- 修改数值域取值:
需要注意的是,当在awk中修改任何域时,实际输入文件是不会被修改的,修改的只是 保存在缓存里的awk复本。
$ awk '{if($1=="M.Tansley") $6=$6-1; print NR, NF $1, $6, $7}' grade.txtM.Tansley 39 44J.Lulu 24 26P.Bunny 35 28J.Troll 26 26L.Transly 30 28- 修改文本域:
$ awk '{if($1=="J.Troll") ($1="J.L.Troll"); print $1}' grade.txtM.TansleyJ.LuluP.BunnyJ.L.Troll赋值表达式通常放在括号中,以增强代码的可读性,非必须,如上一项中的$6=$6-1。
- 只显示修改记录:
$ awk '{if($1=="J.Troll") {$1="J.L.Troll"; print $1}}' grade.txtJ.L.Troll此处使用if语句取得需要修改的位置,而后再使用赋值动作$1="J.L.Troll"并打印。
- 创建新的输出域:
$ awk 'BEGIN {print "Name\tDifference"} {if($6<$7) {$8=$7-$6; print $1, $8}}' grade.txtName DifferenceM.Tansley 4J.Lulu 2或是直接将计算结果赋给变量名并打印:
$ awk 'BEGIN {print "Name\tDifference"} {if($6<$7) {diff=$7-$6; print $1, diff}}' grade.txtName DifferenceM.Tansley 4- 列值累加:
$ awk 'total+=$6; END{print "Club student total points: " total}' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28Club student total points: 155需要注意的是,不必在awk中显式说明打印所有记录,当每一个操作匹配时,打印是缺省动作。
如果不想这种缺省打印动作出现,可以显式声明不带打印命令的动作:
$ awk '{total+=$6} END{print "Club student total points: " total}' grade.txtClub student total points: 155累加动作并不一定要写在圆括号中,但是这样做可以增强代码可读性。
- 另一个累加列的示例,统计当前目录中文件的总大小:
ls -lAG | awk '$1~/^[^d]/ {print $9"\t"$5} {total+=$5} END {print "total " total/1024 " KB"}'...file name followed by size...grade.txt 176total 234.544 KB大概过程是,先把当前目录中的文件信息列表用管道送给awk,而后使用第一个域滤掉目录项(d开头的项目,其实直接用/^[^d]/匹配整行就可以了),再累加第五列,最后在打印处理成以KB为单位的结果。
内置字符串函数
| 函数 | 描述 |
|---|---|
| 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(r,s),gsub(r,s,t),在整个个记录中匹配字符串模式r,并用s进行替换。若不指定t,gsub函数默认在$0上替换:
$ awk 'gsub(/4842/, 4899) {print $0} ' grade.txtJ.Troll 07/99 4899 Brown-3 12 26 26index(s,t),返回字符串s中t第一次出现的位置:
$ awk 'BEGIN {print index("Bunny", "ny")}' grade.txt4length(s),返回字符串s的长度:
$ awk '$1=="J.Troll" {print length($1) " " $1}' grade.txt7 J.Troll或是直接传入字符串:
$ awk 'BEGIN {print length("Hello World~")}'12match(s,r),测试目标串s是否包含匹配模式r的字符串,若包含,则返回r第一次出现的位置,若不包含则返回0:
$ awk 'BEGIN {print match("ABCD", /d/)}'0$ awk 'BEGIN {print match("ABCD", /C/)}'3$ awk '$1=="J.Lulu" {print match($1, "u")}' grade.txt4- `split(s,a,fs),使用分隔符fs将字符串s切割,存入数组a中,并返回切割后数组元素个数:
$ awk 'BEGIN {print split("123#456#789", myarray, "#"), myarray[1], myarray[2], myarray[3]}'3 123 456 789sub(r,s,t),在t中查找模式r,将第一次匹配到的字符串替换为s:
$ awk '$1=="J.Troll" {sub(/26/, 29, $0)} {print $0}' grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 29 26L.Transly 05/99 4712 Brown-2 12 30 28substr(s,p),substr(s,p,n),在字符串s中截取从p开始到字符串结束的后缀并返回,若指定n,则截取从p开始长度为n的字符串并返回:
$ awk '$1=="J.Troll" {print substr($0, 3)}' grade.txtTroll 07/99 4842 Brown-3 12 26 26$ awk '$1=="J.Troll" {print substr($0, 3, 5)}' grade.txtTroll$ awk '{print substr($1, 3)}' grade.txtTansleyLuluBunnyTrollTransly$ awk 'BEGIN {STR="hello world~"} END {print substr(STR,7)}' grade.txtworld~- 另:通过管道使用awk函数的例子:
echo "stand-by" | awk '{print length($0)}'8$ STR="mydoc.txt"$ echo $STR | awk '{print substr($0,1,5)}'mydocawk命令的printf函数
printf的基本用法和C语言类似,形式为printf format, values。
| awk printf修饰符 | 描述 |
|---|
-
|左对齐
width |域的步长,用0表示0步长 .prec |最大字符串长度,或小数点右边的位数(精确度)
| awk 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- 格式化输出:
第一列为行号,步长3字符,左对齐;第二列为姓名,步长15字符,左对齐;第四列为序列号,后接换行:
$ awk '{printf "%-3d%-15s%s\n", NR,$1,$3}' grade.txt1 M.Tansley 483112 J.Lulu 483173 P.Bunny 484 J.Troll 48425 L.Transly 4712或加上表头:
$ awk 'BEGIN{printf "%-3s%-15s%s\n---------------------------\n", "NR","Name","No."} {printf "%-3d%-15s%s\n", NR,$1,$3}' grade.txtNR Name No.---------------------------1 M.Tansley 483112 J.Lulu 483173 P.Bunny 484 J.Troll 48425 L.Transly 4712- 向一行awk命令传参数:
$ awk '{if($5<AGE) print $0}' AGE=10 grade.txtM.Tansley 05/99 48311 Green 8 40 44- awk脚本文件:
将awk写入文件中执行,就不必每次输入大段命令了,而且可以好的排版和注释,来提高代码的可读性。
- 场景1,将上面的命令写入脚本文件:
#!/usr/bin/awk -f# all comment line must start with a hash `#`# name: student_total.awk# to call: student_total.awk grade.txt# print total and average of club student points
# print a header first:BEGIN{ print "Student Date Member No. Grade Age Points Max" print "Name Joined Gained Point Available" print "================================================================"}
# let's add the scores of points gained(total+=$6)
# finished processing, now let's print the total and average points:END{ print "Club students total points: " total print "Average club students points: " total/NR}接下来使用chmod给脚本加上“所有用户可执行”的权限,再使用cp将脚本复制到shell可以找到的地方:
$ chmod a+x student_total.awk$ sudo cp student_total.awk /usr/bin/然后执行:
$ student_total.awk grade.txtStudent Date Member No. Grade Age Points MaxName Joined Gained Point Available================================================================M.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26P.Bunny 02/99 48 Yellow 12 35 28J.Troll 07/99 4842 Brown-3 12 26 26L.Transly 05/99 4712 Brown-2 12 30 28Club students total points: 155Average club students points: 31顺便提一下,在OSX上使用ls命令时,会发现有的目录或文件的权限位的最后附有一个@或+符号,其中@说明该目录或文件具有extended attributes,而+符合说明该目录或文件具有类似ACL的非标准权访问限控制策略。可以通过在ls后添加-@命令查看。详情见”ls” on mac and extended attributes。
- 场景2,清除重复输出:
当调试程序时,遇到这样一个日志文件:
$ cat error.logINVALID LCSD 98GJ23ERROR*ERROR*CAUTION LPSS ERROR ON ACC NO.ERROR*ERROR*ERROR*ERROR*ERROR*PASS FIELD INVALID ON LDPSERROR*ERROR*PASS FIELD INVALID ON GHSIERROR*CAUTION LPSS ERROR ON ACC NO.ERROR*ERROR*我们想将其中的重复的多行ERROR*合并为一行:
#!/usr/bin/awk -f# name: error_strip.awk# to call: error_strip.awk <filename># strips out the ERROR* lines if there are more than one# ERROR* lines after each failed record.
BEGIN{error_line=""}
# tell awk who is "ERROR*"{ if($0=="ERROR*" && error_line=="ERROR*")# goto next line next; error_line=$0; print}执行结果:
$ error_strip.awk error.logINVALID LCSD 98GJ23ERROR*CAUTION LPSS ERROR ON ACC NO.ERROR*PASS FIELD INVALID ON LDPSERROR*PASS FIELD INVALID ON GHSIERROR*CAUTION LPSS ERROR ON ACC NO.ERROR*- 在脚本中制定分隔符FS:
再复习一下,在shell中使用awk命令时,用-F指定分隔符:
$ awk -F: '{print $0}' input-file而在脚本中,则是设置FS变量,值得注意的都是,FS变量需要放在BEGIN部分:
#!/usr/bin/awk -f# to call: passwd.awk /private/etc/passwd# print out the first and fifth fields
BEGIN{FS=":"}
{ # pass the comments of the file if($0 ~ /^#/) next; print $1"\t"$5}执行结果:
nobody Unprivileged Userroot System Administratordaemon System Services...- 向脚本传递参数:
- 前面提到,在shell中使用awk命令时传递参数的形式是:
$ awk '{if($5<AGE) print $0}' AGE=10 grade.txt- 而使用脚本时,传递的方式的形式也基本相同:
awk script_file var=value input_file示例:
#!/usr/bin/awk -f# check on how many fields in a file# name: field_check.awk# to call: field_check.awk MAX=n FS=<separator> input-file
NF!=MAX{ print("line " NR " does not have " MAX " fields")}调用时命令为field_check.awk MAX=7 FS=":" /private/etc/passwd。
- 将上面提到的传递“AGE”参数的例子改写,就应为:
#!/usr/bin/awk -f# name: age.awk# to call: age.awk AGE=10 grade.txt# print students whose age are lower than age supplied on the command line{ if($5<AGE) print $0}执行:
$ age.awk AGE=10 grade.txtM.Tansley 05/99 48311 Green 8 40 44J.Lulu 06/99 48317 green 9 24 26- 通过管道使用脚本:
从du命令获得数据后处理输出:
#!/usr/bin/awk -f# name: du.awk# to call: du | du.awk# print file/direc's in bytes and blocks
BEGIN{ OFS="\t"; print "name", "\t\t", "bytes", "blocks\n"; print "=================================="}
{print $2, "\t\t", $1*512, $1}执行:
$ du | du.awkname bytes blocks
==================================. 20461768704 39964392...- awk数组:
上面也提到了awk数组,现在看看更多关于数组的应用。
数组在使用前,不需要声明,也不需要设定元素个数,我们经常会在循环中使用到数组,基本语法如下:
For (element in array) print array(element)- 如果把上面的例子在脚本中重新实现:
#!/usr/bin/awk -f# name: array_test.awk# prints out an arrayBEGIN{ recode="123#456#789"; split(recode, myarray, "#");}END{ for(i in myarray) print i, myarray[i]}这时我们可以使用/dev/null作为输入文件:
$ array_test.awk /dev/null2 4563 7891 123- 一个稍微复杂的统计示例:
$ cat grade_student.txtYellow#JuniorOrange#SeniorYellow#JuniorPurple#JuniorBrown-2#JuniorWhite#SeniorOrange#SeniorRed#JuniorBrown-2#SeniorYellow#SeniorRed#JuniorBlue#SeniorGreen#SeniorPurple#JuniorWhite#Junior要统计文件中黄、橙、红带各有多少种,分别有多少成年人和未成年人:
#!/usr/bin/awk -f# name: belts.awk# to call: belts.awk grade_student.txt# loop through the file and count how many belts we have# in (yellow, orange, red) also count how many adults and# juniors we have.## start from BEGIN# set FS and load the arrays with our valuesBEGIN{ FS="#"# load the belt colors we are interested in only belt["Yellow"] belt["Orange"] belt["Red"]# load the type of students student["Junior"] student["Senior"]}# loop through array that holds the belt colors against field-1# if we have a match, keep a running total{ for(color in belt) if($1==color) belt[color]++}# loop through array that holds the student type against field-2# if we have a match, keep a running total{ for(type in student) if($2==type) student[type]++}# finish processing, print out the match for each arrayEND{ for(color in belt) print "The club has", belt[color], color, "belts" for(type in student) print "The club has", student[type], type, "students"}执行:
$ belts.awk grade_student.txtThe club has 2 Orange beltsThe club has 2 Red beltsThe club has 3 Yellow beltsThe club has 8 Junior studentsThe club has 7 Senior students小结
awk是shell中的一个重要文本处理工具,在特定情况下可以方便快捷的配合其他命令输出有格式要求的文本并统计,就像像黑客一样使用Linux命令行:我喜欢使用命令行的原因中提到的这样~
-EOF-