文本编辑的一点心得--awk篇

本文模仿十三问,以提问和回答的方式介绍 awk的一些基础知识。论坛里awk高手众多,有错误的地方还请大家指正。


本文所有用例的测试环境采用 unix-center的ubuntu 服务器
http://www.unix-center.net/

具体版本如下:
  1. ly5066113@ubuntu:~$ uname -a
  2. Linux ubuntu 2.6.24-22-generic #1 SMP Mon Nov 24 19:35:06 UTC 2008 x86_64 GNU/Linux
  3. ly5066113@ubuntu:~$ bash --version
  4. GNU bash, version 3.2.39(1)-release (x86_64-pc-linux-gnu)
  5. Copyright (C) 2007 Free Software Foundation, Inc.
  6. ly5066113@ubuntu:~$ awk --version
  7. GNU Awk 3.1.6
  8. Copyright (C) 1989, 1991-2007 Free Software Foundation.
复制代码
一、RS 与 ORS 差在哪

我们经常会说,awk是基于行列操作文本的,但如何定义“行”呢?这就是RS的作用。
默认情况下,RS的值是\n。下面通过实例来理解下RS。
  1. ly5066113@ubuntu:~$ echo '1a2a3a4a5' | awk '{print $1}'
  2. 1a2a3a4a5
  3. ly5066113@ubuntu:~$ echo '1a2a3a4a5' | awk 'BEGIN{RS="a"}{print $1}'
  4. 1
  5. 2
  6. 3
  7. 4
  8. 5
复制代码
我们可以看到,在更改了RS的值后,awk定义的行已经不是我们实际意义中的行了。
上面RS固定的字符串,RS也可以定义为正则表达式。
  1. ly5066113@ubuntu:~$ echo '1ab2bc3cd4de5' | awk 'BEGIN{RS="[a-z]+"}{print $1,RS,RT}'
  2. 1 [a-z]+ ab
  3. 2 [a-z]+ bc
  4. 3 [a-z]+ cd
  5. 4 [a-z]+ de
  6. 5 [a-z]+
复制代码
当我们将RS设置为正则表达式的时候,RT这个变量就有作用了,RS的值始终为我们设定的正则,RT的值则是这个正则实际匹配到的内容。
下面我们看看将RS设置为空会是什么情况
  1. ly5066113@ubuntu:~$ cat urfile
  2. 1

  3. 2


  4. 3








  5. 4
  6. ly5066113@ubuntu:~$ awk 'BEGIN{RS=""}{print $0}' urfile
  7. 1
  8. 2
  9. 3
  10. 4
复制代码
如果RS被设置为空,那么awk会将连续的空行作为行分隔符,与RS设置成"\n\n+"有什么区别???
1、忽略 文件开头和结尾的空行。且文件不以记录分隔符结束,即最后不是空行,会将最后一个记录的尾\n去掉
2、不设置RT变量(测试未发现规律,暂时认为RT变量不可用)
3、影响FS变量
这个怎么理解?对于1、2两点,当作习题留给大家自己测试,3我们下节来讲。

总结下RS的3种情况:
1) 非空字符串
   以固定字符串作为行分隔符,同时设置变量RT为固定字符串
2) 正则表达式
   以正则表达式作为行分隔符,同时设置变量RT为正则表达式实际匹配到的字符串
3) 空字符
   以连续的空行作为行分隔符,如果FS为单个字符,会将\n强制加入到FS变量中

理解了RS,再来理解ORS就简单了。RS是awk读取文件时的行分隔符,ORS则是awk输出时的行结束符。
更简单的讲,就是awk在输出时,会在每行记录后面增加一个ORS变量所设定的值。
ORS的值只能设定为字符串,默认情况下,ORS的值是\n
  1. ly5066113@ubuntu:~$ seq 5 | awk '{print $0}'
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. ly5066113@ubuntu:~$ seq 5 | awk 'BEGIN{ORS="a"}{print $0}'
  8. 1a2a3a4a5a
