翼度科技»论坛 云主机 LINUX 查看内容

一篇文章玩透awk

2

主题

2

帖子

6

积分

新手上路

Rank: 1

积分
6
安装新版本gawk

awk有很多种版本,例如nawk、gawk。gawk是GNU awk,它的功能很丰富。
本教程采用的是gawk 4.2.0版本,4.2.0版本的gawk是一个比较大的改版,新支持的一些特性非常好用,而在低于4.2.0版本时这些语法可能会报错。所以,请先安装4.2.0版本或更高版本的gawk。
查看awk版本
  1. awk --version
复制代码
这里以安装gawk 4.2.0为例。
  1. # 1.下载
  2. wget --no-check-certificate https://mirrors.tuna.tsinghua.edu.cn/gnu/gawk/gawk-4.2.0.tar.gz
  3. # 2.解压、进入解压后目录
  4. tar xf gawk-4.2.0.tar.gz
  5. cd gawk-4.2.0/
  6. # 3.编译,并执行安装目录为/usr/local/gawk4.2
  7. ./configure --prefix=/usr/local/gawk4.2 && make && make install
  8. # 4.创建一个软链接:让awk指向刚新装的gawk版本
  9. ln -fs /usr/local/gawk4.2/bin/gawk /usr/bin/awk
  10. # 此时,调用awk将调用新版本的gawk,调用gawk将调用旧版本的gawk
  11. awk --version
  12. gawk --version
复制代码
本系列的awk教程中,将大量使用到如下示例文件a.txt。
  1. ID  name    gender  age  email          phone
  2. 1   Bob     male    28   abc@qq.com     18023394012
  3. 2   Alice   female  24   def@gmail.com  18084925203
  4. 3   Tony    male    21   aaa@163.com    17048792503
  5. 4   Kevin   male    21   bbb@189.com    17023929033
  6. 5   Alex    male    18   ccc@xyz.com    18185904230
  7. 6   Andy    female  22   ddd@139.com    18923902352
  8. 7   Jerry   female  25   exdsa@189.com  18785234906
  9. 8   Peter   male    20   bax@qq.com     17729348758
  10. 9   Steven  female  23   bc@sohu.com    15947893212
  11. 10  Bruce   female  27   bcbd@139.com   13942943905
复制代码
读取文件的几种方式

读取文件有如下几种常见的方式:

  • 按字符数量读取:每一次可以读取一个字符,或者多个字符,直到把整个文件读取完
  • 按照分隔符进行读取:一直读取直到遇到了分隔符才停止,下次继续从分隔的位置处向后读取,直到读完整个文件
  • 按行读取:每次读取一行,直到把整个文件读完

    • 它是按照分隔符读取的一种特殊情况:将分隔符指定为了换行符\n

  • 一次性读取整个文件

    • 是按字符数量读取的特殊情况
    • 也是按分隔符读取的特殊情况

  • 按字节数量读取:一次读取指定数量的字节数据,直到把文件读完
下面使用Shell的read命令来演示前4种读取文件的方式(第五种按字节数读取的方式read不支持)。
按字符数量读取

read的-n选项和-N选项可以指定一次性读取多少个字符。
  1. # 只读一个字符
  2. read -n 1 data <a.txt
  3. # 读100个字符,但如果不足100字符时遇到换行符则停止读取
  4. read -n 100 data < a.txt
  5. # 强制读取100字符,遇到换行符也不停止
  6. read -N 100 data < a.txt
复制代码
可以修改OFMT,来自定义数值转换为字符串时的格式:
  1. # 正确
  2. while read -N 3 data;do
  3.   echo "$data"
  4. done <a.txt
  5. # 错误
  6. while read -N 3 data < a.txt;do
  7.   echo "$data"
  8. done
复制代码
printf
  1. # 一直读取,直到遇到字符m才停止,并将读取的数据保存到data变量中
  2. read -d "m" data <a.txt
