[TOC] # Awk动作规则详解 ## 表达式 > 表达式就像是积木中形形色色的最小块儿一样,需要组装一个小车时我们会将这些小块儿积木按照一定规则逻辑组装在一起,我们先来看看表达式都包含哪些吧。 一个表达式是通过运算符将主表达式和其它表达式结合在一起的。 `主表达式`包括 常量、变量、数组、函数调用和内建函数或者内建变量。 1. 我们先从常量和变量开始了解。 2. 然后了解连接表达式的运算符, 运算符分为五类:算数运算、比较、逻辑、条件和赋值。 3. 接着是Awk的数组使用介绍,最后是awk内建的数值运算函数和字符串函数。 ### 常量 > 常量有两种:数值 和 字符串 两种类型. - 字符串常量使用双引号扩起来来定义,它可以包含转义符号,例如 "Hello world.\n". - 数值常量可以是整数、小数或者科学计数表达式,他们都是统一使用浮点格式存储的,所以不同表达形式的同一数值的比较是相等的。 ### 变量 > 变量就是通过一个变量名存储一个字符串或者数字,在执行过程中变量可以被修改未其它值,所以成为变量。 变量包含以下几种: - 用户自定义变量: 自定义变量名由字母、数字或下划线序列组成,但是不能以数字开头,变量名区分字母大小写,对于未初始化变量值可以是空字符串""或数值0。例如:0var 就是不合法变量名,而 name01 是合法的变量名。 - 内建变量: awk预定义变量,如FILENAME代表当前正在读的文件名,详细列表后面我们会列出。 - 字段变量: 即 $0,$1,$2...$NF, $0 表示正行数据,$1 代表通过分隔符FS将整行$0分割出的第一个字段,以此类推,$NF表示最后一个字段(NF表示分割的字段数量). #### 内建变量表 - ARGC : 命令行参数数量,默认"-" - ARGV : 命令行参数值数组,与ARGC对应,默认"-". - FILENAME: 当前读取的文件名,默认"-" - FNR : 正在读取的当前文件记录数,当记录分隔符为"\n"换行符时,FNR就是行号。 - FS : 控制输入字段分隔符,默认为空白符(制表符或空格) - NF : 当前记录的字段数量,默认"-" - NR : Awk命令到目前读取的记录总数,当读取多文件时 FNR 与 NR 就存在差别了。 - OFMT : 数字的输出格式,默认"%.6g" - OFS : 输出字段分隔符,默认为制表符"\t" - ORS : 输出记录分隔符,默认为回车符"\n" - RLENGTH : match函数匹配的字符串长度 - RS : 控制输入记录的分隔符,默认为回车符"\n" - RSTART : match函数匹配的字符串开始位置 - SUBSEP : 多维数组下表分隔符,默认为"\034" ### 运算符 ``` 赋值运算符: = , += , -=, *=,/=, %=, ^= 条件运算符: ?: 逻辑运算符: || , && , ! 匹配运算符: ~ , !~ 关系运算符: < <= == != > >= 连接运算符: (没有显示运算符,如 var1="123""xyz" , "123"与"xyz"的连接形成"123xyz"常量赋值给var1) 算数运算符: +,-,*,/,%,^ 增减运算符: ++,-- (前缀和后缀的返回值区别要记住) 一元字段符: $ 分组运算符: () 数组关系符: in ``` ### 内建数值运算函数 - atan2(y,x) : 返回(x,y)的反正切函数值,x,y不为0。 - cos(x) : 返回x的余弦函数 - exp(x) : 返回x的指数e^x - int(x) : 返回x的数值部分截断,如 x="123abc",int(x)返回数值123。 - log(x) : 返回x的自然对数 - rand() : 返回随机数r, 0<= r < 1 - sin(x) : 返回x的正弦函数 - sqrt(x) : 返回x的平方跟 - srand(x) : 设置x 为rand()函数新种子,返回值为前一个种子,默认种子为当前时间戳 随机数生成示例 : ``` ## $RANDOM 为Shell生成[0,32767]范围的伪随机数变量 awk -v seed=$RANDOM 'BEGIN{ srand(seed); for(i=1;i<=10;i++) printf("%d%s", rand()*100, i == 10 ? "\n":",") }' ``` ### 字符串作为正则表达式 > 到目前为止,我们看到的正则表达式都是通过双斜线包含起来的(例如:/\^[0-9]+$/),但实际上任何表达式都可以作为正则表达式匹配。 Awk先计算表达式,有必要时会将值转换成字符串,然后翻译字符串为正则表达式,例如: ``` ## 打印第二列为数字的行数据 BEGIN{ digits = "^[0-9]+$" } $2 ~ digits ``` **既然表达式可以组合,那么正则表达式同样也可以由组件组成** ``` ## 匹配校验有效的浮点数 BEGIN{ sign = "[+-]?" decimal = "[0-9]+[.]?[0-9]*" fraction = "[.][0-9]+" exponent = "([eE]" sign "[0-9]+)?" number = "^" sign "(" decimal "|" fraction ")" exponent "$" } $0 ~ number ``` 组合用法看起来很方便,但是有时候小细节还是要注意的,比如 特殊字符转义问题。 ``` sign1 = "[+-]?" sign2 = "(\\+|-)?" ``` sign1 和 sign2 现在表达相同含义,但是 sign2 要注意使用两个反斜线对加号+进行转移,保证这个加号的语义不被理解为正则匹配1次或多次的含义。 解析: awk 转换字符串 sign2 为正则表达式 `"(\\+|-)?" ==> /(\+|-)?/` ***小技巧***: 如果我们想要检测我们写的正则表达式是否可以匹配某字符串,可以这样写: `awk '$1~$2'` ### 内建字符串处理函数 - gsub(r,s) : 对$0进行全局替换r为s,返回替换次数 - gsub(r,s,t) : 对t进行全局替换r为s,返回替换次数 - index(s,t) : 返回t子串在s字符串的位置,0表示没找到 - length(s) : 返回s字符串长度 - match(s,r) : 测试s字符串是否包含r子串,返回索引值并设置RSTART和RLENGTH,0表示不包含。 - split(s,a) : 将s字符串以FS分隔符拆分到数组a中,返回数组元素数量 - split(s,a,fs) : 将s字符串以指定fs分隔符拆分到数组a中,返回数组元素数量 - sprintf(fmt,expr-list) : 返回按照fmt格式化后的字符串,用法与printf相同(差异是不输出结果而是返回结果) - sub(r,s) : 对$0进行一次替换r为s,返回替换次数,0表示没有匹配 - sub(r,s,t) : 对t进行一次替换r为s,返回替换次数,0表示没有匹配 - substr(s,p) : 字符串截取,从p位置开始截取s字符串到结束位置。 - substr(s,p,n) : 字符串截取,从p位置开始截取s字符串长度为n部分字串。 sprintf 格式化示例: ``` x = sprintf("%10s %6d",$1,$2) ``` ### **如何判断一个变量类型是数值还是字符串?** 答案是,看你如何处理这个变量,如果进行数值运算那么变量就会强制转换成数字,如果进行字符串处理就会转换为字符串,例如: ``` BEGIN{ pop = "12abc" pop += 3 res = 1 2 print pop print res } 输出结果: 15 12 ``` 但是,有些运算符适用于数值和字符串,那就需要特殊规则了: 1. 赋值运算 = , v = e , 变量v的类型将依赖于e变量类型。 2. 比较运算 == , x == y, 如果 x 和 y 都是数值类型,那么就按照数值比较,否则就要按照字符串比较。 如果需要强制按照特定类型进行比较也比较容易,例如: - 按照数值进行比较: $1 + 0 == $2 + 0 - 按照字符串进行比较: $1 "" == $2 ***动手尝试一下,下面这个示例会输出什么呢?*** ``` $1 < 0 { print "abs($1) = " -$1 } 输出结果: ``` ## 控制语句 控制语句包含: - { 声明语句块 } - if( 表达式 ) 声明语句 - if( 表达式 ) 声明语句 else 声明语句 - while(表达式) 声明语句 - for( 表达式; 表达式 ; 表达式 ) 声明语句 - for(变量 in 数组) 声明语句 - do 声明语句 while(表达式) - break - continue - next - exit - exit 表达式 ### if else 条件判断 语法格式: ``` if(expr1) statement1 else statement2 ``` 如果 expr1 为真(非0或者非空) 则执行`statement1`语句,否则执行`statement2`。 为了消除歧义,Awk规定 `else`永远都匹配最近未关联过`else`的 `if` 语句,例如下面: ``` if($1==1) if($2==1) s=1; else s=2; print s; ``` ### while 循环 语法格式: ``` while(expr) statement ``` 换行符是可选的,循环之行时先判断`expr`结果是否为真true,如果为真则执行`statement`语句块,否则结束循环,继续向下执行其它语句。 下面是一段将每列都逐行输出的示例: ``` {i=1;while( i <= NF) print $(i++)} ``` ### for 循环 语法格式: ``` for( exp1;expr2;expr3) statement expr1; while(expr2){ statement expr3 } ``` 换行符是可选的,for语法格式等同于其下面的while语句,for的三个表达式都是可选的, 如果`expr2`表达式没有的话表示永远为真true(称之为死循环)。 另外一种 `for`循环格式是遍历数组的方法: ``` for( var in array) statement ``` ### do while 循环 语法格式: ``` do statement while(expr) ``` do和statement后的换行符是可选的,当statement与while之间没回车时,statement最后必须以分号(;)结束。与while循环区别是`do while`循环的 while 在最后进行判断,也就是会先之行一次 statement后再进行判断`expr`是否为真。 ### 改变循环逻辑 - break : 终止循环 - continue : 中止本次循环,继续下一次的循环 ###next和exit - next : 中止本次awk继续向下数据处理,读取下一条记录重新开始进行模式匹配和处理。 - exit expr : 终止awk命令的运行,可以携带一个返回值`expr`,默认返回0。 ### 空语句- 分号(;) 一个分号(;)就可以作为空语句执行,例如下面语句: ``` ### 判断数据里是否包含空值 BEGIN{ FS = "\t" } { for(i=1; i<=NF && $i != ""; i++) ; if( i <= NF) print } ``` ### 数组的使用 > Awk 提供了一维数组存储字符串或者数字,数组不需要提前声明也不需要定义数组大小。数组在没有设置值时的默认值为0或者""。 Awk数组的元素索引与C语言最大不同是可以是数值也可以是字符串,我们可以理解 数组为哈希数组,数组索引为key,存储内容为value。 倒序输出文件内容: ``` { x[NR] = $0 } END{for(i=NR; i>0;i--) print x[i] } ``` #### 遍历数组 对于数组的遍历也有专门的for循环,语法格式: ``` for( var in array) statement ``` for循环执行过程: 每次循环都从array数组中获取一个不同的数组下标,然后执行statement,直到所有数组元素都遍历完结束循环。如果循环过程中增加了数组元素,那么执行结果是不确定的,数组下标遍历顺序依赖于具体实现规则,通常是无序的。 #### 检测数组是否包含一个数组下标方法 如果想要检测某个数组下标是否存在于数组中,可以使用下面语句: ``` var in array ``` 如果 array[var]已经存在了,则返回1,否则返回0。 ``` if( "Asia" in pop ) print ``` #### 删除数组元素 ``` delete array[index] ``` #### 数组下标区分数字和字符串么? 答案是不区分,字符串做为数组的统一下标类型,也就是 数字1 和 字符串"1"作为下标是一样的,而 01 与 1 下标是不一样的。 #### 如何实现多维数组? > 虽然awk没有支持多维数组,但是我们可以通过一维数组来模拟支持多维数组。 我们以一个10x10的数组的初始化为例 ``` for(i = 1; i <= 10; i++) for(j = 1; j <= 10; j++) arr[i, j] = 0 ## 判断某下标是否存在于arr数组中方法 if( (i,j) in arr) ... ## 遍历数组方法 for( k in arr) ... ``` 实际上我们构建的是一个大小为100的一维数组,下标为 "1SUBSEP1","1SUBSEP2",以此类推。其中SUBSEP默认为"\034"。 ### 用户自定义函数 > 除了Awk内建函数外,用户也可以自己实现需要的函数 自定义函数的语法格式: ``` function name(parameter-list) { statements } ``` 函数的返回值是通过函数最后的`return expr`语句返回 expr 值,如果没有`return`那么返回值为空。 函数内部变量的生命周期是值得我们关注的,内部定义的变量默认是`全局变量`(直到Awk程序执行结束释放), 参数列表变量的生命周期为`局部变量`(调用完成后释放)。 下面是一个体现了局部变量和全局变量差别的例子: ``` $ awk '{ print a,b,c,f($0) }function f(a,b,c){ b = a+1; c = b+1; return c }' 输出结果: 1 3 2 4 $ awk '{ print a,b,c,f($0) }function f(a){ b = a+1; c = b+1; return c }' 输出结果: 1 3 2 2 3 4 ``` 可以看到,输入分别为1和2。结果第二个awk命令的变量b,c就有了值,而第一个awk命令因为b,c是局部变量而输出为空。 注意:自定义函数内部如果需要使用`局部变量`时,最好的办法就是在参数列表的末尾定义。 自定义函数的位置可以是任意`pattern-action`语句出现的位置。 所以,其实一个Awk程序就是一系列的`pattern-action`语句块和函数定义的组合。 ### 输出 > 输出信息的两个函数为 `print` 和 `printf` ,其中 print 用于简单输出, 而 `printf` 用于对输出内容的格式有特定要求的输出。 > 输出信息默认输出到标准输出,当然也可以被重定向到文件中,也可以对接管道进行特殊处理。 ``` print : 默认输出 $0 到标准输出 print 表达式1,表达式2,... : 输出所有表达式结果, 每字段以 OFS 分隔, 以 ORS 结束行 print 表达式1,表达式2,... > filename : 覆盖方式输出内容到 filename 文件中(注意不调用close),而不是默认的标准输出 print 表达式1,表达式2,... >> filename : 追加输出内容到 filename 文件中,不是覆盖输出 print 表达式1,表达式2,... | command : 输出内容 注意: print命令覆盖输出文件是,默认情况 filename 只会在运行awk命令时打开一次,如果调用close关闭过文件,那么再次打开时会覆盖掉之前的内容 ``` 输出分隔符的作用: `OFS` : `output field separator` 输出字段域分隔符,例如`print $1,$2,$3`,每个逗号分隔之间的字段都会用`OFS`替换。 `ORS` : `output record separator` 输出记录分隔符,例如`print $1,$2,$3`,最后一个`$3`后用`ORS`分隔符追加结束。 1. print 如果想要为每个字段域修改分隔符该如何写呢? ``` awk 'BEGIN{OFS=":"; ORS="\n" } { print }' awk 'BEGIN{OFS=":"; ORS="\n" } { $1 = $1 ; print }' ``` 2. `printf` 的格式化用法 printf的用法与C语言非常相似(除了*用法不支持),格式串参数是必须的,它用来表示输出内容格式,包括文本和格式表达式,每个格式表达式以%开始,以一个用来表示转换格式的字符结束,同时中间可以包含以下三个修饰符: - "-" : 表示字段内容进行左对齐, 例如 %-10s - width : 填充字段宽度,0开头则表示以0进行左填充,例如 %5d - ".prec" : 表示字符串最大宽度,或者表示数字小数部分, 例如 %10.5f。 格式控制符号列表: - c : ASCII字符 - d : 十进制数字 - e : 指数表达形式 [-]d.dddddddE[+-]dd - f : 浮点数 [-]ddd.dddddd - g : 等同 e or f ,以更短形式表达,减少无效的零。 - o : 无符号八进制格式 - s : 字符串 - x : 无符号十六进制格式 - % : 输出一个% 下面是一个示例: ``` $ echo "张三 165 65\n李四 190 100\n王五 178 80" | awk '{ printf("姓名:%-6s 身高:%6.02f 厘米 体重:%6.02f 公斤\n", $1,$2,$3)}' 姓名:张三 身高:165.00 厘米 体重: 65.00 公斤 姓名:李四 身高:190.00 厘米 体重:100.00 公斤 姓名:王五 身高:178.00 厘米 体重: 80.00 公斤 ``` 3. 输出内容重定向到标准错误方法? ``` print "hello" | "cat 1>&2" system("echo hello 1>&2") print "hello" > "/dev/stderr" ``` 3. 关闭文件和管道 ``` awk '{ print $0 >> "test.txt" }END{ close( "test.txt") }' awk '{ print $0 | "sort -nrk1" }END{ close( "sort -nrk1") }' ``` 为什么要关闭文件呢? 1. Linux系统对于单个程序打开文件数量有限制 `ulimit -n` . 2. 关闭文件可以将缓冲区数据写入文件中,防止数据异常丢失. ### 输入 `awk`程序的输入方式有如下方式: 1. 通过文件输入 : `awk 'program' datafile` 2. 通过管道方式输入: `cat datafile | awk 'program'` #### 输入分隔符`FS` > 默认内建输入分隔符变量FS取值为“ ”(空白符,可以是空格 和/或 Tab符,默认分隔符可以理解为`[ \t]+`),输入的每行数据都会按照`FS`符分隔出每个字段域(field1 ... fieldNF) , 当然我们也可以根据自己的需要来自定义`FS`分隔符的取值。 下面是设定分隔符自定义方法: 1. 通过`FS`变量修改: `BEGIN{ FS = ",[ \t]*|[ \t]+"}` 2. 通过`-F`变量修改: `awk -F',[ \t]*|[ \t]+' 'program'` #### 多行记录数据的使用 > 默认情况下,记录是以换行符分隔的,当然我们可以通过`RS`变量自定义记录分隔符,这样我们有了更多的可能。 将每个记录都转化为单行数据: `awk 'BEGIN{RS="[ \t]+"; FS="\n" }{print $1, NF}' data.txt` #### getline输入读取函数 `getline` 用于读取输入数据记录,这些数据可以来自当前输入,或者来自一个文件或者管道。 getline 的使用方法: 表达式 : 影响变量 `getline` : $0,NF,NR,FNR `getline var`: var,NR,FNR `getline <file `: $0,NF `getline var < file ` : var `cmd | getline ` : $0,NF `cmd | getline var` : var `getline` 返回值: 1. `0` : 文件读取完毕。 2. 正数 : 返回实际读取到数据的字节数。 3. `-1` : 读取文件失败,可能是文件不存在或者不可读取状态等。 注意: `getline` 函数读取文件时要注意文件是否存在,需要考虑到不存在的场景,否则你的程序会陷入无限的循环当中了。 举例: ``` while ( getline < "file") ... ## 不安全 while ( getline < "file" > 0 )... ## 安全 ``` ### Awk命令行变量赋值 > Awk命令行形式有如下几种: ``` awk 'program' f1 f2 awk -f progfile f1 f2 awk -Fsep 'program' f1 f2 awk -Fsep -f progfile f1 f2 ``` 这些命令行中的`f1`和`f2`都是命令行参数,并且默认代表着文件名。如果一个参数的格式为"var=text",这个参数将代表一个变量赋值操作,而不是代表文件名了。这种类型的赋值可以在一个文件读取之前也可以在之后,例如: ``` $ awk 'FNR<=2{ print v1 ":" v2 ":" FILENAME ":" $0 }' v1="Var1" ls.awk v2="Var2" print.awk 输出结果: Var1::ls.awk:#!/bin/awk -f Var1::ls.awk: Var1:Var2:print.awk:#!/bin/awk -f Var1:Var2:print.awk:### 模拟输出进度条更新 ## ``` ### 命令行参数 > 命令行参数在Awk命令中是通过内建变量ARGC和ARGV进行存储的,其中ARGC存储的是参数数量,第一个参数是"awk"命令名,存放在`ARGV[0]`变量中,之后一次递增直到ARGC个参数。 以下面命令为例说明下参数列表: ``` awk -F'\t' '$3 > 100' countries ``` ARGC为2,ARGV[1] 的值为 "countries"。 下面是一个输出参数列表的示例: ``` BEGIN{ for( i = 1; i < ARGC ; i++) printf("%s%s",ARGV[i], i == ARGC -1 ? "\n" : " ") } ``` 再来看一个输出递增数值序列的示例: ``` $ cat seq.awk #!/usr/bin/awk -f ## 功能: 输出数值数列 ## 参数: q , p q , p q r ; 其中 q >= p ; r > 0 ## 输出: 输出数值 1 - q , p - q , 或者 按照 r 从 p 递增 到 q function usage(f){ printf("Usage: %s q\n", f); printf(" %s p q\n", f); printf(" %s p q r\n", f); printf("其中 q >= p , r > 0\n", f); } BEGIN{ if( ARGC == 2) {s = 1 ; e = ARGV[1] ; step = 1} else if( ARGC == 3 ){s = ARGV[1] ; e = ARGV[2] ; step = 1} else if( ARGC == 4 ){s = ARGV[1] ; e = ARGV[2] ; step = ARGV[3] } else { usage(ARGV[0]) exit 1 } for( i = s; i<= e ; i += step ) printf("%d\n", i) } 执行方法: $ awk -f seq.awk 1 10 2 输出结果: 1 3 5 7 9 ``` `ARGV`变量值是可以被修改和增加的,如果我们将某个变量赋值为空"",表示这个参数将不会被当作文件进行解析处理,当Awk命令行参数值为 "-" 时,代表从标准输入进行数据读取。 ## 与其他程序交互 > 本章节我们会了解到Awk程序与其他命令交互的几种方式。 ### 通过函数 system > `system(cmd)`函数是Awk命令内建命令,可以用来在Awk命令中执行操作系统中的命令,例如 cat/grep/sed/zcat等等,然后返回执行结果状态。 以下是一个执行`cat`命令的示例: ``` $1 == "#include" {gsub(/"/," ",$2); system("cat " $2) ; next} {print} ``` 执行结果: ``` $ cat a.h //filename:a.h int abc = 1024; int f( int v1, int v2) { return v1 + v2; } $ cat a.cpp //filename:a.cpp #include<iostream> #include "a.h" using namespace std; int main(int argc,char argv[][]) { cout << "Hello World!"<<endl; return 0; } $ awk '$1 == "#include" {gsub(/"/," ",$2); system("cat " $2) ; next} { print }' a.cpp //filename:a.cpp #include<iostream> //filename:a.h int abc = 1024; int f( int v1, int v2) { return v1 + v2; } using namespace std; int main(int argc,char argv[][]) { cout << "Hello World!"<<endl; return 0; } ``` ### 与Shell结合实现命令脚本 > 我们可以将Awk命令放在Shell脚本中执行完成指定功能。 接下来,我们来看一下如何编写一个命令脚本: ``` $ cat field1.sh awk '{ print $1}' $@ ``` 这是一个实现功能为输出第一列数据的命令脚本,只会输出文件中第一列数据。 执行方法: ``` ## 增加可执行权限 $ chmod +x field1.sh ## 执行命令 $ ./field.sh file1 file2 ``` ## 总结 > 本章内容比较详细的介绍了Awk的各种使用细节。所以可以在学习过程中详细阅读并实践练习,只有多实践才能更加深刻的理解Awk的使用方法。 我更加希望大家学习完成之后,可以多多的独立编写一些自己的脚本程序,脚本不用很大,通过编写来实现自己需要的功能,这样学习完成后才会记得更加深刻。 --- END