转贴注:原始链接[url]http://phi.sinica.edu.tw/aspac/reports/94/94011/[/url]
AWK Tutorial Guide
中央研究院计算中心
ASPAC计划
[email]aspac@phi.sinica.edu.tw[/email]
技术报告:94011
83年12月5日
Version:2.2
--------------------------------------------------------------------------------
版权声明
Contents
Preface
Overview of AWK
Why AWK
How to get AWK.
How AWK works.
How to Compute and Print Certain Fields
Selection by Text Content and by Comparsion
Arrays in AWK
Making Shell Command in an AWK Program
A Pratical Example
Redirecting Output to Files
Using System Resources
Execute AWK Programs.
Changing Field Separator & User Define Functions
Using getline to Input file
Multi-line Record
Getting Argument on Command Line
Writing Interactive Program in AWK
Recursive Program
Appendix A Patterns
Appendix B Actions
Appendix C Built-in Funtions
Appendix D Built-in Variables
Appendix E Regular Expression
--------------------------------------------------------------------------------
Preface 前言
有关本手册 :
这是一本AWK学习指引, 其重点着重于 :
AWK 适于解决哪些问题 ?
AWK 常见的解题模式为何 ?
为使读者快速掌握AWK解题的模式及特性, 本手册系由一些较具
代表性的范例及其题解所构成; 各范例由浅入深, 彼此间相互连贯,
范例中并对所使用的AWK语法及指令辅以必要的说明. 有关AWK的
指令, 函数,...等条列式的说明则收录于附录中, 以利读者往后撰写
程式时查阅. 如此编排, 可让读者在短时间内顺畅地学会使用AWK
来解决问题. 建议读者循着范例上机实习, 以加深学习效果.
读者宜先具备下列背景 :
[a.] UNIX 环境下的简单操作及基本概念.
例如 : 档案编辑, 档案复制 及 pipe, I/O Redirection 等概念
[b.] C 语言的基本语法及流程控制指令.
(AWK 指令并不多, 且其中之大部分与 C语言中之用法一致, 本手册
中对该类指令之语法及特性不再加以繁冗的说明, 读者若欲深究,
可自行翻阅相关的 C 语言书籍)
例如 : printf(), while()...
参考书目 :
本手册是以学习指引为主要编排方式, 读者若需要有关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
--------------------------------------------------------------------------------
Overview of AWK
Why AWK
AWK 是一种程式语言. 它具有一般程式语言常见的功能.
因AWK语言具有某些特点, 如 : 使用直译器(Interpreter)不需先行
编译; 变数无型别之分(Typeless), 可使用文字当阵列的注标
(Associative Array)...等特色. 因此, 使用AWK撰写程式比起
使用其它语言更简洁便利且节省时间. AWK还具有一些内建
功能, 使得AWK擅于处理具资料列(Record), 栏位(Field)型
态的资料; 此外, AWK内建有pipe的功能, 可将处理中的资料
传送给外部的 Shell命令加以处理, 再将Shell命令处理后的
资料传回AWK程式, 这个特点也使得AWK程式很容易使用
系统资源.
由于AWK具有上述特色, 在问题处理的过程, 可轻易使用
AWK来撰写一些小工具; 这些小工具并非用来解决整个大问题,
它们只个别扮演解决问题过程的某些角色, 可藉由Shell所提供的
pipe将资料按需要传送给不同的小工具进行处理, 以解决整个
大问题. 这种解题方式, 使得这些小工具可因不同需求而被重覆
组合及使用(reuse); 也可藉此方式来先行测试大程式原型的可行性
与正确性, 将来若需要较高的执行速度时再用C语言来改写.
这是AWK最常被应用之处. 若能常常如此处理问题, 读者可以
以更高的角度来思考抽象的问题, 而不会被拘泥于细节的部份.
本手册为AWK入门的学习指引, 其内容将先强调如何撰写AWK程式,
未列入进一步解题方式的应用实例, 这部分将留待UNIX进阶手册中
再行讨论.
如何取得 AWK
一般的UNIX作业系统, 本身即附有AWK. 不同的UNIX作业系统
所附的AWK其版本亦不尽相同. 若读者所使用的系统上未附有AWK,
可透过 anonymous ftp 到下列地方取得 :
phi.sinica.edu.tw:/pub/gnu
ftp.edu.tw:/UNIX/gnu
prep.ai.mit.edu:/pub/gnu
--------------------------------------------------------------------------------
How AWK works
为便于解释AWK程式架构, 及有关术语(terminology), 先以一个
员工薪资档(emp.dat ), 来加以介绍.
A125 & Jenny &100 &210
A341 & Dan &110 &215
P158 & Max &130 &209
P148 & John &125 &220
A123 & Linda & 95 &210
档案中各栏位依次为 员工ID, 姓名, 薪资率,及 实际工时. ID
中的第一码为部门识别码. ``A'',''P''分别表示``组装''及``包装''部门.
本小节着重于说明AWK程式的主要架构及工作原理, 并对一些重要
的名词辅以必要的解 释. 由这部分内容, 读者可体会出AWK语言
的主要精神及AWK与其它语程式言的差异处. 为便于说明, 以条列
方式说明于后.
名词定义
资料列: AWK从资料档上读取资料的基本单位.以上列档案
emp.dat为例, AWK读入的
第一笔资料列是 "A125 Jenny 100 210"
第二笔资料列是 "A341 Dan 110 215"
一般而言, 一笔资料列相当于资料档上的一行资料.
(参考 : 附录 B 内建变数``RS'' )
栏位(Field) : 为资料列上被分隔开的子字串.
以资料列``A125 Jenny 100 210''为例,
第一栏 第二栏 第三栏 第四栏 ``A125'' ``Jenny'' 100 210
一般是以空白字元来分隔相邻的栏位. ( 参考 : 附录 D 内建
变数``FS'' )
如何执行AWK
于UNIX的命令列上键入诸如下列格式的指令: ( ``$''表Shell命令
列上的提示符号)
$awk 'AWK程式' 资料档档名
则AWK会先编译该程式, 然后执行该程式来处理所指定的资料档.
(上列方式系直接把程式写在UNIX的命令列上)
AWK程式的主要结构 :
AWK程式中主要语法是 Pattern { Actions}, 故常见之AWK
程式其型态如下 :
Pattern1 { Actions1 }
Pattern2 { Actions2 }
......
Pattern3 { Actions3 }
Pattern 是什么 ?
AWK 可接受许多不同型态的 Pattern. 一般常使用 ``关系判断式'
(Relational expres sion) 来当成 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''中含有可match /an/的子字串, 故此关系式
成立(true),
整个Pattern的值也是true.
相关细节请参考 附录 A Patterns, 附录 E Regular Expression
[注 一 :] 有少数AWK论着, 把 ~, !~ 当成另一类的 Operator,
并不视为一种 Relational Operator. 本手册中将这两个运算元
当成一种 Relational Operator.
Actions 是什么?
Actions 是由许多AWK指令构成. 而AWK的指令与 C 语言中的
指令十分类似.
例如 :
AWK的 I/O指令 : print, printf( ), getline..
AWK的 流程控制指令 : if(...){..} else{..}, while(...){...}...
(请参考 附录 B --- ``Actions'' )
AWK 如何处理 Pattern { Actions } ?
AWK 会先判断(Evaluate) 该 Pattern 之值, 若 Pattern 判断
(Evaluate)后之值为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 !"
AWK 如何处理{ Actions } 的语法?(缺少Pattern部分)
有时语法 Pattern { Actions }中, Pattern 部分被省略,
只剩 {Actions}.
这种情形表示 ``无条件执行这个 Actions''.
AWK 的栏位变数
AWK 所内建的栏位变数及其涵意如下 :
栏位变数 涵意 $0 为一字串, 其内容为目前 AWK 所读入的资料列. $1 代表 $0 上第一个栏位的资料. $2 代表 $0 上第二栏个位的资料. ... 其余类推
读入资料列时, AWK如何修正(update)这些内建的栏位变数
当 AWK 从资料档中读取一笔资料列时, AWK 会使用内建变数
$0 予以记录.
每当 $0 被异动时 (例如 : 读入新的资料列 或 自行变更 $0,...)
AWK 会立刻重新分析 $0 的栏位情况, 并将 $0 上各栏位的资料
用 $1, $2, ..予以记录.
AWK的内建变数(Built-in Variables)
AWK 提供了许多内建变数, 使用者于程式中可使用这些变数
来取得相关资讯.常见的内建变数有 :
内建变数 涵意 NF (Number of Fields)为一整数, 其值表$0上所存在的栏位数目. NR (Number of Records)为一整数, 其值表AWK已读入的资料列数目. FILENAMEAWK正在处理的资料档档名.
例如 : AWK 从资料档 emp.dat 中读入第一笔资料列
"A125 Jenny 100 210" 之后, 程式中:
$0 之值将是 "A125 Jenny 100 210"
$1 之值为 "A125" $2 之值为 "Jenny"
$3 之值为 100 $4 之值为 210
NF 之值为 4 $NF 之值为 210
NR 之值为 1 FILENAME 之值为 ``emp.dat''
AWK的工作流程 :
执行AWK时, 它会反复进行下列四步骤.
自动从指定的资料档中读取一笔资料列.
自动更新(Update)相关的内建变数之值. 如 : NF, NR, $0...
逐次执行程式中 所有 的 Pattern { Actions } 指令.
当执行完程式中所有 Pattern { Actions } 时, 若资料档中还
有未读取的资料, 则反覆执行步骤1到步骤4.
AWK会自动重覆进行上述4个步骤, 使用者不须于程式中
撰写这个回圈 (Loop).
--------------------------------------------------------------------------------
列印档案中指定的栏位资料并加以计算
AWK 处理资料时, 它会自动从资料档中一次读取一笔记录, 并会
将该资料切分成一个个的栏位; 程式中可使用 $1, $2,... 直接取得
各个栏位的内容. 这个特色让使用者易于用 AWK 撰写 reformatter
来改变资料格式.
[ 范例 :] 以档案 emp.dat 为例, 计算每人应发工资并列印报表.
[ 分析 :] AWK 会自行一次读入一列资料, 故程式中仅需告诉
AWK 如何处理所读入的资料列.
执行如下命令 : ( $ 表UNIX命令列上的提示符号 )
awk '{ print $2, $3 * $4 }' emp.dat
执行结果如下 :
荧幕出现 :
Jenny 21000
Dan 23650
Max 27170
John 27500
Linda 19950
说 明 :
UNIX命令列上, 执行AWK的语法为:
awk 'AWK程式' 欲处理的资料档档名.
本范例中的 程式部分 为 {print $2, $3 * $4}.
把程式置于命令列时, 程式之前后须以 ' 括住.
emp.dat 为指定给该程式处理的资料档档名.
本程式中使用 : Pattern { Actions } 语法.
Pattern Actions print $2, $3 * $4
Pattern 部分被省略, 表无任何限制条件. 故AWK读入每笔资料列
后都将无条件执行这个 Actions.
print为AWK所提供的输出指令, 会将资料输出到stdout(荧幕).
print 的参数间彼此以 ``{ ,}'' 隔开, 印出资料时彼此间会以空白
隔开.
(参考 附录 D 内建变数OFS)
将上述的 程式部分 储存于档案 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 中说明)
AWK中也提供与 C 语言中类似用法的 printf() 函数. 使用
该函数可进一步控制资料的输出格式.
编辑另一个AWK程式如下, 并取名为 pay2.awk
{ printf("\%6s Work hours: %3d Pay: %5d\", $2,\$3, $3* $4) }
执行下列命令
$awk -f pay2.awk emp.dat
执行结果荧幕出现:
Jenny Work hours: 100 Pay: 21000
Dan Work hours: 110 Pay: 23650
Max Work hours: 130 Pay: 27170
John Work hours: 125 Pay: 27500
Linda Work hours: 95 Pay: 19950
选印合乎指定条件的记录
Pattern { Action }为AWK中最主要的语法. 若某Pattern之值为真则执行它后方的 Action. AWK中常使用``关系判断式'' (Relational Expression)来当成 Pattern.
AWK中除了>, <, ==, != ,...等关系运算元( Relational Operators )外,另外提供 ~(match),!~(Not Match) 二个关系运算元. 利用这两个运算元, 可判断某字串是否包含能符合所指定 Regular Expression 的子字串. 由于这些特性, 很容易使用AWK来撰写需要字串比对, 判断的程式. [ 范例 :] 承上例,
组装部门员工调薪5%,(组装部门员工之ID.系以``A''开头)
所有员工最后之薪资率若仍低于100, 则以100计.
撰写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
结果如下 : 荧幕出现 :
A125 Jenny 105
A341 Dan 115
P158 Max 130
P148 John 125
A123 Linda 100
说 明 :
AWK的工作程序是: 从资料档中每次读入一笔资料列, 依序执行完程式中所有的 Pattern{ Action }指令 Pattern Actions
$1~/^A.*/ { $3 *= 1.05 }
$3 < 100 { $3 = 100 }
{printf("%s%8s%d\n",$1,$2,$3)}
再从资料档中读进下一笔记录继续进行处理.
第一个 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 语言中用法相同的运算子或语法将不予赘述.
第二个 Pattern { Actions } 是: $3 <100 {$3 = 100 } 若第三栏的资料内容(表薪资率)小于100, 则调整为100.
第三个 Pattern { Actions } 是: {printf("%s %-8s %d\n",$1, $2, $3 )} 省略了Pattern(无条件执行Actions), 故所有资料列调整后的资料都将被印出.
--------------------------------------------------------------------------------
AWK 中阵列的特色
AWK程式中允许使用字串当做阵列的注标(index). 利用
这个特色十分有助于资料统计工作.(使用字串当注标的阵列称为
Associative Array)
首先建立一个资料档, 并取名为 reg.dat. 此为一学生注册的
资料档; 第一栏为学生姓名, 其后为该生所修课程.
Mary O.S. Arch. Discrete
Steve D.S. Algorithm Arch.
Wang Discrete Graphics O.S.
Lisa Graphics A.I.
Lily Discrete Algorithm
AWK中阵列的特性
使用字串当阵列的注标(index).
使用阵列前不须宣告阵列名称及其大小.
例如 : 希望用阵列来记录 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."]++ .
如何取出阵列中储存的资讯
以 C 语言为例, 宣告 int Arr[100]; 之后, 若想得知 Arr[ ]中所
储存的资料, 只须用一个回圈, 如 :
for(i=0; i<00; 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
END{for(coursein Number)
printf("\%-10s %d\n", course, Number[course] )
}
执行下列命令 :
awk -f course.awk reg.dat
执行结果如下 :
Discrete 3
D.S. 1
O.S. 2
Graphics 2
A.I. 1
Arch. 2
Algorithm 2
说 明 :
这程式包含二个Pattern { Actions }指令.
Pattern Actions {for( i=2; i< NF; i++) Number[$i]++ } END { for( course in Number) printf("\%-10s \%d\n", course, Number[course] )}
第一个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
第二个 Pattern { Actions }指令中
* { END}为AWK之保留字, 为{ Pattern}之一种.
* { END}成立(其值为true)的条件是 :[0.3cm]
``AWK处理完所有资料, 即将离开程式时.
平常读入资料列时, END并不成立, 故其后的Actions
并不被执行;
唯有当AWK读完所有资料时, 该Actions才会被执行 ( 注意,
不管资料列有多少笔, END仅在最后才成立, 故该Actions仅被执行
一次.)
{ BEGIN} 与 { END} 有点类似, 是AWK中另一个保留的{Pattern}.
唯一不同的是 :
``以 { BEGIN 为 Pattern 的 Actions} 于程式一开始执行时, 被执行
一次.''
NF 为AWK的内建变数, 用以表示AWK正处理的资料计列中,
所包含的栏位个数.
AWK程式中若含有以 $ 开头的自定变数, 都将以如下方式解释 :
以 i= 2 为例, $i = $2 表第二个栏位资料. ( 实际上, $ 在 AWK 中
为一运算元(Operator), 用以取得栏位资料.)
--------------------------------------------------------------------------------
AWK 程式中使用 Shell 命令
AWK程式中允许呼叫Shell指令. 并提供pipe解决AWK与系统间
资料传递的问题. 所以AWK很容易使用系统资源. 读者可利用这个
特色来撰写某些适用的系统工具.
范例 : 写一AWK程式来列印出线上人数.
将下列程式建档, 命名为 count.awk
BEGIN {
while ( "who" | getline ) n++
print n
}
并执行下列命令 :
awk { -f} count.awk
执行结果将会印出目前线上人数
说 明 :
AWK 程式并不一定要处理资料档. 以本例而言, 仅输入程式
档count.awk, 未输入任何资料档.
BEGIN 和 END 同为AWK中之种一 Pattern. 以 BEGIN 为
Pattern之Actions,只有在AWK开始执行程式,尚未开启任何输入
档前, 被执行一次.(注意: 只被执行一次 )
``{ |}'' 为 AWK 中表示 pipe的符号. AWK 把 pipe之前的字串
''who''当成Shell上的命令, 并将该命令送往Shell执行, 执行的结果
(原先应于荧幕印出者)则藉由pipe送进AWK程式中.
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便可解决.
--------------------------------------------------------------------------------
AWK 程式的应用实例
本节将示范一个统计上班到达时间及迟到次数的程式.
这程式每日被执行时将读入二个档案 :
员工当日到班时间的资料档 ( 如下列之 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}. 档案中第一栏为
员工代号, 第二栏为到达时间. 本范例中, 将使用该档案为资料档.
1034 7:26
1025 7:27
1101 7:32
1006 7:45
1012 7:46
1028 7:49
1051 7:51
1029 7:57
1042 7:59
1008 8:01
1052 8:05
1005 8:12
--------------------------------------------------------------------------------
将资料直接输出到档案
AWK中并未提供如 C 语言中之fopen() 指令, 也未有fprintf()
档案输出之指令. 但AWK中任何输出函数之后皆可藉由使用与
UNIX 中类似的 I/O Redirection , 将输出的资料 Redirect 到指定
的档案; 其符号仍为 > (输出到一个新产生的档案) 或 >> ( append
输出的资料到档案末端 ).
[例 :]于到班资料档 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, 其内容如下 :
ID Number Arrival Time
============================
1034 7:26
1025 7:27
1101 7:32
1006 7:45
1012 7:46
1028 7:49
1051 7:51
1029 7:57
1042 7:59
1008 8:01
1052 8:05
1005 8:12
说 明 :
AWK程式中, 档案名称 today_rpt1 之前后须以" 括住, 表示
today_rpt1 为一字串常数. 若未以"括住, 则 today_rpt1 将
被AWK解释为一个变数名称.
在AWK中任何变数使用之前, 并不须事先宣告. 其初始值为空字串
(Null string) 或 0.因此程式中若未以 " 将 today_rpt1 括住,
则 today_rpt1 将是一变数, 其值将是空字串, 这会于执行时
造成错误(Unix 无法帮您开启一个以Null String为档名的档案).
* 因此在编辑 AWK程式时, 须格外留心. 因为若敲错变数名称,
AWK在编译程式时会认为是一新的变数, 并不会察觉. 如此
往往会造成 RuntimeError.
BEGIN 为AWK的保留字, 是 Pattern 的一种.
以 BEGIN 为 Pattern 的 Actions 于AWK程式刚被执行尚未读取
资料时被执行一次, 此后便不再被执行.
读者或许觉得本程式中的I/O Redirection符号应使用 `` >>''
(append)而非 `` >''.
\index{ { >} } \index{ { >>} }
[*] 本程式中若使用 ``>'' 将资料重导到 today_rpt1, AWK
第一次执行该指令时会产生一个新档 today_rpt1, 其后再
执行该指令时则把资料append到today_rpt1档末, 并非每执行
一次就重开一个新档.
[*] 若采用">>"其差异仅在第一次执行该指令时, 若已存在
today_rpt1则 AWK 将直接把资料append在原档案之末尾.
[*] 这一点, 与UNIX中的用法不同.
--------------------------------------------------------------------------------
AWK 中如何利用系统资源
AWK程式中很容易使用系统资源. 这包括于程式中途叫用 Shell
命令来处理程式中的部分资料; 或于呼叫 Shell 命令后将其产生
之结果交回 AWK 程式(不需将结果暂存于某个档案). 这过程乃是
藉由 AWK 所提供的 pipe (虽然有些类似 Unix 中的 pipe, 但特性
有些不同),及一个从 AWK 中呼叫 Unix 的 Shell command 的语法
来达成.
[例 :] 承上题, 将资料按员工ID排序后再输出到档案 today_rpt2,
并于表头附加执行时的日期.
分 析 :
AWK 提供与 UNIX 用法近似的 pipe, 其记号亦为 ``|''. 其用法及
涵意如下 :
AWK程式中可接受下列两语法 :
[a. 语法] AWK output 指令 | ``Shell 接受的命令''
( 如 : print $1,$2 | "sort +1n" )
[b. 语法] ``Shell 接受的命令'' |AWK input 指令
( 如 : "ls " | getline)
注 : AWK input 指令只有 getline 一个.
AWK output 指令有 print, printf() 二个.
于 a 语法中, AWK所输出的资料将转送往 Shell, 由 Shell 的
命令进行处理.以上例而言, print 所印出的资料将经由 Shell 命令
``sort +1n'' 排序后再送往荧幕(stdout).
上列AWK程式中, ``print$1, $2'' 可能反覆执行很多次, 其印出的
结果将先暂存于 pipe 中,
等到该程式结束时, 才会一并进行 ``sort +1n''.
须注意二点 : 不论 print \$1, \$2 被执行几次,
``sort +1n'' 之 执行时间是 ``AWK程式结束时'',
``sort +1n'' 之 执行次数是 ``一次''.
于 b 语法中, AWK将先叫用 Shell 命令. 其执行结果将经由
pipe 送入AWK程式以上例而言, AWK先令 Shell 执行 ``ls'',
Shell 执行后将结果存于 pipe, AWK指令 getline再从 pipe 中读取
资料.
使用本语法时应留心 : 以上例而言
AWK ``立刻''呼叫 Shell 来执行 ``ls'', 执行次数是一次.
getline 则可能执行多次(若pipe中存在多行资料).
除上列 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 +2n >>today_rpt2"}
执行如下命令:
awk -f reformat2.awk arr.dat
执行后, 系统会自动将 sort 后的资料加( Append; 因为使用 `` >>'')
到档案 today_rpt2末端. today_rpt2 内容如下 :
Today is Sep 17
=========================
ID Number Arrival Time
1005 8:12
1006 7:45
1008 8:01
1012 7:46
1025 7:27
1028 7:49
1029 7:57
1034 7:26
1042 7:59
1051 7:51
1052 8:05
1101 7:32
说 明 :
AWK程式由三个主要部分构成 :
[ i.] Pattern { Action} 指令
[ ii.] 函数主体. 例如 : function double( x ){ return 2*x }
(参考第11节 Recursive Program )
[ iii.] Comment ( 以 # 开头识别之 )
AWK 的输入指令 getline, 每次读取一列资料. 若getline之后
未接任何变数, 则所读入之资料将以$0 纪录, 否则以所指定的变数
储存之.
[ 以本例而言] :
执行 "date" | getline 后,
$0 之值为 "Wed Aug 17 11:04:44 EAT 1994"
当 $0 之值被更新时, AWK将自动更新相关的内建变数, 如 :
$1,$2,..,NF.故 $2 之值将为"Aug", $3之值将为"17".
(有少数旧版之AWK不允许即使用者自行更新(update)$0之值,或者
update$0时,它不会自动更新 $1,$2,..NF. 这情况下, 可改用gawk,
或nawk. 否则使用者也可自行以AWK字串函数split()来分隔$0上
的资料)
本程式中 printf() 指令会被执行12次( 因为有arr.dat中有12笔
资料), 但读者不用 担心资料被重复sort了12次. 当AWK结束该程式
时才会 close 这个 pipe , 此时才将这12笔资料一次送往系统,
并呼叫 "sort +2n >> today_rpt2" 处理之.
AWK提供另一个叫用Shell命令的方法, 即使用AWK函数
system("shell命令")
例如 :
awk '
BEGIN{
system("date > date.dat")
getline
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
说 明 :
在script档案 mydisplay 中, 指令``awk''与第一个 '
之间须有空格(Shell中并无`` awk' ''指令).
第一个 ' 用以通知 Shell 其后为AWK程式.
第二个 ' 则表示 AWK 程式结束.
故AWK程式中一律以"括住字串或字元, 而不使用 ' ,
以免Shell混淆.
$* 为 shell script中之用法, 它可用以代表命令列上 ``mydisplay
之后的所有参数''.
例如执行 :
$ mydisplay today_rpt1 today_rpt2
事实上 Shell 已先把该指令转换成 :
awk '
{ print}
' today_rpt1 today_rpt2
本例中, $* 用以代表 ``today_rpt1 today_rpt2''. 在Shell的语法中,
可用 $1 代表第一个参数, $2 代表第二个参数. 当不确定命令列上的
参数个数时, 可使用 $* 表之.
AWK命令列上可同时指定多个资料档.
以awk -f dump.awk today_rpt1 today_rpt2hf 为例
AWK会先处理today_rpt1, 再处理 today_rpt2. 此时若档案
无法开启, 将造成错误.
例如: 未存在档案"file_no_exist", 则执行 :
awk -f dump.awk file_no_exit
将产生Runtime Error(无法开启档案).
但某些AWK程式 ``仅'' 包含以 BEGIN 为Pattern的指令. 执行这种
AWK程式时, AWK并不须开启任何资料档.此时命令列上若指定
一个不存在的资料档,并不会产生 ``无法开启档案''的错误.(事实上
AWK并未开启该档案)
例如执行:
awk 'BEGIN {print "Hello,World!!"} ' file_no_exist
该程式中仅包含以 BEGIN 为 Pattern 之 Pattern {actions}, AWK
执行时并不会开启任何资料档; 故不会因不存在档案file_no_exit而
产生 `` 无法开启档案''的错误.
AWK会将 Shell 命令列上AWK程式(或 -f 程式档名)之后的所有
字串, 视为将输入AWK进行处理的资料档档名.
若执行AWK的命令列上 ``未指定任何资料档档名'', 则将stdin视为
输入之资料来源, 直到输入end of file( Ctrl-D )为止.
读者可以下列程式自行测试, 执行如下命令 :
$awk -f dump.awk (未接任何资料档档名)
或
$ mydisplay (未接任何资料档档名)
将会发现 : 此后键入的任何资料将逐行复印一份于荧幕上. 这情况
不是机器当机 ! 是因为AWK程式正处于执行中. 它正按程式指示,
将读取资料并重新dump一次; 只因执行时未指定资料档档名, 故AWK
便以stdin(键盘上的输入)为资料来源.
读者可利用这个特点, 设计可与AWK程式interactive talk的程式.
--------------------------------------------------------------------------------
改变 AWK 切割栏位的方式 & 使用者定义函数
AWK不仅能自动分割栏位, 也允许使用者改变其栏位切割方式以
适应各种格式之需要. 使用者也可自定函数, 若有需要可将该函数
单独写成一个档案,以供其它AWK程式叫用.
范例 : 承接 6.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 )
Regular Expression 中使用中括号 [ ... ] 表一字元集合,
用以表示任意一个位于两中括号间的字元.
故可用``[ \t:]''表示 一个 空白 , tab 或 ``:''
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 +0n>>today_rpt3"
total += arrival
END {
close("today_rpt3") #参考本节说明 5
close("sort +0n >> 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.doc
执行后,档案 today_rpt3 的内容如下:
Today is Sep 21
=========================
ID Number Arrival Time
1005 8:12 *
1006 7:45
1008 8:01 *
1012 7:46
1025 7:27
1028 7:49
1029 7:57
1034 7:26
1042 7:59
1051 7:51
1052 8:05 *
1101 7:32
Average arrival time : 7:49
{verbatim}
说 明 :
AWK 中亦允许使用者自定函数. 函数定义方式请参考本程式,
function 为 AWK 的保留字.
HM_to_M( ) 这函数负责将所传入之小时及分钟数转换成
以分钟为单位. 使用者自定函数时, 还有许多细节须留心, 如
data scope,..
( 请参考 第十节 Recursive Program)
AWK中亦提供与 C 语言中相同的 Conditional Operator. 上式
printf()中使用arrival >480 ? "*" : " "}即为一例
若 arrival 大于 480 则return "*" , 否则return " ".
% 为AWK之运算子(operator), 其作用与 C 语言中之 % 相同
(取余数).
NR(Number of Record) 为AWK的内建变数. 表AWK执行该程式
后所读入的纪录笔数.
AWK 中提供的 close( )指令, 语法如下(有二种) :
close( filename )
close( 置于pipe之前的command )
为何本程式使用了两个 close( ) 指令 :
指令 close( "sort +2n >> today_rpt3" ), 其意思为 close 程式中
置于 "sort +2n >> today_rpt3 " 之前的 Pipe, 并立刻呼叫 Shell 来
执行"sort +2n >> today_rpt3".
(若未执行这指令, AWK必须于结束该程式时才会进行上述动作;
则这12笔sort后的资料将被 append 到档案 today_rpt3 中
"Average arrival time : ..." 的后方)
因为 Shell 排序后的资料也要写到 today_rpt3, 所以AWK必须
先关闭使用中的today_rpt3 以利 Shell 正确将排序后的资料
append 到today_rpt3否则2个不同的 process 同时开启一
档案进行输出将会产生不可预期的结果.
读者应留心上述两点,才可正确控制资料输出到档案中的顺序.
指令 close("sort +0n >> today_rpt3")中字串 "sort +0n >> today_rpt3"
须与 pipe | 后方的 Shell Command 名称一字不差, 否则AWK将视为
二个不同的 pipe.
读者可于BEGIN{}中先令变数 Sys_call = "sort +0n >> today_rpt3",
程式中再一律以 Sys_call 代替该字串.
--------------------------------------------------------------------------------
使用 getline 来读取资料
范 例 : 承上题,从档案中读取当月迟到次数, 并根据当日出勤状况
更新迟到累计数.(按不同的月份累计于不同的档案)
分 析:
程式中自动抓取系统日期的月份名称, 连接上``late.dat'',
形成累计迟到次数的档案名称(如 : Jullate.dat,...), 并以变数
late_file纪录该档名.
累计迟到次数的档案中的资料格式为 :
员工代号(ID) 迟到次数
例如, 执行本程式前档案 Auglate.dat 的内容为 :
1012 0
1006 1
1052 2
1034 0
1005 0
1029 2
1042 0
1051 0
1008 0
1101 0
1025 1
1028 0
编写程式 reformat4.awk 如下:
awk '
BEGIN {
Sys_Sort = "sort +0n >> 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 之内容如下 :
Today is Aug 17
================================
ID Number Arrival Time
1005 8:12 * 1 times
1006 7:45 1 times
1008 8: 1 * 1 times
1012 7:46
1025 7:27 1 times
1028 7:49
1029 7:57 2 times
1034 7:26
1042 7:59
1051 7:51
1052 8: 5 * 3 times
1101 7:32
Average arrival time : 7:49
*
说 明 :
latefile 是一变数, 用以记录迟到次数的档案之档名.
latefile之值由两部分构成, 前半部是当月月份名称(由呼叫
"date"取得)后半部固定为"late.dat" 如 : Junlate.dat.
指令 getline <latefile 表由latefile所代表的档案中
读取一笔纪录, 并存放于$0.
若使用者可自行把资料放入$0, AWK会自动对这新置入 $0 的资料
进行栏位分割. 之后程式中可用$1, $2,..来表示该笔资料的第一栏,
第二栏,..,
(注: 有少数AWK版本不容许使用者自行将资料置于 $0, 遇此情况可改
用gawk或nawk)
执行getline指令时, 若成功读取纪录,它会传回1. 若遇到档案结束,
它传回0; 无法开启档案则传回-1.
利用 while( getline < filename >0 ) {....}可读入档案中
的每一笔资料并予处理. 这是AWK中user自行读取档案资料的
一个重要模式.
阵列 late_cnt[ ] 以员工ID. 当注标(index), 其对应值表其迟到的
次数.
执行结束后, 利用 for(Variable in array ){..}之语法
for( any in late_cnt ) print any, late_cnt[any]> latefile
将更新过的迟到资料重新写回记录迟到次数之档案. 该语法于第5节
中曾有说明.
--------------------------------------------------------------------------------
处理 Multi-line 记录
AWK每次从资料档中只读取一笔Record, 进行处理. AWK系依照其内建变数 RS(Record Separator) 的定义将档案中的资料分隔成一笔一笔的Record. RS 的预设值是 "\n"(跳行符号), 故平常AWK中一行资料就是一笔 Record. 但有些档案中一笔Record涵盖了数行资料, 这种情况下不能再以 "\n" 来分隔Records. 最常使用的方法是相邻的Records之间改以 一个空白行 来隔开. 在AWK程式中, 令 RS = ""(空字串)后, AWK把会空白行当成来档案中Record的分隔符号. 显然AWK对 RS = "" 另有解释方式,简略描述如下, 当 RS = "" 时 :
数个并邻的空白行, AWK仅视成一个单一的Record Saparator. (AWK不会于两个紧并的空白行之间读取一笔空的Record)
AWK会略过(skip)档首或档末的空白行. 故不会因为档首或档末的空白行,造成AWK多读入了二笔空的资料.
请观察下例,首先建立一个资料档 week.rpt如下: 张长弓 GNUPLOT 入门 吴国强 Latex 简介 VAST-2 使用手册 mathematica 入门 李小华 AWK Tutorial Guide Regular Expression 该档案档首有数列空白行, 各笔Record之间使用一个或数个空白行隔开. 读者请细心观察, 当 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. GNUPLOT 入门 二. 报告人 : 吴国强 1. Latex 简介 2. VAST-2 使用手册 3. mathematica 入门 三. 报告人 : 李小华 1. AWK Tutorial Guide 2. Regular Expression 说明:
本程式同时也改变栏位分隔字元( FS= "\n"), 如此一笔资料中的每一行都是一个field. 例如 : AWK读入的第一笔 Record 为张长弓 GNUPLOT 入门 其中 $1 指的是"张长弓", $2 指的是"GNUPLOT 入门"
上式中的C\_Number[ ]是一个阵列(array), 用以记录中文数字. 例如 : C\_Number[1] = "一", C\_Number[2] = "二" 这过程使用AWK字串函数 split( ) 来把中文数字放进阵列 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
结果荧幕出现 :
awk
first-arg
second-arg
说明 :
ARGC, ARGV[ ] 为AWK所提供的内建变数.
ARGC : 为一整数. 代表命令列上, 除了选项-v, -f 及其对应
的参数之外所有参数的数目.
ARGV[ ] : 为一字串阵列. ARGV[0],ARGV[1],...ARGV[ARGC-1].
分别代表命令列上相对应的参数.
例如, 当命令列为 :
$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[ ] 中.
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 以进行处理. 不会开启以
apple,orange 为档名的档案(因为 ARGC 被改成2). 但仍可藉由
ARGV[2], ARGV[3]取得命令列上的参数 apple, orange
可以下列命令来达成上例的效果.
$awk -f test2.awk -v data[2]="apple" -v data[3]="orange" data_file
--------------------------------------------------------------------------------
撰写可与使用者相互交谈的 AWK 程式
执行AWK程式时, AWK会自动由档案中读取资料来进行
处理, 直到档案结束.只要将AWK读取资料的来源改成键盘输入,
便可设计与AWK interactive talk 的程式.
本节将提供一个该类程式的范例.
[范例 :] 本节将撰写一个英语生字测验的程式, 它将印出中文字意,
再由使用者回答其英语生字.
首先编辑一个资料挡 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
荧幕将产生如下的画面:
Press "ctrl-d " to exit
莲雾 的英文生字是:
若输入 bellfruit
程式将产生
You are right !! Press Enter to Continue ---
}
说 明 :
参数 test.dat (ARGV[1]) 表示储存考题的资料档档名.
AWK 由该档案上取得考题资料后, 将 ARGV[1] 改成 "-".
"-" 表示由 stdin(键盘输入) 资料. 键盘输入资料的结束符号 (End of file)
是 Ctrl-d. 当 AWK 读到 Ctrl-d 时就停止由 stdin 读取资料.
AWK的数学函数中提供两个与乱数有关的函数.
rand( ) : 传回介于 0与1之间的(近似)乱数值. 0
--------------------------------------------------------------------------------
使用 AWK 撰写 Recusive Program
AWK 中除了函数的参数列(Argument List)上的参数(Arguments)外,
所有变数不管于何处出现全被视为 Global variable. 其生命持续
至程式结束 --- 该变数不论在function外或 function内皆可使用,
只要变数名称相同所使用的就是同一个变数,直到程式结束.
因 Recusive 函数内部的变数, 会因它呼叫子函数(本身)而重覆使用,
故撰写该类函数时, 应特别留心.
例如 : 执行
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)
} '
结果荧幕印出
Inside the function: arg1= 36,x= 35, y= 46, z= 55
Return to main : arg1= 0, x= 35, y= 46, z= 55
由上可知 :
函数内可任意使用主程式中的任何变数.
函数内所启用的任何变数(除参数外), 于该函数之外依然可以使用.
此特性优劣参半, 最大的坏处是式中的变数不易被保护, 特别是
recursive呼叫本身, 执行子函数时会破坏父函数内的变数.
权变的方法是 : 在函数的 Argument list 上虚列一些 Arguments.
函数执行中使用这些虚列的 Arguments 来记录不想被破坏的资料,
如此执行子函数时就不会破坏到这些资料. 此外AWK 并不会检查,
呼叫函数时所传递的参数个数是否一致.
例如 : 定义 recursive function 如下 :
function demo( arg1 )# 最常见的错误例子
........
for(i=1; i< 20 ; i++){
demo(x)
# 又呼叫本身. 因为 i 是 global variable, 故执行完该子函数后
# 原函数中的 i 已经被坏, 故本函数无法正确执行.
.......
}
..........
}
可将上列函数中的 i 虚列在该函数的参数列上, 如此 i 便是一个
local variable, 不会因执行子函数而被破坏.
将上列函数修改如下:
function demo( arg1, i )
{
......
for(i=1; i< 20; i++)
{
demo(x)#AWK不会检查呼叫函数时, 所传递的参数个数是否一致
.....
}
}
$0, $1,.., NF, NR,..也都是 global variable, 读者于 recusive function
中若有使用这些内建变数, 也应另外设立一些 local variable 来保存,
以免被破坏.
范例 :以下是一个常见的 Recursive 范例. 它要求使用者输入一串元素
(各元素间用空白隔开) 然后印出这些元素所有可能的排列.
编辑如下的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的内容连接(concate)上 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($1,$2,..$j,,)为Global variable已被坏, 故重新
# 把 main\_lst 指定给\$0, 令AWK再做一次栏位分割
new_main_lst = ""
for(j=1; j< =nf; j++) # concate new_main_lst
if( j != i ) new_main_lst = new_main_lst " " $j
permutation( new_main_lst, buffer " " $i )
}
}
'$*
执行 $ permu
荧幕上出现
请输入排列的元素,各元素间请用空白隔开
若输入 1 2 3 结果印出
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
共 6 种排列方式
说 明 :
有些较旧版的AWK,并不容许使用者指定$0之值. 此时可改用
gawk, 或 nawk.
否则也可自行使用 split() 函数来分割 main_lst.
为避免执行子函数时破坏 new_main_lst, nf, i, j 故把这些变数
也列于参数列上. 如此, new_main_lst, nf, i, j 将被当成 local variable,
而不会受到子函数中同名的变数影响. 读者宣告函数时,参数列上
不妨将这些 ``虚列的参数'' 与真正用于传递资讯的参数间以较长
的空白隔开, 以便于区别.
AWK 中欲将字串concatenation(连接)时, 直接将两字串并置
即可(Implicit Operator).
例如 :
awk '
BEGIN{
A = "This "
B = "is a "
C = A B "key." # 变数A与B之间应留空白,
否则''AB''将代表另一新变数.
print C
}
}
结果将印出
This is a key.
AWK使用者所撰写的函数可再reuse, 并不需要每个AWK式中
都重新撰写.
将函数部分单读编写于一档案中, 当需要用到该函数时再以下列方式
include进来.
$ awk -f 函数裆名 -f AWK主程式档名 资料档档名
--------------------------------------------------------------------------------
Appendix A Patterns
AWK 藉由判断 Pattern 之值来决定是否执行其后所对应的
Actions.这里列出几种常见的 Pattern :
BEGIN
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
END 为 AWK 的保留字, 是另一种特殊的 Pattern.
END 成立(其值为true)的时机与 BEGIN 恰好相反, 为 :
``AWK 处理完所有资料, 即将离开程式时''
平常读入资料列时, END并不成立, 故其对应的 Actions 并不被执行;
唯有当AWK读完所有资料时, 该 Actions 才会被执行
注意 : 不管资料列有多少笔, 该 Actions 仅被执行一次.
Relational Expression
使用像 `` A Relation Operator B'' 的 Expression 当成 Pattern.
当 A 与 B 存在所指定的关系(Relation)时, 该 Pattern 就算成立(true).
例如 :
length($0)< = 80 { print }
上式中 { length($0)<= 80 是一个 Pattern, 当 $0(资料列)之长度
小于等于 80 时该 Pattern 之值为true, 将执行其后的 Action
(印出该资料列).
AWK 中提供下列 关系运算元(Relation Operator)
运算元 涵意 > 大于 < 小于 > = 大于或等于 <= 小于或等于 == 等于 != 不等于 ~ match !~ not match
上列关系运算元除~(match)与!~(not match)外与 C 语言中之
涵意一致.
~(match) 与!~(match) 在 AWK 之涵意简述如下 :
若 A 表一字串, B 表一 Regular Expression.
A ~B 判断 字串A 中是否 包含 能合于(match)B式样的子字串.
A !~B 判断 字串A 中是否 未包含 能合于(match)B式样的子字串.
例如 :
$0 ~ /program[0-9]+\.c/ \{ print }
$0 ~/program[0-9]+\.c/ }整个是一个 Pattern, 用来判断$0(资料列)中
是否含有可 match /program[0-9]+\.c/ 的子字串, 若$0 中含有该类
字串, 则执行 print (印出该列资料).
Pattern 中被用来比对的字串为$0 时(如本例), 可仅以 Regular Expression
部分表之.
故本例的 Pattern 部分
$0 ~/program[0-9]+\.c/ 可仅用/program[0-9]+\.c/表之
(有关 match 及 Regular Expression 请参考 附录 E )
Regular Expression
直接使用 Regular Expression 当成 Pattern; 此为 $0 ~
Regular Expression 的简写.
该 Pattern 用以判断 $0(资料列) 中是否含有 match 该 Regular Expression
的子字串; 若含有该成立(true) 则执行其对应的 Actions.
例如 :
/^[0-9]*$/ print "This line is a integer !"
与{ $0 ~/^[0-9]*$/ { print "This line is a integer !" } 相同
Compound Pattern
之前所介绍的各种 Patterns, 其计算(evaluation)后结果为一逻辑值
(True or False).AWK 中逻辑值彼此间可藉由&&(and),||(or), !(not)
结合成一个新的逻辑值.故不同 Patterns 彼此可藉由上述结合符号
来结合成一个新的 Pattern. 如此可进行复杂的条件判断.
例 如 :
FNR > = 23 && FNR <=28 print " " $0 }
上式利用&& (and) 将两个 Pattern 求值的结果合并成一个逻辑值.
该式 将资料档中 第23行 到 28行 向右移5格(先印出5个空白
字元)后印出.
( FNR 为AWK的内建变数, 请参考 附录 D )
Pattern1 , Pattern2
遇到这种 Pattern, AWK 会帮您设立一个 switch(或flag).
当AWK读入的资料列使得 Pattern1 成立时, AWK 会打开(turn on)
这 switch.
当AWK读入的资料列使得 Pattern2 成立时, AWK 会关上(turn off)
这个 switch.
该 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 )
--------------------------------------------------------------------------------
Appendix B Actions
Actions 是由下列指令(statement)所组成 :
expression ( function calls, assignments..)
print expression-list
printf( format, expression-list)
if( expression ) statement [else statement]
while( expression ) statement
do statement while( expression)
for( expression; expression; expression) statement
for( variable in array) statement
delete
break
continue
next
exit [expression]
statement
AWK 中大部分指令与 C 语言中的用法一致, 此处仅介绍较为常用
或容易混淆之指令的用法.
流程控制指令
if 指令
语法
if (expression) statement1 [else statement2 ]
范例 :
if( $1> 25 )
print "The 1st field is larger than 25"
else print "The 1st field is not larger than 25"
(a)与 C 语言中相同, 若 expression 计算(evaluate)后之值不为 0
或空字串, 则执行 statement1; 否则执行 statement2.
(b)进行逻辑判断的expression所传回的值有两种, 若最后的逻辑值
为true, 则传回1, 否则传回0.
(c)语法中else statement2 以[ ] 前后括住表示该部分可视需要而
予加入或省略.
while 指令
语法 :
while( expression ) statement
范例 :
while( match(buffer,/[0-9]+\.c/ ) ){
print "Find :" substr( buffer,RSTART, RLENGTH)
buff = substr( buffer, RSTART + RLENGTH)
}
上列范例找出 buffer 中所有能合于(match) /[0-9]+.c/(数字
之后接上 ``.c''的所有子字串).
范例中 while 以函数 match( )所传回的值做为判断条件. 若buffer
中还含有合于指定条件的子字串(match成功), 则 match()函数传回1,
while 将持续进行其后之statement.
do-while 指令
语法 :
do statement while(expression)
范例 :
do{
print "Enter y or n ! "
getline data < "-"
} while( data !~ /^[YyNn]$/)
(a) 上例要求使用者从键盘上输入一个字元, 若该字元不是
Y, y, N, 或 n则会不停执行该回圈, 直到读取正确字元为止.
(b)do-while 指令与 while 指令 最大的差异是 : do-while 指令会先执行
statement而后再判断是否应继续执行. 所以, 无论如何其 statement 部分
至少会执行一次.
*
for Statement 指令(一)
语法 :
for(variable in array ) statement
范例 : 执行下列命令
awk '
BEGIN{
X[1]= 50; X[2]= 60; X["last"]= 70
for( any in X )
printf("X[%d] = %d\n", any, X[any] )
}'
结果印出 :
X[2] = 60
X[last] = 70
X[1] = 50
(a)这个 for 指令, 专用以搜寻阵中所有的index值, 并逐次使用所指定
的变数予以纪录. 以本例而言, 变数 any 将逐次代表 2, 1,及"last".
(b)以这个 for 指令, 所搜寻出的index之值彼此间并无任何次续关系.
(c)第7节 Arrays in AWK 中有该指令的使用范例, 及解说.
for Statement 指令(二)
语法 :
for(expression1; expression2; expression3) statement
范例 :
for(i=1; i< =10; i++) sum = sum + i
说明 :
(a)上列范例用以计算 1 加到 10 的总合.
(b)expression1 常用于设定该 for 回圈的起始条件, 如上例中的 i=1
expression2 用于设定该回圈的停止条件, 如上例中的 i<= 10
expression3 常用于改变 counter 之值, 如上例中的 i++
break 指令
break 指令用以强迫中断(跳离) for, while, do-while 等回圈.
范例 :
while( getline < "datafile" > 0 )
{
if( $1 == 0 ) # 所读取的资料置于 $0
break # AWK立刻把 $0 上新的栏位资料
else # 指定给 $1, $2, ...$NF
print $2 / $1
}
上例中, AWK 不断地从档案 {datafile}中读取资料, 当$1等于0时,
就停止该执行回圈.
continue 指令
回圈中的 statement 进行到一半时, 执行 continue 指令来掠过回圈
中尚未执行的statement.
范例 :
for( index in X_array)
{
if( index !~ /[0-9]+/ ) continue
print "There is a digital index", index
}
上例中若 index 不为数字则执行 continue, 故将掠过(不执行)其后
的指令.
需留心 continue 与 break 的差异 : 执行 continue 只是掠过其后
未执行的statement, 但并未跳离开该回圈.
next 指令
执行 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 指令
执行 exit 指令时, AWK将立刻跳离(停止执行)该AWK程式.
AWK 中的 I/O 指令
printf 指令
该指令与 C 语言中的用法相同, 可藉由该指令控制资料输出时
的格式.
语法 :
printf("format", item1, item2,.. )
范 例 :
id = "BE-2647"; ave = 89
printf("ID# : %s Ave Score : %d\n", id, ave)
(a)结果印出 :
ID# :BE-647 Ave Score : 89
(b)format 部分系由 一般的字串(String Constant) 及 格式控制字元
(Formatcontrol letter, 其前会加上一个\%字元)所构成. 以上式为例
"ID# : " 及 " Ave Score : " 为一般字串. %s 及 %d 为格式控制字元.
(c)印出时, 一般字串将被原封不动地印出. 遇到格式控制字元时,
则依序把 format后方之 item 转换成所指定的格式后印出.
(d)有关的细节, 读者可从介绍 C 语言的书籍上得到较完整的介绍.
(e)print 及 printf 两个指令, 其后可使用>或< > 将输出到stdout
的资料 Redirct到其它档案, 7.1 Redirect Output to Files 中有完整的
范例说明.
print 指令
范 例 :
id = "BE-267"; ave = 89
print "ID# :", id, "Ave Score :"ave
(a)结果印出 :
ID# : BE-267 Ave Score :89
(b)print 之后可接上字串常数(Constant String)或变数. 它们彼此间
可用``,'' 隔开.
(c)上式中, 字串 "ID# :" 与变数 id 之间使用``,''隔开, 印出时两者之间
会以自动 OFS(请参考 D 内建变数 OFS) 隔开. OFS 之值一般内定为
"一个空白字元"
(d)上式中字串 "Ave Score :" 与变数ave之间并未以``,''隔开, AWK
会将这两者先当成字串concate在一起(变成``Ave Score :89")后,
再予印出
(e)print 及 printf 两个指令, 其后可使用> 或> 将输出到stdout的
资料 Redirct到其它档案, 7.1 Redirect Output to Files 中有完整的
范例说明.
getline 指令
语法 由何处读取资料 资料读入后置于 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
close 指令
该指令用以关闭一个开启的档案, 或 pipe(见下例)
范 例 :
awk '
BEGIN { print "ID # Salary" > "data.rpt" }
{ print $1 , $2 * $3 | "sort +0n > data.rpt" }
END{ close( "data.rpt" )
close( "sort +0n > 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 +0n > data.rpt"
来处理 pipe 中的资料; 并未立即执行, 这点与 Unix 中pipe的用法
不尽相同.
(c)最后希望于档案 {data.rpt}之``末尾''处加上一行 "There are.....".
但此时, Shell尚未执行 "sort +0n > data.rpt" 故各资料列排序后
的 ID 及 Salary 等资料尚未写入data.rpt. 所以得命令 AWK
提前先通知 Shell 执行命令 "sort +0n > 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. 以免造成因二个 processes 同时开启一个档案进行
输出(write)所产生的错误.
system 指令
该指令用以执行 Shell上 的 command.
范 例 :
DataFile = "invent.rpt"
system( "rm " DataFile )
说明 :
(a)system("字串")指令接受一个字串当成Shell的命令. 上例中, 使用
一个字串常数"rm " 连接(concate)一个变数 DataFile 形成要求 Shell
执行的命令.Shell 实际执行的命令为 ``rm invent.rpt''.
*
``|'' pipe指令
``|'' 配合 AWK 输出指令, 可把 output 到 stdout 的资料继续转送给
Shell 上的令一命令%
当成input的资料.
``|'' 配合 AWK getline 指令, 可呼叫 Shell 执行某一命令, 再以 AWK
的 getline 指令将该命令的所产生的资料读进 AWK 程式中.
范 例 :
{ print $1, $2 * $3 | "sort +1n > result" }
"date" | getline Date_data
读者请参考 7.2 Using System Resources 其中有完整的范例说明.
AWK 释放所占用的记忆体的指令
AWK 程式中常使用阵列(Array)来记忆大量资料. delete 指令便是用来
释放阵列中的元素所所占用的记忆空间.
范 例 :
for( any in X_arr )
delete X_arr[any]
读者请留心, delete 指令一次只能释放阵列中的一个``元素''.
AWK 中的数学运算元(Arithmetic Operators)
+(加), -(减), *(乘), /(除), %(求余数), ^(指数) 与 C 语言中用法
相同
AWK 中的 Assignment Operators
=, +=, -=, *= , /=, %=, ^=
x += 5 的意思为 x = x + 5, 其余类推.
AWK 中的 Conditonal Operator
语 法 :
判断条件 ? value1 : value2
若 判断条件 成立(true) 则传回 value1, 否则传回 value2.
AWK 中的逻辑运算元(Logical Operators)
&&( and ), ||or, !(not)
Extended Regular Expression 中使用 ``|'' 表示 or 请勿混淆.
AWK 中的关系运算元(Relational Operators)
>, >=, <, < =, ==, !=, ~, !~
AWK 中其它的运算元
+(正号), -(负号), ++(Increment Operator), - -(Decrement Operator)
AWK 中各运算元的运算优先顺序( Precedence )
(按优先高低排列)
$ (栏位运算元, 例如 : i=3; $i表示第3栏)
^ (指数运算)
+ ,- ,! (正,负号,及逻辑上的 not)
* ,/ ,% (乘,除,余数)
+ ,- (加,减)
>, > =,< , < =, ==, != (大于,大于等于,...,等关系符号)
~, !~ (match, not match)
&& (逻辑上的 and)
|| (逻辑上的 or )
? : (Conditional Operator)
= , +=, -=,*=, /=, %=, ^= (一些指定Assignment运算元)
--------------------------------------------------------------------------------
AWK 的内建函数 Built-in Functions
(一). 字串函数
index( 原字串, 找寻的子字串 ):
若原字串中含有欲找寻的子字串,则传回该子字串在原字串中第一次
出现的位置,若未曾出现该子字串则传回0.
例如执行 :
$awk 'BEGIN{ print index("8-12-94","-" }'
结果印出 2
length( 字串 ) : 传回该字串的长度.
例如执行 :
awk 'BEGIN { print length("John") '}
结果印出 4
match( 原字串, 用以找寻比对的 Regular Expression :
AWK会在原字串中找寻合乎Regular Expression的子字串. 若合乎
条件的子字串有多个, 则以原字串中最左方的子字串为准.
AWK找到该字串后会依此字串为依据进行下列动作:
设定AWK内建变数 RSTART, RLENGTH :
RSTART &= 合条件之子字串在原字串中之位置.
&= 0 ; 若未找到合条件的子字串.
RLENGTH &= 合条件之子字串长度.
&= -1 ; 若未找到合条件的子字串.
传回 RSTART 之值.
例如执行 :
awk ' BEGIN {
match( "banana", /(an)+/ )
print RSTART, RLENGTH
}
'
执行结果印出 2 4
split( 原字串, 阵列名称, 分隔字元(field separator):
AWK将依所指定的分隔字元(field separator)来分隔原字串成
一个个的栏位(field),并以指定的阵列记录各个被分隔的栏位.
例如 :
ArgLst = "5P12p89"
split( ArgLst, Arr, /[Pp]/)
执行后 Arr[1]=5, Arr[2]=12, Arr[3]=89
sprintf( 列印之格式, 列印之资料, 列印之资料,...)
该函数之用法与AWK或C的输出函数printf()相同. 所不同的是
sprintf()会将要求印出的结果当成一个字串传回
一般最常使用sprintf()来改变资料格式. 如: x 为一数值资料, 若欲
将其变成一个含二位小数的资料,可执行如下指令 :
x = 28
x = sprintf("%.2f",x)}
执行后 x = "28.00"
sub( 比对用的 Regular Expression}, 将替换的新字串, 原字串 )
sub( )将原字串中第一个(最左边)合乎所指定的Regular Expression
的子字串改以新字串取代.
第二个参数"将替换的新字串"中可用"&"来代表"合于条件的
子字串"承上例,执行下列指令:
A = "a6b12anan212.45an6a"
sub( /(an)+[0-9]*/, "[&]", A)
结果印出ab12[anan212].45an6a
sub()不仅可执行取代(replacement)的功用,当第二个参数为
空字串("")时,sub()所执行的是``去除指定字串''的功用.
藉由 sub()与 match()的搭配使用,可逐次取出原字串中合乎
指定条件的所有子字串.
例如执行下列程式:
awk '
BEGIN {
data = "p12-P34 P56-p61"
while( match( data ,/[0-9]+/) >0) {
print substr(data,{ RSTART, RLENGTH })
sub(/[0-9]+/,"")
}
}
' $* }
结果印出 :
12
34
56
61
sub( )中第三个参数(原字串)若未指定,则其预设值为$0.
可用 sub( /[9-0]+/,"digital" ) 表示 sub(/[0-9]+/,"digital",$0 )
gsub( 比对用的 Regular Expression}, 将替换的新字串, 原字串)
这个函数与 sub()一样,同样是进行字串取代的函数. 唯一不同点是
gsub()会取代所有合条件的子字串.
gsub()会传回被取代的子字串个数.
请参考 sub().
substr( 字串,起始位置 [,长度] ):
传回从起始位置起,指定长度之子字串. 若未指定长度,则传回起始
位置到自串末尾之子字串.
执行下例
awk {' BEGIN{ print substr( "User:Wei-Lin Liu", 6)}
结果印出
Wei-Lin Liu
(二). 数学函数
int(x) : 传回x的整数部分(去掉小数).
例如 :
int(7.8) 将传回 7
int(-7.8) 将传回 -7
sqrt(x) : 传回x的平方根.
例如 :
sqart(9) 将传回 3
若 x 为负数,则执行 sqrt(x)时将造成 Run Time Error
exp(x) : 将传回e的x次方.
例如 :
exp(1) 将传回 2.71828
log(x) : 将传回x以e为底的对数值.
例如 :
log(e) = 1
若 x< 0 ,则执行 sqrt(x)时将造成 Run Time Error.
sin(x) : x 须以径度量为单位,sin(x)将传回x的sin函数值.
cos(x) : x 须以径度量为单位,cos(x)将传回x的cos函数值
atan2(y,x) : 传回 y/x 的tan反函数之值,传回值系以径度量为单位.
rand( ) : 传回介于 0与1之间的(近似)乱数值; 0 < rand()<1.
除非使用者自行指定rand()函数起始的seed,否则每次执行AWK
程式时, rand()函数都将使用同一个内定的seed,来产生乱数.
srand(x) : 指定以x为rand( )函数起始的seed.
若省略了x,则AWK会以执行时的日期与时间为rand()函数起始的seed.
--------------------------------------------------------------------------------
AWK 的内建变数 Built-in Variables
因内建变数的个数不多, 此处按其相关性分类说明, 并未按其字母
顺序排列.
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 是用以 initialize 程式中的变数 a.
FILENAME 用以表示目前正在处理的资料档档名.
*
FS 栏位分隔字元.
$0 表示目前AWK所读入的资料列.
$1,$2..分别表示所读入的资料列之第一栏, 第二栏,..
(参考下列说明)
当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".
使用者可用 Regexp 自行定义 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 上所记载之资料的
栏位个数.
OFS输出时的栏位分隔字元. 预设值 " "(一个空白), 详见下面说明.
ORS输出时资料列的分隔字元. 预设值 "\n"(跳行), 见下面说明.
OFMT数值资料的输出格式. 预设值 "%.6g"(若须要时最多印出6位小数)
当使用 print 指令一次印出多项资料时,
例如 : print $1, $2
印出资料时, AWK会自动在 $1 与 $2 之间补上一个 OFS 之值
(预设值为 一个空白)
每次使用 print 输出(印)资料后, AWK会自动补上 ORS 之值.
(预设值为 跳行)
使用 print 输出(印)数值资料时, AWK将采用 OFMT 之值为
输出格式.
例如 : print 2/3
AWK 将会印出 0.666667
程式中可藉由改变这些变数之值, 来改变指令 print 的输出格式.
RS( Record Separator) : AWK从资料档上读取资料时,
将依 RS 之定义把资料切割成许多Records,而AWK一次仅读入一个
Record,以进行处理.
RS 的预设值是 "\n". 所以一般 AWK一次仅读入一行资料.
有时一个Record含括了几列资料(Multi-line Record). 这情况下不能
再以"\n"
来分隔并邻的Records, 可改用 空白行 来分隔.
在AWK程式中,令 RS = "" 表示以 空白行 来分隔并邻的Records.
*
RSTART与使用字串函数 match( )有关之变数,详见下面说明.
RLENGTH与使用字串函数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]
}
' $*
执行结果印出:
78
78
78
78
--------------------------------------------------------------------------------
Regular Expression 简介
为什么要使用 Regular Expression
UNIX 中提供了许多 指令 或 tools, 它们具有在档案中 寻找(Search)
字串或置换(Replace)字串 的功能. 像 grep, vi , sed, awk,...
不论是找寻字串或置换字串, 都得先 ``告诉这些指令所要找寻
(被置换)的字串为何''.若未能预先明确知道所要找寻(被置换)的字串
为何, 只知该字串存在的范围或特征时,
例如 :
(一)找寻 ``T0.c'', ``T1.c'', ``T2.c''.... ``T9.c'' 当中的任一字串.
(二)找寻至少存在一个 ``A''的任意字串.
这情况下, 如何告知执行找寻字串的指令所要找寻的字串为何.
例 (一) 中, 要找寻任一在 ``T'' 与 ``.c'' 之间存在一个阿拉伯数字
的字串;当然您可以列举的方式, 一一把所要找寻的字串告诉执行
命令的指令.但例 (二) 中合于该条件的字串有无限种可能, 势必无法
一一列举.
此时,便需要另一种字串表示的方法(协定).
什么是 Regular Expression
Regular Expression(以下简称 (Regexp)是一种字串表达的方式.
可用以指称具有某特征的所有字串.
注 : 为区别于一般字串, 本附录中代表 Regexp 的字串之前皆加
``Regexp''.注 : AWK 程式中常以/..../括住 Regexp; 以区别于一般字串.
组成 Regular Expression 的元素
普通字元 除了. * [ ] + ? ( ) \ \^ $ 外之所有字元.
由普通字元所组成的Regexp其意义与原字串字面意义相同.
例如 : Regexp ``the'' 与一般字串的 ``the'' 代表相同的意义.
. Metacharacter : 用以代表任意一字元.
须留心 UNIX Shell 中使用 ``*''表示 Wildcard, 可用以代表任意长度
的字串.而 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''...``T9.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''
match是什么 ?
讨论 Regexp 时, 经常遇到 ``某字串合于( match )某 Regexp''的字眼.
其意思为 : ``这个 Regexp 可被解释成该字串''.
[ 例如] :
字串 "the" 合于(match) Regexp /[Tt]he/.
因为 Regexp /[Tt]he/ 可解释成字串 "the" 或 "The", 故字串 "the"
或 "The"都合于(match) Regexp /[Th]he/.
AWK 中提供二个关系运算元(Relational Operator,见注一) ~ !~,
它们也称之为 match, not match.但函义与一般常称的 match 略有不同.
其定义如下 :
A 表一字串, B 表一 Regular Expression
只要 A 字串中存在有子字串可 match( 一般定义的 match) Regexp B ,
则 A ~B 就算成立, 其值为 true, 反之则为 false.
! ~ 的定义与~恰好相反.
{itemize}
例 如 :
"another" 中含有子字串 "the" 可 match Regexp /[Tt]he/ , 所以
"another" ~/[Tt]he/ 之值为 true.
*
[注 一] : 有些论着不把这两个运算元( ~, !~)与 Relational Operators
归为一类.
应用 Regular Expression 解题的简例
下面列出一些应用 Regular Expression 的简例, 部分范例中会更动
$0 之值, 若您使用的 AWK
不容许使用者更动 $0时, 请改用 gawk.
将档案中所有的字串 "Regular Expression" 或 "Regular expression"
换成 "Regexp"
awk '
{ gsub( /Regular[ \t]+[Ee]xpression/, "Regexp")
print
}
' $*
去除档案中的空白行(或仅含空白字元或tab)
awk '
$0 !~ /^[ \t]*$/ { print }
' $*
在档案中俱有 ddd-dddd(电话号码型态, d 表digital)的字串前
加上"TEL : "
awk '
{ gsub( /[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/, "TEL : &" )
print
}
' $*
从档案的 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
}
' $*
结果印出
path : /usr/local/bin name : xdvi
将某一数值改以现金表示法表之(整数部分每三位加一撇,
且含二位小数)
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
}
' $*
结果印出
$123,456,789.00
把档案中所有具 ``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
}
' $*
--------------------------------------------------------------------------------
[[i] Last edited by 无奈何 on 2006-10-27 at 02:28 AM [/i]]
☆开始\运行 (WIN+R)☆
%ComSpec% /cset,=何奈无── 。何奈可无是原,事奈无做人奈无&for,/l,%i,in,(22,-1,0)do,@call,set/p= %,:~%i,1%<nul&ping/n 1 127.1>nul