1. 初识awk
本文所述的awk是POSIX awk,也就是说,遵循POSIX标准的awk语言。
1.1. hello world
例1:
$ echo 'this line of data is ignored' > test
$ awk '{print "Hello world"}' test
Hello world
这个脚本只有一条包含在大括号中的语句,这个操作对每个输入行执行print语句。test文件只包含一行,因此,print操作执行一次。
注意:这个输入行将被读入但没有被输出。
例2:
$ cat test2
Hello world
$ awk '{print}' test2
Hello world
print语句没有参数,只简单的输出每个输入行。如果文件中有其它的输入行,它们同样可以被输出。
awk是输入驱动的,也就是说,除非有可以在其上操作的输入行,否则将什么也不能做。
当调用awk程序时,它将读入所提供的脚本,并检查其中的指令的语法。然后awk将对每个输入行执行脚本中的指令。因此,如果没有来自文件中的输入行,以上的print语言将不做任何事情。
例3:
$ awk 'BEGIN {print "Hello world"}'
Hello world
BEGIN模式用于指定的第一个输入行读入之前要执行的动作。
如果一个程序只有一个BEGIN模式,并且没有其它的语句,awk将不处理任何输入文件。
1.2. awk程序设计模型
awk程序是由所谓的主输入(main input)循环组成的。一个循环是一个例程,它将一直重复执行直到有一些存在的条件终止它。
你不必写这个循环,它是现成的,它作为一个框架存在,在这个框架中你编写的代码能够执行。
在awk中的主要输入循环是一个例程,它可以从文件中读取一行并使得进程可以访问它。
主输入循环执行的次数和输入的行数相同,当没有输入行读入时,循环将终止。
awk允许你编写两个特殊的例程,它们在任何输入被读取前和所有输入都被读取后执行。它们是与BEGIN和END规则相关的过程。换句话说,在主输入循环执行前和主输入循环执行后你可以做一些处理。BEGIN和END过程是可选的。
1.3. 记录和字段
awk假设它的输入是有结构的,而不只是一串无规则的字符串。
在最简单的情况下,它将每个输入行作为一条记录,而将由空格或制表符分隔的单词作为字段(用来分隔字段的字符被称为分隔符)。
连续的两个或多个空格和/或制表符被作为一个分隔符。
1.3.1. 字段和引用的分离
awk允许使用字段操作符$来指定字段。在该操作符后面跟着一个数字或变量,用于标识字段的位置。“$1”表示第一个字段,“$2”表示第二个字段等等。"$0"表示整个输入记录。
awk '{print $1, $2, $3}' FILE
print语句中分隔每个参数的逗号使得输入的各值之间有一个空格(是因为输出字段分隔符(OFS),它的值中输出的逗号默认为空格)。
可以用任何计算值为整数的表达式来表示一个字段,而不只是用数字和变量:
$ echo a b c d | awk 'BEGIN { one = 1; two = 2}
> {print $(one + two)}'
c
可以在命令行中使用-F选项改变字段的分隔符。它后面跟着(或者紧跟,或者有空白)分隔符。下面例子将字段分隔符修改为制表符:
$ awk -F"\t" '{print $2}' FILE
"\t"是表示一个实际的制表符的转义序列,它应由单引号或双引号包围着。
在脚本中指定域分隔符是一个好的习惯,并且是非常方便的。可以通过定义系统变量FS来改变字段分隔符。因为这个必须在读取第一个输入行之前执行,所以必须在由BEGIN规则控制的操作中指定这个变量。
BEGIN {FS = ","}
可以编写匹配规则来控制操作符,使得只打印出特定的内容:
/pattern/ {print $1}
使用(~)操作符可以测试一个字段的正则表达式:
$1 ~ /pattern/ {print $2}
可以使用组合符号(!~)来反转这个规则的意义:
$1 !~ /pattern/ {print $2}
1.3.2. 字段的划分:完整的问题
可以使用3个完全不同的方法是awk分隔字段:
第一个方法是用空白字符来分隔字段。要实现这种方法,可将FS设置为一个空格。在这种情况下,记录的前导空白字符和结尾空白字符(空格和/或制表符)将被忽略。并且字段空格和/或制表位来分隔。因为FS的默认值为一个空格,所以这也是通常情况下awk将记录划分为字段的方法。
第二个方法是使用其它单个字符来分隔字段。例如:awk程序经常使用“:”作为分隔符来访问UNIX /etc/passwd文件。当FS表示任何单个字符时,在这个字符出现的任何地方都将分隔出另外一个字段。如果出现两个连续的分隔符,在它们之间的字段值为空串。
第三个方法是,如果设置了不止一个字符作为字段分隔符,它将被作为一个正则表达式来解释。也就是说,字段分隔符将是与正则表达式匹配的“最左边最长的非空的不重叠的”子串。
FS = "\t"
这个命令行将每个制表符作为一个字段分隔符。
FS = "\t+"
这个命令行表示用一个或多个制表符来分隔字段。
可以使用一个正则表达式指定几个字符作为分隔符:
FS = '[':\t]'
在括号中的任何3个字符之一都可以被解释为字段分隔符。
1.4. 表达式
可以使用表达式来存储、操作和检索数据,这些操作与sed中的有很大的区别,但这是大多数程序设计语言所具有与的共同特性。
一个表达式通过计算返回一个值。表达式由数字和字符串常量、变量、操作符、函数和正则表达式组成。
常量有两种类型:字符串型或数字型("red"或1)。
字符串在表达式中必须用引号括起来。在字符串中可以使用下列的转义序列。
——————————————————————————————————
序列 描述
——————————————————————————————————
\a 报警字符,通常是ASCII BEL字符
\b 退格键
\f 走纸符
\n 换行符
\r 回车
\t 水平制表符
\v 垂直制表符
\ddd 将字符表示为1到3位八进制
\xbex 将字符表示为十六进制值
\c 任何需要字面表示的字符c
——————————————————————————————————
变量是引用值的标识符。
定义变量只需要为它定义一个名字并将数据赋给它即可。变量名只能由字母、数字和下划线组成。而且不能以数字开头。
变量名的大小写很重要:Canpool和canpool是两个不同的变量,变量不必进行说明,你不必告诉awk什么类型的数据存储在一个变量中。
每个变量有一个字符串型值和数字型值,awk能够根据表达式的前后关系来选择合适的值(不包含数字的字符串值为0)。
变量不必初始化。awk自动将它们初始化为空字符串,如果作为数字,它的值为0。
下面的表达式将一个值赋给x:
x = 1
x是变量的名字,=是一个赋值操作符,1是一个数字常量。
下面的表达式表示将字符串"hello"赋给z:
z = "hello"
空格是字符串连接操作符,表达式:
z = "hello" "world"
将两个字符串连接在一起,并将结果"helloworld"赋给变量z。
美元符号($)是引用字段操作符。下面的表达式表示把当前输入记录的第一个字段的值赋予变量w:
w = $1
多种操作符可以用在表达式中,下表列出了算术操作符。
——————————————————
操作符 描述
——————————————————
+ 加
- 减
* 乘
/ 除
% 取模
^ 取幂
** 取幂
———————————————————
一旦变量被赋予了一个值,那么就可以用这个变量名来引用这个值。下面的表达式表示将变量x的值和1相加并将结果赋给变量y:
y = x + 1
即计算x的值,使它加1,并将结果赋给变量y。语句:
print y
打印y的值。如果下面的一系列语句将出现在脚本中:
x = 1
y = x + 1
print y
那么y的值为2。可以将这3个语句减少为两个:
x = 1
print x + 1
注意:print语句后面的x的值却仍为1。我们没有改变变量x的值,我们只是将它和1相加并打印结果。换句话说,如果第三个语句是print x,那么将输出1。实际上,如果我们想将x的值增加,我们可以用赋值操作符+=。这个操作符组合了两个操作符:它将1和x的值相加并将结果赋给x。
下表列出了awk表达式中的赋值操作符。
——————————————————————————————————
操作符 定义
——————————————————————————————————
++ 变量加1
-- 变量减1
+= 将加的结果赋给变量
-= 将减的结果赋给变量
*= 将乘的结果赋给变量
/= 将除的结果赋给变量
%= 将取模的结果赋给变量
^= 将取幂的结果赋给变量
**= 将取幂的结果赋给变量
——————————————————————————————————
1.5. 系统变量
awk中有许多系统变量或内置变量。awk有两种类型的系统变量。
1)第一种类型定义的变量默认值可以改变,例如默认的字段和记录分隔符。
2)第二种类型定义的变量的值可用于报告或数据处理中,例如当前记录中字段的数量,当前记录的数量等。
这些可以由awk自动更新,例如,当前记录的编号和输入文件名。
有一组默认值会影响对记录和字段的输入和输出的识别系统变量FS定义字段分隔符。它的默认值为一个空格,这将提示awk可以用若干个空格和/或制表符来分隔字段。FS可以被设置为任何单独的字符或一个正则表达式。
和FS等效的输出是OFS,它的默认值为一个空格。
下表是awk定义的系统变量:
——————————————————————————————————
常量 描述
——————————————————————————————————
FS 定义字段分隔符
OFS 定义字段分隔符(默认值为一个空格)
NF 定义为当前输入记录的字段个数(改变NF的值有副作用)
RS 记录分隔符,定义为一个换行符
ORS 和RS输出等价,它的默认值也是一个换行符
FILENAME 表示当前输入文件的名称
FNR 被用来表示与当前输入文件相关的当前记录的代码
CONVFMT 用来控制数字到字符串的转换,它的默认值为"%.6g"
NR 记录编号,相当于记录索引,每读入一个记录NR递增1,起始值为1
——————————————————————————————————
1.6. 关系操作符和布尔操作符
关系操作符和布尔操作符用于在两个表达式之间进行比较,如下表所示:
1)关系操作符
——————————————————————————————————
操作符 描述
——————————————————————————————————
< 小于
> 大于
<= 小于或等于
>= 大于或等于
== 相等的
!= 不等的
~ 匹配
!~ 不匹配
——————————————————————————————————
2)布尔操作符
——————————————————————————————————
操作符 描述
——————————————————————————————————
|| 逻辑或
&& 逻辑与
! 逻辑非
——————————————————————————————————
&&比||的优先级高
1.7. 格式化打印
printf和print的主要区别是printf没有提供自动换行功能,必须明确地为它指定"\n"
printf语法:
printf(format-expression [, arguments])
其中的圆括号是可选的。
第一部分是一个用来描述格式的表达式,通常以引号括起来的字符串常量的形式提供。
第二部分是一个参数列表,例如变量名列表,它和格式说明相对应。
在格式说明前面有一个百分号(%),而格式说明符号为下表列出的字符之一。两个主要的格式说明符是s和d,s表示字符串,d表示十进制整数。
用在printf的格式说明符:
——————————————————————————————————
字符 定义
——————————————————————————————————
c ASCII字符
d 十进制整数
i 十进制整数(在POSIX中增加的)
e 浮点格式([-]d.precision[+-]dd)
E 浮点格式([-]d.precisionE[+-]dd)
f 浮点格式([-]ddd.precision)
g e或f的转换形式,长度最短,末尾的0被去掉
G E或f的转换形式,长度最短,末尾的0被去掉
0 无符号的八进制
s 字符串
u 无符号的十进制
x 无符号的十六进制,用a-f表示10-15
X 无符号的十六进制,用A-F表示10-15
% 字面字符%
——————————————————————————————————
printf语句可以规定输出域的宽度和对齐方式。一个格式表达式由3个可选的修饰符组成,跟在“%”后面,并出现在格式说明符之前。
%-width.precision format-specifier
描述输出字段宽度的width是一个数值。当指定域宽度时,这个域的内容默认为向右对齐。必须制定“-”来设置左对齐。因此,"%-20s"输出的是向左对齐的一个域长度为20个字符的字符串,如果字符串少于20个字符,那么这个域将用空格来填满。
precision修饰符用于十进制或浮点数,用于控制小数点右边的数字位数。对于字符串型值,它用于控制要打印的字符的最大数量。
注意:数值的默认precision值为“%.6g"
可以根据print或printf的参数列表中的值,动态地指定宽度width和精度precision。通过用星号实际的值来实现这个功能:
printf("%*.*g\n", 5, 3, myvar);
print语句的输出数值的默认精度可以通过设置系统变量OFMT来改变。例如,如果使用awk打印报告,其中包含美元($))数值,可以将OFMT设置为:
"%.2f"
1.8. 向脚本传递参数
在awk中,一个容易引起混乱的地方就是向脚本传递参数。
参数将值赋给一个变量,这个变量可以在awk脚本中访问。这个变量可以在命令行上设置,放在脚本的后面,文件名前面。
awk 'script' var=vaule inputfile
每一项都必须作为单一的参数来解释。因此,在等号的两边不允许出现空格。
也可以用这个方法传递多个参数。例如,如果想在命令行定义变量high和low,可以用下面的代码调用awk:
$ awk -f scriptfile high=100 low=60 datafile
在脚本中,这两个变量可以作为awk的任何变量来访问。如果要将这一脚本写入一个shell脚本的实现中,则可以以数值的形式传递shell的命令行参数(shell按位置提供了命令行参数变量:$1表示第一个参数,$2表示第二个参数,以此类推)。则可以写成:
awk -f scriptfile "high=$1" "low=$2" datafile
另外,环境变量或命令的输出结果也可以作为变量的值来传递。
awk '{....}' directory=`pwd` file1 ...
也可以使用命令行参数定义系统变量:
$ awk '{print NR, $0}' OFS='.' names
命令行参数的一个重要限制是它们在BEGIN过程中是不可用的。也就是说,直到首行输入完成以后它们才可用。为什么?这是一个容易混乱的部分。从命令行传递的参数就好像文件名一样被处理。赋值操作直到这个变量(如果它是一个文件名)被求值时才进行。
POSIX awk提供了一个解决这个问题的方法,即在任何输入被读入前定义参数。用-v选项指定要在执行BEGIN过程之前得到变量赋值(也就是,在读入第一个输入行之前)。-v选项必须在一个命令脚本前说明。例如:下列命令使用-v选项为多行记录设置记录分隔符。
$ awk -F"\n" -v RS="" 'print' phones.block
每个传递给程序的变量赋值都需要一个不同的-v选项。
2. 条件、循环和数组
这一节包含了一些基本的编程结构。它覆盖了awk程序设计语言中的所有控制结构。它还包括数组,即一种可以存储一系列值的变量。
2.1. 条件语句
条件语句用于在执行操作之前做一个测试。条件语句以if开头,并计算放在圆括号中的表达式。语法:
if (expression)
action1
[else
action2]
如果条件表达式expression的值为真(非零或非空),就执行action1。当存在else语句时,如果条件表达式的值为假(零或空),则执行action2。
一个条件表达式可能包含算术运算符、关系操作符、或布尔操作符。也许最简单的条件表达式是测试一个变量是否是一个非零值。
if (x) print x
如果x是零,print语句将不执行。如果x是一个非零值,将打印x的值。
也可以测试x是否等于另一个值:
if (x == y) print
还可以用模式匹配操作符“~"来测试x是否与一个模式匹配:
if (x ~ /[yY](es)?/) print x
以下是几个补充的语法要点:
1)如果操作是由多个语句组成的,要用一对大括号将操作括起来。
if (expression) {
statment1
statment2
}
awk对大括号和语句的位置没有特殊的要去(和sed不同)。左大括号放在条件表达式后面,可以与条件表达式位于一行,也可以在下一行。第一条语句可以紧跟左大括号或从下一行开始,右大括号放在最后一条语句的后面,可以与最后一条语句位于同一行,也可以在下一行。在大括号的前后允许有空格或制表符。
虽然没有要求语句缩进书写,但这样可以改善可读性。
右大括号和else后面的换行是可选的。
if (expression) action1
[else action2]
如果在action1后面加一个分号表示结束,action1后面的换行也是可选的。
if (expression) action1; [else action2]
如果在同一行上用分号分隔多个语句,同样需要使用大括号。
2.1.1. 条件操作符
awk中提供的条件操作符可以在C语言中找到,它的形式为:
expr ? action1 : action2
2.2. 循环
循环是一种用于重复执行一个或多个操作的结构。在awk中循环结构可以用while、do或for语句来指定。
2.2.1. while循环
语法:
while (condition)
action
右圆括号后面的换行是可选的。条件表达式在循环的顶部进行计算,如果为真,就执行循环体action部分。如果表达式不为真,则不执行循环体。
通常情况下,条件表达式的值为真并执行循环体,在循环体中改变某一值,直到最后条件表达式的值为假并退出循环。例如,如果希望执行一个循环4次,可以编写下面的循环语句:
i = 1
while (i <= 4) {
print $i
++i
}
2.2.2. do循环
do循环是whilie循环的一个变型。语法:
do
action
while (condition)
do后面的换行是可选的。如果action后面使用分号,则换行也是可选的。这个结构的主要部分是action后面的条件表达式condition。因此,循环体至少执行一次。
do循环和while循环之间的区别,do循环至少执行一次循环体。在循环的底部都可以决定是否再次执行。
2.2.3. for循环
for语句是同while循环一样,能够得到相同结果的一个更紧凑的语法形式。
尽管它看起来比较困难,但其语法使用更简单,并能保证提供一个循环所需要的所有元素。语法:
for (set_counter; test_counter; increment_counter)
action
右圆括号后面的换行是可选的。for循环由3个表达式组成:
- set_counter
设置计数器变量的初值。 - test_counter
描述在循环开始时要测试的条件。 - increment_counter
每次在循环的底部递增计数器,且恰好在重新测试test_counter之前。
使用for循环打印输入行的每一个字段:
for (i = 1; i <= NF; i++)
print $i
2.3. 影响流控制的其它语句
可以用if、while、for和do语句来改变程序的正常控制流,在本节中,将看到另外几个也能够影响控制流的语句。
在一个循环体中有两个语句可以影响控制流,break和continue。
1)break
break语句顾名思义就是退出循环,这样将不再继续执行循环。
2)continue
continue语句在到达循环底部之前终止当前的循环,并从循环的顶部开始一个新的循环。
有两个语句能影响主输入循环,next和exit。
1)next
next语句能够导致读入下一个输入行,并返回脚本的底部。这可以避免对当前输入行执行其它的操作过程。
next语句的典型应用是可以连续从文件读取内容,忽略脚本的其它的操作直到文件被读完。系统变量FILENAME提供了当前文件的名字。因此,可以如下编写模式:
FILENAME == "acronyms" {
action
next
}
{print}
这使得对文件acronyms的每行都执行action指定的操作。当完成操作后,输入下一个新行。只有当从不同的文件输入内容时,控制才执行print语句。
2)exit
exit语句使主输入循环退出并将控制移到END规则,如果END存在的话。如果没有定义END规则,或在END中应用exit语句,则终止脚本的执行。
exit语句可以使用一个表达式作为参数,该表达式将作为awk的退出状态返回。如果没有提供表达式,那么将返回0,如果为exit语句设置一个初值,然后在END中再次调用没有参数的exit,则使用第一个值,例如:
awk '{
...
exit 5
}
END {exit}'
这里,awk的退出状态是5
2.4. 数组
数组是可以用来存储一组数据的变量。通常这些数据之间具有某种关系。数组中的每一个元素通过它们在数组中的下标来访问。
每个下标都用方括号括起来。下面的语句表示为数组中的一个元素赋值。
array[subscript] = value
在awk中不必指明数组的大小,只需要为数组指定标识符。向数组元素赋值是最容易完成的。
2.4.1. 关联数组
在awk中,所有的数组都是关联数组。关联数组的独特之处在于它的下标可以是一个字符或一个数值。
在大多数的编程语言中,数组的下标都是唯一的数字。在这些语言中,数组是存储数据的一系列存储单元。数组的下标来自数在数组中存储的次数。没有必要跟踪数组的下标。例如,数组中的第一个元素的下标是“1”或是数组的第一个位置。
关联数组在数组的下标和元素之间建立了一种“关联”。对于数组中的每个元素都有两个相关的值:元素的下标和元素的值。
这些元素不像传统的数组那样按一定的顺序存储。尽管在awk中的数组的下标也可以是数据型的,但是这些下标的意义和其它编程语言中所表示的意义不同,它们不一定代表数的位置。然而,对于数值型下标,也能够顺序访问数值中的所有元素,就像创建一个循环来递增计数器并按顺序访问数组元素。
有时,数值型和字符型下标之前的差别是很重要的。例如,如果用“O4”作为数组中一个元素的下标,就不能用“4”作为下标来定位这个元素。
关联数组是awk中的一个独特的特征,它的一个强大功能就是可以使用字符串作为一个数据的下标。例如,可以使用一个单词作为下标来查找它的定义。如果你知道这个单词,你就可以检索到它的定义。
例如,可以使用下面的赋值语句来将输入行的第一个字段作为第二个字段的下标:
array[$1] = $2
有一个特殊的循环语法可以访问关联数组的所有元素。它是for循环的一个版本。
for (variable in array)
do something with array[varialbe]
这个语法可以应用于使用数组型下标的数组。但是,访问数组中的条目的顺序是随机的,在awk实现中这种顺序经常发生变化。
重要的是需要记住awk中的所有数字下标都是字符串类型。即使使用数字作为下标,awk将自动将它们转换为字符串。当使用整数作为下标时,也不必担心,因为它们也被转换成字符串。
2.4.2. 测试数组中的成员资格
关键词in也是一个操作符,用在条件表达式中来测试一个下标是否是数组的成员。表达式为:
item in array
如果array[item]存在则返回1,否则返回0。
2.4.3. 用split()创建数组
内置函数split()能够将任何字符串分解到数组的元素中。这个函数对于从字段中提取“子字段”是很有用的。函数split()的语法为:
n = split(string, array, separator)
string是要被分解到名字为array的元素中的输入字符串。数组的下标从1开始到n,n即为数组中元素的个数。
元素根据指定的separator分隔符来分解。如果没有指定分隔符,那么将使用字段分隔符(FS)。
separator可以是一个完整的正则表达式,而不仅仅是单个字符。
数组的分解与字段的分解相同。
2.4.4. 删除数组元素
awk提供了一个语句用于从数组中删除一个元素。语法:
delete array[subscript]
这里的方括号是必须的。这个语句将删除array中下标为subscript的元素,特别地,使用in测试subscript将返回为假。这与为数组元素赋一个空值是不同的,在这种情况下in将一直为真。
2.4.5. 多维数组
awk支持线性数组,在这种数组中的每个元素的下标是单个下标。如果你将线性数组看成是一行数据,那么两位数组将表示数据的行和列。你可以将第三行第二列的数据元素表示为“array[3,2]”。两维和三维数组是多维数组的例子。
awk不支持多维数组,但它为下标提供了一个语法来模拟引用多维数组。例如,你可以如下编写表达式:
file_array[NR, i] = $i
这里的每个输入记录的字段使用记录编号和字段号做下标。因此,可以如下表示:
file_array[2, 4]
这将得到第二个记录的第四个字段的值。
这个语法不能创建多维数组。它将转换为一个字符串来唯一识别线性数组中的元素。
多维数组下标的分量被解释为单独的字符串(例如“2”和“4”),并使用系统变量SUBSEP的值连接。下标分量的分隔符默认被定义为“\034”,这是一个不可打印的下标,实际为“2\0344”(使用SUBSEP将“2”和“4”连接起来)。模拟多维数组的主要后果是数组越大,访问个别的元素就越慢。
多维数组的语法也支持测试数组的成员资格。下标必须放置在圆括号中:
if ((i, j) in array)
这可以测试下标i,j是否在指定的数组中存在。
对多维数组的循环操作和一维数组相同:
for (item in array)
你必须用split()函数来访问单独的下标分量。即:
split(item, subscr, SUBSEP)
以上split()函数使用下标item创建数组subscr。
2.4.6. 作为系统变量的数组
awk中提供的两个系统变量,它们是数组。
ARGV
这是一个命令行参数的数组,不包括脚本本身和任何调用awk指定的选项,这个数组中的元素的个数可以从ARGC中获得。数组中第一个元素的下标是0(和awk中的其它数组不同,而和C一致),最后一个下标是ARGC-1。
ENVIRON
一个环境变量数组,数组中每个元素是当前环境变量的值,而其下标是环境变量的名字。
- 命令行参数数组
可以编写一个循环来访问ARGV数组中的所有元素:
BEGIN { for (x = 0; x < ARGC; ++x)
print ARGV[x]
print ARGC
}
下标为0的元素为程序名
2)环境变量数组
下面程序循环访问了数组ENVIRON的所有元素:
BEGIN {
for (env in ENVIRON)
print env "=" ENVIRON[env]
}
数组的下标是变量的名字。该脚本产生于env命令相同的输出。
可以使用变量名作为数组的下标访问任意元素:
ENVIRON["LOGNAME"]
也可以修改数组ENVIRON中的任意元素:
ENVIRON["LOGNAME"] = "canpool"
但是这个修改并不改变用户的真实环境(例如,当执行完awk时,LOGNAME的值没有变化),同样也没改变程序的环境,这些程序是awk使用getline()或system()调用的。
3. 函数
函数是一个独立的计算过程,它接受一些参数作为输入并返回一些值,awk有许多内置函数,可分为两组:算术函数和字符串函数。
awk也支持用户自定义函数,允许你编写自己的函数来扩展内置函数。
3.1. 算术函数
有9个内置函数可以被归类为算术函数,它们大多数接受数值型参数并返回数值型值。
——————————————————————————————————
awk函数 描述
——————————————————————————————————
cos(x) 返回x的余弦(x为弧度)
exp(x) 返回e的x次幂
int(x) 返回x的整数部分的值
log(x) 返回x的自然对数(以e为底)
sin(x) 返回x的正弦(x为弧度)
sqrt(x) 返回x的平方根
atan2(y, x) 返回y/x的反正切,其值在-π到π之间
rand() 返回伪随机数r,其中0<=r<1
srand(x) 建立rand()的新的种子数。如果没有指定种子数,
就用当天的时间。返回旧的种子值。
——————————————————————————————————
3.2. 字符串函数
内置字符串函数比算数函数更重要且更有趣。因为awk实质上是被设计成字符串处理语言,它的很多功能都起源于这些函数。
——————————————————————————————————
awk函数 描述
——————————————————————————————————
index(s, t) 返回字串t在字符串s中的位置(字符串的开始位置是1),
如果没有指定s,则返回0
substr(s, p, n) 返回字符串s中从位置p开始最大长度为n的字串。
如果没有给出n,返回从p开始剩余的字符串。
gsub(r, s, t) 在字符串t中用字符串s替换和正则表达式r匹配的所有字符串。
返回替换的个数。如果没有给出t,默认$0
sub(r, s, t) 在字符串t中用s替换正则表达式r的首次匹配。
如果成功则返回1,否则返回0,如果没有给出t,默认为$0
length(s) 返回字符串s的长度,当没有给出s使,返回$0的长度
match(s, r) 如果正则表达式r在s中出现,则返回出现的起始位置;
如果在s中没有发现r,则返回0
设置RSTART和RLENGTH的值
split(s, a, sep) 使用字段分隔符sep将字符串s分解到数组a的元素中,返回元素的个数。
如果没有给出sep,则使用FS。数组分隔和字段分隔采用同样的方式。
sprintf("fmt", expr) 对expr使用printf格式说明
tolower(s) 将字符串s中的所有大写字符转换为小写,并返回新串
toupper(s) 将字符串s中的小写字符转换为大写,并返回新串
——————————————————————————————————
3.2.1. 子串
index()和substr()函数都用于处理子串。
1)index(s, t)
给定字符串s,函数index(s, t)返回t在s中出现的最左边的位置。字符串的开始位置是1。
pos = index("Mississippi" "is")
pos的值为12,如果没有发现子串,函数index()返回0
2)substr(s, p)
给定字符串s,substr(s, p)返回从位置p开始的字符。下面的例子生成一个没有区号的电话号码
phone = substr("707-555-1111", 5)
还可以提供第三个参数来表示字符的个数。下面例子只返回区号:
area_code = substr("707-555-1111", 1, 3)
这两个函数可以一起使用,而且经常被一起使用。
3.2.2. 字符串长度
使用内置函数length()可以知道一个字符串中有多少个字符。
例如,要计算当前输入记录的长度,可以使用length($0)。正巧,如果函数length()被调用时没有给出参数,它将返回$0的长度。
函数length()经常用于计算当前输入记录的长度,以决定是否需要断行。
3.2.3. 替换函数
awk提供了两个替换函数:sub(r, s, t)和gsub(r, s, t)。
两者之间的区别是gsub()可以实现输入字符串中所有位置的替换,而sub()函数只实现第一个位置的替换。
这两个函数都至少需要两个参数:
第一个参数是一个正则表达式(用斜杠包围着),用于和一个模式匹配;正则表达式可以用一个变量来给出,在这种情况下将省略斜杠。
第二个参数是一个字符串,用来替换模式匹配的字符串。
第三个可选的参数指定的字符串是将被替换的目标。如果没有第三个参数,将当前的输入记录($0)作为被替换的字符串。
替换函数直接改变指定的字符串。假设函数能正常工作,你或许希望当发生替换后,函数返回替换后的新串。但替换函数实际上返回替换的数量。
在sub()运行成功时,总是返回1,在不成功时,两个函数都返回0。因此,可以通过测试这个结果来确定是否执行了替换操作。
例如,下面的例子使用gsub()将所有出现的“UNIX”用“POSIX”替代。
if (gsub(/UNIX/, "POSIX"))
print
条件语句测试gsub()返回的值,只有发生变化时,当前输入行才被打印。
和sed一样,如果在替换字符串中出现一个“&”字符,它将被与正则表达式匹配的字符串替换。
3.2.4. match(s, r)函数
match()函数用于确定一个正则表达式是否和指定的字符串匹配。
它需要两个参数,字符串和正则表达式(这个函数容易产生混淆,因为这个函数中正则表达式在第二个位置,而在替换函数中正则表达式在第一个位置)
match()函数返回与正则表达式匹配的子串的开始位置,你可能会认为它和函数index()有紧密的联系。
match()函数也设置两个系统变量:RSTART和RLENGTH
1)RSTART中包含了这个函数的返回值,即匹配子串的开始位置。
2)RLENGTH中也包含了匹配的字符串的字符数(而不是子串的结束位置)。
当模式不匹配时,RSTART设置为0,而RLENGTH设置为-1.
3.3. 自定义函数
使用用户自定义函数,awk允许程序员新手采用C编程语言不同的步骤来编写程序,这就是使用自含式函数。
当正确地编写了一个函数时,也就定义了一个函数组建,这个组建可以被其他的程序重复使用。
随着编写的程序的大小显著增长,以及编写的程序数目的增多,使用自定义函数的优点会变得更明显。
函数定义可以放置在脚本中,模式操作规则可以出现的任何地方。通常情况下,我们将函数定义放在脚本顶部的模式操作规则之前。
函数用下面的语法定义:
function name (parameter-list) {
statments
}
左大括号后面的换行和右大括号前面的换行都是可选的。你也可以在包含参数列表的右圆括号后和左大括号前进行换行。
parameter-list是用逗号分隔的变量列表,当函数被调用时,它被作为参数传递到函数中。
函数体由一个或多个语句组成。函数中通常包含一个return语句,用于将控制返回到脚本中调用该函数的位置;它通常带有一个表达式来返回一个值。
return expression
下面的例子给出了insert()函数的定义:
function insert(STRING, POS, INS) {
before_tmp = substr(STRING, 1, POS)
after_tmp = substr(STRING, POS + 1)
return before_tmp INS after_tmp
}
注:我们习惯用大写表示参数。这主要是为了使解释容易些。在实际中,这不是一个好主意,因为这很容易于系统变量冲突。
这个函数有3个参数,在一个字符串STRING的POS位置之后插入另一个字符串INS。在函数体中用函数substr()将STRING分为两部分。return语句返回一个字符串,这个字符串是将字符串STRING的第一部分、INS和STRING的最后一部分连接起来而得到。
函数调用可以放置在表达式可以出现的地方。因此,下面的语句:
print insert($1, 4, "XX")
如果$1的值为“Hello”,这个函数返回“HellXXo”。
注意:当调用用户自定义函数时,在函数名和左圆括号之间可以没有空格。但这对内置函数是不合适的???
理解局部变量和全局变量的概念是很重要的。一个局部变量是函数的内部变量,不能在这个函数外面访问。全局变量正相反,可以在脚本中的任何地方被访问和修改。当一个函数修改了全局变量而这个变量在其它的地方也被使用时,这可能存在潜在的破坏性副作用。因此,在一个函数内避免使用全局变量是一个好主意。
当我们调用函数insert(),并将$1设置为第一个参数时,这个变量的一个副本就被传递到函数中,在那里被作为一个局部变量STRING来处理。在函数定义的参数列表中的所有变量都是局部的,而且他们的值在这个函数之外是不能被访问。相同地,在函数调用中的参数不会被函数本身修改。当函数insert()返回时,$1的值没有改变。
然而,在函数体中定义的变量默认为全局变量。对于前面给出的函数insert()定义,临时变量before_tmp和after_tmp在函数外是可见的。awk为它的开发者提供了“粗略的”方法来声明变量为函数的局部变量,也就是在参数列表中定义这些变量。
局部的临时变量在参数列表的末尾。最基本的是在参数列表中的参数按顺序接收函数调用传递来的值。任何补充的参数,和awk中的普通变量一样,被初始化为空串。习惯上,局部变量和“真实的”参数用几个空格隔开。例如,下面的例子显示了如何定义带有两个局部变量的insert()函数。
function insert(STRING, POS, INS, before_tmp, after_tmp) {
body
}
3.3.1. 维护函数库
你或许希望把一个有用的函数保存在一个文件中,并保存在一个重要的目录下。awk允许使用多个-f选项来制定多个程序文件。
例如,我们可以将编写的函数sort函数放置在于主程序grade.awk不同的文件中。下面的命令指定了两个程序文件:
$ awk -f grade.awk -f /usr/local/share/awk/sort.awk grades.test
这个命令假设grade.awk在当前工作目录中,并且排序函数定义在目录/usr/local/share/awk下的文件sort.awk
4. 底部抽屉(高级主题)
4.1. getline函数
getline函数用于从输入中读取另一行。geline函数不仅能读取正常的输入数据流,而且也能处理来自文件和管道的输入。
getline函数类似于awk中的next语句。两者都是导致下一个输入行被读取,next语句将控制传递回脚本的顶部。getline函数得到了下一行当没有改变脚本的控制。可能的返回值为:
1 如果能够读取一行
0 如果到了文件末尾
-1 如果遇到错误
注意:尽管getline函数被称为一个函数并且返回了一个值,但它的语法类似于一个语句。不能写成getline(),它的语法不允许有圆括号。
4.1.1. 从文件中读取
getline函数除了能够读取正常的输入流外,还可以从一个文件或管道中读取。
例如,下面的御酒从文件data中读取了一行:
getline < "data"
尽管文件名可以通过一个变量来提供,但它通常被指定为字符串常量。这字符串必须用引号括起来。
4.1.2. 将输入赋给一个变量
getline函数允许你将输入记录赋给一个变量,变量的名字为一个参数来提供。
下面的语句从输入中读取下一行并赋给变量input:
getline input
将输入赋给一个变量不会影响当前的输入行,也就是说,对$0没有影响。新的输入行没有被分解成字段,因此对变量NF也无影响。但它递增了记录计数器NR和FNR。
4.1.3. 从管道读取输入
可以执行一个命令并将输出结果用管道输送到getline。例如,参见下面的表达式:
"whoami" | getline
这个表达式将whoami命令的输出结果赋给$0。这个行被分解为字段并设置了系统变量NF。同样的,你也可以将结果赋给一个变量:
"whoami" | getline me
通过将输出结果赋给一个变量可以避免设置$0和NF,但输入行没有被分解为字段。
当一个命令的输出结果被用管道输送给getline且包含多个行时,getline一次读取一行。第一次调用getline,它将读取输出的第一行。如果再次调用getline,它将读取第二行。要读取输出的所有行,就必须创建一个循环来执行getline,直到不再有输出为止。
例如,下面的例子使用while循环来读取输出的每一行并将它赋给who_out的下一个元素:
while ("who" | getline)
who_out[++i] = $0
每次调用getline函数时,读取输出的下一行。然而,其中的who命令只执行一次。
4.2. close()函数
当读文件或写文件时,文件被打开。每个操作系统对一个正在运行程序能够同时打开的文件的数量都有一定的限制,而且,每个awk实现对打开文件的数量都有内被限制,这个数字可能比系统限制药效。
为了避免打开过多的文件,awk提供了close()函数用于关闭打开的文件。关闭已经处理完的文件可使程序打开更多的文件。
close()函数用于关闭打开的文件和管道。使用它有一下几个原因:
1)每次你只能打开一定数量的管道。为了在一个程序中能够打开你所希望的数量的管道,你必须用close()函数来关闭一个你用过的管道(通常是,当getline返回0或-1时)。它用一个语句来实现,和用于创建管道的表达式相同。下面一个例子:
close("who")
2)关闭一个管道使得你可以运行同一个命令两次。例如,你可以用两次date来定时一个命令。
3)为了得到一个输出管道来完成它的工作,使用close()可能是必要的。例如:
{some processing of $0 | "sort > tmpfile"}
END {
close("sort > tmpfile")
while ((getline < "tmpfile") > 0) {
do more work
}
}
4)为了保证同时打开的文件数不超过系统的限制,关闭打开的文件是必要的。
4.3. system()函数
system()函数执行一个以表达式给出的命令。然而,它的命令没有产生可供程序处理的输出。它返回被执行的命令的退出状态。脚本等待这个命令完成任务后才继续执行。下面的例子执行mkdir命令:
BEGIN {
if (system("mkdir dale") != 0)
print "Command Failed"
}
这里在一个if语句中调用system()函数,来测试一个非零的退出状态。
4.4. 直接向文件和管道输出
任何print和printf语句可以用输出重定向操作符">“或”>>"直接将输出结果写入一个文件中。例如,下面的语句将当前记录写到文件data.out中:
print > "data.out"
文件名可以是任何能产生合法的文件名的表达式。第一次使用重定向操作符将打开文件,随后使用重定向操作符将数据追加到文件中。“>”和“>>”之间的区别和shell中的重定向操作符之间的区别相同。右尖括号“>”在打开一个文件是截断它,而“>>”符号将保存文件中包含的任何东西并向文件中追加数据。
因为重定向操作符">“和关系操作符是一样的,所以当你用表达式作为print命令的参数时,可能会产生混淆。规定当”>“出现在任何打印语句的参数列表中时,被看做是重定向操作符。要想使”>"出现在表达式的参数列表中时,被看做是关系操作符,可以用圆括号将表达式或参数列表括起来。
例如,下面的例子用原括号将条件表达式括起来以确保关系表达式被正确求值:
print "a=", a, "b=", b, "max=", (a > b ? a : b) > "data.out"
4.4.1. 直接输出到一个管道
你也可以将输出直接写入一个管道,命令为:
print | command
该命令在第一次执行时,打开一个管道,并将当前记录作为输入输送给命令command。换句话说,这里的命令只执行了一次,但每执行一次print命令将提供另一个输入行。
4.5. 调试
调试比编程的任何方面更容易令人灰心,也更必不可少。
一个程序有两类问题:
第一类是程序逻辑上真正的错误(bug)。也就是说,这种程序运行完成后并没有报告任何出错信息,但产生的结果却不是所希望的。例如,或许不生成任何输出。这种错误可能是没有print语句打印计算结果引起的。程序错误是思路上的错误。
第二类错误是程序不能运行或不能完全运行。这可能是语法错误引起的,并导致awk向你警告所不能解释的代码。许多语法错误是由于输出错误或漏掉括号的错误。语法错误通常会产生错误信息以引导你找到问题所在。有时,程序引起awk失败(或“core dump”)而没有产生任何有用的出错信息。这可能是语法错误引起的,但有些问题可能与机器有关。我们有几个大脚本在一台机器上运行时出现core dump,而在另一台上运行却没有问题。例如,可能你运行违反了awk对特殊实现的约束。
4.6. 约束
————————————————————————————————————————————————
项目 约束
————————————————————————————————————————————————
每个记录中字段的个数 100
每个输入记录的字符个数 3000
每个输出记录的字符个数 3000
每个字段的字符个数 1024
每个printf字符串的字符个数 3000
字面字符串中的字符个数 400
字符类中的字符个数 400
打开的文件数 15
打开的管道数 1(大多数大于1)
————————————————————————————————————————————————
4.7. 使用#!语法调用awk
“#!”语法是从shell脚本中调用awk的可选的语法。它的优点是你可以在shell脚本的命令行中指定awk的参数和文件名。
运用这个语法的最好方法是将一行作为shell脚本的第一行:
#!/bin/awk -f
"#!"后面跟的是所用的awk所在的路径名,然后是-f选项。
5. 参考
- sed与awk(Dale Dougherty & Arnold Robbins 著,张旭东 杨作梅 田丽华 等译)