首页 > 其他分享 >一篇文章玩透awk

一篇文章玩透awk

时间:2023-11-03 09:34:38浏览次数:31  
标签:BEGIN 一篇 arr awk 玩透 print txt 读取

安装新版本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。

ID  name    gender  age  email          phone
1   Bob     male    28   [email protected]     18023394012
2   Alice   female  24   [email protected]  18084925203
3   Tony    male    21   [email protected]    17048792503
4   Kevin   male    21   [email protected]    17023929033
5   Alex    male    18   [email protected]    18185904230
6   Andy    female  22   [email protected]    18923902352
7   Jerry   female  25   [email protected]  18785234906
8   Peter   male    20   [email protected]     17729348758
9   Steven  female  23   [email protected]    15947893212
10  Bruce   female  27   [email protected]   13942943905

读取文件的几种方式

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

  1. 按字符数量读取:每一次可以读取一个字符,或者多个字符,直到把整个文件读取完
  2. 按照分隔符进行读取:一直读取直到遇到了分隔符才停止,下次继续从分隔的位置处向后读取,直到读完整个文件
  3. 按行读取:每次读取一行,直到把整个文件读完
    • 它是按照分隔符读取的一种特殊情况:将分隔符指定为了换行符\n
  4. 一次性读取整个文件
    • 是按字符数量读取的特殊情况
    • 也是按分隔符读取的特殊情况
  5. 按字节数量读取:一次读取指定数量的字节数据,直到把文件读完

下面使用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

如果按照字符数量读取,直到把文件读完,则使用while循环,且将文件放在while结构的后面,而不能放在while循环的条件位置:

# 正确
while read -N 3 data;do
  echo "$data"
done <a.txt


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

按分隔符读取

read命令的-d选项可以指定读取文件时的分隔符。

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

如果要按分隔符读取并读完整个文件,则使用while循环:

while read -d "m" data ;do
  echo "$data"
done <a.txt

按行读取

read默认情况下就是按行读取的,一次读取一行。

# 从a.txt中读取第一行保存到变量data中
read line <a.txt

如果要求按行读取完整个文件,则使用while循环:

while read line;do
  echo "$line"
done <a.txt

一次性读整个文件

要一次性读取完整个文件,有两种方式:

  • 按照字符数量读取,且指定的字符数要大于文件的总大小
  • 按分隔符读取,且指定的分隔符是文件中不存在的字符,这样的话会一直读取,因为找不到分隔符而读完整个文件
# 指定超出文件大小的字符数量
read -N 1000000 data <a.txt
echo "$data"

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

awk用法入门

awk 'awk_program' a.txt
  • a.txt是awk要读取的文件,可以是0个文件或一个文件,也可以多个文件
    • 如果不给定任何文件,但又需要读取文件,则表示从标准输入中读取
  • 单引号包围的是awk代码,也称为awk程序
    • 尽量使用单引号,因为在awk中经常使用$符号,而$符号在Shell是变量符号,如果使用双引号包围awk代码,则$符号会被Shell解析成Shell变量,然后进行Shell变量替换。使用单引号包围awk代码,则$会脱离Shell的魔掌,使得$符号留给了awk去解析
  • awk程序中,大量使用大括号,大括号表示代码块,代码块中间可以之间连用,代码块内部的多个语句需使用分号";"分隔

awk示例:

# 输出a.txt中的每一行
awk '{print $0}' a.txt

# 多个代码块,代码块中多个语句
# 输出每行之后还输出两行:hello行和world行
awk '{print $0}{print "hello";print "world"}' a.txt

对于awk '{print $0}' a.txt,它类似于shell的while循环while read line;do echo "$line";done <a.txt。awk隐藏了读取每一行的while循环,它会自动读取每一行,其中的{print $0}对应于Shell的while循环体echo "$line"部分。

下面再分析该awk命令的执行过程:

BEGIN和END语句块

awk的所有代码(目前这么认为)都是写在语句块中的。

例如:

awk '{print $0}' a.txt
awk '{print $0}{print $0;print $0}' a.txt

每个语句块前面可以有pattern,所以格式为:

pattern1{statement1}pattern2{statement3;statement4;...}

语句块可分为3类:BEGIN语句块、END语句块和main语句块。其中BEGIN语句块和END语句块都是的格式分别为BEGIN{...}END{...},而main语句块是一种统称,它的pattern部分没有固定格式,也可以省略,main代码块是在读取文件的每一行的时候都执行的代码块。

分析下面三个awk命令的执行结果:

awk 'BEGIN{print "我在前面"}{print $0}' a.txt
awk 'END{print "我在后面"}{print $0}' a.txt
awk 'BEGIN{print "我在前面"}{print $0}END{print "我在后面"}' a.txt

根据上面3行命令的执行结果,可总结出如下有关于BEGIN、END和main代码块的特性:

awk命令行结构和语法结构

awk命令行结构

awk [ -- ] program-text file ...        (1)
awk -f program-file [ -- ] file ...     (2)
awk -e program-text [ -- ] file ...     (3)

其中:

awk语法结构

awk语法结构即awk代码部分的结构。

awk的语法充斥着pattern{action}的模式,它们称为awk rule。

例如:

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

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

上面示例中,有BEGIN语句块,有END语句块,还有2个main代码块,两个main代码块都使用了正则表达式作为pattern。

关于awk的语法:

  • 多个pattern{action}可以直接连接连用
  • action中多个语句如果写在同一行,则需使用分号分隔
  • pattern部分用于筛选行,action表示在筛选通过后执行的操作
  • pattern和action都可以省略,其中:

pattern和action

对于pattern{action}语句结构(都称之为语句块),其中的pattern部分可以使用下面列出的模式:

# 特殊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模式

action部分,可以是任何语句,例如print。

详细分析awk如何读取文件

awk读取输入文件时,每次读取一条记录(record)(默认情况下按行读取,所以此时记录就是行)。每读取一条记录,将其保存到$0中,然后执行一次main代码段。

awk '{print $0}' a.txt

如果是空文件,则因为无法读取到任何一条记录,将导致直接关闭文件,而不会进入main代码段。

touch x.log  # 创建一个空文件
awk '{print "hello world"}' x.log

可设置表示输入记录分隔符的预定义变量RS(Record Separator)来改变每次读取的记录模式。

# RS="\n" 、 RS="m"
awk 'BEGIN{RS="\n"}{print $0}' a.txt
awk 'BEGIN{RS="m"}{print $0}' a.txt

RS通常设置在BEGIN代码块中,因为要先于读取文件就确定好RS分隔符。

RS指定输入记录分隔符时,所读取的记录中是不包含分隔符字符的。例如RS="a",则$0中一定不可能出现字符a。

RS两种可能情况:

  • RS为单个字符:直接使用该字符来分割记录
  • RS为多个字符:将其当做正则表达式,只要匹配正则表达式的符号,都用来分割记录
    • 设置预定义变量IGNORECASE为非零值,正则匹配时表示忽略大小写
    • 兼容模式下,只有首字符才生效,不会使用正则模式去分割记录

特殊的RS值用来解决特殊读取需求:

示例:

# 按段落读取: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='[ab]' a.txt  

预定义变量RT:

在awk每次读完一条记录时,会设置一个称为RT的预定义变量,表示Record Termination。

当RS为单个字符时,RT的值和RS的值是相同的。

当RS为多个字符(正则表达式)时,则RT设置为正则匹配到记录分隔符之后,真正用于划分记录时的字符。

当无法匹配到记录分隔符时,RT设置为控制空字符串(即默认的初始值)。

awk 'BEGIN{RS="(fe)?male"}{print RT}' a.txt

两种行号:NR和FNR

在读取每条记录之后,将其赋值给$0,同时还会设置NR、FNR、RT。

  • NR:所有文件的行号计数器
  • FNR:是各个文件的行号计数器
awk '{print NR}' a.txt a.txt
awk '{print FNR}' a.txt a.txt

详细分析awk字段分割

awk读取每一条记录之后,会将其赋值给$0,同时还会对这条记录按照预定义变量FS划分字段,将划分好的各个字段分别赋值给$1 $2 $3 $4...$N,同时将划分的字段数量赋值给预定义变量NF

引用字段的方式

$N引用字段:

  • N=0:即$0,引用记录本身
  • 0<N<=NF:引用对应字段
  • N>NF:表示引用不存在的字段,返回空字符串
  • N<0:报错

可使用变量或计算的方式指定要获取的字段序号。

awk '{n = 5;print $n}' a.txt
awk '{print $(2+2)}' a.txt   # 括号必不可少,用于改变优先级
awk '{print $(NF-3)}' a.txt

分割字段的方式

读取record之后,将使用预定义变量FS、FIELDWIDTHS或FPAT中的一种来分割字段。分割完成之后,再进入main代码段(所以,在main中设置FS对本次已经读取的record是没有影响的,但会影响下次读取)。

划分字段方式(一):FS或-F

FS或者-F:字段分隔符

# 字段分隔符指定为单个字符
awk -F":" '{print $1}' /etc/passwd
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd

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

划分字段方式(二):FIELDWIDTHS

指定预定义变量FIELDWIDTHS按字符宽度分割字段,这是gawk提供的高级功能。在处理某字段缺失时非常好用。

用法:

  • FIELDWIDTHS="3 5 6 9"表示第一个字段3字符,第二字段5字符...
  • FIELDWIDTHS = "8 1:5 6 2:33"表示:
    • 第一个字段读8个字符
    • 然后跳过1个字符再读5个字符作为第二个字段
    • 然后读6个字符作为第三个字段
    • 然后跳过2个字符在读33个字符作为第四个字段(如果不足33个字符,则读到结尾)
  • FIELDWIDTHS="2 3 *"
    • 第一个字段2个字符
    • 第二个字段3个字符
    • 第三个字段剩余所有字符
    • 星号只能放在最后,且只能单独使用,表示剩余所有

示例1:

# 没取完的字符串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 2

示例2:处理某些字段缺失的数据。

如果按照常规的FS进行字段分割,则对于缺失字段的行和没有缺失字段的行很难统一处理,但使用FIELDWIDTHS则非常方便。

假设a.txt文本内容如下:

ID  name    gender  age  email          phone
1   Bob     male    28   [email protected]     18023394012
2   Alice   female  24   [email protected]  18084925203
3   Tony    male    21   [email protected]    17048792503
4   Kevin   male    21   [email protected]    17023929033
5   Alex    male    18                  18185904230
6   Andy    female  22   [email protected]    18923902352
7   Jerry   female  25   [email protected]  18785234906
8   Peter   male    20   [email protected]     17729348758
9   Steven  female  23   [email protected]    15947893212
10  Bruce   female  27   [email protected]   13942943905

因为email字段有的是空字段,所以直接用FS划分字段不便处理。可使用FIELDWIDTHS。

# 字段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

划分字段方式(三):FPAT

FS是指定字段分隔符,来取得除分隔符外的部分作为字段。

FPAT是取得匹配的字符部分作为字段。它是gawk提供的一个高级功能。

FPAT根据指定的正则来全局匹配record,然后将所有匹配成功的部分组成$1、$2...,不会修改$0

  • awk 'BEGIN{FPAT="[0-9]+"}{print $3"-"}' a.txt
  • 之后再设置FS或FPAT,该变量将失效

FPAT常用于字段中包含了字段分隔符的场景。例如,CSV文件中的一行数据如下:

Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA

其中逗号分隔每个字段,但双引号包围的是一个字段整体,即使其中有逗号。

这时使用FPAT来划分各字段比使用FS要方便的多。

echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |\
awk '
	BEGIN{FPAT="[^,]*|(\"[^\"]*\")"}
	{
        for (i=1;i<NF;i++){
            print "<"$i">"
        }
	}
'

最后,patsplit()函数和FPAT的功能一样。

检查字段划分的方式

有FS、FIELDWIDTHS、FPAT三种获取字段的方式,可使用PROCINFO数组来确定本次使用何种方式获得字段。

