AWK笔记
简介
AWK是一种处理文本文件的语言。它将文件作为记录序列处理。在一般情况下,文件内容的每行都是一个记录。每行内容都会被分割成一系列的域,因此,我们可以认为一行的第一个词为第一个域,第二个词为第二个,以此类推。AWK程序是由一些处理特定模式的语句块构成的。AWK一次可以读取一个输入行。对每个输入行,AWK解释器会判断它是否符合程序中出现的各个模式,并执行符合的模式所对应的动作。
mawk和gawk都是AWK解释器的实现,功能和用法上大同小异。但使用时需要看相应解释器的文档,否则可能出现意外的结果。
本文主要以mawk手册为基础,介绍awk语言和mawk的使用方法。
AWK 程序的结构
一个 awk 程序由一系列 PATTERN {ACTION}二元组组成。
当 PATTERN 为真时,执行 {ACTION}。其中,若省略 PATTERN 则默认总是为真;若省略 {ACTION} 则默认执行 { print }
$ echo -e 'line 0\nline 1' | awk '$2==1 {print $0}'
line 1
$2==1
是 pattern,print $0
是 action。
模式 pattern
pattern 可以有以下4种形式:
BEGIN:表示对应的 action 在脚本其他部分执行前执行,此时 {action} 不可省略。一般我们用 BEGIN 来进行初始化的工作。
END:表示对应的 action 在其他部分执行完后完成后执行,此时 {action} 不可省略。一般用 END 来综合结果。
expression:表达式,为真时执行 action
expression1 , expression2:表示匹配
动作 action
action 中语句顺序执行,语句之间用换行符或 ;
分割开。
条件跳转和循环语句与C语言类似:
if ( expr ) statement
if ( expr ) statement else statement
while ( expr ) statement
do statement while ( expr )
for ( opt_expr ; opt_expr ; opt_expr ) statement
for ( var in array ) statement
continue
break
记录和域
输入被程序使用RS
分割成一条条记录。记录每次一条地读取,并存储在域变量$0
中。
记录被使用FS
分割成域,存储在$1, $2,..., $NF
中,其中内置变量NF
为分割出域的数量,NR
和FNR
比它大1。超过$NF
的域都设置为空串。
默认情况下,文件内容的每行都是一个记录。每行内容都会被分割成一系列的域。
对$0
赋值会重新计算对应的域变量和NF
。对NF
或是域的赋值也会让$0
重组。
输入的数据分割出的各个域类型都为字符串。
数据类型及其转换与比较
AWK只有两种数据类型:数值和字符串。数值类型内部表示均为浮点数,所以true用1.0来表示。
字符串常量表示与C语言类似,可识别下列转义序列:
\\ \
\" "
\a alert, ascii 7
\b backspace, ascii 8
\t tab, ascii 9
\n newline, ascii 10
\v vertical tab, ascii 11\f formfeed, ascii 12
\r carriage return, ascii 13
\ddd 1, 2 or 3 octal digits for ascii ddd
\xhh 1 or 2 hex digits for ascii hh
其他转移序列只表示它的字面意义,如\c
表示 "\c"。
AWK的变量能够在两种类型间隐式的转换。若变量是一个字符串,则使用其最长数字前缀,通过atof
来转换到数值。若变量是一个数值,则通过sprintf(CONVFMT, expr)
转换为字符串。参数中CONVFMT是内建全局变量,默认为 "%.6g";expr为变量存储的数值。特别地,若expr是一个整数,则通过sprintf(%d, expr)
进行转换。需要注意,隐式转换不改变变量的本身的类型和值。
显式的类型转换可以通过: 1). 使用expr ""
从数值转换到字符串 2). expr+0
从字符串转换到数值来实现。
表达式中变量的类型转换是自动(隐式)发生的。通过以下两个表达式来理解转换的规则:
y = x + 2;
z = x "hello"
如果x是数值,则y依然是数值,而第二个表达式中x隐式转换到字符串,与"hello"连接。如果x是字符串,对于第一式则通过之前所说的规则将x转为数值与2相加。
形如expr1 relop expr2
的比较运算表达式。若两个操作数都是数值,则进行数值。若其中一个是字符串,则另一个操作数(如果需要)也转换为字符串进行比较。比较的结果是数值0或1.
在(expr)
形式中,当且仅当expr是非空串条件或数值为非0布尔表达式为真。
表达式和运算符
AWK表达式语法类似C。变量无需声明,第一次出现自动初始化为0(数值)和空串(字符串)。
常量,变量,域,数组和函数调用作为基本表达式。
各个运算符的优先顺序由低至高如下:
assignment = += -= *= /= %= ^=
conditional ? :
logical or ||
logical and &&
array membership in
matching ~ !~
relational < > <= >= == !=
concatenation (no explicit operator)可以使用空格来连接
add ops + -
mul ops * / %
unary + -
logical not !
exponentiation ^
inc and dec ++ -- (both post and pre)
field $
赋值,条件,指数运算是右结合的,其他都是左结合。括号拥有最高优先级。
正则表达式
awk中的正则表达式的“匹配(matches ~)”被定义为,指定的记录,域或串中含有能与正则表达式匹配的子串。比如
$0 ~ /r/ {}
表示当$0中含有能匹配正则表达式 r 的子串时就执行动作。特别地,如果带匹配的目标是$0,则可省略为:
/r/ {}
mawk使用POSIX ERE的语法,相当于grep
使用-E
选项时用的语法,具体规则就不在此赘述了。
AWK 数组
AWK提供一维关联数组(字典)。数组元素通过array[expr]
访问。expr会被转化为字符串,因此A[1]和A["1"]没有区别。初始时数组为空,第一次访问后相应元素就被创建。表达式expr in array
为真当array[expr]
存在,否则为假。
使用for (var in array) statement
对数组索引(字典关键字)进行枚举。但枚举顺序是未定义的。
delete array[expr]
能够删除元素array[expr]
。mawk提供了delete array
来清空字典的拓展。
多维数组使用内置变量SUBSEP
连接合成。array[expr1,expr2]
等价于array[expr1 SUBSEP expr2]
。多维数组元素的条件测试使用加括号的索引:
if ( (i, j) in A ) print A[i, j]
内置变量
以下内置变量在程序执行前就被初始化:
ARGC number of command line arguments.
ARGV array of command line arguments, 0..ARGC-1.
CONVFMT format for internal conversion of numbers to string, initially =
"%.6g".
ENVIRON array indexed by environment variables. An environment string,
var=value is stored as ENVIRON[var] = value.
FILENAME name of the current input file.
FNR current record number in FILENAME.
FS 正则表达式,用来将记录分割为域
NF 当前记录分割出的域的数量
NR current record number in the total input stream.
OFMT format for printing numbers; initially = "%.6g".
OFS inserted between fields on output, initially = " ".
ORS 输出每条记录时的串未终结符, 初始 = "\n".
RLENGTH length set by the last call to the built-in function, match().
RS 用来分割记录, 初始 = "\n".
RSTART index set by the last call to match().
SUBSEP used to build multiple array subscripts, initially = "\034".
内置函数
字符串函数
gsub(r,s,t) gsub(r,s) 全局替换,将目标 **t** 中符合 **r** 的每个匹配都用 **s** 来替换。返回替换发生次数。如果省略 **t** ,则使用`$0`作为目标。替换串中的 & 会被 **t** 中匹配的字串替换。因此使用`\&`和`\\`来分别表示字面值`&`和`\`。 index(s,t) 返回 **t** 在 **s** 中的位置,若无则返回0。字符串第一个字符位置为1。 length(s) 返回字符串或数组的长度。 match(s,r) 返回符合 **r** 的第一个最长匹配在 **s** 中的索引。返回1表示匹配在串首,返回0表明无匹配。同时,`RSTART`被设置为返回值,`RLENGTH`被设置为匹配的长度,无匹配则为-1。。。。。 split(s,A,r) split(s,A) 用正则表达式 **r** 分割字符串 **s** ,结果存入到数组 **A** 中,数组下表从"1"开始。返回域的数量。省略 **r** 则默认使用 `FS`分割。 sprintf(format,expr-list) 类似于C语言的sprintf,返回格式化后的字符串。 sub(r,s,t) sub(r,s) 单次替换,其他与gsub()相同。 substr(s,i,n) substr(s,i) 返回 **s** 从索引 **i** 开始,长度为 **n** 的子串。省略 **n** 则返回 **s** 从索引 **i** 开始的后缀。 tolower(s) toupper(s) 返回将 **s** 中字母替换为小写(大写)的结果字符串。原字符串未改动
时间函数
systime() 返回Unix时间戳(从1970-1-1 0:0:0 UTC到现在的秒数) mktime(spec) 从spec计算时间戳。spec是一个字符串,格式为"YYYY MM DD HH MM SS DST"。DST设置夏时制,默认为0. YYYY 年数 MM 月份 DD 天 HH 小时 MM 分 SS 秒 DST 夏时制 参考C语言的`mktime()`。 strftime(format[,timestamp[,utc]]) 将提供的时间戳转化到指定格式的日期。 参考C语言的`strftime()`的格式化设置。
数学运算函数
cos(x) exp(x) sin(x) log(x) sin(x) sqrt(x) int(x) 返回向0取整后的数值 rand() 返回0到1之间的一个随机数 srand() 使用时间设置随机数种子。
传递参数
有两种方式给awk传递参数:使用ARGV,或者命令行选项 '-v var=value'。
先来说说ARGV传递参数。首先,awk不区分文件名和参数(用户希望成为参数的),而是把传入的参数都当成文件名。就是说
awk -f prog.awk abcd ./txt
传入了两个参数:ARGV[1]=abcd和ARGV[2]=./txt。而awk会尝试把他们当成文件依次打开扫描,因此区分参数和文件名的任务就交给用户。要想让awk不将某个参数当成文件打开,只需要用delete删除参数,例如在BEGIN时delete ARGV[1]
后,程序就会跳过ARGV[1],直接扫描ARGV[2]。值得注意的是,删除ARGV[1]不会使ARGV[2]的内容移动到ARGV[1]中。
使用'-v'传递参数,灵活性不如上一种,但是能简化AWK脚本。如果希望在shell脚本中嵌入awk片段,可以用这种方式传递参数。例如
awk -v arg=blahblah -f prog.awk
就传入了名为'arg',值为'blahblah'的变量。
输入输出
在awk中可以以以下方式执行shell命令:
command | getline
: 执行command并取出一条记录到$0,会改变NF
。
command | getline var
:执行command并取出一条记录到ar。
一些示例
1). 对格式化的记录求和
假如文件foo
内有如下文本记录:
a:13
b:-4
c:77
可以这样对数字求和:
cat foo | awk 'BEGIN{sum=0;FS=":";} {sum+=$2;} END{print(sum):}' -
BEGIN块初始化了sum,将域分割符设置为:
。中间部分对每行记录作处理,$2就是分割后的数字串。END块打印结果到标准输出。
2). 输出dpkg.log中所有在"2020-10-10 0:0:0"之后的记录。
dpkg.log中的一条记录格式如下:
2020-11-01 19:00:23 startup packages remove
prog.awk
脚本如下:
BEGIN {
# 利用 date 程序提取unix时间戳(秒)
dls = mktime(ARGV[2]);
# 删除传入的日期参数,防止被当成文件
delete ARGV[2];
}
{
# 利用 date 提取时间戳
sprintf("date +%%s -d \"%s %s\"", $1, $2) | getline ts;
if (ts>dls) print;
}
命令行输入如下:
awk -f prog.awk -- dpkg.log "2020 10 10 0 0 0 0"
3). 移除重复的行
cat lines.txt | awk '!seen[$0]++'