Awk 命令详解

[TOC]

Awk 脑图

简介

我们常在命令行中使用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,如果匹配成功,则执行pattern1action,然后继续将第一行文本匹配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
      

patternaction任意一个可忽略不写,但不能同时进行忽略,其中:

  • 如果action忽略不写,则隐式执行print命令。
  • 如果pattern忽略不写,则表示匹配所有内容。
  • BEGINEND块如果出现,则必须指定其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 提供了两种基本数据类型:stringnumber,其中:

    • 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():生成01之间的随机数。
    :通常需要结合srand(expr)设置一个随机数种子,保证生成真正的随机数:

    # 内部 srand() 直接以系统时钟作为随机数种子
    $ awk 'BEGIN { srand(); print rand() }'
    
    # 外部传入随机数作为种子
    $ seq 1 5 | awk -v seed=$RANDOM 'BEGIN { srand(seed); print rand() }'
    

参考

推荐阅读更多精彩内容