注:本文来自文章作者@寂天_团团 的推荐,原文链接

Sed&awk笔记之awk篇:快速了解Awk

Awk是什么

Awk、sed与grep,俗称Linux下的三剑客,它们之前有很多相似点,但是同样也各有各的特色,相似的地方是它们都可以匹配文本,其中sed和awk还可以用于文本编辑,而grep则不具备这个功用。sed是一种非交互式且面向字符流的编辑器(a “non-interactive” stream-oriented editor),而awk则是一门模式匹配的编程语言,因为它的主要功能是用于匹配文本并处理,同时它有一些编程语言才有的语法,例如函数、分支循环语句、变量等等,当然比起我们常见的编程语言,Awk相对比较简单。

使用Awk,我们可以做以下事情:

● 将文本文件视为由字段和记录组成的文本数据库;

● 在操作文本数据库的过程中能够使用变量;

● 能够使用数学运算和字符串操作;

● 能够使用常见的编程结构,例如条件分支与循环;

● 能够格式化输出;

● 能够自定义函数;

● 能够在awk脚本中执行UNIX命令;

● 能够处理UNIX命令的输出结果;

装备以上功能,awk能够做得事情非常多。但千里之行,始于足下,我们首先从最基本的命令行语法开始,一步一步得走入awk的编程世界。

 

命令行语法

同sed一样,awk的命令行语法也有两种形式:

1

2

awk [-F ERE] [-v assignment] ...  program [argument ...]

awk [-F ERE] -f  progfile ...  [-v assignment] ...[argument ...]

这里的program类似sed中的script,因为我们一直强调awk是一门编程语言,所以将awk的脚本视为一段代码。而awk的脚本同样可以写到一个文件


中,并通过-f参数指定,这一点和sed是一样的。program一般多个pattern和action序列组成,当读入的记录匹配pattern时,才会执行相应的action命令。这里有一点要注意,在第一种形式中,除去命令行选项外,program参数一定要位于第一个位置。

Awk的输入被解析成多个记录(Record),默认情况下,记录的分隔符是\n,因此可以认为一行就是一个记录,记录的分隔符可以通过内置变量RS更改。当记录匹配某个pattern时,才会执行后续的action命令。

而每个记录由进一步地被分隔成多个字段(Field),默认情况下字段的分隔符是空白符,例如空格、制表符等等,也可以通过-F ERE选项或者内置变量FS更改。在awk中,可以通过$1,$2…来访问对应位置的字段,同时$0存放整个记录,这一点有点类似shell下的命令行位置参数。关于这些内容,我们会在下面详细介绍,这里你只要知道有这些东西就好。

标准的awk命令行参数主要由以下三个:

 -F ERE:定义字段分隔符,该选项的值可以是扩展的正则表达式(ERE);

 -f progfile:指定awk脚本,可以同时指定多个脚本,它们会按照在命令行中出现的顺序连接在一起;

 -v assignment:定义awk变量,形式同awk中的变量赋值,即name=value,赋值发生在awk处理文本之前;

为了便于理解,这里举几个简单的例子。通过-F参数设置冒号:为分隔符,并打印各个字段:

1

2

[kodango@devops ~]$ echo "1:2:3" | awk -F: '{print $1 "  and " $2 " and " $3}'

1 and  2 and 3

在awk的脚本中访问通过-v选项设置的变量:

1

2

[kodango@devops ~]$ echo | awk -v a=1 'BEGIN {print a}'

1

从上面可以看到,通过-v选项设置的变量在BEGIN的位置就可以访问了。BEGIN是一个特殊的pattern,它在awk处理输入之前就会执行,可以认为是一个初始化语句,与此对应的还有END

好像还没介绍如何指定处理的文件,是不是最后的argument就是指定的文件?在看我这本书之前,我也是这样认为的,但是实际上arguemnt有两种形式,它们分别是输入文件(file)和变量赋值(assignment)。

awk可以同时指定多个输入文件,如果输入文件的文件名为’-',表示从标准输入读取内容。

变量赋值类似-v选项,它的形式为name=value。awk中的变量名同一般的编程语言无太多区别,但是不能同awk的保留关键字重名,可以查看awk的man手册查询哪些是保留关键字。而变量值只有两种形式:字符串和数值。变量赋值必须位于脚本参数的后面,与文件名参数无先后顺序的要求,但是位于不同位置的赋值它的执行时机是不同的。

我们用实际的例子来解释这个区别,假设有两个文件:a和b,它们的内容分别如下所示:

1

2

3

4

[kodango@devops awk_temp]$ cat a

file a

[kodango@devops awk_temp]$ cat b