复制代码
我们平常用的 print $0 等价于 printf $0 ORS


二、FS 与 OFS 差在哪

RS是awk用来定义“行”的,那么FS就是awk用来定义“列”的。
设置变量 FS 与使用 -F 参数是一样的。
  1. ly5066113@ubuntu:~$ echo '1,2' | awk -F , '{print $1}'
  2. 1
  3. ly5066113@ubuntu:~$ echo '1,2' | awk 'BEGIN{FS=","}{print $1}'
  4. 1
复制代码
与 RS 类似,FS 同样可以设置为正则表达式
  1. ly5066113@ubuntu:~$ echo '1ab2bc3cd4de5' | awk 'BEGIN{FS="[a-z]+"}{print $1,$2,$5}'
  2. 1 2 5
复制代码
FS 有1个特例,就是将FS设置为一个空格,FS=" " ,这也是FS的默认值
  1. In the special case that FS is a single space, fields are separated by runs of spaces and/or tabs and/or newlines.
复制代码
此时,awk会将连续的 空格 或 制表符(\t) 或 换行符(\n) 作为列的分隔符
那么,FS=" " 与 FS="[ \t\n]+" 有区别么???
答案是肯定的
  1. ly5066113@ubuntu:~$ echo ' 1 2' | awk 'BEGIN{FS=" "}{print $1}'
  2. 1
  3. ly5066113@ubuntu:~$ echo ' 1 2' | awk 'BEGIN{FS="[ \t\n]+"}{print $1}'
复制代码
当FS=" "时,awk会自动去掉行首和行尾的 空格 或 制表符(\t) 或 换行符(\n),但FS="[ \t\n]+"是不会的
同样,FS也可以设置为空
  1. ly5066113@ubuntu:~$ echo '123' | awk 'BEGIN{FS=""}{print $1,$2}'
  2. 1 2
复制代码
当FS被设置为空字符串的时候,awk会将一行记录的每个字符做为单独的一列
类似的,当我们想以固定的长度来分隔列的时候,可以使用 FIELDWIDTHS 来代替 FS
例如,一行记录的前3个字符作为第一列,接下来的2个字符作为第二列,接下来的4个字符作为第三列
  1. ly5066113@ubuntu:~$ echo '123456789' | awk 'BEGIN{FIELDWIDTHS="3 2 4"}{print $1,$2,$3}'
  2. 123 45 6789
  3. ly5066113@ubuntu:~$ echo '123456789' | awk 'BEGIN{FIELDWIDTHS="3 2 3"}{print $1,$2,$3}'
  4. 123 45 678
  5. ly5066113@ubuntu:~$ echo '123456789' | awk 'BEGIN{FIELDWIDTHS="3 2 5"}{print $1,$2,$3}'
  6. 123 45 6789
复制代码
如果定义的长度小于实际的长度,awk会截断,如果大于实际长度,则以实际长度为准。

总结下FS的4种情况:
1) 非空字符串
   以固定字符串作为列分隔符
2) 正则表达式
   以正则表达式作为列分隔符
3) 单个空格
   以连续的 空格 或 制表符(\t) 或 换行符(\n)作为列分隔符
4) 空字符
   以每个字符做为单独的一列

接下来我们来看看上节提到的问题:
当 RS="" 时,会将\n强制加入到FS变量中
  1. ly5066113@ubuntu:~$ cat urfile
  2. 1
  3. a

  4. 2
  5. a


  6. 3
  7. ly5066113@ubuntu:~$ awk -v RS="" '{print "#" $0 "#"}' urfile
  8. #1
  9. a#
  10. #2
  11. a#
  12. #3#
  13. ly5066113@ubuntu:~$ awk -F "b" -v RS="" '{print $1}' urfile
  14. 1
  15. 2
  16. 3
  17. ly5066113@ubuntu:~$ awk -F "c" -v RS="" '{print $1}' urfile
  18. 1
  19. 2
  20. 3
  21. ly5066113@ubuntu:~$ awk -F "c" -v RS="\n\n+" '{print "#" $1 "#"}' urfile
  22. #1
  23. a#
  24. #2
  25. a#
  26. #3
  27. #
