卐風卐 发表于 2023-11-3 14:28:57

一篇文章玩透awk

安装新版本gawk

awk有很多种版本,例如nawk、gawk。gawk是GNU awk,它的功能很丰富。
本教程采用的是gawk 4.2.0版本,4.2.0版本的gawk是一个比较大的改版,新支持的一些特性非常好用,而在低于4.2.0版本时这些语法可能会报错。所以,请先安装4.2.0版本或更高版本的gawk。
查看awk版本
awk --version这里以安装gawk 4.2.0为例。
# 1.下载
wget --no-check-certificate https://mirrors.tuna.tsinghua.edu.cn/gnu/gawk/gawk-4.2.0.tar.gz

# 2.解压、进入解压后目录
tar xf gawk-4.2.0.tar.gz
cd gawk-4.2.0/

# 3.编译,并执行安装目录为/usr/local/gawk4.2
./configure --prefix=/usr/local/gawk4.2 && make && make install

# 4.创建一个软链接:让awk指向刚新装的gawk版本
ln -fs /usr/local/gawk4.2/bin/gawk /usr/bin/awk

# 此时,调用awk将调用新版本的gawk,调用gawk将调用旧版本的gawk
awk --version
gawk --version本系列的awk教程中,将大量使用到如下示例文件a.txt。
IDname    genderageemail          phone
1   Bob   male    28   abc@qq.com   18023394012
2   Alice   female24   def@gmail.com18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female22   ddd@139.com    18923902352
7   Jerry   female25   exdsa@189.com18785234906
8   Peter   male    20   bax@qq.com   17729348758
9   Stevenfemale23   bc@sohu.com    15947893212
10Bruce   female27   bcbd@139.com   13942943905读取文件的几种方式

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

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

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

[*]一次性读取整个文件

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

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

read的-n选项和-N选项可以指定一次性读取多少个字符。
# 只读一个字符
read -n 1 data <a.txt

# 读100个字符,但如果不足100字符时遇到换行符则停止读取
read -n 100 data < a.txt

# 强制读取100字符,遇到换行符也不停止
read -N 100 data < a.txt可以修改OFMT,来自定义数值转换为字符串时的格式:
# 正确
while read -N 3 data;do
echo "$data"
done <a.txt


# 错误
while read -N 3 data < a.txt;do
echo "$data"
doneprintf

# 一直读取,直到遇到字符m才停止,并将读取的数据保存到data变量中
read -d "m" data <a.txt格式化字符:

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

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


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

awk重定向时可以直接使用/dev/stdin、/dev/stdout和/dev/stderr。还可以直接使用某个已打开的文件描述符/dev/fd/N。
例如:
# 指定超出文件大小的字符数量
read -N 1000000 data <a.txt
echo "$data"

# 指定文件中不存在的字符作为分隔符
read -d "_" data <a.txt
echo "$data"awk变量

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

[*]未声明状态:称为untyped类型
[*]引用过但未赋值状态:unassigned类型
[*]已赋值状态
引用未赋值的变量,其默认初始值为空字符串或数值0。
在awk中未声明的变量称为untyped,声明了但未赋值(只要引用了就声明了)的变量其类型为unassigned。
gawk 4.2版提供了typeof()函数,可以测试变量的数据类型,包括测试变量是否声明。
awk 'awk_program' a.txt除了typeof(),还可以使用下面的技巧进行检测:
# 输出a.txt中的每一行
awk '{print $0}' a.txt

# 多个代码块,代码块中多个语句
# 输出每行之后还输出两行:hello行和world行
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=a+6)是怎么赋值的,对应元素结果等于?arr=7。但不要这么做,因为不同awk的赋值语句左右两边的评估顺序有可能不同。
awk中声明变量的位置


awk中使用Shell变量

要在awk中使用Shell变量,有三种方式:
1.在-v选项中将Shell变量赋值给awk变量
awk '{print $0}' a.txt
awk '{print $0}{print $0;print $0}' a.txt-v选项是在awk工作流程的第一阶段解析的,所以-v选项声明的变量在BEGIN{}、END{}和main代码段中都能直接使用。
2.在非选项型参数位置处使用var=value格式将Shell变量赋值给awk变量
pattern1{statement1}pattern2{statement3;statement4;...}非选项型参数设置的变量不能在BEGIN代码段中使用。
3.直接在awk代码部分暴露Shell变量,交给Shell解析进行Shell的变量替换
awk 'BEGIN{print "我在前面"}{print $0}' a.txt
awk 'END{print "我在后面"}{print $0}' a.txt
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"

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