PROCINFO是一个数组,记录了awk进程工作时的状态信息。

如果:

  • PROCINFO["FS"]=="FS",表示使用FS分割获取字段
  • PROCINFO["FPAT"]=="FPAT",表示使用FPAT匹配获取字段
  • PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS",表示使用FIELDWIDTHS分割获取字段

例如:

if(PROCINFO["FS"]=="FS"){
    ...FS spliting...
} else if(PROCINFO["FPAT"]=="FPAT"){
    ...FPAT spliting...
} else if(PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS"){
    ...FIELDWIDTHS spliting...
}

修改字段或NF值的联动效应

注意下面的分割和计算两词:分割表示使用FS(field Separator),计算表示使用预定义变量OFS(Output Field Separator)。

关于$0

当读取一条record之后,将原原本本地被保存到$0当中。

awk '{print $0}' a.txt

但是,只要出现了上面所说的任何一种导致$0重新计算的操作,都会立即使用OFS去重建$0

换句话说,没有导致$0重建,$0就一直是原原本本的数据,所以指定OFS也无效。

awk 'BEGIN{OFS="-"}{print $0}' a.txt  # OFS此处无效

$0重建后,将自动使用OFS重建,所以即使没有指定OFS,它也会采用默认值(空格)进行重建。

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

如果重建$0之后,再去修改OFS,将对当前行无效,但对之后的行有效。所以如果也要对当前行生效,需要再次重建。

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

# 对所有行有效
awk '{$4+=10;OFS="-";$1=$1;print $0}' a.txt

关注$0重建是一个非常有用的技巧。

例如,下面通过重建$0的技巧来实现去除行首行尾空格并压缩中间空格:

$ echo "   a  b  c   d   " | awk '{$1=$1;print}'
a b c d
$ echo "     a   b  c   d   " | awk '{$1=$1;print}' OFS="-"            
a-b-c-d

awk数据筛选示例

筛选行

# 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.txt

处理字段

修改字段时,一定要注意,可能带来的联动效应:即使用OFS重建$0。

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.txt

awk运维面试试题

从ifconfig命令的结果中筛选出除了lo网卡外的所有IPv4地址。

# 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}'

awk工作流程

参考自:man awk的"AWK PROGRAM EXECUTION"段。

man --pager='less -p ^"AWK PROGRAM EXECUTION"' awk

执行步骤

getline用法详解

除了可以从标准输入或非选项型参数所指定的文件中读取数据,还可以使用getline从其它各种渠道获取需要处理的数据,它的用法有很多种。

getline的返回值:

  • 如果可以读取到数据,返回1
  • 如果遇到了EOF,返回0
  • 如果遇到了错误,返回负数。如-1表示文件无法打开,-2表示IO操作需要重试(retry)。在遇到错误的同时,还会设置ERRNO变量来描述错误

为了健壮性,getline时强烈建议进行判断。例如:

if( (getline) <= 0 ){...}
if((getline) < 0){...}
if((getline) > 0){...}

上面的getline的括号尽量加上,因为getline < 0表示的是输入重定向,而不是和数值0进行小于号的比较。

无参数的getline

getline无参数时,表示从当前正在处理的文件中立即读取下一条记录保存到$0中,并进行字段分割,然后继续执行后续代码逻辑

此时的getline会设置NF、RT、NR、FNR、$0和$N。

next也可以读取下一行。

  • getline:读取下一行之后,继续执行getline后面的代码

  • next:读取下一行,立即回头awk循环的头部,不会再执行next后面的代码

它们之间的区别用伪代码描述,类似于:

# 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

为了更健壮,应当对getline的返回值进行判断。

awk '/^1/{print;if((getline)<=0){exit};print}' a.txt

一个参数的getline

没有参数的getline是读取下一条记录之后将记录保存到$0中,并对该记录进行字段的分割。

一个参数的getline是将读取的记录保存到指定的变量当中,并且不会对其进行分割。

getline var

此时的getline只会设置RT、NR、FNR变量和指定的变量var。因此$0和$N以及NF保持不变。

awk '
/^1/{
  if((getline var)<=0){exit}
  print var
  print $0"--"$2
}' a.txt

awk从指定文件中读取数据

  • getline < filename:从指定文件filename中读取一条记录并保存到$0
    • 会进行字段的划分,会设置变量$0 $N NF,不会设置变量NR FNR
  • getline var < filename:从指定文件filename中读取一条记录并保存到指定变量var中
    • 不会划分字段,不会设置变量NR FNR NF $0 $N

filename需使用双引号包围表示文件名字符串,否则会当作变量解析getline < "c.txt"。此外,如果路径是使用变量构建的,则应该使用括号包围路径部分。例如getline < dir "/" filename中使用了两个变量构建路径,这会产生歧义,应当写成getline <(dir "/" filename)

注意,每次从filename读取之后都会做好位置偏移标记,下次再从该文件读取时将根据这个位置标记继续向后读取。

例如,每次行首以1开头时就读取c.txt文件的所有行。

awk '
  /^1/{
    print;
    while((getline < "c.txt")>0){print};
    close("c.txt")
}' a.txt

上面的close("c.txt")表示在while(getline)读取完文件之后关掉,以便后面再次读取,如果不关掉,则文件偏移指针将一直在文件结尾处,使得下次读取时直接遇到EOF。

awk从Shell命令输出结果中读取数据

  • cmd | getline:从Shell命令cmd的输出结果中读取一条记录保存到$0
    • 会进行字段划分,设置变量$0 NF $N RT,不会修改变量NR FNR
  • cmd | getline var:从Shell命令cmd的输出结果中读取数据保存到var中
    • 除了var和RT,其它变量都不会设置

如果要再次执行cmd并读取其输出数据,则需要close关闭该命令。例如close("seq 1 5"),参见下面的示例。

例如:每次遇到以1开头的行都输出seq命令产生的1 2 3 4 5

awk '/^1/{print;while(("seq 1 5"|getline)>0){print};close("seq 1 5")}' a.txt

再例如,调用Shell的date命令生成时间,然后保存到awk变量cur_date中:

awk '
  /^1/{
    print
    "date +\"%F %T\""|getline cur_date
    print cur_date
    close("date +\"%F %T\"")
}' a.txt

可以将cmd保存成一个字符串变量。

awk '
  BEGIN{get_date="date +\"%F %T\""}
  /^1/{
    print
    get_date | getline cur_date
    print cur_date
    close(get_date)
}' a.txt

更为复杂一点的,cmd中可以包含Shell的其它特殊字符,例如管道、重定向符号等:

awk '
  /^1/{
    print
    if(("seq 1 5 | xargs -i echo x{}y 2>/dev/null"|getline) > 0){
      print
    }
    close("seq 1 5 | xargs -i echo x{}y 2>/dev/null")
}' a.txt

awk中的coprocess

awk虽然强大,但是有些数据仍然不方便处理,这时可将数据交给Shell命令去帮助处理,然后再从Shell命令的执行结果中取回处理后的数据继续awk处理。

awk通过|&符号来支持coproc。

awk_print[f] "something" |& Shell_Cmd
Shell_Cmd |& getline [var]

这表示awk通过print输出的数据将传递给Shell的命令Shell_Cmd去执行,然后awk再从Shell_Cmd的执行结果中取回Shell_Cmd产生的数据。

例如,不想使用awk的substr()来取子串,而是使用sed命令来替换。

awk '
    BEGIN{
      CMD="sed -nr \"s/.*@(.*)$/\\1/p\"";
    }

    NR>1{
        print $5;
        print $5 |& CMD;
        close(CMD,"to");
        CMD |& getline email_domain;
        close(CMD);
        print email_domain;
}' a.txt

对于awk_print |& cmd; cmd |& getline的使用,须注意的是:

对于那些要求读完所有数据再执行的命令,例如sort命令,它们有可能需要等待数据已经完成后(遇到EOF标记)才开始执行任务,对于这些命令,可以多次向coprocess中写入数据,最后close(CMD,"to")让coprocess运行起来。

例如,对age字段(即$4)使用sort命令按数值大小进行排序:

awk '
    BEGIN{
      CMD="sort -k4n";
    }

    # 将所有行都写进管道
    NR>1{
      print $0 |& CMD;
    }

    END{
      close(CMD,"to");  # 关闭管道通知sort开始排序
      while((CMD |& getline)>0){
        print;
      }
      close(CMD);
} ' a.txt

close()

close(filename)
close(cmd,[from | to])  # to参数只用于coprocess的第一个阶段

如果close()关闭的对象不存在,awk不会报错,仅仅只是让其返回一个负数返回值。

close()有两个基本作用:

  • 关闭文件,丢弃已有的文件偏移指针
    • 下次再读取文件,将只能重新打开文件,重新打开文件会从文件的最开头处开始读取
  • 发送EOF标记

awk中任何文件都只会在第一次使用时打开,之后都不会再重新打开。只有关闭之后,再使用才会重新打开。

例如一个需求是只要在a.txt中匹配到1开头的行就输出另一个文件x.log的所有内容,那么在第一次输出x.log文件内容之后,文件偏移指针将在x.log文件的结尾处,如果不关闭该文件,则后续所有读取x.log的文件操作都从结尾处继续读取,但是显然总是得到EOF异常,所以getline返回值为0,而且也读取不到任何数据。所以,必须关闭它才能在下次匹配成功时再次从头读取该文件。

awk '
  /^1/{
    print;
    while((getline var <"x.log")>0){
      print var
    }
    close("x.log")
}' a.txt

在处理Coprocess的时候,close()可以指定第二个参数"from"或"to",它们都针对于coproc而言,from时表示关闭coproc |& getline的管道,使用to时,表示关闭print something |& coproc的管道。

awk '
BEGIN{
  CMD="sed -nr \"s/.*@(.*)$/\\1/p\"";
}
NR>1{
    print $5;
    print $5 |& CMD;
    close(CMD,"to");   # 本次close()是必须的
    CMD |& getline email_domain;
    close(CMD);
    print email_domain;
}' a.txt

上面的第一个close是必须的,否则sed会一直阻塞。因为sed一直认为还有数据可读,只有关闭管道发送一个EOF,sed才会开始处理。

执行Shell命令system()

多数时候,使用awk的print cmd | "sh"即可实现调用shell命令的功能。

$ awk 'BEGIN{print "date +\"%s.%N\" | "sh"}'

但也可以使用system()函数来直接执行一个Shell命令,system()的返回值是命令的退出状态码。

$ awk 'BEGIN{system("date +\"%s.%N\"")}'
1572328598.653524342

$ awk 'BEGIN{system("date +\"%s.%N\" >/dev/null")}'

$ awk 'BEGIN{system("date +\"%s.%N\" | cat")}'
1572328631.308807331

system()在开始运行之前会flush gawk的缓冲。特别的,空字符串参数的system(""),它会被gawk特殊对待,它不会去启动一个shell来执行空命令,而是仅执行flush操作。

关于flush的行为,参考下文。

fflush()

gawk会按块缓冲模式来缓冲输出结果,使用fflush()会将缓冲数据刷出。

fflush([filename])

从gawk 4.0.2之后的版本(不包括4.0.2),无参数fflush()将刷出所有缓冲数据。

此外,终端设备是行缓冲模式,此时不需要fflush,而重定向到文件、到管道都是块缓冲模式,此时可能需要fflush()。

此外,system()在运行时也会flush gawk的缓冲。特别的,如果system的参数为空字符串system(""),则它不会去启动一个shell子进程而是仅仅执行flush操作。

没有flush时:

# 在终端输入几行数据,将不会显示,直到按下Ctrl + D
awk '{print "first";print "second"}' | cat

使用fflush():

# 在终端输入几行数据,观察
awk '{print "first";fflush();print "second"}' | cat

使用system()来flush:

# 在终端输入几行数据,观察
awk '{print "first";system("echo system");print "second"}' | cat
awk '{print "first";system("");print "second"}' | cat

