Linux文本处理三剑客之awk学习笔记11:选项、内置变量和内置函数

时间:2021-02-03 14:28:00 来源:互联网 作者: 神秘的大神 字体:

这部分的内容许多在以往的笔记中有涉猎,因此大多数不会详述。

内置(built-in)和预定义(predefined)虽然名字不同,不过含义是等价的,官方文档中也同时使用到这两个英文词汇。

选项

-e:指定awk代码。一般代码可以直接写在CLI或者使用-f来指定代码文件,不过这两种只能二选一。如果已经使用-f指定了代码文件还想在CLI中写代码的话,就得写在-e中。

-e program-text
--source program-text
awk -f code.awk -e '...cliCode...' FILENAME

-f:指定awk的代码文件,一般如果代码内容较多不适合写在CLI的情况下会写在某个单独的文件中。

-f source-file
--file source-file

-F:指定输入字段分隔符。

-F fs
--field-separator fs

-n:默认情况下,awk会将来自输入数据的数值均识别为十进制,即使该数值以0或者0x开头。而使用-n选项的话,可以根据数值的前缀自动识别为八进制或者十六进制。

-n
--non-decimal-data
# echo "030" | awk '{print $1+0}'
30
# echo "030" | awk -n '{print $1+0}'
24

-o:将CLI中的awk代码格式化后输出到外部文件中。

-o[file]
--pretty-print[=file]
# awk -o 'NR==1{print}' a.txt
# cat awkprof.out
NR == 1 {
print $0
}

-v:变量赋值,在BEGIN代码块中可用。

-v var=val
--assign var=val

 

内置变量

内置变量大体可以分为两类:控制awk工作类和携带信息类。

控制awk工作类

RS:(输入)记录分隔符,默认为换行符,详见读取文件。

IGNORECASE:在正则匹配时是否忽略大小写。要注意设置的位置,在设置位置之后的工作流才生效,需要对awk工作流程有一定的了解。

FS:按照字段分隔符取字段,详见读取文件。

FIELDWIDTHS:按照字段宽度取字段,详见读取文件。

FPAT:按照字段的模式取字段,详见读取文件。

OFS:输出字段分隔符,print命令会使用到。

ORS:输出记录分隔符,print命令会使用到。

CONVFMT:数值隐式转换成字符串时所遵循的格式,默认值为“%.6g”。

OFMT:使用print命令输出小数时,数值转换成字符串时所遵循的格式,默认值为“%.6g”。

携带信息类

ARGC:参数的个数,详见ARGC和ARGV等。

ARGV:保存各个参数,详见ARGC和ARGV等。

ARGIND:ARGV中各参数的索引,详见ARGC和ARGV等。

FILENAME:当前正在处理的文件名,详见ARGC和ARGV等。

ENVIRON:引用shell环境变量,详见ARGC和ARGV等。

NR:当前已读取的记录数(可简单理解为行号),当处理多个文件时,NR不会重置而是会一直往上叠加。如果NR修改了,那么下一条记录会基于新的NR值。

FNR:在当前文件中已读取的记录数(可简单理解为行号),当处理多个文件时,NR会重置。如果NR修改了,那么下一条记录会基于新的NR值。

NF:当前记录的字段数,详见读取文件。。

RT:每次记录分隔时所采用的具体的记录分隔符,详见读取文件。

RLENGTH:详见下文内置函数match()。

RSTART:详见下文内置函数match()。

SUBSEP:多维数组中索引分隔字符,详见数组。

 

内置函数

数值类

int(x):取整函数,向0位置方向取整,也可以理解为直接截断小数部分。

# awk 'BEGIN{print int(3)}'
3
# awk 'BEGIN{print int(3.9)}'
3
# awk 'BEGIN{print int(-3)}'
-3
# awk 'BEGIN{print int(-3.9)}'
-3

对于包含字母字符串的,能截断取整就截断取整,不行就返回0。

# awk 'BEGIN{print int("3.14abc")}'
3
# awk 'BEGIN{print int("abc3.14")}'
0

sqrt(x):返回正整数的平方根,遇到负数就报错。

# awk 'BEGIN{print sqrt(9)}'
3
# awk 'BEGIN{print sqrt(4)}'
2
# awk 'BEGIN{print sqrt(-4)}'
awk: cmd. line:1: warning: sqrt: called with negative argument -4
-nan