复制代码
如果FS为单个字符,\n始终存在在 FS 中,而 RS="\n\n+" 则不会。

了解的 FS ,我们来看看 OFS ,FS是awk读入记录时的列分隔符,OFS则是awk输出时的列分隔符。
我们平时使用的 print $1,$2 等价于 print $1 OFS $2
  1. ly5066113@ubuntu:~$ echo '1 2' | awk -v OFS="|" '{print $1,$2}'
  2. 1|2
  3. ly5066113@ubuntu:~$ echo '1 2' | awk -v OFS="|" '{print $1 OFS $2}'
  4. 1|2
复制代码
如果一行记录有很多列,同时想改变输出的分隔符,print $1,$2,$3 ... 岂不是很麻烦?
当然有简单的方法:
  1. ly5066113@ubuntu:~$ echo '1 2 3 4 5' | awk -v OFS="|" '{print $0}'
  2. 1 2 3 4 5
  3. ly5066113@ubuntu:~$ echo '1 2 3 4 5' | awk -v OFS="|" '{$1=$1;print $0}'
  4. 1|2|3|4|5
  5. ly5066113@ubuntu:~$ echo '1 2 3 4 5' | awk -v OFS="|" '{NF+=0;print $0}'
  6. 1|2|3|4|5
复制代码
为了使OFS的设置生效,需要改变 $0 ,这里我们是对 awk 撒了个小谎
$1=$1 或者 NF+=0, $0 本身的内容实际上没有任何改变,只是为了使 OFS 的设置生效

在理解了 RS 和 FS 之后,我们来回顾开始的那句话:“awk是基于行列操作文本的”
这个说法实际上不是很准确,因为在改变了 RS 后,awk 中的“行”已经不是一般的“行”了
同样,改变了 FS 后,awk 中的“列”也已经不是一般的“列”了
因此,准确的应该这样讲:“awk是基于 记录(record) 和 域(field) 操作文本的”


三、0 与 "0" 差在哪

我们先来看一个例子:
  1. ly5066113@ubuntu:~$ awk 'BEGIN{if(0) print "true";else print "false"}'
  2. false
  3. ly5066113@ubuntu:~$ awk 'BEGIN{if("0") print "true";else print "false"}'
  4. true
复制代码
为什么同样是 0 ,结果却不一样?
其实要解释这个问题,只需要弄清楚awk中的“真”与“假”。
以下3种情况是“假”,其他情况都为“真”
1) 数字 0
2) 空字符串
3) 未定义的值
  1. ly5066113@ubuntu:~$ awk 'BEGIN{a=0;if(a) print "true";else print "false"}'
  2. false
  3. ly5066113@ubuntu:~$ awk 'BEGIN{a="";if(a) print "true";else print "false"}'
  4. false
  5. ly5066113@ubuntu:~$ awk 'BEGIN{if(a) print "true";else print "false"}'
  6. false
复制代码
以上是普通的字符测试,对于是表达式的情况,先计算表达式,然后将结果按上面的情况测试
  1. ly5066113@ubuntu:~$ awk 'BEGIN{if(a=1) print "true";else print "false"}'
  2. true
  3. ly5066113@ubuntu:~$ awk 'BEGIN{if(a=0) print "true";else print "false"}'
  4. false
  5. ly5066113@ubuntu:~$ awk 'BEGIN{if(a="0") print "true";else print "false"}'
  6. true
  7. ly5066113@ubuntu:~$ awk 'BEGIN{if(a="") print "true";else print "false"}'
  8. false
  9. ly5066113@ubuntu:~$ awk 'BEGIN{if(a=a) print "true";else print "false"}'
  10. false
复制代码
来看一个被大家称为月经的问题,awk如何去重?
  1. awk '! a[$0] ++'