file b

为了说明赋值操作发生的时机,我们在BEGIN,正常处理,END三个地方都打印变量的值。

第一种情况: 变量赋值位于所有文件名参数之前

1

2

3

4

5

[kodango@devops awk_temp]$ awk 'BEGIN {print "BEGIN: " var}  {print "PROCESS: " var} \

END {print "END:  " var }' var=1 a

BEGIN:

PROCESS:  1

END: 1

结果:赋值操作发生在正常处理之前,BEGIN动作之后。

第二种情况:变量赋值位于所有文件名之后:

1

2

3

4

5

[kodango@devops awk_temp]$ awk 'BEGIN {print "BEGIN: " var}  {print "PROCESS: " var} \

END {print "END:  " var }' a var=1 

BEGIN:

PROCESS:

END: 1

结果:赋值操作发生在正常处理之后,END动作之前。

第三种情况:变量赋值位于文件名之间:

1

2

3

4

5

6

[kodango@devops awk_temp]$ awk 'BEGIN {print "BEGIN: " var}  {print "PROCESS: " var} \

END {print "END:  " var }' a var=1 b

BEGIN:

PROCESS:

PROCESS:  1

END: 1

结果:赋值操作发生在处理前面的文件之后,并且位于处理后面的文件之前;

总结如下:

1. 如果变量赋值在第一个文件参数之前,在BEGIN动作之后执行,影响到正常处理和END动作;

2. 如果变量赋值在最后一个文件参数之后,在END动作之前执行,仅影响END动作;

3. 如果文件参数不存在,情况同1所述;

4. 如果变量赋值位于多个文件参数之间,在变量赋值前面的文件被处理后执行,影响到后续文件的处理和END动作;

所以变量赋值一定要考虑清楚用途,否则比较容易出错,不过一般情况下也不会用到变量赋值。

自然地大家会将变量赋值与-v assignment选项进行比较,赋值的形式是一致的,但是-v选项的执行时机比变量赋值要早:

1

2

[kodango@devops awk_temp]$ echo 1 | awk -v var=a 'BEGIN {print "BEGIN: " var}'

BEGIN:  a

可见,-v选项的赋值操作在BEGIN动作之前就执行了。

变量赋值一定要小心不要与保留关键字重名,否则会报错:

1

2

[kodango@devops awk_temp]$ echo 1 | awk -v BEGIN=1 'BEGIN {print "BEGIN: " BEGIN}'