rand():返回随机数,随机数位于[0,1)。

# awk 'BEGIN{print rand()}'
0.924046

一般来说我们期望获得一个随机整数,那么就会使用一个整数与之相乘,然后再结合int()。例如取得一个位于[0,10)之间的随机数,则乘以10。

# awk 'BEGIN{print 10*rand()}'
9.24046
# awk 'BEGIN{print int(10*rand())}'
9

如果你反复运行rand(),就会发现其每次生成的随机数都是固定的。哪怕在SSH会话中重新连接或者新建会话窗口。

# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print rand()}'
0.924046

因为在大多数awk实现中(包含gawk,不包含mawk),每次运行awk开始生成随机数都会基于一个相同的数值或者说是种子(seed),只要这个种子的值不变,那么随机数就不会变。如果我们期望每次使用rand()生成随机数时得到的数字是真随机的话,就需要使用srand()来修改种子值。

srand([x]):不带参数的srand()可以设置一个随机的种子值,使得rand()返回真随机数。

默认情况下,srand()将当前的日期和时间(精确到秒)作为种子,也就是说如果两次awk执行位于同一秒中,那么使用的种子相同,生成的随机数自然也就相同。

# awk 'BEGIN{srand();print rand()}'
0.929717
# awk 'BEGIN{srand();print rand()}'
0.145049
# awk 'BEGIN{srand();print rand()}'
0.145049

srand()会返回前一次的种子,每次都是1,也就是当我们不使用srand()指定种子的时候,每次都是使用1作为种子,所以结果也就相同了。

# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print rand()}'
0.924046
# awk 'BEGIN{print srand()}'
1
# awk 'BEGIN{print srand()}'
1
# awk 'BEGIN{srand(1);print rand()}'
0.924046
# awk 'BEGIN{srand(1);print rand()}'
0.924046

让我们结合以上所学,生成一个位于[10,100]的随机数。

awk 'BEGIN{srand();print int(10+91*rand())}'

字符串类

sprintf(format, expression1, ...):详见输出操作。

length([string]):这个我们见过很多了,返回字符数量。如果参数是数组则返回数组元素的数量,详见数组。如果参数为空的话则返回$0的字符数量。

在返回字符数量的时候有一些需要注意的点,比如我们返回一个小数的时候,小数点也属于字符数量的统计范围中。有的时候返回的字符数量不对,是因为可能遇到了需要使用OFMT或者CONVFMT来根据默认值“%.6g”来转换的情况。

# awk 'BEGIN{print length(100)}'
3
# awk 'BEGIN{print length(100.123)}'
7
# awk 'BEGIN{print length(100.123456)}'
7
# awk 'BEGIN{print 100.123456}'
100.123
# awk 'BEGIN{print length(1000000000000000.123456)}'
5
# awk 'BEGIN{print 1000000000000000.123456}'
1e+15

strtonum(str):详见语法。如果str以0、0x或者0X开头则会识别成对应的八或者十六进制以后再转换。

# awk 'BEGIN{print strtonum("010")}'
8
# awk 'BEGIN{print strtonum("0x10")}'
16
# awk 'BEGIN{print strtonum("0X10")}'
16

tolower(str)和toupper(str):大小写的转换。

# awk 'BEGIN{print tolower("aBcDeFg")}'
abcdefg
# awk 'BEGIN{print toupper("aBcDeFg")}'
ABCDEFG

index(str,substr):在字符串str中寻找子字符串(简称子串)substr。若找到则返回子串substr在字符串str中的起始位置,若找不到则返回0。

注意:在awk中涉及到字符索引位置的函数,其索引位置都是从1开始计算,其他大多编程语言则是从0开始计算。

# awk 'BEGIN{print index("alongdidi","di")}'
6
# awk 'BEGIN{print index("alongdidi","zzz")}'
0

基于这个特性我们可以使用该函数来判断A字符串是否包含B字符串的功能,类似正则匹配功能。

# awk '$5~/qq.com/{print}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
8   Peter   male    20   bax@qq.com     17729348758
# awk 'index($5,"qq.com"){print}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
8   Peter   male    20   bax@qq.com     17729348758

substr()

substr(string,start[,length])