复制代码
在解释之前,我们先要了解awk的一个特性:
awk 会根据语境来给未定义的变量赋初始值
  1. ly5066113@ubuntu:~$ awk 'BEGIN{print a "" 1}'
  2. 1
  3. ly5066113@ubuntu:~$ awk 'BEGIN{print a + 1}'
  4. 1
复制代码
对于未定义的变量,如果要进行字符串操作,会被赋成空字符串 ""
如果要进行数学运算,会被赋成数字 0

现在我们看看上面的代码 ! a[$0] ++ 等价于 if(! a[$0] ++) print $0
对于首次出现的记录,a[$0]的值是未定义的,由于后面的 ++ 是数学计算,所以a[$0]会被赋值成数字0
也是由于 ++ 操作符,会先取值,再计算,所以对于第一行记录实际上是if(! 0) print $0
! 是取反,0 是假,! 0 就是真,那么就会执行后面的 print $0
对于后面出现的重复记录,a[$0] 经过 ++ 的计算已经变为 1、2、3 。。。
而 ! 1  ! 2  ! 3 ... 都为假,不会打印。

下面我们用黑哥的一段代码来深刻体会一下,用awk打印奇数行:
  1. ly5066113@ubuntu:~$ seq 10 | awk 'i=!i'
  2. 1
  3. 3
  4. 5
  5. 7
  6. 9
复制代码
你想明白了么?


四、NR 与 FNR 差在哪

NR与FNR在awk处理单个文件时是没有区别的,处理多个文件时才有区别,我们先看个例子:
  1. ly5066113@ubuntu:~$ awk '{print FILENAME,"NR="NR,"FNR="FNR,$0}' a.txt b.txt c.txt
  2. a.txt NR=1 FNR=1 a
  3. a.txt NR=2 FNR=2 b
  4. a.txt NR=3 FNR=3 c
  5. b.txt NR=4 FNR=1 d
  6. b.txt NR=5 FNR=2 e
  7. b.txt NR=6 FNR=3 f
  8. c.txt NR=7 FNR=1 g
  9. c.txt NR=8 FNR=2 h
  10. c.txt NR=9 FNR=3 i
复制代码
我们可以看到,NR是awk处理的总记录数,无论多少个文件,是一直累加的
而FNR是awk处理当前文件的记录数,当文件变化的时候是重新记数的

上面还有出现了一个变量FILENAME,这个变量的值就是awk当前处理的文件的文件名
这里我们还有几个变量需要了解:ARGV、ARGC、ARGIND
ARGV 是一个数组,它记录着命令行的所有参数的值
ARGC 是命令行参数的个数,(不包括-F、-v之类的awk参数)
ARGIND 是ARGV数组的索引值,从0到ARGC-1
当我们想去了解这些变量时,最简单并且最有效的方法就是print
  1. ly5066113@ubuntu:~$ awk 'BEGIN{for(i=0;i<ARGC;i++) print "ARGV["i"]="ARGV[i]}{print ARGV[ARGIND],ARGIND,ARGC,$0}' [abc].txt
  2. ARGV[0]=awk
  3. ARGV[1]=a.txt
  4. ARGV[2]=b.txt
  5. ARGV[3]=c.txt
  6. a.txt 1 4 a
  7. a.txt 1 4 b
  8. a.txt 1 4 c
  9. b.txt 2 4 d
  10. b.txt 2 4 e
  11. b.txt 2 4 f
  12. c.txt 3 4 g
  13. c.txt 3 4 h
  14. c.txt 3 4 i
复制代码
那么,ARGV[ARGIND]与FILENAME是否等价呢?
一般情况下,在awk处理文件时,两者是一样的
我们看看其他一些特殊的情况;
  1. ly5066113@ubuntu:~$ awk 'BEGIN{print FILENAME,ARGV[ARGIND]}'
  2. awk
  3. ly5066113@ubuntu:~$ awk 'BEGIN{getline;print FILENAME,ARGV[ARGIND],$0}'
  4. 123
  5. - awk 123
  6. ly5066113@ubuntu:~$ awk 'BEGIN{getline;print FILENAME,ARGV[ARGIND],$0}' a.txt
  7. a.txt a.txt a
  8. ly5066113@ubuntu:~$ awk 'BEGIN{getline<"a.txt";print FILENAME,ARGV[ARGIND],$0}'
  9. awk a
