sed awk 第二版学习(八)—— awk 函数
目录
一、算术函数
二、字符串函数
1. 子串
2. 字符串长度
3. 替换函数
4. match() 函数
三、自定义函数
1. 定义与调用
2. 局部变量与全局变量
2. 编写一个排序函数
3. 维护函数库
4. 另一个排序的例子
一、算术函数
下表概括了 9 个 awk 内置的算术函数。
awk 函数 | 描述 |
cos(x) | 返回x的余弦(x为弧度) |
exp(x) | 返回e的x次幂 |
int(x) | 返回x的整数部分的值。(int()函数单纯舍位,可以使用printf格式的“%.0f”实现舍入) |
log(x) | 返回x的自然对数(以e为底) |
sin(x) | 返回x的正弦(x为弧度) |
sqrt(x) | 返回x的平方根 |
atan2(y,x) | 返回y/x的反正切,其值在-π到π之间 |
rand() | 返回伪随机数r,其中0<=r<1 |
srand(x) | 建立rand()的新的种子,缺省参数为当前时间。返回种子数值。 |
函数 rand() 生成一个在 0 和 1 之间的浮点型的伪随机数。函数 srand() 为随机数发生器设置一个种子。如果调用 srand() 时没有参数,它将用当前时间来生成一个种子。如有参数 x,srand() 使用 x 生成种子。
如果根本没调用 srand(),awk 默认为以某个常量为参数调用 srand(),这使得程序在每次运行时都从同一个种子开始,这可以用于重复测试相同的操作,但是如果希望程序在不同的时间运行具有不同的随机数则不合适。参见下面的程序:
# rand.awk -- 测试随机数的生成
BEGIN {print rand()print rand()srand()print rand()print rand()
}
第一次运行程序的结果如下:
$ awk -f rand.awk
0.237788
0.291066
0.0746476
0.9571
产生了四个随机数。再次运行程序的结果如下:
$ awk -f rand.awk
0.237788
0.291066
0.338372
0.882413
前面两个“随机”数和上次运行程序产生的结果一样,而后面两个数不同。后面两个数不同是因为为 rand() 提供了新的种子。函数 srand() 的返回值是它所生成的种子数值,可被用来跟踪随机数序列,例如需要反复使用这一序列执行程序时。
lotto 脚本从 1 到 y 之间挑选 x 个数。在命令行需要提供两个可选参数:挑选多少个数字(默认为 6)和数据系列中的最大值(默认为 30)。使用默认值将产生 1 到 30 之间的 6 个随机数。脚执行结果如下:
$ lotto
Pick 6 of 30
26 4 18 9 11 1
$ lotto 7 35
Pick 7 of 35
4 18 28 12 13 14 34
下面是脚本 lotto 的代码:
awk -v NUM=$1 -v TOPNUM=$2 '
# lotto - 从 y 个数中挑选 x 个随机数
# 主程序
BEGIN {
# 测试命令行参数,NUM = $1,生成多少个数
# TOPNUM = $2,一系列数中的最大值if (NUM <= 0)NUM = 6if (TOPNUM <= 0)TOPNUM = 30
# 打印“Pick x of y”printf("Pick %d of %d\n", NUM, TOPNUM)
# 利用时间和日期作为种子,只执行一次srand()
# 循环到有 NUM 个选择时for (j = 1; j <= NUM; ++j) {# 用循环寻找一个还没有生成的数do {select = 1 + int(rand() * TOPNUM)} while (select in pick)pick[select] = select}
# 循环访问数组并打印结果for (j in pick)printf("%s ", pick[j])printf("\n")
}'
shell 通过 -v 选项得到两个命令行参数,表示从 y 个数中选 x 个数。主程序中首先检查是否提供了这些参数,如果没有则赋默认值。所有的操作都在 BEGIN 过程中完成。主程序首先调用 srand() 函数产生随机数生成器的种子,然后调用 rand() 函数生成一个随机数:
select = 1 + int(rand() * TOPNUM)
因为 rand() 函数返回的值在 0 和 1 之间,用 TOPNUM 来乘以它得到 0 到 TOPNUM 之间的一个数。然后将这个数的小数部分去掉并加一。最后一步加一操作是必要的,因为 rand() 函数可能返回 0。
for (j = 1; j <= NUM; ++j)
这个 for 循环按需要的次数来执行 rand() 函数,数组 pick 用于保存已选择的随机数。为了得到一个不重复的随机数,使用一个内循环来生成选择,并测试它们是否在数组 pick 内(使用 in 操作符比用循环在数组中比较下标要快)。当 select in pick 条件成立时,说明已经找到相应的元素,所以当前的选择是重复的,于是丢弃这个选择。如果 select in pick 为假,那么将把 select 赋给数组 pick 中的一个元素,这将使下次的 in 测试为真,使 do 循环继续。
最后,程序循环访问数组 pick 并打印它的元素。这个版本的 lotto 程序没有对输出数据排序。后面会编写一个用户自定义函数解决排序问题。尽管没必要把排序代码编写成一个函数,但这种模块化做法是有意义的,因为便于处理更普遍的问题,并将这种解决方案用于其它程序。
二、字符串函数
awk 实质上是被设计成字符串处理语言,它的很多功能都起源于字符串函数。下表列出了 awk 中内置的字符串函数。
awk 函数 | 描述 |
gsub(r,s,t) | 在字符串t中用字符串s替换和正则表达式r匹配的所有字符串。返回替换的个数。如果没有给出t,默认为$0。 |
index(s,t) | 返回字符串t在字符串s中的位置,如果没有指定s,则返回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格式说明。 |
sub(r,s,t) | 在字符串t中用s替换正则表达式r的首次匹配。如果成功则返回1,否则返回0。如果没有给出t,默认为$0。 |
substr(s,p,n) | 返回字符串s中从位置p开始最大长度为n的子串。如果没有给出n,返回从p开始的剩余字符串。 |
tolower(s) | 将字符串s中的所有大写字符转为小写,并返回新串。 |
toupper(s) | 将字符串s中的所有小写字符转为大写,并返回新串。 |
sprintf() 函数对字符串应用 printf 相同的格式说明。它不是将结果打印出来,而是返回一个字符串并可以赋值给一个变量。它可以对输入记录或字段执行字符转换,下面的例子使用 sprintf() 函数将一个数字转换成一个 ASCII 字符。
for (i = 97; i <= 122; ++i) {nextletter = sprintf("%c", i)...
}
以上循环给出的数从 97 到 122,这些数字产生从 a 到 z 的 ASCII 字符。
1. 子串
index() 和 substr() 函数都用于处理子串。给定字符串 s,函数 index(s,t) 返回 t 在 s 中出现的最左边的位置。字符串的开始位置是 1。例如:
pos = index("Mississippi", "is")
pos 的值为 2。如果没有发现子串,函数 index() 返回 0。
给定字符串 s,substr(s,p) 返回从位置 p 开始的字符。下面的例子生成一个没有区号的电话号码:
phone = substr("707-555-1111", 5)
还可以提供第三个参数来表示返回字符的个数。例如只返回区号:
area_code = substr("707-555-1111", 1, 3)
下面的例子将每个输入记录的第一个词的首字母改为大写。
awk '# caps - 将第一个单词的首字母改为大写
# 初始化字符串
BEGIN { upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"lower = "abcdefghijklmnopqrstuvwxyz"
}# 对每个输入行
{
# 得到第一个单词的首字母FIRSTCHAR = substr($1, 1, 1)
# 获取 FIRSTCHAR 在小写字母字符串中的位置,为 0 则忽略if (CHAR = index(lower, FIRSTCHAR))# 改变 $1,用位置来检索大写字母$1 = substr(upper, CHAR, 1) substr($1, 2)
# 打印记录print $0
}'
执行情况:
$ caps
root user
Root user
dale
Dale
Tom
Tom
2. 字符串长度
使用内置函数 length() 可以知道一个字符串中有多少个字符。要得到当前输入记录的长度,可以使用 length($0)(没参数时也返回 $0 的长度)。函数 length() 经常用于计算当前输入记录的长度,以决定是否需要断行。
一种处理断行的方法是使用 length() 函数得到每个字段的长度,这样可能效率更高。通过累计这些长度,当一个新的字段使得行的总长度超过某个特定的数值时,就可以指定一个换行。
3. 替换函数
awk 提供了 sub() 和 gsub() 两个替换函数。两者之间的区别是 gsub() 可以实现字符串中所有位置的替换(与 sed 中用 g 全局标志的替换命令相同),而 sub() 只替换第一个位置。
这两个函数都至少需要两个参数。第一个参数是一个正则表达式(用斜杠包围),用于一个模式匹配;第二个参数是一个字符串,用来替换模式匹配的字符串。正则表达式可以用一个变量给出,这种情况下将省略斜杠。第三个可选的参数指定的字符串是将被替换的目标。如果没有第三个参数,将当前的输入记录($0)作为被替换的字符串。
替换函数直接改变指定的字符串,但返回值不是替换后的新串,而是替换的数量。在 sub() 运行成功时总是返回 1,在不成功时两个函数都返回 0。因此可以通过这个结果来确定是否执行了替换操作。
下面的例子使用 gsub() 将所有出现的“UNIX”用“POSIX”替代。条件语句测试 gsub() 的返回值,只有发生变化时当前输入行才被打印。
if (gsub(/UNIX/, "POSIX"))print
和 sed 一样,如果在替换字符串中出现了一个“&”字符,它将被与正则表达式匹配的字符串代替。用“\&”会输出一个字符“&”。要在字符串中加入一个反斜杠“\”,则须要输入两个反斜杠。注意,awk 与 sed 不同,不能“记住”前面的正则表达式,因此不能用语法“//”来引用最后的正则表达式。
下面的例子用于将“UNIX”的任意出现,用 troff 字体更改转义序列来代替。
$ echo "the UNIX operating system" | awk 'gsub(/UNIX/, "\\fB&\\fR")'
the \fBUNIX\fR operating system
下面的例子使用 awk 替换函数实现以下 sed 的替换功能。
sed -n '
s/"//g
s/^\.Se /Chapter /p
s/^\.Ah / A. /p
s/^\.Bh / B. /p' $*awk '
{
gsub(/"/, "")
if (sub(/^\.Se /, "Chapter ")) print
if (sub(/^\.Ah /, "\tA. ")) print
if (sub(/^\.Bh /, "\t\tB. ")) print
}' $*
这两个脚本的功能完全等价,但 awk 比 sed 快!可以修订 awk 脚本,给标题编号,以代替字母:
awk '# do.outline -- 给文章的标题编号
{
gsub(/"/, "")
}
/^\.Se/ {sub(/^\.Se /, "Chapter ")ch = $2ah = 0bh = 0printnext
}
/^\.Ah/ {sub(/^\.Ah /, "\t " ch "." ++ah " ")bh = 0printnext
}
/^\.Bh/ {sub(/^\.Bh /, "\t\t " ch "." ah "." ++bh " ")print
}' $*
这个版本为每个标题编写了它们自己的模式匹配规则。这虽然不必要但更高效,因为一旦应用了一个规则,就不需要再考虑其它规则。next 语句将跳过对已经识别过的行做进一步的检测。
章编号作为“.Se”宏的第一个参数被读取,也就是行的第二个字段。编号方案通过在每次替换时递增一个变量来完成。和章一级标题相关的操作将下一级的标题计数器初始化为 0。和顶层标题“.Ah”相关的操作将第二层标题的计数器初始化为 0。这里将字符串和变量连接在一起作为函数 sub() 的一个参数。
这个脚本产生类似下面的输出:
$ do.outline ch02
Chapter 2 Understanding Basic Operations2.1 Awk, by Sed and Grep, out of Ed2.2 Command-line Syntax2.2.1 Scripting2.2.2 Sample Mailing List2.3 Using Sed2.3.1 Specifying Simple Instructions2.3.2 Script Files2.4 Using Awk2.5 Using Sed and Awk Together
4. match() 函数
match() 函数用于确定一个正则表达式是否和指定的字符串匹配,返回与正则表达式匹配的子串的开始位置(只返回第一个匹配的结果)。例如,下面的 match() 函数匹配输入字符串中的所有大写字母序列,函数的返回值为 5(即字符串中第一个大写字母“U”的位置):
match("the UNIX operating system", /[A-Z]+/)
match() 函数也设置了两个系统变量:RSTART 和 RLENGTH。RSTART 中包含这个函数的返回值,即匹配子串的开始位置。RLENGTH 中包含匹配的字符串的字符数(而不是子串的结束位置)。当模式不匹配时,RSTART 设置为 0,而 RLENGTH 设置为 -1。在前面例子中,RSTART 的值是 5,而 RLENGTH 的值是 4。将它们相加可以得到匹配之后的第一个字符的位置。
下面的例子打印与指定的正则表达式匹配的字符串。
awk '# match -- 打印匹配行的字符串
# 对于匹配模式的行
match($0, pattern) {# 提取匹配模式的字符串,用字符串在 $0 中的开始位置和长度,打印字符串print substr($0, RSTART, RLENGTH)
}' pattern="$1" $2
该 shell 脚本包含两个命令行参数:正则表达式(这个正则表达式必须用引号括起来)以及要查找的文件名。第一个命令行参数被作为 pattern 的值传递。注意,$1 是用引号括起来的,这用于保护出现在正则表达式中的任何空格。match() 函数出现在条件表达式中,用于控制 awk 脚本中唯一的一个过程的执行。如果匹配的模式不存在,那么 match() 函数返回 0;如果存在,则返回非零值(RSTART),可以将这个返回值作为一个条件来使用。如果当前记录与模式匹配,那么将从 $0 中提取字符串,在 substr() 函数中使用 RSTART 和 RLENGTH 的值来指定被提取的子串的开始位置和长度,同时打印该子串。这个过程只和 $0 中第一次出现的子串匹配。
下面是一个验证运行,给出一个正则表达式来匹配“emp”和一个空格之间的所有字符。
$ match "emp[^ ]*" personnel.txt
employees
employee
employee.
employment,
employer
employment
employee's
employee
match 脚本对于进一步理解正则表达式是一个很有用的工具。下一个脚本使用 match() 函数来定位任意的大写字母并将它转换为小写。
awk '# lower - 将大写字母转换成小写字母
# 初始化字符串
BEGIN { upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"lower = "abcdefghijklmnopqrstuvwxyz"
}# 对于每个输入行
{
# 匹配所有大写字母while (match($0, /[A-Z]+/))# 得到每个大写字母for (x = RSTART; x < RSTART+RLENGTH; ++x) {CAP = substr($0, x, 1)CHAR = index(upper, CAP)# 用小写字母替换大写字母sub(CAP, substr(lower, CHAR, 1))}
# 打印记录print $0
}' $*
在这个脚本中,match() 函数出现在条件表达式中,用来确定 while 循环是否执行。通过在循环中使用这个函数,可以使循环体的执行次数和当前输入记录中模式出现的次数一样多。
这里的正则表达式用于匹配 $0 中任意的大写字母序列。如果得到一个匹配,那么 for 循环将对被匹配的子串中的每一个字符进行搜索。RSTART 初始化计数器变量 x,它用于函数 substr() 中,一次从 $0 中提取一个字符,提取位置从与模式匹配的第一个字符处开始。通过将 RSTART 和 RLENGTH 相加,可以得到与模式匹配之后的第一个字符的位置,这就是为什么循环用“<”而不用“<=”。最后使用 sub() 将大写字母用对应的小写字母来替换。(原文中用的 gsub(),按这个脚本循环遍历的算法不但没必要,而且用 gsub() 还会重复多次替换相同的字母)
执行情况:
$ cat test
Every NOW and then, a WORD I type appears in CAPS.
$ lower test
every now and then, a word i type appears in caps.
就这个例子来说,用 tolower 函数一条语句就解决了:
$ cat test | awk '{print tolower($0)}'
every now and then, a word i type appears in caps.
在 awk 的标准替换函数中没有类似于 sed 的“反向引用”的功能,但可用 match() 函数来解决这样的问题。用 match() 函数来匹配一个字符串,可以确定出匹配的子串在一个字符串中的起始和结束位置。给出 RSTART 和 RLENGTH 的值,就可以用 substr() 函数来提取这些子串。在下面的例子中,用分号将两个冒号中的第二个替换掉。
# 使用 match 函数和 substr 函数用分号取代第二个冒号
awk '{if (match($0, /:[^:]*:/)) {before = substr($0, 1, (RSTART + RLENGTH - 2))after = substr($0, (RSTART + RLENGTH))$0 = before ";" after
};print;}'
放置于循环语句中的 match() 函数用于检测是否有一个匹配。如果有,则使用 substr() 函数提取第二个冒号前后的子串。然后将 before、“;”和 after 连接起来,并赋给 $0。
执行情况:
$ echo "aaa : bbb : ccc : ddd" | awk '{if (match($0, /:[^:]*:/)) {before = substr($0, 1, (RSTART + RLENGTH - 2))after = substr($0, (RSTART + RLENGTH))$0 = before ";" after
};print;}'
aaa : bbb ; ccc : ddd
三、自定义函数
正确编写了一个自定义函数时,也就定义了一个函数组件,这个组件可以被其它程序重复使用。随着编写的程序大小显著增长,以及编写的程序数目的增多,使用自定义函数的优点会变得更加明显。
1. 定义与调用
函数定义可以放置在脚本中模式操作规则可以出现的任何地方。通常将函数定义放在顶部的模式操作规则之前。函数用下面的语法定义:
function name (parameter-list) {statements
}
左大括号后面的换行和右大括号前面的换行都是可选的,也可以在包含参数列表的右圆括号后和左大括号前进行换行。parameter-list 是用逗号分隔的变量列表,当函数被调用时,它被作为参数传递到函数中。函数体由一个或多个语句组成。函数中通常包含一个 return 语句,用于将控制返回到脚本中调用该函数的位置;它通常带有一个表达式来返回一个值:
return expression
下面的例子给出了 insert() 函数的定义,它在一个字符串 STRING 的 POS 位置之后插入另一个字符串 INS:
function insert(STRING, POS, INS) {before_tmp = substr(STRING, 1, POS)after_tmp = substr(STRING, POS + 1)return before_tmp INS after_tmp
}
函数调用可以放置在表达式可以出现的地方,例如:
$ echo "Hello" | awk '
function insert(STRING, POS, INS) {before_tmp = substr(STRING, 1, POS)after_tmp = substr(STRING, POS + 1)return before_tmp INS after_tmp
}
{print insert($1, 4, "XX")}'
HellXXo
注意在函数名和左圆括号之间没有空格。
2. 局部变量与全局变量
理解局部变量和全局变量的概念是很重要的。一个局部变量是函数的内部变量,不能在这个函数外面访问。全局变量正相反,可以在脚本的任何地方被访问和修改。当一个函数修改了全局变量而这个变量在其它地方也被使用时,这可能有潜在的破坏性副作用,因此应避免在一个函数内使用全局变量。
当调用函数 insert(),并将 $1 设置为第一个参数时,这个变量的一个副本就被传递到函数中,在那里被作为一个局部变量 STRING 来处理。函数定义的参数列表中的所有变量都是局部的,而且它们的值在这个函数之外不能被访问。在函数调用中的参数不会被函数本身修改,当函数 insert() 返回时,$1 的值没有改变。
然而,在函数体中定义的变量默认为全局变量。对于前面给出的函数 insert() 的定义,临时变量 before_tmp 和 after_tmp 在函数外是可见的。awk 提供的声明局部变量的方法是:在参数列表中定义这些变量。
局部的临时变量放在参数列表的末尾。最基本的是在参数列表中的参数按顺序接收函数调用传递来的值。任何补充的参数,和 awk 中的普通变量一样,被初始化为空串。习惯上,局部变量和“真实的”参数用几个空格隔开。下面的例子显示了如何定义带有两个局部变量的 insert() 函数。
function insert(STRING, POS, INS, before_tmp, after_tmp) {body
}
通过下面的程序看下这种“补充语法”如何工作。insert.awk 脚本内容如下:
function insert(STRING, POS, INS, before_tmp) {before_tmp = substr(STRING, 1, POS)after_tmp = substr(STRING, POS + 1)return before_tmp INS after_tmp
}
# 主程序
{
print "Function returns", insert($1, 4, "XX")
print "The value of $1 after is:", $1
print "The value of STRING is:", STRING
print "The value of before_tmp:", before_tmp
print "The value of after_tmp:", after_tmp
}
现在运行上面的脚本并观察它的输出结果:
$ echo "Hello" | awk -f insert.awk
Function returns HellXXo
The value of $1 after is: Hello
The value of STRING is:
The value of before_tmp:
The value of after_tmp: o
函数 insert() 返回预期的“HellXXo”。$1 的值在函数被调用前后是一样的。变量 STRING 是函数的局部变量,当在主程序中调用时它没有值。对 before_tmp 也是一样,因为它的名字放在函数定义的参数列表中。变量 after_tmp 没有在参数列表中指定,因此它是一个全局变量,可以在函数外访问,值是字母“o”。
正如这个例子所显示的,$1 将“按值”传递给函数。这就意味着,当调用函数时产生 $1 的值的一个备份,并且函数对这个备份执行操作,而不是对原来的 $1 执行操作。然而,数组是“按引用”传递的,也就是函数操作的不是数组的备份而是数组本身,因此函数对数组的任何修改在函数外部都是可见的。下节给出了一个对数组进行操作的函数的例子。
2. 编写一个排序函数
本篇前面部分给出了 lotto 程序,用来从 y 个数据中挑选 x 个随机数。这个程序没有对被选择的数据排序。本节将为一个数组的元素建立排序函数 sort,代码如下:
# 用升序排序数字
function sort(ARRAY, ELEMENTS, temp, i, j) {for (i = 2; i <= ELEMENTS; ++i) {for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {temp = ARRAY[j]ARRAY[j] = ARRAY[j-1]ARRAY[j-1] = temp}}return
}
此函数有数组名和数组中元素个数两个参数,此外还有三个局部变量。函数体实现了一个插入排序算法,即循环访问数组的每个元素,并与它前面的值相比较。如果第一个元素比第二个大,则将第一个元素与第二个元素交换。为了真正交换数据,用一个临时变量存储将要被覆盖的值的一个备份,循环将不停地交换相邻的数据直到所有的数据都按顺序排列。在函数末尾用 return 语句返回到程序的调用点。这里 return 是可选的,它和函数“在尾部退出”有相同的效果。由于函数可能要返回一个值,建议用 return 语句。函数不必将数组返回给主程序,因为数组已经被修改,它可以直接被访问。
注意,lotto 脚本中的 pick 数组没有为排序做好准备,因为元素的下标和元素值相同,而不是有序的数据。排序算法需要按下标顺序遍历数组元素,因此必须建立一个单独的数组以便用排序函数来排序:
# 创建用于排序的下标为数值的数组
i = 1
for (j in pick)sortedpick[i++] = pick[j]
完整的 lotto 脚本代码如下:
awk -v NUM=$1 -v TOPNUM=$2 '
# lotto - 从 y 个数中挑选 x 个随机数
# 用升序排序数字
function sort(ARRAY, ELEMENTS, temp, i, j) {for (i = 2; i <= ELEMENTS; ++i) {for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {temp = ARRAY[j]ARRAY[j] = ARRAY[j-1]ARRAY[j-1] = temp}}return
}# 主程序
BEGIN {
# 测试命令行参数,NUM = $1,生成多少个数
# TOPNUM = $2,一系列数中的最大值if (NUM <= 0)NUM = 6if (TOPNUM <= 0)TOPNUM = 30
# 打印“Pick x of y”printf("Pick %d of %d\n", NUM, TOPNUM)
# 利用当前时间作为种子,只执行一次srand()
# 循环到有 NUM 个选择时for (j = 1; j <= NUM; ++j) {# 用循环寻找一个还没有生成的数do {select = 1 + int(rand() * TOPNUM)} while (select in pick)pick[select] = select}# 创建用于排序的下标为递增数值的数组i = 1for (j in pick)sortedpick[i++] = pick[j]sort(sortedpick, NUM)
# 循环访问数组并打印结果for (j = 1; j <= NUM; ++j)printf("%s ", sortedpick[j])printf("\n")
}'
下面是实际的排序结果:
$ lotto
Pick 6 of 30
3 4 5 11 14 21
$ lotto 7 35
Pick 7 of 35
1 5 22 26 29 30 34
以通用的方式编写 sort() 函数的意义是可以很容易地重用它。在下面的脚本中,将学生成绩读到一个数组中,并调用函数 sort() 对学生的成绩按升序排列。
# grade.sort.awk -- 对学生成绩升序排列
# 输入: 后面跟有一系列成绩的学生姓名# sort 函数 -- 按升序排列数字
function sort(ARRAY, ELEMENTS, temp, i, j) {for (i = 2; i <= ELEMENTS; ++i) {for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {temp = ARRAY[j]ARRAY[j] = ARRAY[j-1]ARRAY[j-1] = temp}}return
}# 主程序
{
# 通过循环将第 2 到第 NF 字段的值赋给名为 grades 的数组
for (i = 2; i <= NF; ++i)grades[i-1] = $i# 调用函数排序数组元素sort(grades, NF-1)# 打印学生姓名
printf("%s:\t", $1)# 循环输出成绩
for (j = 1; j <= NF-1; ++j)printf("%d ", grades[j])printf("\n")
}
执行情况:
$ awk -f grade.sort.awk grades.test
mona: 70 70 77 83 85 89
john: 78 85 88 91 92 94
andrea: 85 89 90 90 94 95
jasper: 80 82 84 84 88 92
dunce: 60 60 61 62 64 80
ellis: 89 90 92 96 96 98
如果希望在删除最低分数后计算学生的平均成绩,可以通过删除排序后的数组的第一个元素来实现。作为练习,还可以编写排序函数的另一版本,它包含第三个参数用于指示按升序还是降序排序。
3. 维护函数库
或许希望把一个有用的函数单独保存在一个文件中。awk 允许使用多个 -f 选项指定多个程序文件。例如将前面编写的排序函数放置在与主程序 grade.awk 不同的文件中。代码如下:
$ cat sort.awk
# sort 函数 -- 按升序排列数字
function sort(ARRAY, ELEMENTS, temp, i, j) {for (i = 2; i <= ELEMENTS; ++i) {for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {temp = ARRAY[j]ARRAY[j] = ARRAY[j-1]ARRAY[j-1] = temp}}return
}$ cat grade.awk
# 主程序
{
# 通过循环将第 2 到第 NF 字段的值赋给名为 grades 的数组
for (i = 2; i <= NF; ++i)grades[i-1] = $i# 调用函数排序数组元素
sort(grades, NF-1)# 打印学生姓名
printf("%s:\t", $1)# 循环输出成绩
for (j = 1; j <= NF-1; ++j)printf("%d ", grades[j])printf("\n")
}
调用执行:
$ awk -f sort.awk -f grade.awk grades.test
注意:
- 对命令行中多个 awk 脚本文件没有强制的引用顺序(-f sort.awk -f grade.awk 与 -f grade.awk -f sort.awk 等价)。
- 不能将一段脚本放在命令行,同时使用 -f 选项为一个脚本指定一个文件名。
4. 另一个排序的例子
下面给出了 Xlib 帮助页的一个简单的版本:
.SH "Name"
XSubImage - create a subimage from part of an image.
.
.
.
.SH "Related Commands"
XDestroyImage, XPutImage, XGetImage,
XCreateImage, XGetSubImage, XAddPixel,
XPutPixel, XGetPixel, ImageByteOrder.
相关命令的列表位于文件的最后部分。需求是对 "Related Commands" 后面的命令列表排序输出,每个命令一行,其它行原样输出。
脚本包含四个规则,它们匹配的是:
- "Related Commands" 标题
- 该标题后面的行
- 所有其它行
- 在所有行被读入后(END)
排序和输出命令列表等大多数操作在 END 过程中执行。下面是这个脚本的代码:
# sorter.awk -- 排序相关命令列表
# 要调用 sort.awk 文件中的 sort 函数
BEGIN { relcmds = 0 }#1 匹配相关命令,将标志 relcmds 设为 1
/\.SH "Related Commands"/ {printrelcmds = 1next
}#2 应用于跟在 "Related Commands" 后面的行
(relcmds == 1) {commandList = commandList $0
}#3 按照原样打印所有其它行
(relcmds == 0) { print }#4 现在排序并输出命令列表
END {
# 删除前导空格和最后的句点gsub(/, */, ",", commandList)gsub(/\. *$/, "", commandList)
# 将各列分解到数组中sizeOfArray = split(commandList, comArray, ",")
# 排序sort(comArray, sizeOfArray)
# 输出元素for (i = 1; i < sizeOfArray; i++)printf("%s,\n", comArray[i])printf("%s.\n", comArray[i])
}
一旦匹配了 "Related Commands" 标题,则打印那一行并设置一个标记变量 relcmds,该变量用于指示后续的输入行是要收集的行(next 是必须的)。第二个过程将每行插入变量 commandList 中。第三个过程打印处理所有其它行。
当读完所有输入行之后执行 END 过程,这时形成了完整的命令列表。在将命令分解插入数组前先将逗号后面的所有空格去掉,然后再去掉最后的句点及尾部空格。最后使用 split() 函数创建一个数组 comArray,将这个数组作为参数传递给 sort() 函数,再打印排好序的值。
该程序输出如下:
$ awk -f sorter.awk -f sort.awk test
.SH "Name"
XSubImage - create a subimage from part of an image.
.
.
.
.SH "Related Commands"
ImageByteOrder,
XAddPixel,
XCreateImage,
XDestroyImage,
XGetImage,
XGetPixel,
XGetSubImage,
XPutImage,
XPutPixel.
调用一个函数与复制代码来完成相同的任务相比较,其优点是函数时经过测试的模块,而且有一个标准的接口。对于一个函数,调用程序所需要知道的只是它需要什么类型的参数以及参数序列。使用函数可以通过减少所要解决问题的复杂度来降低产生错误的机会。