前面的话:
这几天写了一个程序,在同一个目录里生成了很多文件,需要统计其中部分文件的总大小,发现经常用到的ls、du等命令都无济于事,我甚至都想到了最笨的方法,写一个脚本:mkdir一个新目录,把要统计总大小的文件mv过去,然后du或者ls -lh新目录。诚然,这个办法又笨又不精确,于是求助万能的网络,找到的都是同一篇用了3个很长的循环来统计的脚本,还是自己先苦读“经书”吧。鸟哥的书第十二章就有现成的示例,就用到了马上要出场的awk工具,用法如下(统计目录下所有tmp*文件的总大小,以KB为单位输出):
ls -l tmp* | awk 'BEGIN{total=0} {total+=$5} END{printf "%.2f KB\n", total/1024}'
鸟哥的书第十二章后面推荐了一篇awk的高级文献,我下载来看发现头疼的是,全篇都是繁体字,虽然是80年代的文献,既然鸟哥的书里面推荐了,说明还是很有参考价值的。于是我萌发了把全篇“翻译”过来的念头,网上也有很多“译文”了,但是想要好好学习,还是自己再全部“推敲”一遍吧,而且能保证有始有终。关于原文,鸟哥的网站有备份:
http://linux.vbird.org/linux_basic/0330regularex/awk.pdf
另外在网上搜索的过程中,也找到了一些不错的教程和笔记,这里贴一个链接mark一下。
http://man.lupaworld.com/content/manage/ringkee/awk.htm
下面进入正题,有些贴图是本人在机器上执行过之后截取贴上来的,也希望大家能自己动手,切实掌握AWK的知识。
这是一本AWK学习指南,其重点在于:
AWK适用于解决哪些问题?
AWK常见的解题模式是什么?
为使读者快速掌握awk解题的模式及特性,本手册系由一些较具代表性的范例及其题解所构成;各范例由浅入深,彼此间相互连贯,范例中并对所使用的awk语法及指令辅以必要的说明。有关awk的指令、函数、...等条列式的说明则收录于附录中,以利读者往后撰写程序时查阅。 如此编排,可让读者在短时间内顺利地学会使用awk来解决问题。建议读者循着范例上机实习,以加深学习效果。
a. UNIX 环境下的简单操作及基本概念。
例如:文件编辑, 文件复制 及 管道, 输入/输出重定向 等概念。
b. C 语言的基本语法及流程控制指令。
例如:printf(), while() ...
(注:awk 指令并不多,且其中的大部分与 C语言中的用法一致,本手册中对该类指令的语法及特性不再加以繁冗的说明,读者若欲深究,可自行翻阅相关的 C 语言书籍)
本文以学习指引为主要编排方式,读者若需要有关AWK介绍详尽的参考书,可以参考下列两本书:
— Alfred V. Aho, Brian W. Kernighan and Peter J. Weinberger, “The AWK Programming Language", Addison-Wesley Publishing Company
— Dale Dougherty, "sed & awk", O`Reilly & Associates, Inc
由于awk具有上述特色,在问题处理的过程中,可轻易使用awk来撰写一些小工具;这些小工具并非用来解决整个大问题,它们只扮演解决个别问题过程的某些角色,可通过Shell所提供的pipe将数据按需要传送给不同的小工具进行处理,以解决整个大问题。这种解题方式,使得这些小工具可因不同需求而被重复组合及使用(reuse);也可通过这种方式来先行测试大程序原型的可行性与正确性,将来若需要较高的执行速度时再用C语言来改写。这是awk最常被应用之处。若能常常如此处理问题,读者可以以更高的角度来思考抽象的问题,而不会被拘泥于细节的部分。本手册作为awk入门的学习指引,其内容将先强调如何撰写awk程序,未列入进一步解题方式的应用实例,这部分将留待UNIX进阶手册中再行讨论。
一般的UNIX操作系统,本身即带有awk。不同的UNIX操作系统所带的awk其版本亦不尽相同。若读者所使用的系统上未带有awk,可通过anonymous ftp到下列地方取得:
phi.sinica.edu.tw:/pub/gnu
ftp.edu.tw:/UNIX/gnu
prep.ai.mit.edu:/pub/gnu
为便于解释awk程序架构,及有关术语(terminology),先以一个员工薪资数据文件(emp.dat),来加以介绍。
数据文件中各字段依次为 员工ID、姓名、时薪 及 实际工时。ID中的第一个字母为部门识别码,"A"、"P"分别表示"组装"及"包装"部门。
本小节着重于说明awk程序的主要架构及工作原理,并对一些重要的名词加以必要的解释。通过学习这部分内容,读者可体会出awk语言的主要精神及awk与其它语程序言的差别。为便于说明,之后以条列方式说明。
1. 记录(Record):awk从数据文件上读取数据的基本单位。以上列数据文件emp.dat为例,awk读入的
第一条记录是 "A125 Jenny 100 210"
第二条记录是 "A341 Dan 110 215"
一般而言, 一条 记录 就相当于数据文件上的一行资料。 (参考 : 附录 B 内建变量"RS")
2. 字段(Field):为记录中被分隔开的子字符串。以数据行"A125 Jenny 100 210"为例,
第一个 | 第二个 | 第三个 | 第四个 |
“A125" | "Jenny" | 100 | 210 |
一般是以空格符来分隔相邻的字段。( 参考:附录 D 内建变量"FS" )
在UNIX的命令行上输入下列格式的指令:("$"表示Shell命令行上的提示符号)
$ awk 'awk程序' 数据文件名
则awk会先编译该程序,然后执行该程序来处理所指定的数据文件。(上述方式直接把程序写在UNIX的命令行上)
awk程序中主要语法是 Pattern { Actions },故常见的awk程序其形式如下:
Pattern1 { Actions1 }
Pattern2 { Actions2 }
......
Pattern3 { Actions3 }
awk 可接受许多不同形式的 Pattern。一般常使用 "关系表达式"(Relational expression)来当作 Pattern。
例如:
x > 34 是一个Pattern,判断变量 x 与 34 是否存在大于的关系。
x == y 是一个Pattern,判断变量 x 与变量 y 是否存在等于的关系。
上式中 x >34 、 x == y 便是典型的Pattern。
awk 提供 C 语言中常见的关系运算符(Relational Operators) 如 >, <, >=, <=, ==, !=。此外,awk 还提供 ~ (match) 及 !~(not match) 二个关系运算符(注一)。
其用法与涵义如下:
若 A 为一字符串,B 为一正则表达式(Regular Expression)
A ~ B 判断 字符串A 中是否 包含 能匹配(match)B表达式的子字符串。
A !~ B 判断 字符串A 中是否 不包含 能匹配(match)B表达式的子字符串。
例如 :
"banana" ~ /an/ 整个是一个Pattern。
因为"banana"中含有可以匹配 /an/ 的子字符串,故此关系式成立(true),整个Pattern的值也是true。
相关细节请参考 附录 A Patterns, 附录 E Regular Expression
(注一:) 有少数awk文献,把 ~, !~ 当成另一类的 Operator,并不视为一种 Relational Operator。本手册中将这两个运算符当成一种 Relational Operator。
Actions 是由许多awk指令构成。而awk的指令与 C 语言中的指令十分类似。
例如:
awk的 I/O指令:print, printf( ), getline, ...
awk的 流程控制指令:if(...){..} else{..}, while(...){...}, ...
(请参考 附录 B --- "Actions" )
awk 会先判断(Evaluate) 该 Pattern 的值,若 Pattern 判断后的值为true (或不为0的数字,或不是空的字符串),则awk将执行该 Pattern 所对应的 Actions。反之,若 Pattern 的值不为 true,则awk将不执行该 Pattern所对应的 Actions。
例如:若awk程序中有下列两指令
50 > 23 {print "Hello! The word!!" }
"banana" ~ /123/ {print "Good morning !" }
awk会先判断 50 >23 是否成立。因为该式成立,所以awk将打印出"Hello! The word!!"。而另一 Pattern 为"banana"~/123/,因为"banana" 内未含有任何子字符串可 match /123/,该 Pattern 的值为false,故awk将不会打印出 "Good morning !"
有时语法 Pattern { Actions }中,Pattern 部分被省略,只剩 {Actions}。这种情形表示 "无条件执行这个 Actions"。
awk 所内建的字段变量及其涵意如下 :
字段变量 |
含义 |
$0 |
一字符串,其内容为目前 awk 所读入的整行数据。 |
$1 |
$0 上第一个字段的数据。 |
$2 |
$0 上第二个字段的数据。 |
... |
其余类推 |
1. 当 awk 从数据文件中读取一行数据时,awk 会使用内置变量$0 予以记录。
2. 每当 $0 被改动时 (例如:读入新的数据行 或 自行变更 $0) awk 会立刻重新分析 $0 的字段情况,并将 $0 上各字段的数据用 $1、$2、...等予以记录。
awk 提供了许多内置变量,使用者在程序中可使用这些变量来取得相关信息(不用加$)。常见的内置变量有:
内置变量 |
含义 |
NF (Number of Fields) |
为一整数,其值表示$0上所存在的字段总数。 |
NR (Number of Records) |
为一整数,其值表示awk已读入的数据行数目。 |
FILENAME |
awk正在处理的数据文件名。 |
例如 : awk 从数据文件 emp.dat 中读入第一行记录"A125 Jenny 100 210" 之后,程序中:
$0 的值将是 "A125 Jenny 100 210"
$1 的值为 "A125" $2 的值为 "Jenny"
$3 的值为 100 $4 的值为 210
NF 的值为 4 $NF 的值为 210 (笔者注:$NF即为$4)
NR 的值为 1 FILENAME 的值为 "emp.dat"
执行awk时,它会反复进行下列四步骤。
awk会自动重复进行上述4个步骤,使用者不须在程序中编写这个循环 (Loop)。
awk 处理数据时,它会自动从数据文件中一次读取一条记录,并会将该记录切分成一个个的字段;程序中可使用 $1, $2,... 直接取得各个字段的内容。这个特色让使用者易于用 awk 编写 reformatter 来改变数据格式。
范例:以数据文件 emp.dat 为例,计算每人应发工资并打印报表。
分析:awk 会自行一次读入一条记录,故程序中仅需告诉 awk 如何处理所读入的数据行。
执行如下命令:($ 表UNIX命令行上的提示符)
$ awk '{ print $2, $3 * $4 }' emp.dat
执行结果如下:
屏幕出现:
说明:
1. UNIX命令行上,执行awk的语法为:
$ awk 'awk程序' 要处理的数据文件名
本范例中的 程序部分为 {print $2, $3 * $4}。把程序置于命令行时,程序之前后必须以 ' (单引号)括住。
2. emp.dat 为指定给该程序处理的数据文件名。
3. 本程序中使用:Pattern { Actions } 语法。
Pattern | Actions |
print $2, $3 * $4 |
4. print为 awk 所提供的输出指令,会将数据输出到stdout(屏幕)。print 的参数间彼此以 "," (逗号) 隔开,打印出数据时彼此间会以空白隔开。(参考 附录 D 内置变量OFS)
5. 将上述的 程序部分 储存于文件 pay1.awk 中,执行命令时再指定 awk程序文件 的文件名。这是执行awk的另一种方式,特别适用于程序较大的情况,其语法如下:
$ awk -f awk程序文件名 数据文件名
故执行下列两命令,将产生同样的结果。
$ awk -f pay1.awk emp.dat
$ awk '{ print $2, $3 * $4 }' emp.dat
读者可使用 "-f" 参数,让awk主程序使用“其它 仅含 awk函数 的程序文件中的函数 ”
其语法如下:
$ awk -f awk主程序文件名 -f awk函数文件名 数据文件名
(有关 awk 中函数的声明与使用于 7.4 中说明)
6. awk中也提供与 C 语言中类似用法的 printf() 函数,使用该函数可进一步控制数据的输出格式。
编辑另一个awk程序如下,并取名为 pay2.awk
{ printf("%6s Work hours: %3d Pay: %5d\n", $2, $3, $3 * $4) }
执行下列命令
$ awk -f pay2.awk emp.dat
执行结果屏幕出现:
Pattern { Action }为awk中最主要的语法。若某Pattern的值为真则执行它后面的 Action。 awk中常使用"关系表达式" (Relational Expression)来当成 Pattern。
awk 中除了>, <, ==, != ,...等关系运算符( Relational Operators )外,另外提供 ~(match),!~(Not Match) 二个关系运算符。利用这两个运算符,可判断某字符串是否包含能匹配所指定正则表达式的子字符串。由于这些特性,很容易使用awk来编写需要字符串比对、判断的程序。
范例:接上例,
1. 组装部门员工调薪5%,(组装部门员工的ID以"A"开头)
2. 所有员工最后的薪资率若仍低于100,则以100计。
3. 编写 awk 程序打印新的员工薪资率报表。
分析:这个程序须先判断所读入的数据行是否满足指定条件,再进行某些动作。awk中 Pattern { Actions } 的语法已涵盖这种 " if ( 条件) { 动作} "的架构。
编写如下的程序, 并取名 adjust1.awk
$1 ~ /^A.*/ { $3 *= 1.05 }
$3 < 100 { $3 = 100 }
{ printf("%s %8s %d\n", $1, $2, $3)}
执行下列命令:
$ awk -f adjust1.awk emp.dat
结果如下:屏幕出现:
说 明:
1. awk的工作流程是:从数据文件中每次读入一行数据,依序执行完程序中所有的 Pattern{ Action }指令
Pattern | Actions |
$1~/^A.*/ | { $3 *= 1.05 } |
$3 < 100 | { $3 = 100 } |
{printf("%s %8s %d\n",$1,$2,$3)} |
再从数据文件中读进下一条记录继续进行处理。
2. 第一个 Pattern { Action }是:
$1 ~ /^A.*/ { $3 *= 1.05 }
$1 ~ /^A.*/ 是一个Pattern,用来判断该行数据的第一个字段是否包含以"A"开头的子字符串。其中 /^A.*/ 是一个Regular Expression,用以表示任何以"A"开头的字符串。(有关 Regular Expression 的用法 参考 附录 E )。
Actions 部分为 $3 *= 1.05。$3 *= 1.05 与 $3 = $3 * 1.05 意义相同,运算符"*=" 的用法则与 C 语言中一样。此后与 C 语言中用法相同的运算符或语法将不予赘述。
3. 第二个 Pattern { Actions } 是:
$3 < 100 { $3 = 100 }
若第三个字段内容(即时薪)小于100,则调整为100。
4. 第三个 Pattern { Actions } 是:
{printf("%s %8s %d\n",$1, $2, $3)}
省略了Pattern(无条件执行Actions),故所有数据行调整后的数据都将被打印。
awk程序中允许使用字符串当做数组的下标(index)。利用这个特色十分有助于资料统计工作。(使用字符串当下标的数组称为Associative Array)
首先建立一个数据文件,并取名为 reg.dat。此为一学生注册的资料文件;第一栏为学生姓名,其后为该生所修课程。
awk中数组的特性
1. 使用字符串当数组的下标(index)。
2. 使用数组前不须声明数组名及其大小。
例如:希望用数组来记录 reg.dat 中各门课程的修课人数。这情况,有两项信息必须储存:
(a) 课程名称,如: "O.S.","Arch.".. ,共有哪些课程事先并不明确。
(b) 各课程的修课人数。 如:有几个人修"O.S."
在awk中只要用一个数组就可同时记录上列信息。其方法如下:
使用一个数组 Number[ ]:
* 以课程名称当 Number[ ] 的下标。
* 以 Number[ ] 中不同下标所对映的元素代表修课人数。
例如:
有2个学生修 "O.S.",则以 Number["O.S."] = 2 表示。
若修"O.S."的人数增加一人,则 Number["O.S."] = Number["O.S."] + 1
或 Number["O.S."]++ 。
3. 如何取出数组中储存的信息
以 C 语言为例,声明 int Arr[100];之后,若想得知 Arr[ ]中所储存的数据,只须用一个循环,如:
for(i=0; i<100; i++)
printf("%d\n", Arr[i]);
即可。上式中:
数组 Arr[ ] 的下标: 0, 1, 2,..., 99
数组 Arr[ ] 中各下标所对应的值: Arr[0], Arr[1],...Arr[99]
但 awk 中使用数组并不须事先声明。以刚才使用的 Number[ ] 而言,程序执行前,并不知将来有哪些课程名称可能被当成Number[ ]的下标。
awk 提供了一个指令,通过该指令awk会自动查找数组中使用过的所有下标。以 Number[ ] 为例,awk将会找到 "O.S.","Arch.",...
使用该指令时,须指定所要查找的数组,及一个变量。awk会使用该变量来记录从数组中找到的每一个下标。例如
for(course in Number){
...
}
指定用 course 来记录 awk 从Number[ ] 中所找到的下标。awk每找到一个下标时,就用course记录该下标的值且执行{....}中的指令。通过这个方式便可取出数组中储存的信息。(详见下例)
范例:统计各科修课人数,并印出结果。
建立如下程序,并取名为 course.awk:
{ for( i=2; i <= NF; i++) Number[$i]++ }
END{
for(course in Number)
printf("%10s %d\n", course, Number[course] )
}
执行下列命令:
$ awk -f course.awk reg.dat
执行结果如下:
说 明:
1. 这程序包含两个Pattern { Actions }指令。
Pattern | Actions |
{ for( i=2; i <= NF; i++) Number[$i]++ } | |
END | { for(course in Number) printf("%10s %d\n", course, Number[course] )} |
2. 第一个Pattern { Actions }指令中省略了Pattern 部分。故随着每行数据的读入其Actions部分将逐次无条件被执行。以awk读入第一条记录 " Mary O.S. Arch. Discrete" 为例,因为该笔数据 NF = 4(有4个字段),故该 Action 的for Loop中i = 2,3,4。
i | $i | 最初 Number[$i] | Number[$i]++ 之后 |
2 | "O.S." | AWK default Number["O.S."] = 0 | 1 |
3 | "Arch." | AWK default Number["Arch."] = 0 | 1 |
4 | "Discrete" | AWK default Number["Discrete"] = 0 | 1 |
3. 第二个 Pattern { Actions }指令中
* END 为awk的保留字,为 Pattern 的一种。
* END 成立(其值为true)的条件是:"awk处理完所有数据,即将离开程序时。"
平常读入数据行时,END并不成立,故其后的Actions 并不被执行;唯有当awk读完所有数据时,该Actions才会被执行(注意,不管有多少行数据,END仅在最后才成立,故该Actions仅被执行一次。)
BEGIN 与 END 有点类似,是awk中另一个保留的Pattern。唯一不同的是:
"以 BEGIN 为 Pattern 的 Actions 于程序一开始执行时,被执行一次。"
4. NF 为awk的内置变量,用以表示awk正处理的数据行中,所包含的字段个数。
5. awk程序中若含有以 $ 开头的自定变量,都将以如下方式解释:
以 i= 2 为例,$i = $2 表第二个字段数据。 (实际上,$ 在 awk 中为一运算符(Operator),用以取得字段数据。)
awk程序中允许调用Shell指令,并提供管道解决awk与系统间数据传递的问题。所以awk很容易使用系统资源,读者可利用这个特点来编写某些适用的系统工具。
范例:写一个awk程序来打印出线上人数。
将下列程序建文件,命名为 count.awk
BEGIN {
while ( "who" | getline ) n++
print n
}
并执行下列命令:
$ awk -f count.awk
执行结果将会打印出目前在线人数。
说 明:
1. awk 程序并不一定要处理数据文件,以本例而言,仅输入程序文件count.awk,未输入任何数据文件。
2. BEGIN 和 END 同为awk中的一种 Pattern。以 BEGIN 为 Pattern的Actions,只有在awk开始执行程序、尚未打开任何输入文件前, 被执行一次。(注意:只被执行一次)
3. "|" 为 awk 中表示管道的符号。awk 把 管道 之前的字符串"who"当成Shell上的命令,并将该命令送往Shell执行,执行的结果(原先应打印在屏幕上的)则通过pipe送进awk程序中。
4. getline为awk所提供的输入指令。
其语法如下:
语法 |
由何处读取数据 |
数据读入后置于 |
getline var < file |
所指定的 file |
变量 var(var省略时,表示置于$0) |
| getline var |
pipe |
变量 var(var省略时,表示置于$0) |
getline var |
见 注一 |
变量 var(var省略时,表示置于$0) |
注一:当 Pattern 为 BEGIN 或 END 时,getline 将由 stdin 读取数据,否则由awk正处理的数据文件上读取数据。
getline 一次读取一行数据,若读取成功则return 1;
若读取失败则return -1;
若遇到文件结束(EOF),则return 0。
本程序使用 getline 所 return 的数据来做为 while 判断循环停止的条件,某些awk版本较旧,并不容许使用者改变 $0 的值。这种版的 awk 执行本程序时会产生 Error,读者可于 getline 之后置上一个变量 (如此,getline 读进来的数据便不会被置于 $0 ),或直接改用gawk便可解决。
本节将示范一个统计上班到达时间及迟到次数的程序。
这程序每日被执行时将读入两个数据文件:
* 员工当日到班时间的数据文件 ( 如下列的 arr.dat )
* 存放员工当月迟到累计次数的文件
当程序执行执完毕后将更新第二个数据文件的数据(迟到次数),并打印当日的报表。这程序将分成下列数小节逐步完成,其大纲如下:
7.1 在到班资料文件 arr.dat 之前增加一行抬头"ID Number Arrvial Time",并产生报表输出到文件today_rpt1 中。
<在awk中如何将数据输出到文件>
7.2 将 today_rpt1 上的数据按员工代号排序,并加注执行当日日期;产生文件 today_rpt2
<awk中如何运用系统资源及awk中Pipe的特性>
7.3 将awk程序包含在一个shell script文件中
7.4 于 today_rpt2 每日报表上,迟到者之前加上"*",并加注当日平均到班时间;产生文件 today_rpt3
7.5 从文件中读取当月迟到次数,并根据当日出勤状况更新迟到累计数。
<使用者在awk中如何读取文件数据>
某公司其员工到勤时间文件内容如下,取名为 arr.dat。文件中第一栏为员工代号,第二栏为到达时间。本范例中,将使用该文件为数据文件。
awk中并未提供如 C 语言中的fopen() 指令,也没有fprintf() 文件输出这样的指令。但awk中任何输出函数之后皆可借助使用与UNIX 中类似的 I/O 重定向符,将输出的数据重定向到指定的文件;其符号仍为 > (输出到一个新产生的文件) 或 >> ( 添加输出的数据到文件末尾 )。
例:在到班数据文件 arr.dat 之前增加一行抬头如下:"ID Number Arrival Time",并产生报表输出到文件 today_rpt1中。
建立如下文件并取名为reformat1.awk
BEGIN { print " ID Number Arrival Time" > "today_rpt1"
print "===========================" > "today_rpt1"
}
{ printf(" %s %s\n", $1,$2 ) > "today_rpt1" }
执行:
$ awk -f reformat1.awk arr.dat
执行后将产生文件 today_rpt1,其内容如下:
说 明:
1. awk程序中,文件名称 today_rpt1 的前后须以" (双引号)括住,表示 today_rpt1 为一字符串常量。若未以"括住,则 today_rpt1 将被awk解释为一个变量名称。
在awk中任何变量使用之前,并不须事先声明。其初始值为空字符串(Null string) 或 0。因此程序中若未以 " 将 today_rpt1 括住,则 today_rpt1 将是一变量,其值将是空字符串,这会在执行时造成错误(Unix 无法帮您开启一个以空字符串为文件名的文件)。
因此在编辑awk程序时,须格外留心。因为若敲错变量名称,awk在编译程序时会认为是一新的变量,并不会察觉。因此往往会造成运行时错误。
2. BEGIN 为awk的保留字,是 Pattern 的一种。
以 BEGIN 为 Pattern 的 Actions 于awk程序刚被执行尚未读取数据文件时被执行一次,此后便不再被执行。
3. 读者或许觉得本程序中的I/O重定向符号应使用 " >>" (append)而非 " >"。
本程序中若使用 ">" 将数据重定向到 today_rpt1,awk 第一次执行该指令时会产生一个新文件 today_rpt1,其后再执行该指令时则把数据追加到today_rpt1文件末,并非每执行一次就重开一个新文件。
若采用">>"其差异仅在第一次执行该指令时,若已存在today_rpt1则 awk 将直接把数据append在原文件的末尾。
这一点,与UNIX中的用法不同。
awk程序中很容易使用系统资源。这包括在程序中途调用 Shell 命令来处理程序中的部分数据;或在调用 Shell 命令后将其产生的结果交回 awk 程序(不需将结果暂存于某个文件)。这一过程是借助 awk 所提供的管道 (虽然有些类似 Unix 中的管道,但特性有些不同),及一个从 awk 中调用 Unix 的 Shell 命令的语法来达成的。
例: 承上题,将数据按员工ID排序后再输出到文件 today_rpt2,并于表头附加执行时的日期。
分 析:
1. awk 提供与 UNIX 用法近似的 pipe,其记号亦为 "|"。其用法及含意如下:
awk程序中可接受下列两种语法:
a.语法
awk output 指令 | "Shell 接受的命令"
(如: print $1,$2 | "sort -k 1")
b.语法
"Shell 接受的命令" | awk input 指令
(如: "ls " | getline)
注: awk input 指令只有 getline 一个。
awk output 指令有 print, printf() 两个。
2. 在a 语法中,awk所输出的数据将转送往 Shell,由 Shell 的命令进行处理。以上例而言,print 所输出的数据将经由 Shell 命令 "sort -k 1" 排序后再送往屏幕(stdout)。
上列awk程序中,"print$1, $2" 可能反复执行很多次,其输出的结果将先暂存于 pipe 中,等到该程序结束时,才会一并进行 "sort -k 1"。
须注意两点:不论 print $1, $2 被执行几次,
"sort -k 1" 的执行时间是 "awk程序结束时",
"sort -k 1" 的执行次数是 "一次"。
3. 在 b 语法中,awk将先调用 Shell 命令。其执行结果将通过 pipe 送入awk程序,以上例而言,awk先让 Shell 执行 "ls",Shell 执行后将结果存于 pipe,awk指令 getline 再从 pipe 中读取数据。
使用本语法时应留心:
以上例而言,awk "立刻"调用 Shell 来执行 "ls",执行次数是一次。
getline 则可能执行多次(若pipe中存在多行数据)。
4. 除上列a、b二种语法外,awk程序中其它地方如出现像 "date", "cls", "ls"... 这样的字符串,awk只把它当成一般字符串处理。
建立如下文件并取名为 reformat2.awk
# 程序 reformat2.awk
# 这程序用以练习awk中的pipe
BEGIN {
"date" | getline #Shell 执行 "date",getline 取得结果并以$0记录
print " Today is " , $2, $3 > "today_rpt2"
print "=========================" > "today_rpt2"
print " ID Number Arrival Time" > "today_rpt2"
close( "today_rpt2" )
}
{ printf( "%s %s\n", $1 ,$2 ) | "sort -k 1 >> today_rpt2" }
执行如下命令:
$ awk -f reformat2.awk arr.dat
执行后,系统会自动将 sort 后的数据追加( Append; 因为使用 " >>") 到文件 today_rpt2末端。today_rpt2 内容如下:
说 明:
1. awk程序由三个主要部分构成:
i. Pattern { Action} 指令
ii. 函数主体。 例如: function double( x ){ return 2*x } (参考第11节 Recursive Program )
iii. Comment ( 以 # 开头识别之 )
2. awk 的输入指令 getline,每次读取一行数据。若getline之后未接任何变量,则所读入的内容将以$0 记录;否则以所指定的变量储存之。
以本例而言:
执行 "date" | getline 后,
$0 的值为 "Tue Nov 19 00:15:31 CST 2013" (笔者注:该时间为笔者本机上程序的执行时间)
当 $0 的值被更新时,awk将自动更新相关的内置变量,如: $1,$2,..,NF。故 $2 的值将为"Nov",$3的值将为"19"。
(有少数旧版的awk不允许即使用者自行更新(update)$0的值,或者更新$0时,它不会自动更新 $1,$2,..NF。这情况下,可改用gawk或nawk。否则使用者也可自行以awk字符串函数split()来分隔$0上的数据)
3. 本程序中 printf() 指令会被执行12次( 因为有arr.dat中有12行数据),但读者不用担心数据被重复sort了12次。当awk结束该程序时才会 close 这个 pipe,此时才将这12行数据一次送往系统,并调用 "sort -k 1 >> today_rpt2" 处理之。
4. awk提供另一个调用Shell命令的方法,即使用awk函数
system("shell命令")
例如:
awk '
BEGIN{
system("date > date.dat")
getline < "date.dat"
print "Today is ", $2, $3
}
'
但使用 system( "shell 命令" ) 时,awk无法直接将执行中的部分数据输出给Shell 命令,且 Shell 命令执行的结果也无法直接输入到awk中。
本小节中描述如何将awk程序直接写在 shell script 之中。此后使用者执行 awk 程序时,就不需要每次都键入 " awk -f program datafile"。script 中还可包含其它 Shell 命令,如此更可增加执行过程的自动化。
建立一个简单的 awk程序 mydump.awk,如下:
{print}
这个程序执行时会把数据文件的内容 print 到屏幕上( 与cat功用类似 )。print 之后未接任何参数时,表示 "print $0"。
若欲执行该awk程序,来打印出文件 today_rpt1 及 today_rpt2 的内容时,必须于 UNIX 的命令行上执行下列命令:
方式一
awk -f mydump.awk today_rpt1 today_rpt2
方式二
awk '{print}' today_rpt1 today_rpt2
第二种方式系将awk 程序直接写在 Shell 的命令行上,这种方式仅适合较短的awk程序。
方式三 建立如下的 shell script,并取名为 mydisplay,
awk ' # 注意以下的 awk 与 ' 之间须有空白隔开
{print}
' $* # 注意以上的 ' 与 $* 之间须有空白隔开
执行 mydisplay 之前,须先将它改成可执行的文件(此步骤往后不再赘述)。
请执行如下命令:
$ chmod +x mydisplay
往后使用者就可直接把 mydisplay 当成指令,来display任何文件。
例如:
$ ./mydisplay today_rpt1 today_rpt2
说 明:
1. 在script文件 mydisplay 中,指令"awk"与第一个 ' 之间须有空格(Shell中并无" awk' "指令)。
第一个 ' 用以通知 Shell 其后为awk程序。
第二个 ' 则表示 awk 程序结束。
故awk程序中一律以"括住字符串或字符,而不使用 ' ,以免Shell混淆。
2. $* 为 shell script中的用法,它可用来代表命令行上 "mydisplay之后的所有参数"。
例如执行:
$ mydisplay today_rpt1 today_rpt2
事实上 Shell 已先把该指令转换成:
awk '
{ print}
' today_rpt1 today_rpt2
本例中,$* 用以代表 "today_rpt1 today_rpt2"。在Shell的语法中,可用 $1 代表第一个参数,$2 代表第二个参数。当不确定命令行上的参数个数时,可使用 $* 表示。
3. awk命令行上可同时指定多个数据文件。
以 $ awk -f dump.awk today_rpt1 today_rpt2hf 为例,
awk会先处理today_rpt1,再处理 today_rpt2。此时若文件无法打开,将造成错误。
例如:不存在文件"file_no_exist",则执行:
$ awk -f dump.awk file_no_exit
将产生运行时错误(无法打开文件)。
但某些awk程序 "仅" 包含以 BEGIN 为Pattern的指令。执行这种awk程序时,awk并不须开启任何数据文件。此时命令行上若指定一个不存在的数据文件,并不会产生 "无法打开文件"的错误。(事实上awk并未打开该文件)
例如执行:
$ awk 'BEGIN {print "Hello,World!!"} ' file_no_exist
该程序中仅包含以 BEGIN 为 Pattern 的 Pattern {actions},awk 执行时并不会打开任何数据文件;所以不会因不存在文件file_no_exit而产生 " 无法打开文件"的错误。
4. awk会将 Shell 命令行上awk程序(或 -f 程序文件名)之后的所有字符串,视为将输入awk进行处理的数据文件文件名。若执行awk的命令行上 "未指定任何数据文件文件名",则将stdin视为输入的数据来源,直到输入end of file( Ctrl-D )为止。
读者可以用下列程序自行测试, 执行如下命令:
$ awk -f mydump.awk #(未接任何数据文件文件名)
或
$ ./mydisplay #(未接任何数据文件文件名)
将会发现:此后键入的任何数据将逐行复印一份于屏幕上。这情况不是机器当机!是因为awk程序正处于执行中。它正按程序指示,将读取数据并重新dump一次;只因执行时未指定数据文件文件名,故awk 便以stdin(键盘上的输入)为数据来源。读者可利用这个特点,设计可与awk即时聊天的程序。
awk不仅能自动分割字段,也允许使用者改变其字段切割方式以适应各种格式的需要。使用者也可自定义函数,若有需要可将该函数单独写成一个文件,以供其它awk程序调用。
范例:承接 6.2 的例子,若八点为上班时间,请加注 "*"于迟到记录之前,并计算平均上班时间。
分析:
1. 因八点整到达者不为迟到,故仅以到达的小时数做判断是不够的;仍应参考到达时的分钟数。若 "将到达时间转换成以分钟为单位",不仅易于判断是否迟到,同时也易于计算到达平均时间。
2. 到达时间($2)的格式为 dd:dd 或 d:dd;数字当中含有一个 ":"。但文本数字交杂的数据awk无法直接做数学运算。(注:awk中字符串"26"与数字26 并无差异,可直接做字符串或数学运算,这是awk重要特色之一。但awk对文本数字交杂的字符串无法正确进行数学运算)。
解决的方法:
方法一
对到达时间($2) d:dd 或 dd:dd 进行字符串运算,分别取出到达的小时数及分钟数。
首先判断到达小时数为一位或两位字符,再调用函数分别截取分钟数及小时数。此解法需使用下列awk字符串函数:
length( 字符串 ):返回该字符串的长度。
substr( 字符串,起始位置,长度):返回从起始位置起,指定长度的子字符串。若未指定长度,则返回从起始位置到字符串末尾的子字符串。
所以:
小时数 = substr( $2, 1, length($2) - 3 )
分钟数 = substr( $2, length($2) - 2 )
方法二
改变输入列字段的切割方式,使awk切割字段后分别将小时数及分钟数隔开于二个不同的字段。
字段分隔字符 FS (field seperator) 是awk的内置变量,其默认值是空白及tab。awk每次切割字段时都会先参考FS 的内容。若把":"也当成分隔字符,则awk 便能自动把小时数及分钟数分隔成不同的字段。
故令
FS = "[ \t:]+" (注:[ \t:]+ 为一Regular Expression )
1. Regular Expression 中使用中括号 [ ... ] 表示一个字符集合,用以表示任意一个位于中括号内的字符。故可用"[ \t:]"表示 一个 空白,tab 或 ":"
2. Regular Expression中使用 "+" 形容其前方的字符可出现一次或一次以上。
故 "[ \t:]+" 表示由一个或多个 "空白,tab 或 : " 所组成的字符串。
设定 FS = "[ \t:]+" 后,数据行如: "1034 7:26" 将被分割成3个字段
字段一 | 字段二 | 字段三 |
$1 | $2 | $3 |
1034 | 7 | 26 |
明显地,awk程序中使用方法二比方法一更简洁方便。本例子中采用方法二,也借此示范改变字段切割方式的用途。
编写awk程序 reformat3,如下:
awk '
BEGIN {
FS= "[ \t:]+" #改变字段切割的方式
"date" | getline #Shell 执行 "date". getline 取得结果以$0记录
print " Today is " ,$2, $3 > "today_rpt3"
print "=========================">"today_rpt3"
print " ID Number Arrival Time" > "today_rpt3"
close( "today_rpt3" )
}
{
#已更改字段切割方式, $2表到达小时数, $3表分钟数
arrival = HM_to_M($2, $3)
printf(" %s %s:%s %s\n", $1, $2, $3, arrival > 480 ? "*": " ")|"sort -k 1 >> today_rpt3"
total += arrival
}
END{
close("today_rpt3")
close("sort -k 1 >> today_rpt3")
printf(" Average arrival time : %d:%d\n",total/NR/60, (total/NR)%60 ) >> "today_rpt3"
}
function HM_to_M( hour, min ){
return hour*60 + min
}
' $*
并执行如下指令:
$ ./reformat3 arr.dat
执行后,文件 today_rpt3 的内容如下:
说 明:
1. awk 中也允许使用者自定义函数。函数定义方式请参考本程序,function 为 awk 的保留字。HM_to_M( ) 这函数负责将所传入的小时及分钟数转换成以分钟为单位。使用者自定函数时,还有许多细节须留心,如data scope,... ( 请参考 第十节 Recursive Program)
2. awk中亦提供与 C 语言中相同的 Conditional Operator。上式printf()中使用arrival >480 ? "*" : " " 即为一例。若 arrival 大于 480 则return "*" ,否则return " "。
3. % 为awk的运算符(operator),其作用与 C 语言中的 % 相同(取余数)。
4. NR(Number of Record) 为awk的内置变量。表示awk执行该程序后所读入的记录条数。
5. awk 中提供的 close( )指令,语法如下(有两种):
① close( filename )
② close( 置于pipe之前的command )
为何本程序使用了两个 close( ) 指令:
读者应留心上述两点,才可正确控制数据输出到文件中的顺序。
6. 指令 close("sort -k 1 >> today_rpt3")中字符串 "sort -k 1 >> today_rpt3" 必须与 pipe | 后面的 Shell Command 名称一字不差,否则awk将视为二个不同的 pipe。
读者可于BEGIN{}中先令变量 Sys_call = "sort -k 1 >> today_rpt3",程序中再一律以 Sys_call 代替该字符串。
范例:承上题,从文件中读取当月迟到次数,并根据当日出勤状况更新迟到累计数。(按不同的月份累计于不同的文件)
分析:
1. 程序中自动抓取系统日期的月份名称,连接上"late.dat",形成累计迟到次数的文件名称(如:Jullate.dat,...),并以变量late_file记录该文件名。
2. 累计迟到次数的文件中的数据格式为:
员工代号(ID) 迟到次数
例如,执行本程序前文件 Novlate.dat 的内容为:
编写程序 reformat4 如下:
awk '
BEGIN {
Sys_Sort = "sort -k 1 >> today_rpt4"
Result = "today_rpt4"
# 改变字段切割的方式
# 令 Shell执行"date"; getline 读取结果,并以$0记录
FS = "[ \t:]+"
"date" | getline
print " Today is " , $2, $3 > Result
print "=========================" > Result
print " ID Number Arrival Time" > Result
close( Result )
# 从文件按中读取迟到数据, 并用数组cnt[ ]记录. 数组cnt[ ]中以
# 员工代号为下标, 所对应的值为该员工的迟到次数.
late_file = $2"late.dat"
while( getline < late_file >0 )
cnt[$1] = $2
close( late_file )
}
{
# 已更改字段切割方式, $2表小时数,$3表分钟数
arrival = HM_to_M($2, $3)
if( arrival > 480 ){
mark = "*" # 若当天迟到,应再增加其迟到次数, 且令mark 为"*".
cnt[$1]++
}
else mark = " "
# message 用以显示该员工的迟到累计数, 若未曾迟到message为空字符串
message = cnt[$1] ? cnt[$1] " times" : ""
printf("%s %2d:%2d %5s %s\n", $1, $2, $3, mark, message ) | Sys_Sort
total += arrival
}
END {
close( Result )
close( Sys_Sort )
printf(" Average arrival time : %d:%d\n", total/NR/60, (total/NR)%60 ) >> Result
#将数组cnt[ ]中新的迟到数据写回文件中
for( any in cnt )
print any, cnt[any] > late_file
}
function HM_to_M( hour, min ){
return hour*60 + min
}
' $*
执行后,today_rpt4 的内容如下:
说 明:
1. late_file 是一变量,用以记录迟到次数的文件的文件名。late_file的值由两部分构成,前半部是当月月份名称(由调用"date"取得),后半部固定为"late.dat",如: Junlate.dat。
2. 指令 getline < late_file 表示从late_file所代表的文件中读取一条记录,并存放于$0。若使用者可自行把数据放入$0,awk会自动对这新置入 $0 的数据进行字段分割。之后程序中可用$1, $2,..来表示该笔资料的字段一,字段二,...
(注:有少数awk版本不容许使用者自行将数据置于 $0,遇此情况可改用gawk或nawk)
执行getline指令时,若成功读取记录,它会返回1;若遇到文件结束,它返回0;无法打开文件则返回-1。
3. 利用 while( getline < filename >0 ) {....}可读入文件中的每一笔数据并予处理。这是awk中用户自行读取数据文件的一个重要模式。
4. 数组 cnt[ ] 以员工ID 当下标(index),其对应值表示其迟到的次数。
5. 执行结束后,利用 for(Variable in array ){...}的语法 for( any in cnt ) print any, cnt[any] > late_file
将更新过的数据重新写回到记录迟到次数的文件。该语法在前面曾有说明。
awk 每次从数据文件中只读取一行数据进行处理。awk是依照其内置变量 RS(Record Separator) 的定义将文件中的数据分隔成一行一行的Record。RS 的默认值是 "\n"(换行符),故平常awk中一行数据就是一条 Record。
但有些文件中一条Record涵盖了多行数据,这种情况下不能再以 "\n" 来分隔Records。最常使用的方法是相邻的Records之间改以 一个空白行 来隔开。
在awk程序中,令 RS = ""(空字符串)后,awk把会空白行当成来文件中Record的分隔符。显然awk对 RS = "" 另有解释方式,简略描述如下,
当 RS = "" 时:
1. 数个相邻的空白行,awk仅视成一个单一的Record Saparator。(awk不会于两个相邻的空白行之间读取一条空的Record)
2. awk会略过(skip)文件头或文件尾的空白行。故不会因为这样的空白行,造成awk多读入了两条空的记录。
请观察下例,首先建立一个数据文件 week.rpt 如下:
张长弓
GNUPLOT 入门
吴国强
Latex 简介
VAST-2 使用手册
mathematic 入门
李小华
awk Tutorial Guide
Regular Expression
该文件的开头有数行空白行,各条记录之间使用一个或数个空白行隔开。读者请细心观察,当 RS = "" 时,awk读取该数据文件的方式。
编辑一个awk程序文件 make_report 如下:
awk '
BEGIN {
FS = "\n"
RS = ""
split( "一. 二. 三. 四. 五. 六. 七. 八. 九.", C_Number, " " )
}
{
printf("\n%s 报告人 : %s \n",C_Number[NR],$1)
for( i=2; i <= NF; i++)
printf(" %d. %s\n", i-1, $i)
}
' $*
执行
$ ./make_report week.rpt
屏幕产生结果如下:
说 明:
1. 本程序同时也改变字段分隔字符( FS= "\n" ),如此一条记录中的每一行都是一个字段。
例如: awk读入的第一条记录为
张长弓
GNUPLOT 入门
其中 $1 指的是"张长弓",$2 指的是"GNUPLOT 入门"
2. 上式中的C_Number[ ]是一个数组(array),用以记录中文数字。
例如:C_Number[1] = "一.", C_Number[2] = "二."
这过程使用awk字符串函数 split( ) 来把中文数字放进数组 C_Number[ ]中。
函数 split( )用法如下:
split( 原字符串, 数组名, 分隔字符(field separator) ):
awk将依所指定的分隔字符(field separator)分隔原字符串成一个个的字段(field),并以指定的 数组 记录各个被分隔的字段。
大部分的应用程序都允许使用者在命令之后增加一些选择性的参数。执行awk时这些参数大部分用于指定数据文件文件名,有时希望在程序中能从命令行上得到一些其它用途的数据。本小节中将叙述如何在awk程序中取用这些参数。
建立文件如下,命名为 see_arg:
awk '
BEGIN {
for( i=0; i<ARGC ; i++)
print ARGV[i] # 依次印出awk所记录的参数
}
' $*
执行如下命令:
$ ./see_arg first-arg second-arg
结果屏幕出现:
说明:
1. ARGC,ARGV[ ] 为awk所提供的内置变量。
例如,当命令行为:
$ awk -vx=36 -f program1 data1 data2
或
$ awk '{ print $1 ,$2 }' data1 data2
其 ARGC 的值为 3
ARGV[0] 的值为 "awk"
ARGV[1] 的值为 "data1"
ARGV[2] 的值为 "data2"
命令行上的 "-f program1"," -vx=36",或程序部分 '{ print $1, $2}' 都不会列入 ARGC 及 ARGV[ ] 中。
2. awk 利用 ARGC 来判断应打开的数据文件个数。
但使用者可强行改变 ARGC;当 ARGC 的值被使用者设为 1 时,awk将被蒙骗,误以为命令行上并无数据文件文件名,故不会以 ARGV[1],ARGV[2],...为文件名来打开文件读取数据;但在程序中仍可通过 ARGV[1],ARGV[2],...来取得命令行上的数据。
某一程序 test1.awk 如下:
BEGIN{
number = ARGC #先用number 记住实际的参数个数.
ARGC = 2 # 自行更改 ARGC=2, awk将以为只有一个资料文件
# 仍可藉由ARGV[ ]取得命令行上的资料.
for( i=2; i<number; i++)
data[i] = ARGV[i]
}
........
于命令行上键入
$ awk -f test1.awk data_file apple orange
执行时 awk 会打开数据文件 data_file 以进行处理,但不会打开以appleo、range 为文件名的文件(因为 ARGC 被改成2)。但仍可通过ARGV[2]、ARGV[3]取得命令行上的参数 apple、orange。
3. 也可以用下列命令来达成上例的效果。
$ awk -f test2.awk -v data[2]="apple" -v data[3]="orange" data_file
执行awk程序时,awk会自动从文件中读取数据来进行处理,直到文件结束。只要将awk读取数据的来源改成键盘输入,便可设计与awk 交互的程序。本节将提供一个该类程序的范例。
范例:本节将编写一个英语生字测验的程序,它将印出中文字意,再由使用者回答其英语生字。
首先编辑一个数据文件 test.dat (内容不限,格式如下)
apple 苹果
orange 柳橙
banana 香蕉
pear 梨子
starfruit 杨桃
bellfruit 莲雾
kiwi 奇异果
pineapple 菠萝
watermelon 西瓜
编辑awk程序"c2e"如下:
awk '
BEGIN {
while( getline < ARGV[1] ){ #由指定的文件中读取测验数据
English[++n] = $1 # 最后, n 将表示题目的题数
Chinese[n] = $2
}
ARGV[1] = "-" # "-"表示由stdin(键盘输入)
srand() # 以系统时间为随机数启始的种子
question() #产生考题
}
{# awk自动读入由键盘上输入的数据(使用者回答的答案)
if( $1 != English[ind] )
print "Try again!"
else{
print "\nYou are right !! Press Enter to Continue --- "
getline
question() #产生考题
}
}
function question(){
ind = int(rand()* n) + 1 #以随机数选取考题
system("clear")
print " Press \"ctrl-d\" to exit"
printf("\n%s ", Chinese[ind] " 的英文生字是: ")
}
' $*
执行时输入如下指令:
$./c2e test.dat
屏幕将产生如下的画面:
若输入 starfruit
程序将产生
说明:
1. 参数 test.dat (ARGV[1]) 表示储存考题的数据文件文件名。awk 由该文件上取得考题资料后,将 ARGV[1] 改成 "-"。
"-" 表示由 stdin(键盘输入) 数据。键盘输入数据的结束符号 (End of file)是 ctrl-d。当 awk 读到 ctrl-d 时就停止由 stdin 读取数据。
2. awk的数学函数中提供两个与随机数有关的函数。
rand( ): 返回介于 0与1之间的(近似)随机数值。 0 < rand() < 1.
除非使用者自行制定rand()函数起始的seed,否则每次执行awk程序时,rand()都将以同一个内定的seed为起始。
srand(x):制定以x作为rand()函数起始的种子。若省略了x,则awk会以执行时的日期与时间为rand()函数起始的seed。(参考 附录 C AWK的Built-in Functions)
awk 中除了函数的参数列表(Argument List)上的参数(Arguments)外,所有变量不管于何处出现,全被视为全局变量。其生命持续至程序结束——该变量不论在function外或 function内皆可使用,只要变量名称相同所使用的就是同一个变量,直到程序结束。因递归函数内部的变量,会因它调用子函数(本身)而重复使用,故编写该类函数时应特别留心。
例如:执行
awk '
BEGIN {
x = 35
y = 45
test_variable( x )
printf("Return to main : arg1= %d, x= %d, y= %d, z= %d\n", arg1, x, y, z)
}
function test_variable( arg1 )
{
arg1++ # arg1 为参数列上的参数, 是local variable. 离开此函数后将消失.
y++ # 会改变主式中的变量 y
z = 55 # z 为该函数中新使用的变量, 主程序中变量 z 仍可被使用.
printf("Inside the function: arg1=%d, x=%d, y=%d, z=%d\n", arg1, x, y, z)
} '
结果屏幕打印出
由上可知:
此特性优劣参半,最大的坏处是程序中的变量不易被保护,特别是递归调用本身,执行子函数时会破坏父函数内的变量。
一个变通的方法是:在函数的参数列中虚列一些参数。函数执行中使用这些虚列的参数来记录不想被破坏的数据,如此执行子函数时就不会破坏到这些数据。此外awk 并不会检查调用函数时所传递的参数个数是否一致。
例如:定义递归函数如下:
function demo( arg1 ) { # 最常见的错误例子
........
for(i=1; i< 20 ; i++){
demo(x)
# 又调用本身. 因为 i 是 global variable, 故执行完该子函数后
# 原函数中的 i 已经被坏, 故本函数无法正确执行.
.......
}
..........
}
可将上列函数中的 i 虚列在该函数的参数列上,如此 i 便是一个局部变量,不会因执行子函数而被破坏。
将上列函数修改如下:
function demo( arg1, i ) {
......
for(i=1; i< 20; i++) {
demo(x) #awk不会检查呼叫函数时, 所传递的参数个数是否一致
.....
}
}
$0, $1,.., NF, NR,..也都是 global variable,读者于递归函数中若有使用这些内置变量,也应另外设立一些局部变量来保存,以免被破坏。
范例:以下是一个常见的递归调用范例。它要求使用者输入一串元素(各元素间用空白隔开) 然后打印出这些元素所有可能的排列。
编辑如下的awk程序,取名为 permu
awk '
BEGIN {
print "请输入排列的元素,各元素间请用空白隔开"
getline
permutation($0, "")
printf("\n共 %d 种排列方式\n", counter)
}
function permutation( main_lst, buffer, new_main_lst, nf, i, j )
{
$0 = main_lst # 把main_lst指定给$0之后awk将自动进行字段分割.
nf = NF # 故可用 NF 表示 main_lst 上存在的元素个数.
# BASE CASE : 当main_lst只有一个元素时.
if( nf == 1){
print buffer main_lst #buffer的内容再加上main_lst就是完成一次排列的结果
counter++
return
}
# General Case : 每次从 main_lst 中取出一个元素放到buffer中
# 再用 main_lst 中剩下的元素 (new_main_lst) 往下进行排列
else for( i=1; i<=nf ;i++)
{
$0 = main_lst # $0为全局变量已被破坏, 故重新把main_lst赋给$0,令awk再做一次字段分割
new_main_lst = ""
for(j=1; j<=nf; j++) # 连接 new_main_lst
if( j != i )
new_main_lst = new_main_lst " " $j
permutation( new_main_lst, buffer " " $i )
}
}
' $*
执行
$ ./permu
屏幕上出现提示信息,若输入 1 2 3 回车,结果打印出:
说明:
1. 有些较旧版的awk,并不容许使用者指定$0的值。此时可改用gawk 或 nawk。否则也可自行使用 split() 函数来分割 main_lst。
2. 为避免执行子函数时破坏 new_main_lst, nf, i, j 故把这些变量也列于参数列上。如此,new_main_lst, nf, i, j 将被当成局部变量,而不会受到子函数中同名的变量影响。读者声明函数时,参数列上不妨将这些 "虚列的参数" 与真正用于传递信息的参数间以较长的空白隔开,以便于区别。
3. awk 中欲将字符串concatenation(连接)时,直接将两字符串并置即可(Implicit Operator)。
例如:
awk '
BEGIN{
A = "This "
B = "is a "
C = A B "key." # 变量A与B之间应留空白,否则"AB"将代表另一新变量.
print C
} '
结果将印出
4. awk使用者所编写的函数可再重用,并不需要每个awk式中都重新编写。
将函数部分单独编写于一文件中,当需要用到该函数时再以下列方式include进来。
$ awk -f 函数文件名 -f awk主程序文件名 数据文件文件名
awk 通过判断 Pattern 的值来决定是否执行其后所对应的Actions。这里列出几种常见的Pattern:
BEGIN 为 awk 的保留字,是一种特殊的 Pattern。
BEGIN 成立(其值为true)的时机是:
"awk 程序一开始执行,尚未读取任何数据之前。"
所以在 BEGIN { Actions } 语法中,其 Actions 部份仅于程序一开始执行时被执行一次。当 awk 从数据文件读入数据行后, BEGIN 便不再成立,故不论有多少数据行,该 Actions 部份仅被执行一次。
一般常把 "与数据文件内容无关" 与 "只需执行一次" 的部分置于该Actions(以 BEGIN 为 Pattern)中。
例如:
BEGIN {
FS = "[ \t:]" # 于程序一开始时, 改变awk切割字段的方式
RS = "" # 于程序一开始时, 改变awk分隔数据行的方式
count = 100 # 设定变量 count 的起始值
print " This is a title line " # 印出一行 title
}
.......
# 其它 Pattern { Actions }
.....
有些awk程序甚至"不需要读入任何数据行"。遇到这情况可把整个程序置于以 BEGIN 为 Pattern的 Actions 中。
例如:
BEGIN { print " Hello ! the Word ! " }
注意:执行该类仅含 BEGIN { Actions } 的程序时,awk 并不会开启任何数据文件进行处理。
END 为 awk 的保留字,是另一种特殊的 Pattern。
END 成立(其值为true)的时机与 BEGIN 恰好相反,为:
"awk 处理完所有数据,即将离开程序时"
平常读入数据行时,END并不成立,故其对应的 Actions 并不被执行;唯有当awk读完所有数据时,该 Actions 才会被执行。
注意:不管数据有多少行,该 Actions 仅被执行一次。
使用像 " A 关系运算符 B" 的表达式当成 Pattern。
当 A 与 B 存在所指定的关系(Relation)时,该 Pattern 就算成立(true)。
例如:
length($0) <= 80 { print $0 }
上式中 length($0) <= 80 是一个 Pattern,当 $0(数据行)的长度小于等于80时该 Pattern 的值为true,将执行其后的 Action (打印该行数据)。
awk 中提供下列 关系运算符(Relation Operator)
运算符 | 含意 |
> | 大于 |
< | 小于 |
>= | 大于或等于 |
<= | 小于或等于 |
== | 等于 |
!= | 不等于 |
~ | match |
!~ | not match |
上列关系运算符除~(match)与!~(not match)外,与 C 语言中的含意一致。
~(match) 与!~(match) 在 awk 的含意简述如下:
若 A 为一字符串,B 为一正则表达式:
例如:
$0 ~ /program[0-9]+\.c/ { print $0 }
$0 ~ /program[0-9]+\.c/ 整个是一个 Pattern,用来判断$0(数据行)中是否含有可 match /program[0-9]+\.c/ 的子字符串,若$0 中含有该类字符串,则执行 print (打印该行数据)。
Pattern 中被用来比对的字符串为$0 时(如本例),可仅以正则表达式部分表示整个Pattern。故本例的 Pattern 部分$0 ~/program[0-9]+\.c/ 可仅用/program[0-9]+\.c/表之(有关匹配及正则表达式请参考 附录 E )
直接使用正则表达式当成 Pattern,此为 $0 ~ 正则表达式 的简写。
该 Pattern 用以判断 $0(数据行) 中是否含有匹配该正则表达式的子字符串,若含有,该式成立(true),则执行其对应的 Actions。
例如:
/^[0-9]*$/ { print "This line is an integer !" }
与
$0 ~ /^[0-9]*$/ { print "This line is an integer !" }
相同。
之前所介绍的各种 Patterns,其计算后结果为一逻辑值(True or False)。awk 中逻辑值彼此间可通过&&(and)、||(or)、!(not) 结合成一个新的逻辑值。故不同 Patterns 彼此可通过上述结合符号来结合成一个新的 Pattern。如此可进行复杂的条件判断。
例如:
FNR >= 23 && FNR <= 28 { print " " $0 }
上式利用&& (and) 将两个 Pattern 求值的结果合并成一个逻辑值。该式将数据文件中 第23行 到 28行 向右移5格(先输出5个空白字符)后输出。( FNR 为awk的内置变量, 请参考 附录 D )
遇到这种 Pattern(笔者注:逗号表达式),awk 会帮您设立一个 switch(或flag)。
该 Pattern 成立的条件是:
当这个 switch 被打开(turn on)时 (包括 Pattern1 或 Pattern2 成立的情况)
例 如:
FNR >= 23 && FNR <= 28 { print " " $0 }
可改写为
FNR == 23 , FNR == 28 { print " " $0 }
说 明:
当 FNR >= 23 时,awk 就 turn on 这个 switch;因为随着数据行的读入,awk不停的累加 FNR。当 FNR = 28 时,Pattern2 (FNR == 28) 便成立,这时 awk 会关上这个 switch。
当 switch 打开的期间,awk 会执行 print " " $0
( FNR 为awk的内置变量, 请参考 附录 D )
Actions 是由下列指令(statement)所组成:
1 表达式 ( 函数调用,赋值...)
2 print 表达式列表
3 printf( 格式化字符串, 表达式列表)
4 if( 表达式 ) 语句 [else 语句]
5 while( 表达式 ) 语句
6 do 语句 while( 表达式)
7 for( 表达式; 表达式; 表达式) 语句
8 for( variable in array) 语句
9 delete
10 break
11 continue
12 next
13 exit [表达式]
14 语句
awk 中大部分指令与 C 语言中的用法一致,此处仅介绍较为常用或容易混淆的指令的用法。
语法:
if(表达式) 语句1 [else 语句2 ]
范例:
if( $1 > 25 )
print "The 1st field is larger than 25"
else
print "The 1st field is not larger than 25"
(a)与 C 语言中相同,若 表达式 计算(evaluate)后的值不为 0 或 空字符串,则执行 语句1;否则执行 语句2。
(b)进行逻辑判断的表达式所返回的值有两种,若最后的逻辑值为true,则返回1;否则返回0。
(c)语法中else 语句2 以[ ] 前后括住表示该部分可视需要而予加入或省略。
语法:
while( 表达式 ) 语句
范例:
while( match(buffer,/[0-9]+\.c/ ) )
{
print "Find :" substr( buffer,RSTART, RLENGTH)
buff = substr( buffer, RSTART + RLENGTH)
}
上列范例找出 buffer 中所有能匹配 /[0-9]+.c/(数字之后接上 ".c"的所有子字符串)。范例中 while 以函数 match( )所返回的值做为判断条件。若buffer 中还含有匹配指定条件的子字符串(match成功),则 match()函数返回1,while 将持续进行其后的语句。
语法:
do 语句 while(表达式)
范例:
do{
print "Enter y or n ! "
getline data
} while( data !~ /^[YyNn]$/)
(a)上例要求用户从键盘上输入一个字符,若该字符不是Y, y, N, 或 n则会不停执行该循环,直到读取正确字符为止。
(b)do-while 指令与 while 指令 最大的差异是:do-while 指令会先执行 语句 而后再判断是否应继续执行。所以,无论如何其 语句 部分至少会执行一次。
语法:
for(variable in array ) 语句
范例:执行下列命令
awk '
BEGIN{
X[1]= 50; X[2]= 60; X["last"]= 70
for( any in X )
printf("X[%s] = %d\n", any, X[any] )
}'
结果输出:
(a)这个 for 指令,专用以查找数组中所有的下标值,并依次使用所指定的变量予以记录。以本例而言,变量 any 将逐次代表 "last"、1及2。
(b)以这个 for 指令,所查找出的下标的值彼此间并无任何次序关系。
(c)第5节中有该指令的使用范例及解说。
语法:
for(表达式1; 表达式2; 表达式3) 语句
范例:
for(i=1; i< =10; i++)
sum = sum + i
说明:
(a)上列范例用以计算 1 加到 10 的总和。
(b)表达式1 常用于设定该 for 循环的起始条件,如上例中的 i=1
表达式2 常用于设定该循环的停止条件,如上例中的 i <= 10
表达式3 常用于改变 counter 的值,如上例中的 i++
break 指令用以强迫中断(跳出) for, while, do-while 等循环。
范例:
while( getline < "datafile" > 0 )
{
if( $1 == 0 )
break
else
print $2 / $1
}
上例中,awk 不断地从文件 datafile 中读取资料,当$1等于0时就停止该循环。
循环中的 语句 进行到一半时,执行 continue 指令来略过循环中尚未执行的 语句。
范例:
for( index in X_array )
{
if( index !~ /[0-9]+/ )
continue
print "There is a digital index", index
}
上例中若 index 不为数字则执行 continue,故将略过(不执行)其后的指令。
需留心 continue 与 break 的差异:执行 continue 只是跳过其后未执行的statement,但并未跳出该循环。
执行 next 指令时,awk 将跳过位于该指令(next)之后的所有指令(包括其后的所有Pattern { Actions }),接著读取下一行数据,继续从第一个 Pattern {Actions} 执行起。
范例:
/^[ \t]*$/ { print "This is a blank line! Do nothing here !"
next
}
$2 != 0 { print $1, $1/$2 }
上例中,当 awk 读入的数据行为空白行时( match /^[ \]*$/ ),除打印消息外,只执行 next,故 awk 将跳过其后的指令,继续读取下一行数据,从头(第一个 Pattern { Actions })执行起。
执行 exit 指令时,awk将立刻跳出(停止执行)该awk程序。
该指令与 C 语言中的用法相同,可通过该指令控制数据输出时的格式。
语法:
printf("format", item1, item2,.. )
范例:
id = "BE-2647"; ave = 89
printf("ID# : %s Ave Score : %d\n", id, ave)
(a)结果印出:
(b)format 部分是由 一般的字串(String Constant) 及 格式控制字符(Formatcontrol letter, 其前会加上一个%字符)所构成。以上式为例,"ID# : " 及 " Ave Score : " 为一般字串,%s 及 %d 为格式控制字符。
(c)打印时,一般字串将被原封不动地打印出来。遇到格式控制字符时,则依序把 format后方的 item 转换成所指定的格式后进行打印。
(d)有关的细节,读者可从介绍 C 语言的书籍上得到较完整的介绍。
(e)print 及 printf 两个指令,其后可使用 > 或 >> 将输出到stdout 的数据重定向到其它文件,7.1 节中有完整的范例说明。
范例:
id = "BE-267"; ave = 89
print "ID# :", id, "Ave Score :"ave
(a)结果印出:
(b)print 之后可接上字串常数(Constant String)或变量。它们彼此间可用"," 隔开。
(c)上式中,字串 "ID# :" 与变量 id 之间使用","隔开,打印时两者之间会以自动 OFS(请参考 附录D 內建变量 OFS) 隔开。OFS 的值一般內定为 "一个空格"
(d)上式中,字串 "Ave Score :" 与变量ave之间并未以","隔开,awk会将这两者先当成字串concate在一起(变成"Ave Score :89")后,再予打印
语法:
语法 |
由何处读取数据 |
数据读入后置于 |
getline var < file |
所指定的 file |
变量 var(var省略时表示置于$0) |
| getline var |
pipe 变量 |
变量 var(var省略时表示置于$0) |
getline var |
见 注一 |
变量 var(var省略时表示置于$0) |
注一:当Pattern为BEGIN或END时,getline将由stdin读取数据,否则由awk正处理的文件上读取数据。
getline 一次读取一行数据,若读取成功则return 1;若读取失败则return -1;若遇到文件结束(EOF)则return 0。
该指令用以关闭一个打开的 文件 或 pipe (见下例)
范例:
awk '
BEGIN { print "ID # Salary" > "data.rpt" }
{ print $1 , $2 * $3 | "sort -k 1 > data.rpt" }
END { close( "data.rpt" )
close( "sort -k 1 > data.rpt" )
print " There are", NR, "records processed."
} '
说明:
(a)上例中, 一开始执行 print "ID # Salary" > "data.rpt" 指令来输出一行抬头。它使用 I/O Redirection ( > )将数据转输出到data.rpt,此时文件 data.rpt 是处于 Open 状态。
(b)指令 print $1, $2 * $3 不停的将输出的数据送往 pipe(|),awk在程序将结束时才会调用 shell 使用指令 "sort -k 1 > data.rpt" 来处理 pipe 中的数据;并未立即执行,这点与 Unix 中pipe的用法不尽相同。
(c)最后希望在文件 data.rpt 的末尾处加上一行 "There are....."。但此时,Shell尚未执行 "sort -k 1 > data.rpt",故各行数据排序后的 ID 及 Salary 等数据尚未写入data.rpt。所以得命令 awk 提前先通知 Shell 执行命令 "sort -k 1 > data.rpt" 来处理 pipe 中的数据。awk中这个动作称为 close pipe,通过执行 close ( "shell command" )来完成。需留心 close( )指令中的 shell command 需与"|"后方的 shell command 完全相同(一字不差),较佳的方法是先为该字串定义一个简短的变量,程序中再以此变量代替该shell command。
(d)为什么执行 close("data.rpt")?因为 sort 完后的资料也将写到data.rpt,而该文件正为awk所打开使用(write)中,故awk程序中应先关闭data.rpt,以免造成因两个 进程 同时打开一个文件进行输出(write)所产生的错误。
该指令用以执行 Shell上的 command。
范例:
DataFile = "invent.rpt"
system( "rm " DataFile )
说明:
(a)system("字符串")指令接受一个字符串当成Shell的命令。上例中,使用一个字串常数"rm " 连接(concate)一个变量 DataFile 形成要求 Shell 执行的命令。Shell 实际执行的命令为 "rm invent.rpt"。
"|" 配合 awk 输出指令,可把 output 到 stdout 的数据继续转送给Shell 上的某一命令当成input的数据。"|" 配合 awk getline 指令, 可调用 Shell 执行某一命令,再以 awk 的 getline 指令将该命令的所产生的数据读进 awk 程序中。
范例:
{ print $1, $2 * $3 | "sort -k 1 > result" }
"date" | getline Date_data
读者请参考7.2 节,其中有完整的范例说明。
awk 程序中常使用数组(Array)来保存大量数据,delete 指令便是用来释放数组中的元素所占用的内存空间。
范例:
for( any in X_arr )
delete X_arr[any]
读者请留心,delete 指令一次只能释放数组中的一个元素。
+(加)、 -(減)、 *(乘)、 /(除)、 %(求余数)、 ^(指数) 与 C 语言中用法相同。
=、 +=、 -=、 *=、 /=、 %=、 ^=
x += 5 的意思为 x = x + 5,其余类推。
语法:
判断条件 ? value1 : value2
若 判断条件 成立(true) 则返回 value1,否则返回 value2。
&&( and )、 ||(or)、 !(not)
Extended Regular Expression 中使用 "|" 表示 or 请勿混淆。
>、 >=、 <、 <=、 ==、 !=、 ~、 !~
+(正号)、 -(负号)、 ++(Increment Operator)、 - -(Decrement Operator)
按优先级从高到低排列:
$ |
字段运算元,例如: i=3; $i表示第3个字段 |
^ | 指数运算 |
+, -, ! | 正、负号,及逻辑上的 非 |
* ,/ ,% | 乘,除,余数 |
+ ,- | 加,減 |
>, > =,< , < =, ==, != | 关系运算符 |
~, !~ | match, not match |
&& | 逻辑上的 and |
|| | 逻辑上的 or |
? : | 条件运算符 |
= , +=, -=,*=, /=, %=, ^= | 赋值运算符 |
若原字串中含有欲寻找的子字串,则返回该子字串在原字串中第一次出现的位置,若未曾出现该子字串则返回0。
例如:
$ awk 'BEGIN{ print index("8-12-94","-") }'
结果打印 2
例如:
$ awk 'BEGIN { print length("John") }'
结果打印 4
awk会在原字串中查找合乎正则表达式的子字串,若合乎条件的子字串有多个,则以原字串中最左方的子字串为准。awk找到该字串后会依此字串为依据进行下列动作:
1. 设定awk內建变量 RSTART、RLENGTH:
RSTART = 合条件的子字串在原字串中的位置。
= 0 ;若未找到合条件的子字串。
RLENGTH = 合条件的子字串长度。
= -1 ;若未找到合条件的子字串。
2. 返回 RSTART 的值.
例如:
awk ' BEGIN {
match( "banana", /(an)+/ )
print RSTART, RLENGTH
} '
结果打印 2 4
awk将依所指定的分隔字符(field separator)来分隔原字串成一个个的字段(field),并以指定的数组记录各个被分隔的字段。
例如:
ArgLst = "5P12p89"
split( ArgLst, Arr, /[Pp]/)
执行后: Arr[1]=5, Arr[2]=12, Arr[3]=89
该函数的用法与 awk 或 C 的输出函数printf()相同。所不同的是sprintf()会将要求印出的结果当成一个字串返回。一般最常使用sprintf()来改变数据格式。如:x 为一数值数据,若欲将其变成一个含二位小数的数据,可执行如下指令:
x = 28
x = sprintf("%.2f",x)
执行后: x = "28.00"
sub( )将原字串中第一个(最左边)合乎所指定的正则表达式的子字串改以新字串取代。
1. 第二个参数"新字串"中可用"&"来代表"合乎条件的子字串"。承上例,执行下列指令:
A = "a6b12anan212.45an6a"
sub( /(an)+[0-9]*/, "[&]", A)
print A
结果打印 ab12[anan212].45an6a
2. sub()不仅可执行替换(replacement)的功用,当第二个参数为空字串("")时,sub()所执行的是"去除指定字串"的功用。
3. 通过 sub() 与 match() 的搭配使用,可逐次取出原字串中合乎指定条件的所有子字串。
例如执行下列程序:
awk '
BEGIN {
data = "p12-P34 P56-p61"
while( match( data ,/[0-9]+/) > 0) {
print substr(data, RSTART, RLENGTH )
sub(/[0-9]+/,"",data)
}
}'
结果打印:
4. sub( )中第三个参数(原字串)若未指定,则其缺省值为$0。
可用 sub( /[9-0]+/,"digital" ) 表示 sub(/[0-9]+/,"digital",$0 )
这个函数与 sub()一样,同样是进行字串取代的函数。唯一不同点是
1. gsub()会取代所有合条件的子字串。
2. gsub()会返回被取代的子字串个数。
请参考 sub()。
返回从起始位置起,指定长度的子字串。若未指定长度,则返回起始位置到字串末尾的子字串。
例如:
$ awk 'BEGIN { print substr("User:Wei-Lin Liu", 6)}'
结果打印 Wei-Lin Liu
例如:
int(7.8) 将返回 7
int(-7.8) 将返回 -7
例如:
sqrt(9) 将返回 3
若 x 为负数,则执行 sqrt(x) 时将造成 Run Time Error (笔者注:本机上提示的是"-nan",如下图)
例如:
exp(1) 将返回 2.71828
例如:
log(exp(1)) 将返回 1 (笔者注:本机上log(e)打印出来是-inf,所以用exp(1)代替e)
若 x< 0,则执行 sqrt(x)时将造成 Run Time Error(笔者注:本机上提示的是"nan",同上)
除非使用者自行指定rand()函数起始的种子,否则每次执行awk程式时,rand()函数都将使用同一个缺省的种子来产生随机数。
若省略了x,则awk会以执行时的日期与时间为rand()函数起始的种子。
因内置变量的个数不多,此处按其相关性分类说明,并未按其字母顺序排列。
ARGC表示命令行上除了选项 -F, -v, -f 及其所对应的参数之外的所有参数的个数。若将"awk程序"直接写在命令列上,则 ARGC 亦不将该"程序部分"列入计算。
ARGV数组用以记录命令列上的参数。
例:执行下列命令
$ awk -F\t -v a=8 -f prg.awk file1.dat file2.dat
或
$ awk -F\t -v a=8 '{ print $1 * a }' file1.dat file2.dat
执行上述任一程序后
ARGC = 3
ARGV[0] = "awk"
ARGV[1] = "file1.dat"
ARGV[2] = "file2.dat"
读者请留心:当 ARGC = 3 时,命令行上仅指定了 2 个文件。
注:
-F\t 表示以 tab 为字段分隔字符 FS(field seporator)。
-v a=8 用以初始化程序中的变量 a。
FILENAME用以表示目前正在处理的文件的文件名。
字段分隔字符。
表示目前awk所读入的数据行。
分別表示所读入的数据行的第一个字段,第二个字段,...(参考下列说明)
当awk读入一行数据 "A123 8:15" 时,会先以$0 记录,故 $0 = "A123 8:15"。若程序中进一步使用了 $1, $2.. 或 NF 等内置变量时,awk才会自动分割 $0以便取得字段相关的数据,切割后各个字段的数据会分別以$1, $2, $3...记录。
awk缺省(default)的 字段分隔字符(FS) 为 空白字符(空格及tab)。以本例而言,读者若未改变 FS,则分割后:
第一个字段($1)="A123", 第二个字段($2)="8:15"。
使用者可用正则表达式自行定义 FS。awk每次需要分割数据行时,都会参考目前FS的值。
例如:
令 FS = "[ :]+" 表示任何由 空白" " 或 冒号":" 所组成的字串都可当成分隔字符,则分割后:
第一个字段($1) = "A123",第二个字段($2) = "8",第三个字段($3) = "15"
NR 表示从 awk 开始执行该程序后所读取的数据行数。
FNR 与 NR 功用类似,不同的是awk每打开一个新的文件,FNR 便从 0 重新累计。
NF表示目前的数据行所被切分的字段数。awk 每读入一行数据后,在程序中可用 NF 来得知该行数据包含的字段个数。在下一行数据被读入之前,NF 并不会改变。但使用者若自行使用$0来记录数据,例如:使用 getline,此时 NF 将代表新的 $0 上所记载的数据的字段个数。
输出时的字段分隔字符。缺省为 " "(一个空白),详见下面说明。
输出时数据行的分隔字符。缺省为 "\n"(换行),见下面说明。
数值数据的输出格式。缺省为 "%.6g"(若须要时最多打印6位小数)
当使用 print 指令一次打印多项数据时,
例如:print $1, $2
输出时,awk会自动在 $1 与 $2 之间补上一个 OFS 的值(缺省为一个空白)。
每次使用 print 输出后,awk会自动补上 ORS 的值(缺省为换行符)。
使用 print 输出数值数据时,awk将采用 OFMT 的值为输出格式。
例如:
$ awk 'BEGIN { print 2/3,1; OFS=":"; OFMT="%.2g"; print 2/3,1 }'
输出:
程序中通过改变OFS和OFMT的值,改变了指令 print 的输出格式。
RS( Record Separator):awk从文件上读取数据时,将根据 RS 的定义把数据切割成许多记录,而awk一次仅读入一条记录进行处理。
RS 的缺省值是 "\n",所以一般 awk一次仅读入一行数据。有时一个Record含括了几行数据(Multi-line Record),这情況下不能再以"\n"
来分隔相邻的记录,可改用 空白行 来分隔。
在awk程序中,令 RS = "" 表示以 空白行 来分隔相邻的记录。
与使用字串函数 match( )有关的变量,详见下面说明。
与使用字串函数match( )有关的变量。
当使用者使用 match(...) 函数后,awk会将 match(...) 执行的结果以RSTART、RLENGTH 记录。
请参考 附录 C awk的内置函数 match()。
SUBSEP(Subscript Separator) 数组下标的分隔字符,缺省值为"\034"。
实际上,awk中的 数组 只接受 字串 当它的下标,如: Arr["John"]。但使用者在 awk 中仍可使用 数字 当阵列的下标,甚至可使用多维的数组(Multi-dimenisional Array) 如:Arr[2,79]。事实上,awk在接受 Arr[2,79] 之前,就已先把其下标转换成字串"2\03479",之后便以Arr["2\03479"] 代替 Arr[2,79]。
可参考下例:
awk 'BEGIN {
Arr[2,79] = 78
print Arr[2,79]
print Arr[ 2 , 79 ]
print Arr["2\03479"]
idx = 2 SUBSEP 79
print Arr[idx]
}
' $*
执行结果:
UNIX 中提供了许多 指令 和 tools,它们具有在文件中 查找(Search)字串或替换(Replace)字串 的功能。像 grep, vi , sed, awk,...
不论是查找字串或替换字串,都得先告诉这些指令所要查找(被替换)的字串为何。若未能预先明确知道所要查找(被替换)的字串为何,只知该字串存在的范围或特征时,例如:
(一)查找 "T0.c", "T1.c", "T2.c".... "T9.c" 当中的任一字串。
(二)查找至少存在一个 "A"的任意字串。
这情況下,如何告知执行查找字串的指令所要查找的字串为何。
例 (一) 中,要查找任一在 "T" 与 ".c" 之间存在一个阿拉伯数字的字串,当然您可以列举的方式,一一把所要查找的字串告诉执行命令的指令。但例 (二) 中合乎该条件的字串有无限种可能,势必无法一一列举。此时,便需要另一种字串表示的方法(协定)。
正则表达式(以下简称 Regexp)是一种字串表达的方式。可用以指定具有某特征的所有字串。
注:为区別于一般字串,本附录中代表 Regexp 的字串之前皆加 "Regexp"。
注:awk 程序中常以 /..../ 括住 Regexp,以区別于一般字串。
普通字符:除了 . * [ ] + ? ( ) \ ^ $ 外的所有字符。
由普通字符所组成的Regexp其意义与原字串字面意义相同。
例如:Regexp "the" 与一般字串的 "the" 代表相同的意义。
. (Meta character):用以代表任意一字符。
须留心 UNIX Shell 中使用 "*"表示 Wild card(通配符),可用以代表任意长度的字串。而 Regexp 中使用 "." 来代表一个任意字符(注意:并非任意长度的字串)。Regexp 中 "*" 另有其它涵意,并不代表任意长度的字串。
^ 表示该字串必须出现于行首。
$ 表示该字串必须出现于行末。
例如:
Regexp /^The/ 用以表示所有 "The"出现于行首 的字串 。
Regexp /The$/ 用以表示所有 "The"出现于行末 的字串。
\ 将特殊字符还原成字面意义的字符(Escape character)。
Regexp 中特殊字符将被解释成特定的意义,若要表示特殊字符的字面(literal meaning)意义时,在特殊字符之前加上"\"即可。
例如:
使用Regexp来表示字串 "a.out"时,不可写成 /a.out/。因为 "."是特殊字符,表示任一字符。可符合 Regexp / a.out/ 的字串将不只 "a.out" 一个;字串 "a2out"、"a3out"、"aaout" ...都符合 Regexp /a.out/ 。正确的用法为:/ a\.out/
[...] 字符集合,用以表示两中括号间所有的字符当中的任一个。
例如:
Regexp /[Tt]/ 可用以表示字符 "T" 或 "t"。故 Regexp /[Tt]he/ 表示 字串 "The" 或 "the"。字符集合 [...] 内不可随意留空白。
例如:
Regexp /[ Tt ]/ 其中括号内有空白字符,除表示"T"、"t" 中任一个字符,也可代表一个 " "(空白字符)。
- 字符集合中可使用 "-" 来指定字符的区间。
例如:
Regexp /[0-9]/ 等于 /[0123456789]/ ,用以表示任意一个阿拉伯数字。
同理 Regexp /[A-Z]/ 用以表示任意一个大写英文字母。
但应留心:
Regexp /[0-9a-z]/ 并不等于 /[0-9][a-z]/ ;前者表示一个字符,后者表示两个字符。
Regexp /[-9]/ 或 /[9-]/ 只代表字符 "9"或 "-"。
[^...] 使用[^..] 产生字符集合[..]的补集(complement set)。
例如:
要指定 "T" 或 "t" 之外的任一个字符,可用 /[^Tt]/ 表示。
同理 Regexp /[^a-zA-Z]/ 表示英文字母之外的任一个字符。
须留心:
"^" 的位置:"^"必须紧接於"["之后,才代表字符集合的补集。
例如:
Regexp /[0-9\^]/ 只是用以表示一个阿拉伯数字或字符"^"。
* 形容字符重复次数的特殊字符。"*" 形容它前方的字符可以不出现,也可以出现 1 次或多次。
例如:
Regexp /T[0-9]*\.c/ 中 * 形容其前 [0-9] (一个阿拉伯数字)出现的次数可为 0次或 多次,故Regexp /T[0-9]*\.c/ 可用以表示"T.c"、"T0.c"、"T1.c"、...、"T19.c"。
+ 形容其前的字符出现一次或一次以上。
例如:
Regexp /[0-9]+/ 用以表示一位或一位以上的数字。
? 形容其前的字符可出现一次或不出现。
例如:
Regexp /[+-]?[0-9]+/ 表示数字(一位以上)之前可出现正负号或不出现正负号。
(...) 用以括住一群字符,且将之视成一个group(见下面说明)。
例如:
Regexp /12+/ 表示字串 "12", "122", "1222", "12222",...
Regexp /(12)+/ 表示字串 "12", "1212", "121212", "12121212"....
上式中 12 以( )括住,故 "+" 所形容的是 12,重复出现的也是 12。
| 表示逻辑上的"或"(or)
例如:
Regexp / Oranges? | apples? | water/ 可用以表示:字串 "Orange", "Oranges" 或 "apple", "apples" 或 "water"
讨论 Regexp 时,经常遇到 "某字串匹配( match )某 Regexp"的字眼。其意思为:"这个 Regexp 可被解释成该字串"。
例如:
字串 "the" 匹配(match) Regexp /[Tt]he/。
因为 Regexp /[Tt]he/ 可解释成字串 "the" 或 "The",故字串 "the" 或 "The"都匹配(match) Regexp /[Th]he/。
它们也称之为 match、not match。但函义与一般常称的 match 略有不同。
其定义如下:
A 表示一字串,B 表示一 Regular Expression
只要 A 字串中存在有子字串可 match( 一般定义的 match) Regexp B,则 A ~ B 就算成立,其值为 true,反之则为 false。
! ~ 的定义与 ~ 恰好相反。
例如:
"another" 中含有子字串 "the" 可 match Regexp /[Tt]he/ ,所以 "another" ~ /[Tt]he/ 的值为 true。
注一:有些论著不把这两个运算符( ~, !~)与 Relational Operators 归为一类。
下面列出一些应用 Regular Expression 的简例,部分范例中会更改$0 的值,若您使用的 awk不允许用户更改 $0时 请改用 gawk。
例1:
将文件中所有的字串 "Regular Expression" 或 "Regular expression" 换成 "Regexp"
awk '
{
gsub( /Regular[ \t]+[Ee]xpression/, "Regexp")
print
}
' $*
例2:
去除文件中的空白行(或仅含空白字符或tab的行)
awk '
$0 !~ /^[ \t]*$/ { print }
' $*
例3:
在文件中具有 ddd-dddd (电话号码型态,d 表示digital)的字串前加上"TEL : "
awk '
{
gsub( /[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/, "TEL : &" )
print
}
' $*
例4:
从文件的 Fullname 中分离出 路径 与 文件名
awk '
BEGIN{
Fullname = "/usr/local/bin/xdvi"
match( Fullname, /.*\//)
path = substr(Fullname, 1, RLENGTH-1)
name = substr(Fullname, RLENGTH+1)
print "path :", path," name :",name
}
' $*
结果打印:
例5:
将某一数值改以现金表示法表示(整数部分每三位加一撇,且含二位小数)
awk '
BEGIN {
Number = 123456789
Number = sprintf("$%.2f",Number)
while( match(Number,/[0-9][0-9][0-9][0-9]/ ) )
sub(/[0-9][0-9][0-9][.,]/, ",&", Number)
print Number
}
' $*
结果输出
例6:
把文件中所有具 "program数字.f"形态的字串改为"[Ref : program数字.c]"
awk ' { while( match( $0, /program[0-9]+\.f/ ) ){ Replace = "[Ref : " substr( $0, RSTART, RLENGTH-2) ".c]" sub( /program[0-9]+\.f/, Replace) } print } ' $*