复制代码
我们可以看到,在任何情况下,ARGV[ARGIND]始终是有值的,而FILENAME则不一定
  1. FILENAME    The name of the current input file.  If no files are specified on the command line, the value of FILENAME is "-".  However, FILENAME is undefined  inside
  2.                    the BEGIN block (unless set by getline).
复制代码
下面我们用一个简单的例子来理解下上面的内容;
实现 head 命令的功能
我想这个问题大家一定都会:
  1. awk 'NR<=10'
复制代码
但这里我们希望是用awk处理多个文件,先来看看head处理多个文件的情况:
  1. ly5066113@ubuntu:~$ head [abc].txt
  2. ==> a.txt <==
  3. a
  4. b
  5. c

  6. ==> b.txt <==
  7. d
  8. e
  9. f

  10. ==> c.txt <==
  11. g
  12. h
  13. i
复制代码
下面我们用awk来达到这个效果:
  1. ly5066113@ubuntu:~$ awk 'FNR==1{if(NR>1) print "";print "==> "FILENAME" <=="}FNR<=10' [abc].txt
  2. ==> a.txt <==
  3. a
  4. b
  5. c

  6. ==> b.txt <==
  7. d
  8. e
  9. f

  10. ==> c.txt <==
  11. g
  12. h
  13. i
复制代码
让我们再深入考虑下,head在处理文件时,无论文件多大,速度都是很快的,awk是否也可以呢?
上面的代码当然不可以,因为awk会处理整个文件,文件越大,速度越慢。
单个文件时可以这样改写:
  1. awk '1;NR==10{exit}'
复制代码
多个文件时用exit就不行了,因为这样awk处理一个文件后就结束了,我们可以用nextfile
  1. awk 'FNR==1{if(NR>1) print "";print "==> "FILENAME" <=="}1;FNR==10{nextfile}' [abc].txt
复制代码
nextfile就是停止处理当前文件,开始处理下一个文件。

我们再来看一个例子:
每三个文件合并为一个大文件
http://bbs.chinaunix.net/viewthread. php?tid=1792571
帖子里的方法是用while read的方式实现的,下面给一个纯awk的方法:
  1. ly5066113@ubuntu:~$ awk 'FNR==1&&ARGIND%3==1{if(ARGIND>1){print s>f;s=""};f=sprintf("file%02d-%02d",ARGIND,ARGIND+2)}/^set/{print $0>f}/^plot/{$1=s?"":$1;s=s?s","$0:$0}END{print s>f}' file[0-9][0-9]
  2. ly5066113@ubuntu:~$ cat file01-03
  3. set arrow from 7,1633 to 8,1383
  4. set label "9575(100916104th)" at 6.5,1683
  5. set arrow from 7,1633 to 8,1383
  6. set label "9575(100916104th)" at 6.5,1683
  7. set arrow from 7,1633 to 8,1383
  8. set label "9575(100916104th)" at 6.5,1683
  9. plot "diff_029.file" with linespoints, "diff_029.file" with linespoints, "diff_029.file" with linespoints
  10. ly5066113@ubuntu:~$ cat file04-06
  11. set arrow from 7,1633 to 8,1385
  12. set label "9575(100916104th)" at 6.5,1685
  13. set arrow from 7,1633 to 8,1383
  14. set label "9575(100916104th)" at 6.5,1683
  15. set arrow from 7,1633 to 8,1383
  16. set label "9575(100916104th)" at 6.5,1683
  17. plot "diff_029.file" with linespoints, "diff_029.file" with linespoints, "diff_029.file" with linespoints
复制代码
awk处理多个文件,是不是没想象中那么复杂呢?


五、> 与 >> 差在哪