substr()函数的作用是从给定的字符串string中,根据给定的索引起始位置和长度来提取子串。

# awk 'BEGIN{print substr("alongdidi",3,3)}'
ong

长度可以省略,表示提取到字符串结束为止。

# awk 'BEGIN{print substr("alongdidi",3)}'
ongdidi

起始位置如果是非正整数,那么表示从1开始。如果起始位置大于字符串长度,那么返回空字符串。

# awk 'BEGIN{print substr("alongdidi",-1)}'
alongdidi
# awk 'BEGIN{print substr("alongdidi",0)}'
alongdidi
# awk 'BEGIN{print substr("alongdidi",20)}'

如果子串的长度是非正整数,那么返回空字符串。

# awk 'BEGIN{print substr("alongdidi",1,0)}'

# awk 'BEGIN{print substr("alongdidi",1,-1)}'

split()和patsplit()

split(string,array[,fieldsep[,seps]])

split()函数根据字段分隔符fieldsep将字符串string分割成各个字段并存入数组array中。由于fieldsep支持正则表达式,因此每次切分字段的字段分隔符可能不同,将每次实际的字段分隔符存入数组seps中。这个关系有点类似于RS和RT的关系。

# awk 'BEGIN{split("a b  c   d",arr);for(i in arr){print i"-->"arr[i]}}'
1-->a
2-->b
3-->c
4-->d

如果不指定字段分隔符,那么默认使用FS,即默认值为空格。因此一个或多个空格均可作为默认的字段分隔符。

关联数组array的索引是从1开始的。

函数的返回值是切割得到的字段个数(即数组元素的长度)。

# awk 'BEGIN{print split("a b  c   d",arr)}'
4

使用正则字段分隔符分割字段。实际字段分隔符的数组索引也是从1开始。

# awk 'BEGIN{split("a1b22c333d",arr,"[[:digit:]]+",seps);for(i in arr){print i"-->"arr[i]};for(i in seps){print i"-->"seps[i]}}'
1-->a
2-->b
3-->c
4-->d
1-->1
2-->22
3-->333

split()函数在将分割后的字段写入数组之前,会清空该数组。因此可以将待分割的字符串设置为空,从而用来清空某个数组。不过清空数组直接”delete arr“即可,此可是了解split()的工作原理。

# awk 'BEGIN{for(i=1;i<=3;i++){arr[i]=i};print length(arr);split("",arr);print length(arr)}'
3
0

如果split()函数根据已有条件无法分割字符串的话,则会将整个字符串当作一个字段存入数组。

# awk 'BEGIN{split("alongdidi",arr);for(i in arr){print i"-->"arr[i]}}'
1-->alongdidi

patsplit()函数。

patsplit(string,array[,fieldpat[,seps]])

split()和patsplit()的区别在于前者使用字段分隔符分隔字段,后者使用字段模式来匹配字段。它们的关系类似于FS和FPAT的关系(因此这里就不再赘述patsplit()的用法,不懂的就去看看链接中的文章。)。如果省略fieldpat,则按照FPAT的值来。

# awk 'BEGIN{patsplit("aaa1bbb22ccc333",arr,"[[:alpha:]]+",seps);for(i in arr){print i"-->"arr[i]};for(j in seps){print j"-->"seps[j]}}'
1-->aaa
2-->bbb
3-->ccc
0--> # 请留意这里的实际字段分隔符以0开始。
1-->1
2-->22
3-->333

match()

match(string,reg[,arr])

使用正则表达式在字符串中进行匹配,匹配成功则返回匹配成功部分最开始的索引位置,匹配失败则返回0。

# awk 'BEGIN{print match("alongdidi","(di)+")}'    # 从字符串alongdidi的第6个位置开始匹配成功。
6
# awk 'BEGIN{print match("alongdidi","z+")}'
0

可以指定数组参数arr,将整个匹配到的结果存入arr[0]。如果正则中使用了分组捕获,那么依照顺序将每个分组捕获到的数据存入arr[1], arr[2], ...。

# awk 'BEGIN{match("foo+++bar+++baz+++","(foo)\\++(bar)\\++(baz)\\++",arr);for(i=0;i<=3;i++){print i"-->"arr[i]}}'
0-->foo+++bar+++baz+++ 1-->foo 2-->bar 3-->baz