复制代码
格式化字符:

修饰符:均放在格式化字符的前面
  1. while read -d "m" data ;do
  2.   echo "$data"
  3. done <a.txt
复制代码
sprintf()

sprintf()采用和printf相同的方式格式化字符串,但是它不会输出格式化后的字符串,而是返回格式化后的字符串。所以,可以将格式化后的字符串赋值给某个变量。
  1. # 从a.txt中读取第一行保存到变量data中
  2. read line <a.txt
复制代码
重定向输出


print[f] something | Shell_Cmd时,awk将创建一个管道,然后启动Shell命令,print[f]产生的数据放入管道,而命令将从管道中读取数据。
  1. while read line;do
  2.   echo "$line"
  3. done <a.txt
复制代码
print[f] something |& Shell_Cmd时,print[f]产生的数据交给Coprocess。之后,awk再从Coprocess中取回数据。这里的|&有点类似于能够让Shell_Cmd后台异步运行的管道。
stdin、stdout、stderr

awk重定向时可以直接使用/dev/stdin、/dev/stdout和/dev/stderr。还可以直接使用某个已打开的文件描述符/dev/fd/N。
例如:
  1. # 指定超出文件大小的字符数量
  2. read -N 1000000 data <a.txt
  3. echo "$data"
  4. # 指定文件中不存在的字符作为分隔符
  5. read -d "_" data <a.txt
  6. echo "$data"
复制代码
awk变量

awk的变量是动态变量,在使用时声明。
所以awk变量有3种状态:

  • 未声明状态:称为untyped类型
  • 引用过但未赋值状态:unassigned类型
  • 已赋值状态
引用未赋值的变量,其默认初始值为空字符串或数值0
在awk中未声明的变量称为untyped,声明了但未赋值(只要引用了就声明了)的变量其类型为unassigned。
gawk 4.2版提供了typeof()函数,可以测试变量的数据类型,包括测试变量是否声明。
  1. awk 'awk_program' a.txt
复制代码
除了typeof(),还可以使用下面的技巧进行检测:
  1. # 输出a.txt中的每一行
  2. awk '{print $0}' a.txt
  3. # 多个代码块,代码块中多个语句
  4. # 输出每行之后还输出两行:hello行和world行
  5. awk '{print $0}{print "hello";print "world"}' a.txt
复制代码
变量赋值

awk中的变量赋值语句也可以看作是一个有返回值的表达式。
例如,a=3赋值完成后返回3,同时变量a也被设置为3。
基于这个特点,有两点用法:

  • 可以x=y=z=5,等价于z=5 y=5 x=5
  • 可以将赋值语句放在任意允许使用表达式的地方

    • x != (y = 1)
    • awk 'BEGIN{print (a=4);print a}'

问题:a=1;arr[a+=2] = (a=a+6)是怎么赋值的,对应元素结果等于?arr[3]=7。但不要这么做,因为不同awk的赋值语句左右两边的评估顺序有可能不同。
awk中声明变量的位置


awk中使用Shell变量

要在awk中使用Shell变量,有三种方式:
1.在-v选项中将Shell变量赋值给awk变量
  1. awk '{print $0}' a.txt
  2. awk '{print $0}{print $0;print $0}' a.txt
复制代码
-v选项是在awk工作流程的第一阶段解析的,所以-v选项声明的变量在BEGIN{}、END{}和main代码段中都能直接使用。
2.在非选项型参数位置处使用var=value格式将Shell变量赋值给awk变量
  1. pattern1{statement1}pattern2{statement3;statement4;...}
复制代码
非选项型参数设置的变量不能在BEGIN代码段中使用。
3.直接在awk代码部分暴露Shell变量,交给Shell解析进行Shell的变量替换
  1. awk 'BEGIN{print "我在前面"}{print $0}' a.txt
  2. awk 'END{print "我在后面"}{print $0}' a.txt
  3. awk 'BEGIN{print "我在前面"}{print $0}END{print "我在后面"}' a.txt