这里的>和>>是awk内部的,不要和 shell本身的>和>>混淆。
  1. ly5066113@ubuntu:~$ awk '{print NR,$0}' a.txt > b.txt
  2. ly5066113@ubuntu:~$ cat b.txt
  3. 1 a
  4. 2 b
  5. 3 c
  6. ly5066113@ubuntu:~$ awk '{print NR,$0 > "b.txt"}' a.txt
  7. ly5066113@ubuntu:~$ cat b.txt
  8. 1 a
  9. 2 b
  10. 3 c
复制代码
第一句awk命令中的 > 就是shell本身的IO重定向,第二句awk命令中的 > 是awk内部的IO重定向
awk中的 > 和 >> 类似 perl的文件句柄,只在首次打开文件时有区别:
  1. ly5066113@ubuntu:~$ rm b.txt
  2. ly5066113@ubuntu:~$ awk '{print NR,$0 > "b.txt"}' a.txt
  3. ly5066113@ubuntu:~$ cat b.txt
  4. 1 a
  5. 2 b
  6. 3 c
  7. ly5066113@ubuntu:~$ rm b.txt
  8. ly5066113@ubuntu:~$ awk '{print NR,$0 >> "b.txt"}' a.txt
  9. ly5066113@ubuntu:~$ cat b.txt
  10. 1 a
  11. 2 b
  12. 3 c
  13. ly5066113@ubuntu:~$ awk '{print NR,$0 > "b.txt"}' a.txt
  14. ly5066113@ubuntu:~$ cat b.txt
  15. 1 a
  16. 2 b
  17. 3 c
  18. ly5066113@ubuntu:~$ awk '{print NR,$0 >> "b.txt"}' a.txt
  19. ly5066113@ubuntu:~$ cat b.txt
  20. 1 a
  21. 2 b
  22. 3 c
  23. 1 a
  24. 2 b
  25. 3 c
复制代码
如果文件不存在,那么 > 和 >> 是一样的
如果文件已经存在,> 会覆盖原文件,>> 则是追加

我们看到,在打开文件后,awk会一直向文件里追加记录,那么可以在awk里关闭文件么?
这时我们就需要用到 close
  1. ly5066113@ubuntu:~$ awk '{print NR,$0 > "b.txt";close("b.txt")}' a.txt
  2. ly5066113@ubuntu:~$ cat b.txt
  3. 3 c
复制代码
每次写入后都关闭文件,那么下次写入时就又重新打开文件
而每次打开又是覆盖的方式,这样b.txt里最后就只剩下a.txt的最后一行记录

说到awk中的IO就不得不提 getline
  1. getline               Set $0 from next input record; set NF, NR, FNR.
  2. getline <file         Set $0 from next record of file; set NF.
  3. getline var           Set var from next input record; set NR, FNR.
  4. getline var <file     Set var from next record of file.
  5. command | getline [var]
  6.                       Run command piping the output either into $0 or var, as above.
复制代码
getline的用法很灵活,可以从awk当前处理的文件中获取下一行记录
也可以从外部的文件或者管道中获取记录,每次只取一行,赋给$0或者变量var
先来看个简单的例子,实现 grep -A1
  1. ly5066113@ubuntu:~$ seq 10 | grep -A1 5
  2. 5
  3. 6
  4. ly5066113@ubuntu:~$ seq 10 | awk '/5/{print;getline;print}'
  5. 5
  6. 6
复制代码
再看个例子:
有没有将两个输出或者说变量上每列分别合在一起的方法
http://bbs3.chinaunix.net/viewthread.php?tid=1752341
  1. $ var1='a
  2. > b
  3. > c
  4. > d'
  5. $ var2='A
  6. > B
  7. > C
  8. > D'
  9. $ export var2
  10. $ echo "$var1" | awk '{"echo \042$var2\042"|getline var2;print $1""var2}'
  11. aA
  12. bB
  13. cC
  14. dD
复制代码
你看懂了么?




未完,待续 。。。

你可能感兴趣的:(linux,shell,职场,awk,休闲)