awk: fatal: cannot  use gawk  builtin  `BEGIN' as variable name

 

记录(Record)与字段(Field)

对于数据库来说,一个数据库表是由多条记录组成的,每一行表示一条记录(Record)。每条记录由多列组成,每一列表示一个字段(Field)。Awk将一个文本文件视为一个文本数据库,因此它也有记录和字段的概念。默认情况下,记录的分隔符是回车,字段的分隔符是空白符,所以文本文件的每一行表示一个记录,而每一行中的内容被空白分隔成多个字段。利用字段和记录,awk就可以非常灵活地处理文件的内容。

可以通过-F选项来修改默认的字段分隔符,例如/etc/passwd的每一行都是由冒号分隔成多个字段的,所以这里就需要将分隔符设置成冒号:

1

2

3

4

[kodango@devops awk_temp]$ awk -F: '{print $1}' /etc/passwd  | head  -3

root

bin

daemon

这里通过$1引用第一人字段,类似地$2表示第二个字段,$3表示第三个字段…. $0则表示整个记录。内置变量NF记录着字段的个数,所以$NF表示最后一个字段:

1

2

3

4

[kodango@devops awk_temp]$ awk -F: '{print $NF}' /etc/passwd  | head  -3

/bin/bash

/bin/false

/bin/false

当然,$(NF-1)表示倒数第二个。

内置变量FS也可以用于更改字段分隔符,它记录着当前的字段分隔符:

1

2

3

4

[kodango@devops awk_temp]$ awk -F: '{print FS}' /etc/passwd  | head  -1

:

[kodango@devops awk_temp]$ awk -v FS=: '{print $1}' /etc/passwd  | head  -1

root

记录的分隔符可以通过内置变量RS更改:

1

2

[kodango@devops awk_temp]$ awk -v RS=: '{print $0}' /etc/passwd  | head  -1

root

如果将RS设置成空,行为有就一点怪异了,它会将连续不为空行的所有行(一个段落)当作一个记录,而且强制回车为字段分隔符:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

[kodango@devops awk_temp]$ cat awk_man.txt

 

The awk utility shall execute programs written in  the awk  programming language,

which is specialized for  textual data manipulation. An awk program is a sequence

of patterns and corresponding actions.  When   input  is  read  that matches a

pattern,  the action associated with that pattern is carried out.

 

Input  shall be interpreted as a sequence of records. By default, a record is a  line,

less its terminating  <newline>, but this can be changed by using the RS built-in

variable. Each record of input shall be matched in  turn against each pattern in the

program.  For each pattern matched, the associated action shall be executed.

 

[kodango@devops awk_temp]$ awk 'BEGIN {RS="";FS=":"} {print "First line:  " $1}' awk_man.txt

First line: The awk utility shall execute programs written in  the awk  programming language,

First  line: Input shall be interpreted as a sequence of records. By default, a  record is a line,

这里,我们将变量赋值放到BEGIN动作中执行,因为BEGIN动作是在文件处理之前执行的,专门用于放初始化的语句。FS的赋值在这里是无效的,awk依然使用回车符来分隔字段。

 

脚本(Script)组成

命令行中的program部分,可以称为awk代码,也可以称为awk脚本。一段awk脚本是由多个’pattern { action }‘序列组成的。action是一个或者多个语句,它在输入行匹配pattern的时候被执行。如果pattern为空,表明这个action会在每一行处理时都会被执行。下面的例子简单地打印文件的每一行,这里不带任何参数的print语句打印的是整个记录,类似’print $0‘:

1

2

3

[kodango@devops awk_temp]$ echo -e 'line1\nline2' | awk '{print}'

line1

line2

除了pattern { action },还可以在脚本中定义自定义的函数,函数定义格式如下所示:

1

function name(parameter list) { statements }

函数的参数列表用逗号分隔,参数默认是局部变量,无法在函数之外访问,而在函数中定义的变量为全局变量,可以在函数之外访问,如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

[kodango@devops awk_temp]$ echo line1 | awk '

function t(a) {

    b=a;

    print  a;

}

 

{

    print  b;

    t("kodango.me");

    print  b;

}'

 

kodango.me

kodango.me

Awk脚本中的语句使用空行或者分号分隔,使用分号可以放在同一行,不过有时候会影响可读性,尤其是分支或循环结构中,很容易出错。

如果Awk中的一个语句太长,要分成多行,可以在行为使用反斜杠’\':

1

2

3

4

5

6

7

8

9

10

11

12

13

14

[kodango@devops awk_temp]$ cat test.awk

 

function t(a)

{

    b=a

    print  "This is a very long line, so use backslash to escape the newline \

then  we will print the variable a: a=" a

}

 

{ print b; t("kodango.me"); print b;}

[kodango@devops awk_temp]$ echo 1 | awk -f test.awk

 

This is a very long line, so use backslash to escape the  newline then we will print the variable a: a=kodango.me

kodango.me

这里我们将脚本写到文件中,并通过-f参数来指定。但是,在一些特殊符号之后,是可以直接换行的,例如”, { && ||”。

 

模式(Pattern)

模式是awk中比较重要的一部分,它有以下几种情况:

 /regular expression/ 扩展的正则表达式(Extended Regular Expression), 关于ERE可以参考这篇文章

 relational expression 关系表达式,例如大于、小于、等于,关系表达式结果为true表示匹配;

 BEGIN 特殊的模式,在第一个记录处理之前被执行,常用于初始化语句的执行;

 END 特殊的模式,在最后一个记录处理之前被执行,常用于输出汇总信息;

 pattern, pattern:模式对,匹配两者之间的所有记录,类似sed的地址对;

例如查找匹配数字3的行:

1

2

3

[kodango@devops awk_temp]$ seq 1 20 | awk '/3/ {print}'

3

13

相反地,可以在在正则表达式之前加上’!'表示不匹配:

1

2

3

4

5

[kodango@devops awk_temp]$ seq 1 5 | awk '!/3/ {print}'

1

2

4

5

除了BEGINEND这两个特殊的模式外,其余的模式都可以使用’&&’或者’||’运算符组合,前者表示逻辑与,后者表示逻辑或:

1

2

3

[kodango@devops awk_temp]$ seq 1 50 | awk '/3/ && /1/ {print}'

13

31

前面的正则都是整行匹配,有时候仅仅需要匹配某个字符,这样我们可以用表达式$n ~ /ere/

1

2

[kodango@devops ~]$ awk '$1 ~ /ko/ {print}' /etc/passwd

kodango:x:1000:1000::/home/kodango:/bin/bash

有时候我们只想显示特定和行,例如显示第一行:

1

2

[kodango@devops ~]$ seq 1 5 | awk 'NR==1 {print}'

1