复制代码
这种方式最灵活,但可读性最差,可能会出现大量的引号。
数据类型

gawk有两种基本的数据类型:数值和字符串。在gawk 4.2.0版本中,还支持第三种基本的数据类型:正则表达式类型。
数据是什么类型在使用它的上下文中决定:在字符串操作环境下将转换为字符串,在数值操作环境下将转换为数值。这和自然语言中的一个词语、一个单词在不同句子内的不同语义是一样的。
隐式转换:

  • 算术加0操作可转换为数值类型

    • "123" + 0返回数值123
    • "  123abc" + 0转换为数值时为123
    • 无效字符串将转换成0,例如"abc"+3返回3

  • 连接空字符串可转换为字符串类型

    • 123""转换为字符串"123"

  1. awk [ -- ] program-text file ...        (1)
  2. awk -f program-file [ -- ] file ...     (2)
  3. awk -e program-text [ -- ] file ...     (3)
复制代码
显式转换:

  • 数值->字符串:

    • CONVFMT或sprintf():功能等价。都是指定数值转换为字符串时的格式

  1. awk '
  2.         BEGIN{n=3}
  3.         /^[0-9]/{$1>5{$1=333;print $1}
  4.         /Alice/{print "Alice"}
  5.         END{print "hello"}
  6. ' a.txt
  7. # 等价的单行式:
  8. awk 'BEGIN{n=3} /^[0-9]/{$1>5{$1=333;print $1} /Alice/{print "Alice"} END{print "hello"}' a.txt
复制代码

  • 字符串->数值:strtonum()
  1. # 特殊pattern
  2. BEGIN
  3. END
  4. # 布尔代码块
  5. /regular expression/    # 正则匹配成功与否 /a.*ef/{action}
  6. relational expression   # 即等值比较、大小比较 3>2{action}
  7. pattern && pattern      # 逻辑与 3>2 && 3>1 {action}
  8. pattern || pattern      # 逻辑或 3>2 || 3<1 {action}
  9. ! pattern               # 逻辑取反 !/a.*ef/{action}
  10. (pattern)               # 改变优先级
  11. pattern ? pattern : pattern  # 三目运算符决定的布尔值
  12. # 范围pattern,非布尔代码块
  13. pattern1, pattern2      # 范围,pat1打开、pat2关闭,即flip,flop模式
复制代码
awk字面量

awk中有3种字面量:字符串字面量、数值字面量和正则表达式字面量。
数值字面量


  • 整数、浮点数、科学计数

    • 105、105.0、1.05e+2、1050e-1

  • awk内部总是使用浮点数方式保存所有数值,但用户在使用可以转换成整数的数值时总会去掉小数点

    • 数值12.0面向用户的值为12,12面向awk内部的值是12.0000000...0

  1. awk '{print $0}' a.txt
复制代码
算术运算
  1. touch x.log  # 创建一个空文件
  2. awk '{print "hello world"}' x.log
复制代码
赋值操作(优先级最低):
  1. # RS="\n" 、 RS="m"
  2. awk 'BEGIN{RS="\n"}{print $0}' a.txt
  3. awk 'BEGIN{RS="m"}{print $0}' a.txt
复制代码
疑惑:b = 6;print b += b++输出结果?可能是12或13。不同的awk的实现在评估顺序上不同,所以不要用这种可能产生歧义的语句。
字符串字面量

awk中的字符串都以双引号包围,不能以单引号包围。

  • "abc"
  • ""
  • "\0"、"\n"
字符串连接(串联):awk没有为字符串的串联操作提供运算符,可以直接连接或使用空格连接。
  1. # 按段落读取:RS=''
  2. $ awk 'BEGIN{RS=""}{print $0"------"}' a.txt     
  3. # 一次性读取所有数据:RS='\0' RS="^$"
  4. $ awk 'BEGIN{RS="\0"}{print $0"------"}' a.txt     
  5. $ awk 'BEGIN{RS="^$"}{print $0"------"}' a.txt  
  6. # 忽略空行:RS='\n+'
  7. $ awk 'BEGIN{RS="\n+"}{print $0"------"}' a.txt
  8. # 忽略大小写:预定义变量IGNORECASE设置为非0值
  9. $ awk 'BEGIN{IGNORECASE=1}{print $0"------"}' RS='[ab]' a.txt  
复制代码
注意:字符串串联虽然方便,但是要考虑串联的优先级。例如下面的:
  1. awk '{print NR}' a.txt a.txt
  2. awk '{print FNR}' a.txt a.txt
复制代码
正则表达式字面量

普通正则:

  • /[0-9]+/
  • 匹配方式:"str" ~ /pattern/或"str" !~ /pattern/
  • 匹配结果返回值为0(匹配失败)或1(匹配成功)
  • 任何单独出现的/pattern/都等价于$0 ~ /pattern/

    • if(/pattern/)等价于if($0 ~ /pattern/)
    • 坑1:a=/pattern/等价于将$0 ~ /pattern/的匹配返回值(0或1)赋值给a
    • 坑2:/pattern/ ~ $1等价于$0 ~ /pattern/ ~ $1,表示用$1去匹配0或1
    • 坑3:/pattern/作为参数传给函数时,传递的是$0~/pat/的结果0或1
    • 坑4.坑5.坑6...

强类型的正则字面量(gawk 4.2.0才支持):

gawk支持的正则
  1. awk '{n = 5;print $n}' a.txt
  2. awk '{print $(2+2)}' a.txt   # 括号必不可少,用于改变优先级
  3. awk '{print $(NF-3)}' a.txt
复制代码
gawk不支持正则修饰符,所以无法直接指定忽略大小写的匹配。
如果想要实现忽略大小写匹配,则可以将字符串先转换为大写、小写再进行匹配。或者设置预定义变量IGNORECASE为非0值。
  1. # 字段分隔符指定为单个字符
  2. awk -F":" '{print $1}' /etc/passwd
  3. awk 'BEGIN{FS=":"}{print $1}' /etc/passwd
  4. # 字段分隔符指定为正则表达式
  5. awk 'BEGIN{FS=" +|@"}{print $1,$2,$3,$4,$5,$6}' a.txt
复制代码
awk布尔值

在awk中,没有像其它语言一样专门提供true、false这样的关键字。
但它的布尔值逻辑非常简单:
  1. # 没取完的字符串DDD被丢弃,且NF=3
  2. $ awk 'BEGIN{FIELDWIDTHS="2 3 2"}{print $1,$2,$3,$4}' <<<"AABBBCCDDDD"
  3. AA BBB CC
  4. # 字符串不够长度时无视
  5. $ awk 'BEGIN{FIELDWIDTHS="2 3 2 100"}{print $1,$2,$3,$4"-"}' <<<"AABBBCCDDDD"
  6. AA BBB CC DDDD-
  7. # *号取剩余所有,NF=3
  8. $ awk 'BEGIN{FIELDWIDTHS="2 3 *"}{print $1,$2,$3}' <<<"AABBBCCDDDD"      
  9. AA BBB CCDDDD
  10. # 字段数多了,则取完字符串即可,NF=2
  11. $ awk 'BEGIN{FIELDWIDTHS="2 30 *"}{print $1,$2,NF}' <<<"AABBBCCDDDD"  
  12. AA BBBCCDDDD 2
复制代码
awk中比较操作


strnum类型

awk最基本的数据类型只有string和number(gawk 4.2.0版本之后支持正则表达式类型)。但是,对于用户输入数据(例如从文件中读取的各个字段值),它们理应属于string类型,但有时候它们看上去可能像是数值(例如$2=37),而有时候有需要这些值是数值类型。

注意,strnum类型只针对于awk中除数值常量、字符串常量、表达式计算结果外的数据。例如从文件中读取的字段$1、$2、ARGV数组中的元素等等。
  1. ID  name    gender  age  email          phone
  2. 1   Bob     male    28   abc@qq.com     18023394012
  3. 2   Alice   female  24   def@gmail.com  18084925203
  4. 3   Tony    male    21   aaa@163.com    17048792503
  5. 4   Kevin   male    21   bbb@189.com    17023929033
  6. 5   Alex    male    18                  18185904230
  7. 6   Andy    female  22   ddd@139.com    18923902352
  8. 7   Jerry   female  25   exdsa@189.com  18785234906
  9. 8   Peter   male    20   bax@qq.com     17729348758
  10. 9   Steven  female  23   bc@sohu.com    15947893212
  11. 10  Bruce   female  27   bcbd@139.com   13942943905
复制代码
大小比较操作

比较操作符:
  1. # 字段1:4字符
  2. # 字段2:8字符
  3. # 字段3:8字符
  4. # 字段4:2字符
  5. # 字段5:先跳过3字符,再读13字符,该字段13字符
  6. # 字段6:先跳过2字符,再读11字符,该字段11字符
  7. awk '
  8. BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
  9. NR>1{
  10.     print "<"$1">","<"$2">","<"$3">","<"$4">","<"$5">","<"$6">"
  11. }' a.txt
  12. # 如果email为空,则输出它
  13. awk '
  14. BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
  15. NR>1{
  16.     if($5 ~ /^ +$/){print $0}
  17. }' a.txt
复制代码
比较规则:
  1. Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA
复制代码
简单来说,string优先级最高,只要string类型参与比较,就都按照string的比较方式,所以可能会进行隐式的类型转换。
其它时候都采用num类型比较。
  1. echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |\
  2. awk '
  3.         BEGIN{FPAT="[^,]*|("[^"]*")"}
  4.         {
  5.         for (i=1;i<NF;i++){
  6.             print "<"$i">"
  7.         }
  8.         }
  9. '
复制代码
对于相同优先级的运算符,通常都是从左开始运算,但下面2种例外,它们都从右向左运算:

  • 赋值运算:如= += -= *=
  • 幂运算
  1. if(PROCINFO["FS"]=="FS"){
  2.     ...FS spliting...
  3. } else if(PROCINFO["FPAT"]=="FPAT"){
  4.     ...FPAT spliting...
  5. } else if(PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS"){
  6.     ...FIELDWIDTHS spliting...
  7. }
复制代码
再者,注意print和printf中出现的>符号,这时候它表示的是重定向符号,不能再出现优先级比它低的运算符,这时可以使用括号改变优先级。例如:
  1. awk '{print $0}' a.txt
复制代码
流程控制语句

注:awk中语句块没有作用域,都是全局变量。
  1. awk 'BEGIN{OFS="-"}{print $0}' a.txt  # OFS此处无效
复制代码
代码块
  1. awk '{$1=$1;print $0}'  a.txt  # 输出时将以空格分隔各字段
  2. awk '{print $0;$1=$1;print $0}' OFS="-" a.txt
复制代码
if...else
  1. # OFS对第一行无效
  2. awk '{$4+=10;OFS="-";print $0}' a.txt
  3. # 对所有行有效
  4. awk '{$4+=10;OFS="-";$1=$1;print $0}' a.txt
复制代码
搞笑题:妻子告诉程序员老公,去买一斤包子,如果看见卖西瓜的,就买两个。结果是买了两个包子回来。
  1. $ echo "   a  b  c   d   " | awk '{$1=$1;print}'
  2. a b c d
  3. $ echo "     a   b  c   d   " | awk '{$1=$1;print}' OFS="-"            
  4. a-b-c-d
复制代码
  1. # 1.根据行号筛选
  2. awk 'NR==2' a.txt   # 筛选出第二行
  3. awk 'NR>=2' a.txt   # 输出第2行和之后的行
  4. # 2.根据正则表达式筛选整行
  5. awk '/qq.com/' a.txt       # 输出带有qq.com的行
  6. awk '$0 ~ /qq.com/' a.txt  # 等价于上面命令
  7. awk '/^[^@]+$/' a.txt      # 输出不包含@符号的行
  8. awk '!/@/' a.txt           # 输出不包含@符号的行
  9. # 3.根据字段来筛选行
  10. awk '($4+0) > 24{print $0}' a.txt  # 输出第4字段大于24的行
  11. awk '$5 ~ /qq.com/' a.txt   # 输出第5字段包含qq.com的行
  12. # 4.将多个筛选条件结合起来进行筛选
  13. awk 'NR>=2 && NR<=7' a.txt
  14. awk '$3=="male" && $6 ~ /^170/' a.txt      
  15. awk '$3=="male" || $6 ~ /^170/' a.txt  
  16. # 5.按照范围进行筛选 flip flop
  17. # pattern1,pattern2{action}
  18. awk 'NR==2,NR==7' a.txt        # 输出第2到第7行
  19. awk 'NR==2,$6 ~ /^170/' a.txt
复制代码
switch...case
  1. awk 'NR>1{$4=$4+5;print $0}' a.txt
  2. awk 'BEGIN{OFS="-"}NR>1{$4=$4+5;print $0}' a.txt
  3. awk 'NR>1{$6=$6"*";print $0}' a.txt
复制代码
awk 中的switch分支语句功能较弱,只能进行等值比较或正则匹配。
各分支结尾需使用break来终止。
  1. # 1.法一:多条件筛选
  2. ifconfig | awk '/inet / && !($2 ~ /^127/){print $2}'
  3. # 2.法二:按段落读取,然后取IPv4字段
  4. ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'
  5. # 3.法三:按段落读取,每行1字段,然后取IPv4字段
  6. ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2}'
复制代码
分支穿透:
  1. man --pager='less -p ^"AWK PROGRAM EXECUTION"' awk
复制代码
while和do...while
  1. if( (getline) <= 0 ){...}
  2. if((getline) < 0){...}
  3. if((getline) > 0){...}
复制代码
while先判断条件再决定是否执行statements,do...while先执行statements再判断条件决定下次是否再执行statements。
  1. # next
  2. exec 9<> filename
  3. while read -u 9 line;do
  4.   ...code...
  5.   continue  # next
  6.   ...code...  # 这部分代码在本轮循环当中不再执行
  7. done
  8. # getline
  9. while read -u 9 line;do
  10.   ...code...
  11.   read -u 9 line  # getline
  12.   ...code...
  13. done
复制代码
再比如,按数组元素值的字符大小来比较。
  1. awk '/^1/{print;getline;print;exit}' a.txt
复制代码
再比如,对元素值按数值升序比较,且相等时再按第一个字段ID进行数值降序比较。
  1. awk '/^1/{print;if((getline)<=0){exit};print}' a.txt
复制代码
上面使用的arr[x,y]来存储额外信息,下面使用arr[x][y]多维数组的方式来存储额外信息实现同样的排序功能。
  1. getline var
复制代码
此外,gawk还提供了两个内置函数asort()和asorti()来对数组进行排序。
awk ARGC和ARGV

预定义变量ARGV是一个数组,包含了所有的命令行参数。该数组使用从0开始的数值作为索引。
预定义变量ARGC初始时是ARGV数组的长度,即命令行参数的数量。
ARGV数组的数量和ARGC的值只有在awk刚开始运行的时候是保证相等的。
  1. awk '
  2. /^1/{
  3.   if((getline var)<=0){exit}
  4.   print var
  5.   print $0"--"$2
  6. }' a.txt
复制代码
awk读取文件是根据ARGC的值来进行的,有点类似于如下伪代码形式:
[code]while(i=1;i

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具