[TOC]
简介
我们常在命令行中使用awk
命令提取转换文件文本内容,可以说,awk
是命令行中文本处理的瑞士军刀,其功能十分强大,几乎其他文本处理命令能做的,awk
都可以做。
awk
之所以能具备如此强大的文本处理能力,其原因在于awk
不仅仅只是一个命令行程序,它本质上是一门图灵完备的动态类型的领域特定编程语言,专门用于操作文本内容。
可以这样认为,系统中存在的awk
命令本质上是一个解释器,它可以对 AWK 编程语言源码进行解释执行。
本篇博文介绍下 AWK 语言相关内容,涵盖语言绝大多数特性。
熟读本篇博文,基本上日常生活遇到的与文本处理相关的操作,都能熟练使用awk
进行处理。
安装
默认情况下,类 Unix 系统已内置awk
命令,但版本可能比较旧,可以从 Awk 源码 下载最新版本。
当前的最新版本为 gawk-5.1.0,其源码安装方式如下所示:
注:gawk
表示GNU awk
。
# 卸载 awk
$ sudo apt remove --purge gawk
# 下载最新版本 awk
$ sudo wget -c 'http://git.savannah.gnu.org/cgit/gawk.git/snapshot/gawk-5.1.0.tar.gz'
$ sudo tar xvf gawk-5.1.0.tar.gz
# 生成 Makefile
$ sudo ./configure
# 编译,本地生成 gawk
$ sudo make
# 安装
$ sudo make install
注:安装完成后,需要重启下终端,让系统定位到新安装的 Awk,而不是卸载的 Awk。
最后可通过以下命令查看awk
版本:
$ awk --version | head -n 1
GNU Awk 5.1.0, API: 3.0
执行模型
awk
命令的格式如下所示:
awk [options] 'pattern { action }' [file]
注:除了从文件中读取文本外,awk
也支持直接从标准输入流中读取文本。
其中,pattern { action }
该部分就是 AWK 语言进行编写的脚本源码,其结构如下:
pattern1 { action; }
pattern2 { action; }
...
patternn { action; }
即可以有一个或多个pattern { action }
块,当进行文本处理时,首先会将第一行文本匹配pattern1
,如果匹配成功,则执行pattern1
的action
,然后继续将第一行文本匹配pattern2
,如果不匹配,则跳过pattern2
,继续匹配pattern3
,依此类推,直至最后一个匹配完成,然后就可以进行第二行匹配,重复上述逻辑,直至全部文本匹配完成,这整个过程就是awk
的执行模型。
对于pattern { action }
块,其中:
-
action
:表示要执行的语句或表达式。 -
pattern
:表示匹配模式,其有如下可选值:-
BEGIN { actions }
:预处理块,在读取文件内容前触发执行。$ echo -n '111' | awk 'BEGIN { print("Preprocessing") } { print($0) }' Preprocessing 111
注:
BEGIN
必须大写。 -
END { actions }
:后置处理块,在文本文件读取完成后,进行触发执行。$ echo -n '111' | awk '{ print($0) } END { print("Postprocessing") }' 111 Postprocessing
注:
END
必须大写。 -
expression
:一个条件表达式,用于过滤满足条件的行。$ echo -e '1\n20\n300' | awk '$1 > 100 { print $0 }' 300
该种模式的另一种常用过滤匹配是使用正则表达式,正则表达式由一对反斜杠
/
包裹起来:$ echo -n '1\n20\n300' | awk '/[0-9]{3}/ { print($0) }' 300
-
expression, expression
:多个条件表达式,用于过滤满足条件的行。# 打印第 2 到 3 行内容 $ echo -e '1\n20\n300' | awk 'NR == 2, NR == 3 { print $0 }' 20 300
-
注:pattern
或action
任意一个可忽略不写,但不能同时进行忽略,其中:
- 如果
action
忽略不写,则隐式执行print
命令。 - 如果
pattern
忽略不写,则表示匹配所有内容。 -
BEGIN
或END
块如果出现,则必须指定其action
。
命令行选项
awk
是一个命令行工具,他本身支持一些选项options
,这里主要介绍三个参数选项:
-
-f progfile, --file=progfile
:表达读取执行 AWK 源码文件。$ echo -E '{ printf("content line: %s\n",$0) }' > 1.awk $ cat 1.awk { printf("content line: %s\n",$0) } $ echo -e '111\n222' | awk -f 1.awk content line: 111 content line: 222
-
-F fs, --field-separator=fs
:自定义字段分隔符。
注:默认情况下,对于一行文本,awk
默认以空格或制表符进行字段分隔,但是可通过-F
命令自定义字段分隔符。$ echo -e '111,222' | awk -F ',' '{ print $1 }' 111
-
-v var=value
:从外部设置值给变量var
。$ awk -v myVar='hello world' 'BEGIN { print myVar }' hello world
数据类型
AWK 语言提供了如下几种数据类型:
-
基本数据类型:AWK 提供了两种基本数据类型:
string
和number
,其中:string
:字符串类型,字符串常量用双引号包裹(比如"hello"
)。
注:字符串类型太长时,可使用\
进行拼接。number
:数值类型,数值类型可以是负数(比如-2
),也可以是小数(比如-1.08
),也可以是科学计数法(比如-1.1e4
或.28E-3
),所有数值类型底层都使用浮点数进行计算。
注:AWK 未显示提供布尔值类型,其布尔值类型使用整型数值进行表示,其中,0
表示假,1.0
表示真,即与 C 语言一样,非0
即为真。
AWK 语言并不严格区分数值类型和字符串类型,两者之间可直接进行运算,AWK 会自动依据上下文自动进行类型转换。
-
array
:AWK 提供了数组类型,其格式为array[expr]
,expr
会自动转换为字符串类型,因此,A[1]
和A["1"]
都表示获取索引为"1"
的元素。数组类型的相关操作如下所示:-
增:因为 AWK 是动态类型语言,所以其变量可直接定义:
$ echo | awk '{ myArray[0] = 100; print myArray[0] }' 100 $ echo | awk '{ myArray[x] = "hello world"; print myArray[x] }' hello world
-
查:依据索引直接获取即可,也可以使用
for in
语句:# 查询 $ echo | awk '{ myArray[x] = "hi"; } END { if (x in myArray) print myArray[x] }' hi # 遍历 $ echo | awk 'BEGIN { myArray[0] = "zero"; myArray[x] = "hello world" } END { for (idx in myArray) { print myArray[idx] } }' hello world zero
-
删:使用
delete
关键字可删除某个元素,也可删除整个数组:# 删除一个元素 echo | awk 'BEGIN { myArray[0] = "hello"; myArray[1] = "world"; } \ { for (idx in myArray) { printf("%d: %s\n", idx, myArray[idx]) } } \ END { delete myArray[1]; printf("%s\n", myArray[1] ? "exists" : "deleted") }' 0: hello 1: world deleted # 删除数组,相当于删除数组所有内容 echo | awk 'BEGIN { myArray[0] = "hello"; myArray[1] = "world"; } \ { for (idx in myArray) { printf("%d: %s\n", idx, myArray[idx]) } } \ END { delete myArray; printf("%s, %s", myArray[0], myArray[1]) }' 0: hello 1: world ,
从以上操作其实可以看出,与其说
array
是数组类型,它的使用形式其实更像是字典类型。array
也支持二维数组类型,其形式如array[x,y]
,大致使用过程如下:if ( (i,j) in A ) print A[i,j]
-
变量
AWK 是动态类型语言,因此其变量可直接定义,无需声明:
$ echo | awk '{ var = "hello world"; print(var) }'
hello world
注:当引用未定义的变量时,其值为0
或""
。
AWK 还内置了一些变量,可以方便我们获取当前行字符串相关信息,具体内置变量如下表所示:
内置变量 | 含义 |
---|---|
ARGC |
命令行参数个数 |
ARGV |
命令行参数数组 |
CONVFMT |
数字转字符串的内部转换格式,默认为%.6g
|
ENVIRON |
环境变量数组 |
FILENAME |
当前操作的文件名 |
FNR |
当前遍历的文件的行记录(即行号) |
OFMT |
数值类型打印格式,默认为%.6g
|
OFS |
字段输出分隔符,默认为" "
|
ORS |
每条输出记录的终止符,默认为\n
|
RLENGTH |
上一次调用match() 时设置的长度 |
RSTART |
上一次调用match() 时的索引 |
RS |
输入记录(即行)的分隔符,默认为\n
|
SUBSEP |
用以构建多维数组子脚本,默认值为\034
|
FS |
自定义分隔符(支持正则表达) |
NR |
当前输入流的记录数量,对应当前遍历的行号 |
NF |
当前行的字段数量 |
$0 |
当前行的内容 |
$1 |
当前行的第一个字段内容 |
$2 |
当前行的第二个字段内容 |
$n |
当前行的第 n 个字段内容 |
注:FNR
表示文件记录数量,每遍历一个新文件,该数值从0
开始计起。而NR
表示所有输入流的记录总数量,当输入多个文件时,会进行叠加,最终结果就是这些文件的所有记录数,即所有文件的总行数。
注:NR
大意为number of rows
,表示当前遍历的行号;NF
大意为number of fields
,表示当前行的字段数量。
下面是一个使用内置变量的大概结构:
BEGIN { # 用户可以修改
FS = ","; # 内容分割符
RS = "\n"; # 行(记录)分割符
OFS = " "; # 输出内容分割符
ORS = "\n"; # 输出行(记录)分割符
}
{ # 用户无法修改
NF # 当前行字段(列)数量
NR # 当前行的行数
ARGV / ARGC # 脚本参数
}
遇到具体问题时,套用上面的结构进行适当修改即可。
举个例子:模拟命令行工具wc -w
,计算文件总字数:
echo -e 'line1 2 3\nline2 5' | awk 'BEGIN { count = 0 } \
/[a-zA-Z0-9]+/ { \
printf("line%d: content = %s, words=%d\n", NR, $0, NF); \
count += NF; \
} \
END { print("total words:",count) }'
line1: content = line1 2 3, words=3
line2: content = line2 5, words=2
total words: 5
运算符/操作符
AWK 语言支持以下运算符:
算术运算符(Arithmetic Operators)
AWK 语言支持的算法运算符如下表所示:
Operator | Description |
---|---|
+ |
加法运算 |
- |
减法运算 |
* |
乘法运算 |
/ |
除法运算 |
% |
取余 |
^ |
幂运算 |
赋值运算符(Assignment Operators)
AWK 语言支持的赋值运算符如下表所示:
Operator | Description |
---|---|
= |
等于(赋值) |
+= |
加等 |
-= |
减等 |
*= |
乘等 |
/= |
除等 |
%= |
余等 |
^= |
幂等 |
关系/比较运算符(Relational (Comparison) Operators)
AWK 语言支持的关系/比较运算符如下表所示:
Operator | Description |
---|---|
< |
小于 |
> |
大于 |
<= |
小于或等于 |
>= |
大于或等于 |
== |
等于 |
!= |
不等于 |
注:关系运算中,==
对不同类型的变量进行比较,对隐式自动进行类型转换,这与 JavaScript 语言的==
操作符效果一样:
$ echo | awk '{ a = 10; b = "10"; print( a==b ? "true" : "false" ) }'
true
逻辑运算符(Logical Operators)
AWK 语言支持的逻辑运算符如下表所示:
Operator | Description |
---|---|
|| |
或运算 |
&& |
与运算 |
! |
非运算 |
一元运算符(Unary Operators)
如下表所示:
Operator | Description |
---|---|
+ |
正号 |
- |
负号(正负数转换) |
++ |
自增 |
-- |
自减 |
注:AWK 语言的自增、自减操作均支持前置/后置运算。
三目运算符
三目运算符操作如下所示:
$ echo | awk '{ ret = 10 > 2 ? "true" : "false"; print(ret) }'
true
流程控制
AWK 语言流程控制语句主要有如下三类:
条件判断
条件判断语句使用if
关键字,其格式如下例子所示:
# 格式一:if()
$ awk 'BEGIN { \
if ( 10 > 1 ) { # 单条语句则括号可省略
print ("true")
}
}'
true
# 格式二:if else
$ awk 'BEGIN { \
if ( 1 > 10 ) {
print("false")
} else {
print ("true")
}
}'
true
循环控制语句
AWK 语言主要提供两种类型循环控制语句:
-
while
语句:其使用如下例子所示:# 格式一:while() awk 'BEGIN { \ count = 1 while ( count <= 3 ) { print count++ } }' 1 2 3 # 格式二:do while awk 'BEGIN { \ count = 1 do { print count }while( ++count <= 3 ) }' 1 2 3
-
for
语句:其使用如下例子所示:# 格式一:for () $ awk 'BEGIN { for (i = 1; i <= 3; ++i) print i }' 1 2 3 # 格式二:for in $ echo | awk 'BEGIN { for (i = 1; i <=3; ++i) myArray[i] = i } END { for (idx in myArray) { print myArray[idx] } }' 1 2 3
中断控制语句
AWK 语言主要提供的中断操作有:
-
continue
:表示继续执行循环。 -
break
:表示退出循环。 -
return
:表示退出函数,并且返回一个结果。 -
exit
:表示退出程序。
举个例子:
seq 1 5 | awk -v seed=$RANDOM 'BEGIN { srand(see) }
{
for ( i = 0; i < 10; ++i ) {
# random number
num = sprintf("%d",rand() * 10)
print("num=",num)
if ( num % 2 ) {
# skip odd
continue
}
if (num >= 8) {
print("break-------")
break
}
if (num == 6 ) {
print("binggo!!!!!!!!")
exit(0)
}
}
}'
函数
AWK 语言支持函数定义与调用,如下例子所示:
这里创建一个文件test.awk
,编写如下代码:
# 函数定义
function myfunc(n) {
for (i = 1; i <= n; ++i){
print $i;
}
}
# 主体执行块
{
# 调用函数
myfunc(NF)
}
命令行允许该源码文件:
$ echo -e 'field1 field2' | awk -f test.awk
field1
field2
其实如果需要使用到自定义函数,那直接使用 Python 等脚本语言其实更方便,一般使用awk
命令都不会使用到自定义函数,但是 AWK 语言也提供了一些内置函数,可以方便我们使用,这确实非常不错的。
AWK 提供的内置函数有很多,具体内容可查看:Built-in Functions。
以下就简单介绍几个本人较常用的内置函数:
输入输出
-
print:打印到标准输出。
# 括号可加可不加 $ awk 'BEGIN { print "hello"; print("world") }' hello world
-
printf
:进行格式化输出:$ awk 'BEGIN { printf("%s",123) }' 123
-
system(command)
:执行外部命令。$ awk 'BEGIN { cmd = "ls ~"; system(cmd) }'
注:
awk
结合system()
函数简直完美。
字符串函数
length(s)
:返回字符串长度。-
split(s,A,r)
:以正则表达式r
作为分隔符,切割字符串s
,存储进数组A
中,该函数执行则返回切割字符串个数:$ echo -e 'one,two,three' | awk '{ split($0,myArray,",") } END { for (idx in myArray) { print myArray[idx] }}' one two three
-
sprintf(format,expr-list)
:格式化字符串,其返回值为格式化完成的字符串:$ awk 'BEGIN { str = sprintf("%s",123); print str }' 123
-
substr(s,i,n), substr(s,i)
:获取字符串片段。i
为其实索引,n
为字符串片段长度,忽略则默认取到字符串s
末尾:$ awk 'BEGIN { print substr("0123456",1,1) }' 0 $ awk 'BEGIN { print substr("0123456",1) }' 0123456
注:从上述执行结果可以看到,字符串索引是从
1
开始计数。 tolower(s)
:字符串转小写。toupper(s)
:字符串转大写。
数值操作函数
-
int(x)
:字符串转整型:$ awk 'BEGIN { print int("10") }' 10 $ awk 'BEGIN { print typeof(int("10")) }' number
-
rand()
:生成0
到1
之间的随机数。
注:通常需要结合srand(expr)
设置一个随机数种子,保证生成真正的随机数:# 内部 srand() 直接以系统时钟作为随机数种子 $ awk 'BEGIN { srand(); print rand() }' # 外部传入随机数作为种子 $ seq 1 5 | awk -v seed=$RANDOM 'BEGIN { srand(seed); print rand() }'