[*]数值->字符串:

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

awk '
        BEGIN{n=3}
        /^/{$1>5{$1=333;print $1}
        /Alice/{print "Alice"}
        END{print "hello"}
' a.txt

# 等价的单行式:
awk 'BEGIN{n=3} /^/{$1>5{$1=333;print $1} /Alice/{print "Alice"} END{print "hello"}' a.txt

[*]字符串->数值:strtonum()
# 特殊pattern
BEGIN
END

# 布尔代码块
/regular expression/    # 正则匹配成功与否 /a.*ef/{action}
relational expression   # 即等值比较、大小比较 3>2{action}
pattern && pattern      # 逻辑与 3>2 && 3>1 {action}
pattern || pattern      # 逻辑或 3>2 || 3<1 {action}
! pattern               # 逻辑取反 !/a.*ef/{action}
(pattern)               # 改变优先级
pattern ? pattern : pattern# 三目运算符决定的布尔值

# 范围pattern,非布尔代码块
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

awk '{print $0}' a.txt算术运算

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

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

[*]"abc"
[*]""
[*]"\0"、"\n"
字符串连接(串联):awk没有为字符串的串联操作提供运算符,可以直接连接或使用空格连接。
# 按段落读取:RS=''
$ awk 'BEGIN{RS=""}{print $0"------"}' a.txt   

# 一次性读取所有数据:RS='\0' RS="^$"
$ awk 'BEGIN{RS="\0"}{print $0"------"}' a.txt   
$ awk 'BEGIN{RS="^$"}{print $0"------"}' a.txt

# 忽略空行:RS='\n+'
$ awk 'BEGIN{RS="\n+"}{print $0"------"}' a.txt

# 忽略大小写:预定义变量IGNORECASE设置为非0值
$ awk 'BEGIN{IGNORECASE=1}{print $0"------"}' RS='' a.txt注意:字符串串联虽然方便,但是要考虑串联的优先级。例如下面的:
awk '{print NR}' a.txt a.txt
awk '{print FNR}' a.txt a.txt正则表达式字面量

普通正则:

[*]/+/
[*]匹配方式:"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支持的正则

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

# 字段分隔符指定为正则表达式
awk 'BEGIN{FS=" +|@"}{print $1,$2,$3,$4,$5,$6}' a.txtawk布尔值

在awk中,没有像其它语言一样专门提供true、false这样的关键字。
但它的布尔值逻辑非常简单:

# 没取完的字符串DDD被丢弃,且NF=3
$ awk 'BEGIN{FIELDWIDTHS="2 3 2"}{print $1,$2,$3,$4}' <<<"AABBBCCDDDD"
AA BBB CC

# 字符串不够长度时无视
$ awk 'BEGIN{FIELDWIDTHS="2 3 2 100"}{print $1,$2,$3,$4"-"}' <<<"AABBBCCDDDD"
AA BBB CC DDDD-

# *号取剩余所有,NF=3
$ awk 'BEGIN{FIELDWIDTHS="2 3 *"}{print $1,$2,$3}' <<<"AABBBCCDDDD"      
AA BBB CCDDDD

# 字段数多了,则取完字符串即可,NF=2
$ awk 'BEGIN{FIELDWIDTHS="2 30 *"}{print $1,$2,NF}' <<<"AABBBCCDDDD"
AA BBBCCDDDD 2awk中比较操作


strnum类型

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

注意,strnum类型只针对于awk中除数值常量、字符串常量、表达式计算结果外的数据。例如从文件中读取的字段$1、$2、ARGV数组中的元素等等。
IDname    genderageemail          phone
1   Bob   male    28   abc@qq.com   18023394012
2   Alice   female24   def@gmail.com18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18                  18185904230
6   Andy    female22   ddd@139.com    18923902352
7   Jerry   female25   exdsa@189.com18785234906
8   Peter   male    20   bax@qq.com   17729348758
9   Stevenfemale23   bc@sohu.com    15947893212
10Bruce   female27   bcbd@139.com   13942943905大小比较操作

比较操作符:
# 字段1:4字符
# 字段2:8字符
# 字段3:8字符
# 字段4:2字符
# 字段5:先跳过3字符,再读13字符,该字段13字符
# 字段6:先跳过2字符,再读11字符,该字段11字符
awk '
BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
NR>1{
    print "<"$1">","<"$2">","<"$3">","<"$4">","<"$5">","<"$6">"
}' a.txt

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

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

注:awk中语句块没有作用域,都是全局变量。
awk 'BEGIN{OFS="-"}{print $0}' a.txt# OFS此处无效代码块

awk '{$1=$1;print $0}'a.txt# 输出时将以空格分隔各字段
awk '{print $0;$1=$1;print $0}' OFS="-" a.txtif...else

# OFS对第一行无效
awk '{$4+=10;OFS="-";print $0}' a.txt

# 对所有行有效
awk '{$4+=10;OFS="-";$1=$1;print $0}' a.txt搞笑题:妻子告诉程序员老公,去买一斤包子,如果看见卖西瓜的,就买两个。结果是买了两个包子回来。
$ echo "   abc   d   " | awk '{$1=$1;print}'
a b c d
$ echo "   a   bc   d   " | awk '{$1=$1;print}' OFS="-"            
a-b-c-d# 1.根据行号筛选
awk 'NR==2' a.txt   # 筛选出第二行
awk 'NR>=2' a.txt   # 输出第2行和之后的行

# 2.根据正则表达式筛选整行
awk '/qq.com/' a.txt       # 输出带有qq.com的行
awk '$0 ~ /qq.com/' a.txt# 等价于上面命令
awk '/^[^@]+$/' a.txt      # 输出不包含@符号的行
awk '!/@/' a.txt         # 输出不包含@符号的行

# 3.根据字段来筛选行
awk '($4+0) > 24{print $0}' a.txt# 输出第4字段大于24的行
awk '$5 ~ /qq.com/' a.txt   # 输出第5字段包含qq.com的行

# 4.将多个筛选条件结合起来进行筛选
awk 'NR>=2 && NR<=7' a.txt
awk '$3=="male" && $6 ~ /^170/' a.txt      
awk '$3=="male" || $6 ~ /^170/' a.txt

# 5.按照范围进行筛选 flip flop
# pattern1,pattern2{action}
awk 'NR==2,NR==7' a.txt      # 输出第2到第7行
awk 'NR==2,$6 ~ /^170/' a.txtswitch...case

awk 'NR>1{$4=$4+5;print $0}' a.txt
awk 'BEGIN{OFS="-"}NR>1{$4=$4+5;print $0}' a.txt
awk 'NR>1{$6=$6"*";print $0}' a.txtawk 中的switch分支语句功能较弱,只能进行等值比较或正则匹配。
各分支结尾需使用break来终止。
# 1.法一:多条件筛选
ifconfig | awk '/inet / && !($2 ~ /^127/){print $2}'

# 2.法二:按段落读取,然后取IPv4字段
ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'

# 3.法三:按段落读取,每行1字段,然后取IPv4字段
ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2}'分支穿透:
man --pager='less -p ^"AWK PROGRAM EXECUTION"' awkwhile和do...while

if( (getline) <= 0 ){...}
if((getline) < 0){...}
if((getline) > 0){...}while先判断条件再决定是否执行statements,do...while先执行statements再判断条件决定下次是否再执行statements。
# next
exec 9<> filename
while read -u 9 line;do
...code...
continue# next
...code...# 这部分代码在本轮循环当中不再执行
done

# getline
while read -u 9 line;do
...code...
read -u 9 line# getline
...code...
done再比如,按数组元素值的字符大小来比较。
awk '/^1/{print;getline;print;exit}' a.txt再比如,对元素值按数值升序比较,且相等时再按第一个字段ID进行数值降序比较。
awk '/^1/{print;if((getline)<=0){exit};print}' a.txt上面使用的arr来存储额外信息,下面使用arr多维数组的方式来存储额外信息实现同样的排序功能。
getline var此外,gawk还提供了两个内置函数asort()和asorti()来对数组进行排序。
awk ARGC和ARGV

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