也可以使用stdbuf -oL命令来强制gawk按行缓冲而非默认的按块缓冲。

# 在终端输入几行数据,观察
stdbuf -oL awk '{print "first";print "second"}' | cat

fflush()也可以指定文件名或命令,表示只刷出到该文件或该命令的缓冲数据。

# 刷出所有流向到标准输出的缓冲数据
awk '{print "first";fflush("/dev/stdout");print "second"}' | cat

最后注意,fflush()刷出缓冲数据不代表发送EOF标记。

输出操作

awk可以通过print、printf将数据输出到标准输出或重定向到文件。

print

print elem1,elem2,elem3...
print(elem1,elem2,elem3...)

逗号分隔要打印的字段列表,各字段都会自动转换成字符串格式,然后通过预定义变量OFS(output field separator)的值(其默认值为空格)连接各字段进行输出。

$ awk 'BEGIN{print "hello","world"}'
hello world
$ awk 'BEGIN{OFS="-";print "hello","world"}'
hello-world

print要输出的数据称为输出记录,在print输出时会自动在尾部加上输出记录分隔符,输出记录分隔符的预定义变量为ORS,其默认值为\n

$ awk 'BEGIN{OFS="-";ORS="_\n";print "hello","world"}'
hello-world_

括号可省略,但如果要打印的元素中包含了特殊符号>,则必须使用括号包围(如print("a" > "A")),因为它是输出重定向符号。

如果省略参数,即print;等价于print $0;

print输出数值

print在输出数据时,总是会先转换成字符串再输出。

对于数值而言,可以自定义转换成字符串的格式,例如使用sprintf()进行格式化。

print在自动转换数值(专指小数)为字符串的时候,采用预定义变量OFMT(Output format)定义的格式按照sprintf()相同的方式进行格式化。OFMT默认值为%.6g,表示有效位(整数部分加小数部分)最多为6。

$ awk 'BEGIN{print 3.12432623}'
3.12433

可以修改OFMT,来自定义数值转换为字符串时的格式:

$ awk 'BEGIN{OFMT="%.2f";print 3.99989}'
4.00

# 格式化为整数
$ awk 'BEGIN{OFMT="%d";print 3.99989}' 
3
$ awk 'BEGIN{OFMT="%.0f";print 3.99989}' 
4

printf

printf format, item1, item2, ...

格式化字符:

修饰符:均放在格式化字符的前面

N$      N是正整数。默认情况下,printf的字段列表顺序和格式化字符
        串中的%号顺序是一一对应的,使用N$可以自行指定顺序。
        printf "%2$s %1$s","world","hello"输出hello world
        N$可以重复指定,例如"%1$s %1$s"将取两次第一个字段

宽度     指定该字段占用的字符数量,不足宽度默认使用空格填充,超出宽度将无视。
         printf "%5s","ni"输出"___ni",下划线表示空格

-       表示左对齐。默认是右对齐的。
        printf "%5s","ni"输出"___ni"
        printf "%-5s","ni"输出"ni___"

空格     针对于数值。对于正数,在其前添加一个空格,对于负数,无视
        printf "% d,% d",3,-2输出"_3,-2",下划线表示空格

+       针对于数值。对于正数,在其前添加一个+号,对于负数,无视
        printf "%+d,%+d",3,-2输出"+3,-2",下划线表示空格

#       可变的数值前缀。对于%o,将添加前缀0,对于%x或%X,将添加前缀0x或0X

0       只对数值有效。使用0而非默认的空格填充在左边,对于左对齐的数值无效
        printf "%05d","3"输出00003
        printf "%-05d","3"输出3
        printf "%05s",3输出____3

'       单引号,表示对数值加上千分位逗号,只对支持千分位表示的locale有效
        $ awk "BEGIN{printf \"%'d\n\",123457890}"
        123,457,890
        $ LC_ALL=C awk "BEGIN{printf \"%'d\n\",123457890}"
        123457890

.prec   指定精度。在不同格式化字符下,精度含义不同
        %d,%i,%o,%u,%x,%X 的精度表示最大数字字符数量
        %e,%E,%f,%F 的精度表示小数点后几位数
        %s 的精度表示最长字符数量,printf "%.3s","foob"输出foo
        %g,%G 的精度表示表示最大有效位数,即整数加小数位的总数量

sprintf()

sprintf()采用和printf相同的方式格式化字符串,但是它不会输出格式化后的字符串,而是返回格式化后的字符串。所以,可以将格式化后的字符串赋值给某个变量。

awk '
    BEGIN{
        a = sprintf("%03d", 12.34)
        print a  # 012
    }
'

重定向输出

print[f] something | Shell_Cmd时,awk将创建一个管道,然后启动Shell命令,print[f]产生的数据放入管道,而命令将从管道中读取数据。

# 例1:
awk '
    NR>1{
      print $2 >"name.unsort"
      cmd = "sort >name.sort"
      print $2 | cmd
      #print $2 | "sort >name.sort"
    }
    END{close(cmd)}
' a.txt

# 例2:awk中构建Shell命令,通过管道交给shell执行
awk 'BEGIN{printf "seq 1 5" | "bash"}'

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

例如:

awk 'BEGIN{print "something OK" > "/dev/stdout"}'
awk 'BEGIN{print "something wrong" > "/dev/stderr"}'
awk 'BEGIN{print "something wrong" | "cat >&2"}'

awk 'BEGIN{getline < "/dev/stdin";print $0}'

$ exec 4<> a.txt
$ awk 'BEGIN{while((getline < "/dev/fd/4")>0){print $0}}'

awk变量

awk的变量是动态变量,在使用时声明。

所以awk变量有3种状态:

  • 未声明状态:称为untyped类型
  • 引用过但未赋值状态:unassigned类型
  • 已赋值状态

引用未赋值的变量,其默认初始值为空字符串或数值0

在awk中未声明的变量称为untyped,声明了但未赋值(只要引用了就声明了)的变量其类型为unassigned。

gawk 4.2版提供了typeof()函数,可以测试变量的数据类型,包括测试变量是否声明。

awk 'BEGIN{
  print(typeof(a))            # untyped
  if(b==0){print(typeof(b))}  # unassigned
}'

除了typeof(),还可以使用下面的技巧进行检测:

awk 'BEGIN{
  if(a=="" && a==0){    # 未赋值时,两个都true
    print "untyped or unassigned"
  } else {
      print "assigned"
  }
}'

变量赋值

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变量

num=$(cat a.txt | wc -l)
awk -v n=$num 'BEGIN{print n}'

-v选项是在awk工作流程的第一阶段解析的,所以-v选项声明的变量在BEGIN{}、END{}和main代码段中都能直接使用。

2.在非选项型参数位置处使用var=value格式将Shell变量赋值给awk变量

num=$(cat a.txt | wc -l)
awk  '{print n}' n=$num a.txt

非选项型参数设置的变量不能在BEGIN代码段中使用。

3.直接在awk代码部分暴露Shell变量,交给Shell解析进行Shell的变量替换

num=$(cat a.txt | wc -l)
awk 'BEGIN{print '"$num"'}'

这种方式最灵活,但可读性最差,可能会出现大量的引号。

数据类型

gawk有两种基本的数据类型:数值和字符串。在gawk 4.2.0版本中,还支持第三种基本的数据类型:正则表达式类型。

数据是什么类型在使用它的上下文中决定:在字符串操作环境下将转换为字符串,在数值操作环境下将转换为数值。这和自然语言中的一个词语、一个单词在不同句子内的不同语义是一样的。

隐式转换:

  • 算术加0操作可转换为数值类型
    • "123" + 0返回数值123
    • " 123abc" + 0转换为数值时为123
    • 无效字符串将转换成0,例如"abc"+3返回3
  • 连接空字符串可转换为字符串类型
    • 123""转换为字符串"123"
awk 'BEGIN{a="123";print typeof(a+0)}' # number
awk 'BEGIN{a=123;print typeof(a"")}'   # string

awk 'BEGIN{a=2;b=3;print(a b)+4}' # 27

显式转换:

  • 数值->字符串:
    • CONVFMT或sprintf():功能等价。都是指定数值转换为字符串时的格式
awk 'BEGIN{a=123.4567;CONVFMT="%.2f";print a""}' #123.46
awk 'BEGIN{a=123.4567;print sprintf("%.2f", a)}' #123.46
awk 'BEGIN{a=123.4567;printf("%.2f",a)}' 
  • 字符串->数值:strtonum()
gawk 'BEGIN{a="123.4567";print strtonum(a)}' # 123.457

awk字面量

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

数值字面量

  • 整数、浮点数、科学计数
    • 105、105.0、1.05e+2、1050e-1
  • awk内部总是使用浮点数方式保存所有数值,但用户在使用可以转换成整数的数值时总会去掉小数点
    • 数值12.0面向用户的值为12,12面向awk内部的值是12.0000000...0
# 结果是123而非123.0
awk 'BEGIN{a=123.0;print a}'

算术运算

++ --    自增、自减,支持i++和++i或--i或i--  
^        幂运算(**也用于幂运算)
+ -      一元运算符(正负数符号)
* / %    乘除取模运算
+ -      加减法运算

# 注:
# 1.++和--既可以当作独立语句,也可以作为表达式,如:
#     awk 'BEGIN{a=3;a++;a=++a;print a}'
# 2.**或^幂运算是从右向左计算的:print 2**1**3得到2而不是8

赋值操作(优先级最低):

= += -= *= /= %= ^= **=

疑惑:b = 6;print b += b++输出结果?可能是12或13。不同的awk的实现在评估顺序上不同,所以不要用这种可能产生歧义的语句。

字符串字面量

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

  • "abc"
  • ""
  • "\0""\n"

字符串连接(串联):awk没有为字符串的串联操作提供运算符,可以直接连接或使用空格连接。

awk 'BEGIN{print ("one" "two")}'  # "onetwo"
awk 'BEGIN{print ("one""two")}'
awk 'BEGIN{a="one";b="two";print (a b)}'

注意:字符串串联虽然方便,但是要考虑串联的优先级。例如下面的:

# 下面第一个串联成功,第二个串联失败,
# 因为串联优先级低于加减运算,等价于`12 (" " -23)`
# 即:先转为数值0-23,再转为字符串12-23
$ awk 'BEGIN{a="one";b="two";print (12 " " 23)}'
12 23
$ awk 'BEGIN{a="one";b="two";print (12 " " -23)}'
12-23

正则表达式字面量