注意:CLI中想要正确将第一个+字符识别为字面量,必须使用双反斜线,如果使用单反斜线会有警告,并且结果会异常。

awk: cmd. line:1: warning: escape sequence `\+' treated as plain `+'

如果我们使用遍历数组的方式会发现还存储有其他数组元素。顾名思义是每个元素的在原字符串中的起始位置以及长度。

0start-->1
0length-->18
3start-->13
1start-->1
2start-->7
3length-->3
2length-->3
1length-->3

如果匹配成功,则会将arr[0]的起始位置和长度存入预定义变量RSTART和RLENGTH,等同于arr[0start]和arr[0length]。

# awk 'BEGIN{match("alongdidi","(di)+");print RSTART,RLENGTH}'
6 4

如果匹配失败,则RSTART等于0,RLENGTH等于-1。

# awk 'BEGIN{match("alongdidi","z+");print RSTART,RLENGTH}'
0 -1

这种匹配成功返回正整数匹配失败返回0的特性,可以用来表示布尔值的真假。因此可以用作if条件判断或者pattern。

# awk 'match($2,"A.+"){print}' a.txt 
2   Alice   female  24   def@gmail.com  18084925203
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352

sub()、gsub()和gensub()

这三个函数都是字符串替换(substitute)函数。它们的工作方式和sed或者vim中的基于正则匹配替换相似。

sub(regexp,replacement[,target])
gsub(regexp,replacement[,target])

在target中匹配正则regexp,如果匹配到则将其替换成replacement,并把替换后的结果重新赋值给target。因此target必须是可赋值的(变量名、数组元素名或者$N等),不能是字面量。

sub()和gsub()唯一的区别在于后者是全局替换(global)。sub()返回1或者0来表示替换成功或者失败。gsub()返回正整数或者0来表示替换成功的次数或者替换失败。

如果省略target的话,则使用$0。如果target是$0或者$N,由于函数会将target重新赋值,因此就涉及到$0或者$N的重建/重新计算,详见读取文件中的【字段与记录的重建】部分。

# awk 'BEGIN{str="aooboocoo";count=sub("oo","xx",str);print count;print str}'
1
axxboocoo
# awk 'BEGIN{str="aooboocoo";count=gsub("oo","xx",str);print count;print str}'
3
axxbxxcxx

在替换时(replacement中)可以使用&来引用匹配成功的部分。

# awk 'BEGIN{str="aooboocoo";count=sub("oo","x&x",str);print str}'
axooxboocoo
# awk 'BEGIN{str="aooboocoo";count=gsub("oo","x&x",str);print str}'
axooxbxooxcxoox

但是,sub()和gsub()均不支持“\N”(N为正整数)的形式来反向引用。

gensub()相对于前两者的最大区别在于其支持反向引用,并且gensub()可以完全代替前两者。

gensub(regexp, replacement, how[, target])

how:用来指定对第几个匹配到的字符串进行替换。1等同于sub(),使用g/G开头的字符串等同于gsub(),也可以指定其他正整数。

gensub()返回替换成功后的结果而不是替换成功的次数;如果匹配失败则返回target本身。

# awk 'BEGIN{str="aooboocoo";count=gensub("oo","x",1,str);print count;print str}'
axboocoo
aooboocoo
# awk 'BEGIN{str="aooboocoo";count=gensub("oo","x","g",str);print count;print str}'
axbxcx
aooboocoo
# awk 'BEGIN{str="aooboocoo";count=gensub("zz","x","g",str);print count;print str}'
aooboocoo
aooboocoo

反向引用需要使用两根反斜线。awk对于转义(反斜线)的解释比较混乱,有时候需要1根,有需要需要2根,这个需要自行测试。\0等同于&。

# awk 'BEGIN{str="abc def";new=gensub("(.+) (.+)","\\2|\\1|\\0|&","global",str);print new}'
def|abc|abc def|abc def

asort()和asorti()

asort(src[, dest[, how]])
asorti(src[, dest[, how]])

这两个函数用来对数组进行排序。

asort()对数组的元素值进行排序,排序的原则基于how(也就是说可以按照预定排序规则或者自定义函数),排序完成以后将数组的索引值修改为正整数序列(1, 2, 3, ...)。

asorti()与asort()的区别在于其是对数组的索引值(index/indices)进行排序。