普通正则:

  • /[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支持的正则

.       # 匹配任意字符,包括换行符
^
$
[...]
[^...]
|
+
*
?
()
{m}
{m,}
{m,n}
{,n}

[:lower:]
[:upper:]
[:alpha:]
[:digit:]
[:alnum:]
[:xdigit:]
[:blank:]
[:space:]
[:punct:]
[:graph:]
[:print:]
[:cntrl:]

以下是gawk支持的:
\y    匹配单词左右边界部分的空字符位置 "hello world"
\B    和\y相反,匹配单词内部的空字符位置,例如"crate" ~ `/c\Brat\Be/`成功
\<    匹配单词左边界
\>    匹配单词右边界
\s    匹配空白字符
\S    匹配非空白字符
\w    匹配单词组成字符(大小写字母、数字、下划线)
\W    匹配非单词组成字符
\`    匹配字符串的绝对行首  "abc\ndef"
\'    匹配字符串的绝对行尾

gawk不支持正则修饰符,所以无法直接指定忽略大小写的匹配。

如果想要实现忽略大小写匹配,则可以将字符串先转换为大写、小写再进行匹配。或者设置预定义变量IGNORECASE为非0值。

# 转换为小写
awk 'tolower($0) ~ /bob/{print $0}' a.txt

# 设置IGNORECASE
awk '/BOB/{print $0}' IGNORECASE=1 a.txt

awk布尔值

在awk中,没有像其它语言一样专门提供true、false这样的关键字。

但它的布尔值逻辑非常简单:

awk '
BEGIN{
    if(1){print "haha"}
    if("0"){print "hehe"}
    if(a=3){print "hoho"}  # if(3){print "hoho"}
    if(a==3){print "aoao"}
    if(/root/){print "heihei"}  # $0 ~ /root/
}'

awk中比较操作

strnum类型

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

注意,strnum类型只针对于awk中除数值常量、字符串常量、表达式计算结果外的数据。例如从文件中读取的字段$1$2、ARGV数组中的元素等等。

$ echo "30" | awk '{print typeof($0) " " typeof($1)}'
strnum strnum
$ echo "+30" | awk '{print typeof($1)}'
strnum
$ echo "30a" | awk '{print typeof($1)}'
string
$ echo "30 a" | awk '{print typeof($0) " " typeof($1)}'
string strnum
$ echo " +30 " | awk '{print typeof($0) " " typeof($1)}'
strnum strnum

大小比较操作

比较操作符:

< > <= >= != ==  大小、等值比较
in     数组成员测试

比较规则:

       |STRING NUMERIC STRNUM
-------|-----------------------
STRING |string string  string
NUMERIC|string numeric numeric
STRNUM |string numeric numeric

简单来说,string优先级最高,只要string类型参与比较,就都按照string的比较方式,所以可能会进行隐式的类型转换。

其它时候都采用num类型比较。

$ echo ' +3.14' | awk '{print typeof($0) " " typeof($1)}'  #strnum strnum
$ echo ' +3.14' | awk '{print($0 == " +3.14")}'    #1
$ echo ' +3.14' | awk '{print($0 == "+3.14")}'     #0
$ echo ' +3.14' | awk '{print($0 == "3.14")}'      #0
$ echo ' +3.14' | awk '{print($0 == 3.14)}'        #1
$ echo ' +3.14' | awk '{print($1 == 3.14)}'        #1
$ echo ' +3.14' | awk '{print($1 == " +3.14")}'    #0
$ echo ' +3.14' | awk '{print($1 == "+3.14")}'     #1
$ echo ' +3.14' | awk '{print($1 == "3.14")}'      #0 
$ echo 1e2 3|awk ’{print ($1<$2)?"true":"false"}’  #false

采用字符串比较时需注意,它是逐字符逐字符比较的。

"11" < "9"  # true
"ab" < 99   # false

逻辑运算

&&          逻辑与
||          逻辑或
!           逻辑取反

expr1 && expr2  # 如果expr1为假,则不用计算expr2
expr1 || expr2  # 如果expr1为真,则不用计算expr2

# 注:
# 1. && ||会短路运算
# 2. !优先级高于&&和||
#    所以`! expr1 && expr2`等价于`(! expr1) && expr2`

!可以将数据转换成数值的1或0,取决于数据是布尔真还是布尔假。!!可将数据转换成等价布尔值的1或0。

$ awk 'BEGIN{print(!99)}'   # 0
$ awk 'BEGIN{print(!"ab")}' # 0
$ awk 'BEGIN{print(!0)}'    # 1
$ awk 'BEGIN{print(!ab)}'   # 1,因为ab变量不存在

$ awk 'BEGIN{print(!!99)}'   # 1
$ awk 'BEGIN{print(!!"ab")}' # 1
$ awk 'BEGIN{print(!!0)}'    # 0
$ awk 'BEGIN{print(!!ab)}'   # 0

由于awk中的变量未赋值时默认初始化为空字符串或数值0,也就是布尔假。那么可以直接对一个未赋值的变量执行!操作。

下面是一个非常有意思的awk技巧,它通过多次!对一个flag取反来实现只输出指定范围内的行。

# a.txt
$1==1{flag=!flag;print;next}    # 在匹配ID=1的行时,flag=1
flag{print}               # 将输出ID=2,3,4,5的行
$1==5{flag=!flag;next}    # ID=5时,flag=0

借此,就可以让awk实现一个多行处理模式。例如,将指定范围内的数据保存到一个变量当中去。

$1==1{flag=!flag;next}
flag{multi_line=multi_line$0"\n"}
$1==5{flag=!flag;next}
END{printf multi_line}

运算符优先级

优先级从高到低:man awk

()
$      # $(2+2)
++ --
^ **
+ - !   # 一元运算符
* / %
+ -
space  # 这是字符连接操作 `12 " " 23`  `12 " " -23`
| |&
< > <= >= != ==   # 注意>即是大于号,也是print/printf的重定向符号
~ !~
in
&&
||
?:
= += -= *= /= %= ^=

对于相同优先级的运算符,通常都是从左开始运算,但下面2种例外,它们都从右向左运算:

  • 赋值运算:如= += -= *=
  • 幂运算
a - b + c  =>  (a - b) + c
a = b = c  =>  a =(b = c)
2**2**3    =>  2**(2**3)

再者,注意print和printf中出现的>符号,这时候它表示的是重定向符号,不能再出现优先级比它低的运算符,这时可以使用括号改变优先级。例如:

awk 'BEGIN{print "foo" > a < 3 ? 2 : 1)'   # 语法错误
awk 'BEGIN{print "foo" > (a < 3 ? 2 : 1)}' # 正确

流程控制语句

注:awk中语句块没有作用域,都是全局变量。

if (condition) statement [ else statement ]
expr1?expr2:expr3
while (condition) statement
do statement while (condition)
for (expr1; expr2; expr3) statement
for (var in array) statement
break
continue
next
nextfile
exit [ expression ]
{ statements }
switch (expression) {
    case value|regex : statement
    ...
    [ default: statement ]
}

代码块

{statement}

if...else

# 单独的if
if(cond){
    statements
}

# if...else
if(cond1){
    statements1
} else {
    statements2
}

# if...else if...else
if(cond1){
    statements1
} else if(cond2){
    statements2
} else if(cond3){
    statements3
} else{
    statements4
}

搞笑题:妻子告诉程序员老公,去买一斤包子,如果看见卖西瓜的,就买两个。结果是买了两个包子回来。

# 自然语言的语义
买一斤包子
if(有西瓜){
    买两个西瓜
}

# 程序员理解的语义
if(没有西瓜){
    买一斤包子
}else{
    买两个包子
}
awk '
  BEGIN{
    mark = 999
    if (mark >=0 && mark < 60) {
      print "学渣"
    } else if (mark >= 60 && mark < 90) {
      print "还不错"
    } else if (mark >= 90 && mark <= 100) {
      print "学霸"
    } else {
      print "错误分数"
    }
  }
'

三目运算符?:

expr1 ? expr2 : expr3

if(expr1){
    expr2
} else {
    expr3
}
awk 'BEGIN{a=50;b=(a>60) ? "及格" : "不及格";print(b)}'
awk 'BEGIN{a=50; a>60 ? b="及格" : b="不及格";print(b)}' 

switch...case

switch (expression) {
    case value1|regex1 : statements1
    case value2|regex2 : statements2
    case value3|regex3 : statements3
    ...
    [ default: statement ]
}

awk 中的switch分支语句功能较弱,只能进行等值比较或正则匹配。

各分支结尾需使用break来终止。

{
    switch($1){
        case 1:
            print("Monday")
            break
        case 2:
            print("Tuesday")
            break
        case 3:
            print("Wednesday")
            break
        case 4:
            print("Thursday")
            break
        case 5:
            print("Friday")
            break
        case 6:
            print("Saturday")
            break
        case 7:
            print("Sunday")
            break
        default:
            print("What day?")
            break
    }
}

分支穿透:

{
    switch($1){
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            print("Weekday")
            break
        case 6:
        case 7:
            print("Weekend")
            break
        default:
            print("What day?")
            break
    }
}

while和do...while

while(condition){
    statements
}

do {
    statements
} while(condition)

while先判断条件再决定是否执行statements,do...while先执行statements再判断条件决定下次是否再执行statements。

awk 'BEGIN{i=0;while(i<5){print i;i++}}'
awk 'BEGIN{i=0;do {print i;i++} while(i<5)}'

多数时候,while和do...while是等价的,但如果第一次条件判断失败,则do...while和while不同。

awk 'BEGIN{i=0;while(i == 2){print i;i++}}'
awk 'BEGIN{i=0;do {print i;i++} while(i ==2 )}'

所以,while可能一次也不会执行,do...while至少会执行一次。

一般用while,do...while相比while来说,用的频率非常低。

for循环

for (expr1; expr2; expr3) {
    statement
}

for (idx in array) {
    statement
}

break和continue

break可退出for、while、do...while、switch语句。

continue可让for、while、do...while进入下一轮循环。

awk '
BEGIN{
  for(i=0;i<10;i++){
    if(i==5){
      break
    }
    print(i)
  }

  # continue
  for(i=0;i<10;i++){
    if(i==5)continue
    print(i)
  }
}'

next和nextfile

next会在当前语句处立即停止后续操作,并读取下一行,进入循环顶部。

例如,输出除第3行外的所有行。

awk 'NR==3{next}{print}' a.txt
awk 'NR==3{getline}{print}' a.txt

nextfile会在当前语句处立即停止后续操作,并直接读取下一个文件,并进入循环顶部。

例如,每个文件只输出前2行:

awk 'FNR==3{nextfile}{print}' a.txt a.txt

exit

exit [exit_code]

直接退出awk程序。

注意,END语句块也是exit操作的一部分,所以在BEGIN或main段中执行exit操作,也会执行END语句块。

如果exit在END语句块中执行,则立即退出。

所以,如果真的想直接退出整个awk,则可以先设置一个flag变量,然后在END语句块的开头检查这个变量再exit。

BEGIN{
    ...code...
    if(cond){
        flag=1
        exit
    }
}
{}
END{
    if(flag){
        exit
    }
    ...code...
}

awk '
    BEGIN{print "begin";flag=1;exit}
    {}
    END{if(flag){exit};print "end2"}
' 

exit可以指定退出状态码,如果触发了两次exit操作,即BEGIN或main中的exit触发了END中的exit,且END中的exit没有指定退出状态码时,则采取前一个退出状态码。

$ awk 'BEGIN{flag=1;exit 2}{}END{if(flag){exit 1}}' 
$ echo $?
1

$ awk 'BEGIN{flag=1;exit 2}{}END{if(flag){exit}}'   
$ echo $?
2

数组

awk数组特性:

  • awk的数组是关联数组(即key/value方式的hash数据结构),索引下标可为数值(甚至是负数、小数等),也可为字符串
    • 在内部,awk数组的索引全都是字符串,即使是数值索引在使用时内部也会转换成字符串
    • awk的数组元素的顺序和元素插入时的顺序很可能是不相同的
  • awk数组支持数组的数组

awk访问、赋值数组元素

arr[idx]
arr[idx] = value

索引可以是整数、负数、0、小数、字符串。如果是数值索引,会按照CONVFMT变量指定的格式先转换成字符串。

例如:

awk '
  BEGIN{
    arr[1]   = 11
    arr["1"] = 111
    arr["a"] = "aa"
    arr[-1]  = -11
    arr[4.3] = 4.33
# 本文来自骏马金龙:www.junmajinlong.com
    print arr[1]     # 111
    print arr["1"]   # 111
    print arr["a"]   # aa
    print arr[-1]    # -11
    print arr[4.3]   # 4.33
  }
'

通过索引的方式访问数组中不存在的元素时,会返回空字符串,同时会创建这个元素并将其值设置为空字符串

awk '
  BEGIN{
    arr[-1]=3;
    print length(arr);  # 1
    print arr[1];
    print length(arr)   # 2
  }'

awk数组长度

awk提供了length()函数来获取数组的元素个数,它也可以用于获取字符串的字符数量。还可以获取数值转换成字符串后的字符数量。

awk 'BEGIN{arr[1]=1;arr[2]=2;print length(arr);print length("hello")}'

awk删除数组元素

  • delete arr[idx]:删除数组arr[idx]元素
    • 删除不存在的元素不会报错
  • delete arr:删除数组所有元素
$ awk 'BEGIN{arr[1]=1;arr[2]=2;arr[3]=3;delete arr[2];print length(arr)}'
2

awk检测是否是数组

isarray(arr)可用于检测arr是否是数组,如果是数组则返回1,否则返回0。

typeof(arr)可返回数据类型,如果arr是数组,则其返回"array"。

awk 'BEGIN{
    arr[1]=1;
    print isarray(arr);
    print (typeof(arr) == "array")
}'

awk测试元素是否在数组中

不要使用下面的方式来测试元素是否在数组中:

if(arr["x"] != ""){...}

这有两个问题:

  • 如果不存在arr["x"],则会立即创建该元素,并将其值设置为空字符串
  • 有些元素的值本身就是空字符串

应当使用数组成员测试操作符in来测试:

# 注意,idx不要使用index,它是一个内置函数
if (idx in arr){...}

它会测试索引idx是否在数组中,如果存在则返回1,不存在则返回0。

awk '
    BEGIN{
    # 本文来自骏马金龙:www.junmajinlong.com
        arr[1]=1;
        arr[2]=2;
        arr[3]=3;

        arr[1]="";
        delete arr[2];

        print (1 in arr);  # 1
        print (2 in arr);  # 0
    }'

awk遍历数组

awk提供了一种for变体来遍历数组:

for(idx in arr){print arr[idx]}

因为awk数组是关联数组,元素是不连续的,也就是说没有顺序。遍历awk数组时,顺序是不可预测的。

例如:

# 本文来自骏马金龙:www.junmajinlong.com
awk '
    BEGIN{
        arr["one"] = 1
        arr["two"] = 2
        arr["three"] = 3
        arr["four"] = 4
        arr["five"] = 5

        for(i in arr){
            print i " -> " arr[i]
        }
    }
'

此外,不要随意使用for(i=0;i<length(arr);i++)来遍历数组,因为awk数组是关联数组。但如果已经明确知道数组的所有元素索引都位于某个数值范围内,则可以使用该方式进行遍历。

例如:

# 本文来自骏马金龙:www.junmajinlong.com
awk '
    BEGIN{
        arr[1] = "one"
        arr[2] = "two"
        arr[3] = "three"
        arr[4] = "four"
        arr[5] = "five"
        arr[10]= "ten"

        for(i=0;i<=10;i++){
            if(i in arr){
                print arr[i]
            }
        }
    }
'

awk复杂索引的数组

在awk中,很多时候单纯的一个数组只能存放两个信息:一个索引、一个值。但在一些场景下,这样简单的存储能力在处理复杂需求的时候可能会捉襟见肘。

为了存储更多信息,方式之一是将第3份、第4份等信息全部以特殊方式存放到值中,但是这样的方式在实际使用过程中并不方便,每次都需要去分割值从而取出各部分的值。

另一种方式是将第3份、第4份等信息存放在索引中,将多份数据组成一个整体构成一个索引。

gawk中提供了将多份数据信息组合成一个整体当作一个索引的功能。默认方式为arr[x,y],其中x和y是要结合起来构建成一个索引的两部分数据信息。逗号称为下标分隔符,在构建索引时会根据预定义变量SUBSEP的值将多个索引组合起来。所以arr[x,y]其实完全等价于arr[x SUBSEP y]

例如,如果SUBSEP设置为"@",那么arr[5,12] = 512存储时,其真实索引为5@12,所以要访问该元素需使用arr["5@12"]

SUBSEP的默认值为\034,它是一个不可打印的字符,几乎不可能会出现在字符串当中。

如果我们愿意的话,我们也可以自己将多份数据组合起来去构建成一个索引,例如arr[x" "y]。但是awk提供了这种更为简便的方式,直接用即可。

为了测试这种复杂数组的索引是否在数组中,可以使用如下方式:

arr["a","b"] = 12
if (("a", "b") in arr){...}

例如,顺时针倒转下列数据:

1 2 3 4 5 6
2 3 4 5 6 1
3 4 5 6 1 2
4 5 6 1 2 3

结果:
4 3 2 1
5 4 3 2
6 5 4 3
1 6 5 4
2 1 6 5
3 2 1 6
{
  nf = NF
  nr = NR
  for(i=1;i<=NF;i++){
    arr[NR,i] = $i
  }
}

END{
  for(i=1;i<=nf;i++){
    for(j=nr;j>=1;j--){
      if(j%nr == 1){
        printf "%s\n", arr[j,i]
      }else {
        printf "%s ", arr[j,i]
      }
    }
  }
}

awk子数组

子数组是指数组中的元素也是一个数组,即Array of Array,它也称为子数组(subarray)。

awk也支持子数组,在效果上即是嵌套数组或多维数组。

a[1][1] = 11
a[1][2] = 12
a[1][3] = 13
a[2][1] = 21
a[2][2] = 22
a[2][3] = 23
a[2][4][1] = 241
a[2][4][2] = 242
a[2][4][1] = 241
a[2][4][3] = 243

通过如下方式遍历二维数组:

awk指定数组遍历顺序

由于awk数组是关联数组,默认情况下,for(idx in arr)遍历数组时顺序是不可预测的。

但是gawk提供了PROCINFO["sorted_in"]来指定遍历的元素顺序。它可以设置为两种类型的值:

  • 设置为用户自定义函数
  • 设置为下面这些awk预定义好的值:
    • @unsorted:默认值,遍历时无序
    • @ind_str_asc:索引按字符串比较方式升序遍历
    • @ind_str_desc:索引按字符串比较方式降序遍历
    • @ind_num_asc:索引强制按照数值比较方式升序遍历。所以无法转换为数值的字符串索引将当作数值0进行比较
    • @ind_num_desc:索引强制按照数值比较方式降序遍历。所以无法转换为数值的字符串索引将当作数值0进行比较
    • @val_type_asc:按值升序比较,此外数值类型出现在前面,接着是字符串类型,最后是数组类(即认为num<str<arr)
    • @val_type_desc:按值降序比较,此外数组类型出现在前面,接着是字符串类型,最后是数值型(即认为num<str<arr)
    • @val_str_asc:按值升序比较,数值转换成字符串再比较,而数组出现在尾部(即认str<arr)
    • @val_str_desc:按值降序比较,数值转换成字符串再比较,而数组出现在头部(即认str<arr)
    • @val_num_asc:按值升序比较,字符串转换成数值再比较,而数组出现在尾部(即认num<arr)
    • @val_num_desc:按值降序比较,字符串转换成数值再比较,而数组出现在头部(即认为num<arr)

例如:

awk '
  BEGIN{
    arr[1] = "one"
    arr[2] = "two"
    arr[3] = "three"
    arr["a"] ="aa"
    arr["b"] ="bb"
    arr[10]= "ten"

    #PROCINFO["sorted_in"] = "@ind_num_asc"
    #PROCINFO["sorted_in"] = "@ind_str_asc"
    PROCINFO["sorted_in"] = "@val_str_asc"
    for(idx in arr){
      print idx " -> " arr[idx]
    }
}'

a -> aa
b -> bb
1 -> one
2 -> two
3 -> three
10 -> ten

# 本文来自骏马金龙:www.junmajinlong.com

如果指定为用户自定义的排序函数,其函数格式为:

function sort_func(i1,v1,i2,v2){
    ...
    return <0;0;>0
}

其中,i1和i2是每次所取两个元素的索引,v1和v2是这两个索引的对应值。

如果返回值小于0,则表示i1在i2前面,i1先被遍历。如果等于0,则表示i1和i2具有等值关系,它们的遍历顺序不可保证。如果大于0,则表示i2先于i1被遍历。

例如,对数组元素按数值大小比较来决定遍历顺序。

awk '
function cmp_val_num(i1, v1, i2, v2){
  if ((v1 - v2) < 0) {
    return -1
  } else if ((v1 - v2) == 0) {
    return 0
  } else {
    return 1
  }
  # return (v1-v2)
}

NR > 1 {
  arr[$0] = $4
}

END {
  PROCINFO["sorted_in"] = "cmp_val_num"
  for (i in arr) {
    print i
  }
}' a.txt

再比如,按数组元素值的字符大小来比较。

function cmp_val_str(i1,v1,i2,v2) {
    v1 = v1 ""
    v2 = v2 ""
    if(v1 < v2){
        return -1
    } else if(v1 == v2){
        return 0
    } else {
        return 1
    }
    # return (v1 < v2) ? -1 : (v1 != v2)
}

NR>1{
    arr[$0] = $2
}

END{
    PROCINFO["sorted_in"] = "cmp_val_str"
    for(line in arr)
    {
        print line
    }
}

再比如,对元素值按数值升序比较,且相等时再按第一个字段ID进行数值降序比较。

awk '
function cmp_val_num(i1,v1,i2,v2,    a1,a2) {
    if (v1<v2) {
        return - 1
    } else if(v1 == v2){
        split(i1, a1, SUBSEP)
        split(i2, a2, SUBSEP)
        return a2[2] - a1[2]
    } else {
        return 1
    }
}

NR>1{
    arr[$0,$1] = $4
}

END{
    PROCINFO["sorted_in"] = "cmp_val_num"
    for(str in arr){
        split(str, a, SUBSEP)
        print a[1]
    }
}

' a.txt

上面使用的arr[x,y]来存储额外信息,下面使用arr[x][y]多维数组的方式来存储额外信息实现同样的排序功能。

NR>1{
  arr[NR][$0] = $4
}

END{
  PROCINFO["sorted_in"] = "cmp_val_num"
  for(nr in arr){
    for(line in arr[nr]){
      print line
    }
  # 本文来自骏马金龙:www.junmajinlong.com
  }
}

function cmp_val_num(i1,v1,i2,v2,   ii1,ii2){
  # 获取v1/v2的索引,即$0的值
  for(ii1 in v1){ }
  for(ii2 in v2){ }

  if(v1[ii1] < v2[ii2]){
    return -1
  }else if(v1[ii1] > v2[ii2]){
    return 1
  }else{
    return (i2 - i1)
  }
}

此外,gawk还提供了两个内置函数asort()和asorti()来对数组进行排序。

awk ARGC和ARGV

预定义变量ARGV是一个数组,包含了所有的命令行参数。该数组使用从0开始的数值作为索引。

预定义变量ARGC初始时是ARGV数组的长度,即命令行参数的数量。

ARGV数组的数量和ARGC的值只有在awk刚开始运行的时候是保证相等的。

$ awk -va=1 -F: '
  BEGIN{
    print ARGC;
    for(i in ARGV){
      print "ARGV[" i "]= " ARGV[i]
    }
}' b=3 a.txt b.txt

4
ARGV[0]= awk
ARGV[1]= b=3
ARGV[2]= a.txt
ARGV[3]= b.txt

awk读取文件是根据ARGC的值来进行的,有点类似于如下伪代码形式:

while(i=1;i<ARGC;i++){
    read from ARGV[i]
}

默认情况下,awk在读完ARGV中的一个文件时,会自动从它的下一个元素开始读取,直到读完所有文件。

直接减小ARGC的值,会导致awk不会读取尾部的一些文件。此外,增减ARGC的值,都不会影响ARGV数组,仅仅只是影响awk读取文件的数量。

# 不会读取b.txt
awk 'BEGIN{ARGC=2}{print}' a.txt b.txt

# 读完b.txt后自动退出
awk 'BEGIN{ARGC=5}{print}' a.txt b.txt

可以将ARGV中某个元素赋值为空字符串"",awk在选择下一个要读取的文件时,会自动忽略ARGV中的空字符串元素。

也可以delete ARGV[i]的方式来删除ARGV中的某元素。

用户手动增、删ARGV元素时,不会自动修改ARGC,而awk读取文件时是根据ARGC值来确定的。所以,在增加ARGV元素之后,要手动的去增加ARGC的值。

# 不会读取b.txt文件
$ awk 'BEGIN{ARGV[2]="b.txt"}{print}' a.txt

# 会读取b.txt文件
$ awk 'BEGIN{ARGV[2]="b.txt";ARGC++}{print}' a.txt 

对awk ARGC和ARGV进行操刀

awk判断命令行中给定文件是否可读

awk命令行中可能会给出一些不存在或无权限或其它原因而无法被awk读取的文件名,这时可以判断并从中剔除掉不可读取的文件。

  1. 排除命令行尾部(非选项型参数)的var=val、-、和/dev/stdin这3种特殊情况
  2. 如果不可读,则从ARGV中删除该参数
  3. 剩下的都是可在main代码段正常读取的文件
BEGIN{
  for(i=1;i<ARGC;i++){
    if(ARGV[i] ~ /[a-zA-Z_][a-zA-Z0-9_]*=.*/ \
    || ARGV[i]=="-" || ARGV[i]=="/dev/stdin"){
      continue
    } else if((getline var < ARGV[i]) < 0){
      delete ARGV[i]
    } else{
      close(ARGV[i])
    }
  }
}

awk 自定义函数

可以定义一个函数将多个操作整合在一起。函数定义之后,可以到处多次调用,从而方便复用。

使用function关键字来定义函数:

function func_name([parameters]){
    function_body
}

对于gawk来说,也支持func关键字来定义函数。

func func_name(){}

函数可以定义在下面使用下划线的地方:

awk '_ BEGIN{} _ MAIN{} _ END{} _'

无论函数定义在哪里,都能在任何地方调用,因为awk在BEGIN之前,会先编译awk代码为内部格式,在这个阶段会将所有函数都预定义好。

例如:

awk '
    BEGIN{
        f()
        f()
        f()
    }
    function f(){
        print "星期一"
        print "星期二"
        print "星期三"
        print "星期四"
        print "星期五"
        print "星期六"
        print "星期日"
    }
'

awk 函数的return语句

如果想要让函数有返回值,那么需要在函数中使用return语句。

return语句也可以用来立即结束函数的执行。

例如:

awk '
    function add(){
        return 40
    }
    BEGIN{
        print add()
        res = add() 
        print res
    }
'

如果不使用return或return没有参数,则返回值为空,即空字符串。

awk '
  function f1(){        }
  function f2(){return  }
  function f3(){return 3}
  BEGIN{
    print "-"f1()"-"
    print "-"f2()"-"
    print "-"f3()"-"
  }
'

awk函数参数

为了让函数和调用者能够进行数据的交互,可以使用参数。

awk '
  function f(a,b){
    print a
    print b
    return a+b
  }
  BEGIN{
    x=10
    y=20
    res = f(x,y)
    print res
    print f(x,y)
  }
'

例如,实现一个重复某字符串指定次数的函数:

awk '
    function repeat(str,cnt  ,res_str){
        for(i=0;i<cnt;i++){
            res_str = res_str""str
        }
        return res_str
    }
    BEGIN{
        print repeat("abc",3)
        print repeat("-",30)
    }
'

调用函数时,实参数量可以比形参数量少,也可以比形参数量多。但是,在多于形参数量时会给出警告信息。

awk '
  function f(a,b){
    print a
    print b
    return a+b
  }
  BEGIN{
    x=10
    y=20
    
    print "---1----"
    print "-"f()"-"          # 不传递参数

    print "---2----"
    print "-"f(30)"-"        # 传递1个参数

    print "---3----"
    print "-"f(10,20,30)"-"  # 传递多个参数
  }
'

awk函数参数数据类型冲突问题

如果函数内部使用参数的类型和函数外部变量的类型不一致,会出现数据类型不同而导致报错。

awk '
    function f(a){
        a[1]=30
    }
    BEGIN{
        a="hello world"
        f(a)   # 报错

        f(x)
        x=10   # 报错
    }
'

函数内部参数对应的是数组,那么外面对应的也必须是数组类型。

awk参数按值传递还是按引用传递

在调用函数时,将数据作为函数参数传递给函数时,有两种传递方式:

# 传递普通变量:按值拷贝
awk '
  function modify(a){
    a=30
    print a
  }
  BEGIN{
    a=40
    modify(a)
    print a
  }
'

# 传递数组:按引用拷贝
awk '
  function modify(a){
    a[1]=20
  }

  BEGIN{
    a[1]=10
    modify(a)
    print a[1]
  }
'

awk作用域问题

awk只有在函数参数中才是局部变量,其它地方定义的变量均为全局变量。

函数内部新增的变量是全局变量,会影响到全局,所以在函数退出后仍然能访问。例如上面的e变量。

awk '
  function f(){
    a=30  # 新增的变量,是全局变量
    print "in f: " a
  }
  BEGIN{
    a=40
    f()
    print a  # 30
  }
'

函数参数会遮掩全局同名变量,所以在函数执行时,无法访问到或操作与参数同名的全局变量,函数退出时会自动撤掉遮掩,这时才能访问全局变量。所以,参数具有局部效果。

awk '
  function f(a){
    print a    # 50,按值拷贝,和全局a已经没有关系
    a=40
    print a    # 40
  }
  BEGIN{
    a=50
    f(a)
    print a     # 50,函数退出,重新访问全局变量
  }
'

由于函数内部新增变量均为全局变量,awk也没有提供关键字来修饰一个变量使其成为局部变量。所以,awk只能将本该出现在函数体内的局部变量放在参数列表中,只要调用函数时不要为这些参数传递数据即可,从而实现局部变量的效果。

awk '
  function f(a,b       ,c,d){
  
    # a,b是参数,调用时需传递两个参数
    # c,d是局部变量,调用时不要给c和d传递数据
    a=30
    b=40
    c=50
    d=60
    e=70  # 全局变量

    print a,b,c,d,e  # 30 40 50 60 70
  }
  BEGIN{
    a=31
    b=41
    c=51
    d=61
    f(a,b)  # 调用函数时值传递两个参数
    print a,b,c,d,e  # 31 41 51 61 70
  }
'

所以,awk对函数参数列表做了两类区分:

  • arguments:调用函数时传递的参数
  • local variables:调用函数时省略的参数

local variables是awk实现真正局部变量的技巧,只是因为函数内部新增的变量都是全局变量,所以退而求其次将其放在参数列表上来实现局部变量。

自定义函数示例

1.一次性读取一个文件所有数据

function readfile(file    ,rs_bak,data){
  rs_bak=RS
  RS="^$"
  if ( (getline data < file) < 0 ){
    print "read file failed"
    exit 1
  }
  close(file)
  RS=rs_bak
  return data
}


/^1/{
  print $0
  content = readfile("c.txt")
  print content
}

将RS设置为^$是永远不可能出现的分隔符,除非这个文件为空文件。

2.重读文件

实现一个rewind()功能来重置文件偏移指针,从而模拟实现重读当前文件。

function rewind(    i){
    # 将当前正在读取的文件添加到ARGV中当前文件的下一个元素
    for(i=ARGC;i>ARCIND;i--){
        ARGV[i] = ARGV[i-1]
    }

    # 随着增加ARGC,以便awk能够读取到因ARGV增加元素后的最后一个文件
    ARGC++

    # 直接进入下一个文件
    nextfile
}

要注意可能出现无限递归的场景:

awk -f rewind.awk 'NR==3{rewind()}{print FILENAME, FNR, $0}' a.txt

# 下面这个会无限递归,因为FNR==3很可能每次重读时都会为真
awk -f rewind.awk 'FNR==3{rewind()}{print FILENAME, FNR, $0}' a.txt

3.格式化数组的输出

实现一个a2s()函数。

BEGIN{
  arr["zhangsan"]=21
  arr["lisi"]=22
  arr["wangwu"]=23
  print a2s(arr)
}

function a2s(arr       ,content,i,cnt){
  for(i in arr){
    if(cnt){
      content=content""(sprintf("\t%s:%s\n",i,arr[i]))
    } else {
      content=content""(sprintf("\n\t%s:%s\n",i,arr[i]))
    }
    cnt++
  }
  return "{"content"}"
}

4.禁用命令行尾部的赋值语句

awk '{}' ./a=b a.txt中,a=b会被awk识别为变量赋值操作。但是,如果用户想要处理的正好是包含了等号的文件名,则应当去禁用该赋值操作。

禁用的方式很简单,只需为其加上一个路径前缀./即可。

为了方便控制,可通过-v设置一个flag类型的选项标记。

function disable_assigns(argc,argv,    i){
    for(i=1;i<argc;i++){
        if(argv[i] ~ /[[:alpha:]_][[:alnum:]_]*=.*/){
            argv[i] = ("./"argv[i])
        }
    }
}

BEGIN{
    if(assign_flag){
        disable_assigns(ARGC,ARGV)
    }
}

那么,调用awk时采用如下方式:

awk -v assign_flag=1 -f assigns.awk '{print}' a=b.txt a.txt

awk选项、内置变量

选项

-e program-text
--source program-text
指定awk程序表达式,可结合-f选项同时使用
在使用了-f选项后,如果不使用-e,awk program是不会执行的,它会被当作ARGV的一个参数

-f program-file
--file program-file
从文件中读取awk源代码来执行,可指定多个-f选项

-F fs
--field-separator fs
指定输入字段分隔符(FS预定义变量也可设置)

-n
--non-decimal-data
识别文件输入中的8进制数(0开头)和16进制数(0x开头)
echo '030' | awk -n '{print $1+0}'

-o [filename]
格式化awk代码。
不指定filename时,则默认保存到awkprof.out
指定为`-`时,表示输出到标准输出

-v var=val
--assign var=val
在BEGIN之前,声明并赋值变量var,变量可在BEGIN中使用

预定义变量

预定义变量分为两类:控制awk工作的变量和携带信息的变量。

第一类:控制AWK工作的预定义变量

  • RS:输入记录分隔符,默认为换行符\n,参考RS
  • IGNORECASE:默认值为0,表示所有的正则匹配不忽略大小写。设置为非0值(例如1),之后的匹配将忽略大小写。例如在BEGIN块中将其设置为1,将使FS、RS都以忽略大小写的方式分隔字段或分隔record
  • FS:读取记录后,划分为字段的字段分隔符。参考FS
  • FIELDWIDTHS:以指定宽度切割字段而非按照FS。参考FIELDWIDTHS
  • FPAT:以正则匹配匹配到的结果作为字段,而非按照FS划分。参考FPAT
  • OFS:print命令输出各字段列表时的输出字段分隔符,默认为空格" "
  • ORS:print命令输出数据时在尾部自动添加的记录分隔符,默认为换行符\n
  • CONVFMT:在awk中数值隐式转换为字符串时,将根据CONVFMT的格式按照sprintf()的方式自动转换为字符串。默认值为"%.6g
  • OFMT:在print中,数值会根据OFMT的格式按照sprintf()的方式自动转换为字符串。默认值为"%.6g

第二类:携带信息的预定义变量

  • ARGCARGV:awk命令行参数的数量、命令参数的数组。参考ARGC和ARGV
  • ARGIND:awk当前正在处理的文件在ARGV中的索引位置。所以,如果awk正在处理命令行参数中的某文件,则ARGV[ARGIND] == FILENAME为真
  • FILENAME:awk当前正在处理的文件(命令行中指定的文件),所以在BEGIN中该变量值为空
  • ENVIRON:保存了Shell的环境变量的数组。例如ENVIRON["HOME"]将返回当前用户的家目录
  • NR:当前已读总记录数,多个文件从不会重置为0,所以它是一直叠加的
    • 可以直接修改NR,下次读取记录时将在此修改值上自增
  • FNR:当前正在读取文件的第几条记录,每次打开新文件会重置为0
    • 可以直接修改FNR,下次读取记录时将在此修改值上自增
  • NF:当前记录的字段数,参考NF
  • RT:在读取记录时真正的记录分隔符,参考RT
  • RLENGTH:match()函数正则匹配成功时,所匹配到的字符串长度,如果匹配失败,该变量值为-1
  • RSTART:match()函数匹配成功时,其首字符的索引位置,如果匹配失败,该变量值为0
  • SUBSEParr[x,y]中下标分隔符构建成索引时对应的字符,默认值为\034,是一个不太可能出现在字符串中的不可打印字符。参考复杂数组

awk预定义内置函数

预定义函数分为几类:

  • 数值类内置函数
  • 字符串类内置函数
  • 时间类内置函数
  • 位操作内置函数
  • 数据类型相关内置函数:isarray()、typeof()
  • IO类内置函数:close()、system()、fflush()

awk数值类内置函数

int(expr)     截断为整数:int(123.45)和int("123abc")都返回123,int("a123")返回0
sqrt(expr)    返回平方根
rand()        返回[0,1)之间的随机数,默认使用srand(1)作为种子值
srand([expr]) 设置rand()种子值,省略参数时将取当前时间的epoch值(精确到秒的epoch)作为种子值

例如:

$ awk 'BEGIN{srand();print rand()}'
0.0379114
$ awk 'BEGIN{srand();print rand()}'
0.0779783
$ awk 'BEGIN{srand(2);print rand()}'
0.893104
$ awk 'BEGIN{srand(2);print rand()}'
0.893104

生成[10,100]之间的随机整数。

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

awk字符串类内置函数

注意,awk中涉及到字符索引的函数,索引位都是从1开始计算,和其它语言从0开始不一样。

基本函数

  • sprintf(format, expression1, ...):返回格式化后的字符串,参考sprintf

    • a=sprintf("%s\n","abc")
  • length():返回字符串字符数量、数组元素数量、或数值转换为字符串后的字符数量

    awk '
        BEGIN{
            print length(1.23)     # 4   # CONVFMT %.6g
    
            print 1.234567         # 1.23457
            print length(1.234567) # 7 
            print length(122341223432.1213241234)  # 11
        }'
    
  • strtonum(str):将字符串转换为十进制数值

    • 如果str以0开头,则将其识别为8进制
    • 如果str以0x或0X开头,则将其识别为16进制
  • tolower(str):转换为小写

  • toupper(str):转换为大写

  • index(str,substr):从str中搜索substr(子串),返回搜索到的索引位置(索引从1开始),搜索不到则返回0

awk substr()

  • substr(string,start[,length]):从string中截取子串

start是截取的起始索引位(索引位从1开始而非0),length表示截取的子串长度。如果省略length,则表示从start开始截取剩余所有字符。

awk '
    BEGIN{
        str="abcdefgh"
        print substr(str,3)   # cdefgh
        print substr(str,3,3) # cde
    }
'

如果start值小于1,则将其看作为1对待,如果start大于字符串的长度,则返回空字符串。

如果length小于或等于0,则返回空字符串。

awk split()和patsplit()

  • split(string, array [, fieldsep [, seps ] ]):将字符串分割后保存到数组array中,数组索引从1开始存储。并返回分割得到的元素个数

其中fieldsep指定分隔符,可以是正则表达式方式的。如果不指定该参数,则默认使用FS作为分隔符,而FS的默认值又是空格。

seps是一个数组,保存了每次分割时的分隔符。

例如:

split("abc-def-gho-pq",arr,"-",seps)

其返回值为4。同时得到的数组a和seps为:

arr[1] = "abc"
arr[2] = "def"
arr[3] = "gho"
arr[4] = "pq"

seps[1] = "-"
seps[2] = "-"
seps[3] = "-"

split在开始工作时,会先清空数组,所以,将split的string参数设置为空,可以用于清空数组。

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

如果分隔符无法匹配字符串,则整个字符串当作一个数组元素保存到数组array中。

awk 'BEGIN{split("abcde",arr,"-");print arr[1]}' # abcde
  • patsplit(string, array [, fieldpat [, seps ] ]):用正则表达式fieldpat匹配字符串string,将所有匹配成功的部分保存到数组array中,数组索引从1开始存储。返回值是array的元素个数,即匹配成功了多少次

如果省略fieldpat,则默认采用预定义变量FPAT的值。

awk '
    BEGIN{
        patsplit("abcde",arr,"[a-z]")
        print arr[1]   # a
        print arr[2]   # b
        print arr[3]   # c
        print arr[4]   # d
        print arr[5]   # e
    }
'

awk match()

  • match(string,reg[,arr]):使用reg匹配string,返回匹配成功的索引位(从1开始计数),匹配失败则返回0。如果指定了arr参数,则arr[0]保存的是匹配成功的字符串,arr[1]、arr[2]、...保存的是各个分组捕获的内容

match匹配时,同时会设置两个预定义变量:RSTART和RLENGTH

  • 匹配成功时:
    • RSTART赋值为匹配成功的索引位,从1开始计数
    • RLENGTH赋值为匹配成功的字符长度
  • 匹配失败时:
    • RSTART赋值为0
    • RLENGTH赋值为-1

例如:

awk '
    BEGIN{
        where = match("foooobazbarrrr","(fo+).*(bar*)",arr)
        print where   # 1
        print arr[0]  # foooobazbarrrr
        print arr[1]  # foooo
        print arr[2]  # barrrr
        print RSTART  # 1
        print RLENGTH # 14
    }
'

因为match()匹配成功时返回值为非0,而匹配失败时返回值为0,所以可以直接当作条件判断:

awk '
  {
    if(match($0,/A[a-z]+/,arr)){
      print NR " : " arr[0]
    }
  }
' a.txt

awk sub()和gsub()

  • sub(regexp, replacement [, target])
  • gsub(regexp, replacement [, target]):sub()的全局模式

sub()从字符串target中进行正则匹配,并使用replacement对第一次匹配成功的部分进行替换,替换后保存回target中。返回替换成功的次数,即0或1。

target必须是一个可以赋值的变量名、$N或数组元素名,以便用它来保存替换成功后的结果。不能是字符串字面量,因为它无法保存数据。

如果省略target,则默认使用$0

需要注意的是,如果省略target,或者target是$N,那么替换成功后将会使用OFS重新计算$0

awk '
    BEGIN{
        str="water water everywhere"
        #how_many = sub(/at/, "ith", str)
        how_many = gsub(/at/, "ith", str)
        print how_many   # 1
        print str        # wither water everywhere
    }
'

在replacement参数中,可以使用一个特殊的符号&来引用匹配成功的部分。注意sub()和gsub()不能在replacement中使用反向引用\N

awk '
    BEGIN{
        str = "daabaaa"
        gsub(/a+/,"C&C",str)
        print str  # dCaaCbaaa
    }
'

如果想要在replacement中使用&纯字符,则转义即可。

sub(/a+/,"C\\&C",str)

两根反斜线:
因为awk在正则开始工作时,首先会扫描所有awk代码然后编译成awk的内部格式,扫描期间会解析反斜线转义,使得\\变成一根反斜线。当真正开始运行后,sub()又要解析,这时\&才表示的是对&做转义。
扫描代码阶段称为词法解析阶段,运行解析阶段称为运行时解析阶段。

awk gensub()

gawk支持的gensub(),完全可以取代sub()和gsub()。

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

可以替代sub()和gsub()。

how指定替换第几个匹配,例如指定为1表示只替换第一个匹配。此外,还可以指定为gG开头的字符串,表示全局替换。

awk 'BEGIN{
    a = "abc def"
    b = gensub(/(.+) (.*)/, "\\2 \\1, \\0 , &", "g", a)
    print b  # def abc, abc def , abc def
}'

awk asort()和asorti()

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

asort对数组src的值进行排序,然后将排序后的值的索引改为1、2、3、4...序列。返回src中的元素个数,它可以当作排序后的索引最大值。

asorti对数组src的索引进行排序,然后将排序后的索引值的索引改为1、2、3、4...序列。返回src中的元素个数,它可以当作排序后的索引最大值。

arr["last"] = "de"
arr["first"] = "sac"
arr["middle"] = "cul"

asort(arr)得到:

arr[1] = "cul"
arr[2] = "de"
arr[3] = "sac"

asorti(arr)得到:

arr[1] = "first"
arr[2] = "last"
arr[3] = "middle"

如果指定dest,则将原始数组src备份到dest,然后对dest进行排序,而src保持不变。

how参数用于指定排序时的方式,其值指定方式和PROCINFO["sorted_in"]一致:可以是预定义的排序函数,也可以是用户自定义的排序函数。参考指定数组遍历顺序

IO类内置函数

  • close(filename [, how]):关闭文件或命令,参考close
  • system(command):执行Shell命令,参考system
  • fflush([filename]):gawk会按块缓冲模式来缓冲输出结果,使用fflush()会将缓冲数据刷出

从gawk 4.0.2之后的版本(不包括4.0.2),无参数fflush()将刷出所有缓冲数据。

此外,终端设备是行缓冲模式,此时不需要fflush,而重定向到文件、到管道都是块缓冲模式,此时可能需要fflush()。

此外,system()在运行时也会flush gawk的缓冲。特别的,如果system的参数为空字符串system(""),则它不会去启动一个shell子进程而是仅仅执行flush操作。

使用system()来flush:

awk '{print "first";system("echo system");print "second"}' | cat
awk '{print "first";system("");print "second"}' | cat

也可以使用stdbuf -oL命令来强制gawk按行缓冲而非默认的按块缓冲。

stdbuf -oL awk '{print "first";print "second"}' | cat

fflush()也可以指定文件名或命令,表示只刷出到该文件或该命令的缓冲数据。

# 刷出所有流向到标准输出的缓冲数据
awk '{print "first";fflush("/dev/stdout");print "second"}' | cat

最后注意,fflush()刷出缓冲数据不代表发送EOF标记。

数据类型内置函数

  • isarray(var):测试var是否是数组,返回1(是数组)或0(不是数组)
  • typeof(var):返回var的数据类型,有以下可能的值:
    • "array":是一个数组
    • "regexp":是一个真正表达式类型,强正则字面量才算是正则类型,如@/a.*ef/
    • "number":是一个number
    • "string":是一个string
    • "strnum":是一个strnum,参考strnum类型
    • "unassigned":曾引用过,但未赋值,例如"print f;print typeof(f)"
    • "untyped":从未引用过,也从未赋值过

例如,输出awk进程的内部信息,但跳过数组

awk '
  BEGIN{
    for(idx in PROCINFO){
      if(typeof(PROCINFO[idx]) == "array"){
        continue
      }
      print idx " -> "PROCINFO[idx]
    }
  }'

时间类内置函数

awk常用于处理日志,它支持简单的时间类操作。有下面3个内置的时间函数:

  • mktime("YYYY MM DD HH mm SS [DST]"):构建一个时间,返回这个时间点的秒级epoch,构建失败则返回-1
  • systime():返回当前系统时间点,返回的是秒级epoch值
  • strftime([format [, timestamp [, utc-flag] ] ]):将时间按指定格式转换为字符串并返回转的结果字符串

注意,awk构建时间时都是返回秒级的epoch值,表示从1970-01-01 00:00:00开始到指定时间已经过的秒数。

awk 'BEGIN{print systime();print mktime("2019 2 29 12 32 59")}'
1572364974
1551414779

awk mktime()

mktime在构建时间时,如果传递的DD给定的值超出了月份MM允许的天数,则自动延申到下个月。例如,指定"2019 2 29 12 30 59"中2月只有28号,所以构建出来的时间是2019-03-01 12:30:59

此外,其它部分也不限定必须在范围内。例如,2019 2 23 12 32 65的秒超出了59,那么多出来的秒数将进位到分钟。

awk 'BEGIN{
    print mktime("2019 2 23 12 32 65") | "xargs -i date -d@{} +\"%F %T\""
}'
2019-02-23 12:33:05

如果某部位的数值为负数,则表示在此时间点基础上减几。例如:

# 2019-02-23 12:00:59基础上减1分钟
$ awk 'BEGIN{print mktime("2019 2 23 12 -1 59") | "xargs -i date -d@{} +\"%F %T\""}'  
2019-02-23 11:59:59

# 2019-02-23 00:32:59基础上减1小时
$ awk 'BEGIN{print mktime("2019 2 23 -1 32 59") | "xargs -i date -d@{} +\"%F %T\""}'  
2019-02-22 23:32:59

awk strftime()

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

将指定的时间戳tiemstamp按照给定格式format转换为字符串并返回这个字符串。

如果省略timestamp,则对当前系统时间进行格式化。

如果省略format,则采用PROCINFO["strftime"]的格式,其默认格式为%a %b %e %H:%M%:S %Z %Y。该格式对应于Shell命令date的默认输出结果。

$ awk 'BEGIN{print strftime()}'
Wed Oct 30 00:20:01 CST 2014

$ date
Wed Oct 30 00:20:04 CST 2014

$ awk 'BEGIN{print strftime(PROCINFO["strftime"], systime())}'
Wed Oct 30 00:24:00 CST 2014

支持的格式包括:

%a 星期几的缩写,如Mon、Sun Wed Fri
%A 星期几的英文全名,如Monday
%b 月份的英文缩写,如Oct、Sep
%B 月份的英文全名,如February、October
%C 2位数的世纪,例如1970对应的世纪是19
%y 2位数的年份(00–99),通过年份模以100取得,例如2019/100的余数位19
%Y 四位数年份(如2015)
%m 月份(01–12)
%j 年中天(001–366)
%d 月中天(01–31)
%e 空格填充的月中天
%H 24小时制的小时(00–23)
%I 12小时制的小时(01–12)
%p 12小时制时的AM/PM
%M 分钟数(00–59)
%S 秒数(00–60)
%u 数值的星期几(1–7),1表示星期一
%w 数值的星期几(0–6),0表示星期日
%W 年中第几周(00–53)
%z 时区偏移,格式为"+HHMM",如"+0800"
%Z 时区偏移的英文缩写,如CST

%k 24小时制的时间(0-23),1位数的小时使用空格填充
%l 12小时制的时间(1-12),1位数的小时使用空格填充
%s 秒级epoch

##### 特殊符号
%n 换行符
%t 制表符
%% 百分号%

##### 等价写法:
%x 等价于"%A %B %d %Y"
%F 等价于"%Y-%m-%d",用于表示ISO 8601日期格式
%T 等价于"%H:%M:%S"
%X 等价于"%T"
%r 12小时制的时间部分格式,等价于"%I:%M:%S %p"
%R 等价于"%H:%M"
%c 等价于"%A %B %d %T %Y",如Wed 30 Oct 2015 12:34:48 AM CST
%D 等价于"%m/%d/%y"
%h 等价于"%b"

例如:

$ awk 'BEGIN{print strftime("%s", mktime("2077 11 12 10 23 32"))}'
3403909412

$ awk 'BEGIN{print strftime("%F %T %Z", mktime("2077 11 12 10 23 32"))}' 
2077-11-12 10:23:32 CST

$ awk 'BEGIN{print strftime("%F %T %z", mktime("2077 11 12 10 23 32"))}' 
2077-11-12 10:23:32 +0800

awk将字符串转换为时间:strptime1()

例如:

2019-11-11T03:42:42+08:00

1.将日期时间字符串中的年月日时分秒全都单独保存起来
2.将年月日时分秒构建成mktime()的字符串格式"YYYY MM DD HH mm SS"
3.使用mktime()可以构建出时间点

function strptime(str,    time_str,arr,Y,M,D,H,m,S){
    time_str = gensub(/[-T:+]/," ","g",str)
    split(time_str, arr, " ")
    Y = arr[1]
    M = arr[2]
    D = arr[3]
    H = arr[4]
    m = arr[5]
    S = arr[6]
    # mktime失败返回-1
    return mktime(sprintf("%d %d %d %d %d %d", Y,M,D,H,m,S))
}

BEGIN{
  str = "2019-11-11T03:42:42+08:00"
  print strptime(str)
}

awk将字符串转换为时间:strptime2()

下面是更难一点的,月份使用的是英文或英文缩写,日期时间分隔符也比较特殊。

Sat 26. Jan 15:36:24 CET 2013
function strptime(str,     time_str,arr,Y,M,D,H,m,S){
    time_str = gensub(/[.:]+/, " ", "g", str)
    split(time_str, arr, " ")
    Y = arr[8]
    M = month_map(arr[3])
    D = arr[2]
    H = arr[4]
    m = arr[5]
    S = arr[6]
    return mktime(sprintf("%d %d %d %d %d %d", Y,M,D,H,m,S))
}

function month_map(str,   mon){
    # mon = substr(str,1,3)
    # return (((index("JanFebMarAprMayJunJelAugSepOctNovDec", mon)-1)/3)+1)
    mon["Jan"] = 1
    mon["Feb"] = 2
    mon["Mar"] = 3
    mon["Apr"] = 4
    mon["May"] = 5
    mon["Jun"] = 6
    mon["Jul"] = 7
    mon["Aug"] = 8
    mon["Sep"] = 9
    mon["Oct"] = 10
    mon["Nov"] = 11
    mon["Dec"] = 12
    return mon[str]
}

BEGIN{
    str = "Sat 26. Jan 15:36:24 CET 2013"
    print strptime(str)
}

几个常见的gawk扩展

使用扩展的方式:

awk -l ext_name 'BEGIN{}{}END{}'
awk '@load "ext_name";BEGIN{}{}END{}'

1.文件相关的扩展

awk和文件相关的扩展是"filefuncs"。

它支持chdir()、stat()函数。

2.awk文件名匹配扩展

"fnmatch"扩展提供文件名通配。

@load "fnmatch"
result = fnmatch(pattern, string, flags)

3.awk原处修改文件

awk通过加载inplace.awk,也可以实现sed -i类似的功能,即内容直接修改源文件(其本质是先写入临时文件,写完后将临时文件重命名为源文件进行覆盖)。

例如:

4.awk多进程扩展

"fork"扩展提供多进程相关功能。

@load "fork"

pid = fork()
创建一个子进程,对子进程返回值为0,对父进程返回值为子进程的PID,返回-1表示错误。
在子进程中,PROCINFO["pid"]和PROCINFO["ppid"]会随之更新。

ret = waitpid(pid)
等待某个子进程退出。awk的waitpid是非阻塞的,如果等待的进程还未退出,则返回值为0,等待的进程已经退出,则返回该进程pid。

ret = wait()
等待任意一个子进程退出。wait()是阻塞的,必须等待到一个子进程退出,同时返回该子进程PID。

例如:

awk '
    @load "fork"
    BEGIN{
        if( (pid=fork()) == 0 ){
            print "Child Process"
            print "CHILD PID: "PROCINFO["pid"]
            print "CHILD PPID: "PROCINFO["ppid"]
            system("sleep 1")
        } else {
            while(waitpid(pid) == 0){
                system("sleep 1")
            }
            print "Parent PID: "PROCINFO["pid"]
            print "Parent PPID: "PROCINFO["ppid"]
            print "Parent Process"
        }
    }
'

5.awk日期时间扩展

"time"扩展提供了两个函数。

@load "time"

the_time = gettimeofday()
    获取当前系统时间,以浮点数方式返回,精确的浮点小数位由操作系统决定  

res = sleep(sec)
    睡眠指定时间,可以是小数秒
$ awk '@load "time";BEGIN{printf "%.9f\n",gettimeofday()}'
1572422333.740148067

$ awk '@load "time";BEGIN{printf "%.19f\n",gettimeofday()}'
1572422391.5475890636444091797

睡眠是很好用的功能:

awk '@load "time";BEGIN{sleep(1.2);print "hello world"}'

标签:BEGIN,一篇,arr,awk,玩透,print,txt,读取
From: https://www.cnblogs.com/f-ck-need-u/p/17806881.html

相关文章

  • 如何做 Nginx 安全日志分析可视化,看完这一篇,秒懂!
    之前介绍过ModSecurity这款优秀的开源WAF,它是一个入侵检测与阻止的引擎,原本是Apache的一个模块,现在可作为单独模块编译添加到Nginx服务中虽然这款WAF很优秀,但是使用起来并没有那么容易,之前也整理了文章介绍它的原理和规则,然而还有一个问题,就是它的日志分析,之前介绍原理规则......
  • [转]Elasticsearch全文检索入门这一篇就够了
    本文仅针对搜索与Elasticsearch小白,先介绍了全文搜索的原理,然后介绍了Elasticsearch中的一些基本概念,接着讲解如何在Elasticsearch中插入文档构建查询索引,最后介绍Elasticsearch的线上查询API的使用方式。全文搜索原理顺序扫描如何实现全文搜索?最简单的方法就是用正则去匹配文......
  • 一篇搞定Sentinel-搭建Spring Cloud Alibaba服务组件Sentinel实现服务资源控制
    1、Sentinel介绍随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性......
  • 第一篇 静态代码检查工具
    一、静态测试简介  静态测试包括代码检查、静态结构分析、代码质量度量等。它可以由人工进行,充分发挥人的逻辑思维优势,也可以借助软件工具自动进行。  代码检查包括代码走查、桌面检查、代码审查等,主要检查代码和设计的一致性,代码对标准的遵循、可读性,代码的逻辑表达的正确性......
  • Linux小技巧之awk必知必会
    LinuxShell三剑客之一,废话不多说直接上干货。目录1.指定分隔符2.指定打印行数3.打印最后一列4.打印倒数第二列5.匹配输出6.if判断7.统计列总数值8.时间格式转换示例数据:1.指定分隔符awk-F'|''{print$1}'log.txt#awk-F'|''{print$1}'log.txttbname============......
  • 去上海出庭:因用户2020年转载一篇公众号文章被起诉
    10月30日10:30,因用户2020年转载一篇公众号文章而产生的信息网络传播权纠纷一案将在上海市宝山区人民法院开庭,我们将去上海出庭,亲眼目睹原告如何振振有词地在法庭上表演。我们原本准备最近注销上海的公司,专心在杭州运营,没想到会以这样的方式与上海告别:2位上海的自媒体个人,通过同......
  • 第一篇随笔
    当然可以,以下是一个简单的Markdown格式的测试文本,你可以使用它来测试发布你的第一篇博客:欢迎来到我的博客!这是我的第一篇博客文章,用于测试发布。为什么选择博客园我选择博客园作为我的博客平台,因为它有:丰富的技术文章活跃的社区互动方便的Markdown编辑功能我的技术领......
  • 这是一篇入职3周.NET 实习开发的感悟
    这是一篇入职3周.NET实习开发的感悟吐槽开篇先吐槽一下吧!首先吐槽的就是加班费的问题,公司加班费挺多,但是我是试用期(3个月)没有加班费,但是公司带我的组长特喜欢加班,老是问我加不加班,懂点人情的都会一起加班,虽然公司规定17.30下班,但是基本都是18.00才开始走人,然后加班一般都是8.0......
  • 这是我在51CTO博客的第一篇博文Loki 实现 Kubernetes1.24 容器日志监控
    使用Loki实现Kubernetes1.24容器日志监控一、基本介绍1.Loki架构2.Loki工作原理二、使用Loki实现容器日志监控1.安装Loki2.安装Promtail3.安装Grafana4.验证一、基本介绍Loki是由GrafanaLabs团队开发的,基于Go语言实现,是一个水平可扩展,高可用性,多租户的日志聚合系......
  • 第一篇文章
    Markdown学习标题三级标题四级标题1.#+空格为一级标题,以此类推###+空格为三级标题字体Hello.World!Hello.World!Hello.World!Hello.World!引用大于号为引用选择分割线---或***为分割线图片 !+[图片名字]+()+图片路径为插入图片超链接点击跳转到我的博......