这部分,作者在视频中没有讲解,可能是较少使用或者较复杂,暂时留白。

IO类

1、close()。在介绍getline时已讲解。

close(filename[,how])

2、fflush()。

fflush([filename])

flush任何与filename相关的输出,filename可以是一个已打开用于写入的文件或者是一个用于重定向输出到管道或者协程的shell命令。

许多程序都会缓冲自己的输出,相对于一有一点点数据就立刻将其输出来说,缓冲机制会是效率更高。不过有时候也有必要在缓冲区还没有满的时候就输出数据,因此awk提供了fflush()函数来实现这个功能。

从4.0.2开始,如果fflush()函数没有参数或者参数是空字符串,awk会flush所有的缓冲。

fflush()    # 无参数
fflush("")    # 参数为空字符串

当将数据输出至管道或者协程时可能会被缓冲。而输出到终端是行缓冲,遇到换行即输出,此前我们说的缓冲一般叫块缓冲,可以缓冲多行的数据。

# awk '{print $1+$2}'
1 1    # 键入1空格1回车
2 # 立即返回2 2 3 # 键入2空格3回车 5 # 立即返回 5 # 键入Ctrl+d表示终止输入
# awk '{print $1+$2}' | cat
1 1    # 键入1空格1回车
2 3 # 没有返回输出结果,再次键入2空格3回车 2 # 依然没有返回,输入Ctrl+d终止输入后才返回结果,此前的数据都被缓冲起来了。 5

使用flush就可以使得原本输出到管道需要缓冲的数据立刻被flush。

awk '{print $1+$2;fflush()}' | cat

3、system()。在介绍getline时已讲解。

system(command)

时间类

由于awk常用于处理日志文件,而时间对于日志文件来说是一个非常重要的概念。因此与时间相关的内置函数就显得很重要了。

systime()

返回当前系统时间距离epoch的秒数,我们可以勉强把它称之为【epoch值】。在计算机领域中,和时间相关的epoch指的是“1970-01-01 00:00:00”。

# awk 'BEGIN{print systime()}'
1612173172

mktime()

mktime("YYYY MM DD HH MM SS [DST]"[,utc-flag])

根据用于给出的日期时间格式信息,返回对应的epoch值。

# awk 'BEGIN{print mktime("2021 02 01 18 05 00")}'
1612173900

mktime()比较智能,假如用户输入的时间超出范围,会自动换算成对应的时间,甚至支持负数。

# awk 'BEGIN{print mktime("2021 02 01 18 05 65");print mktime("2021 02 01 18 06 05")}'
1612173965
1612173965
# awk 'BEGIN{print mktime("2021 02 01 18 05 -5");print mktime("2021 02 01 18 04 55")}'
1612173895
1612173895

如果日期时间格式不对或者返回的时间戳超出范围的话,mktime()返回-1。

strftime()

strftime([format[,timestamp[,utc-flag]]])

将时间戳(timestamp)转换成用户给定的格式(format)输出。

这里的格式和date命令中的格式类似,所有的格式详见官方文档。时间戳则是epoch值,因此该函数一般可以结合mktime()函数一起使用。

# awk 'BEGIN{print strftime("%F %T",mktime("2021 02 01 00 00 00"))}'
2021-02-01 00:00:00

如果省略时间戳,那么使用当前的日期和时间对应的时间戳。

# awk 'BEGIN{print strftime("%F %T")}'
2021-02-01 18:25:43

如果省略格式,则使用PROCINFO["strftime"]对应的值。

# awk 'BEGIN{print PROCINFO["strftime"];print strftime()}'
%a %b %e %H:%M:%S %Z %Y
Mon Feb  1 18:26:56 CST 2021

数据类型类

isarray()

isarray(x)

判断变量x是否是数组,如果是则返回1,否则返回0。

typeof()

typeof(x)

返回变量x的数据类型。有以下值:

  • array:数组。
  • regexp:正则字面量。
  • number:数值。
  • string:字符串。
  • strnum:形似数值的字符串,详见语法。
  • unassigned:未赋值状态,有引用过但是没有赋值过,详见语法。
  • untyped:未声明的状态,也可以理解为未键入的(type本身也有从键盘键入的意思),就是既没引用更没赋值的,详见语法。