linux-awk【行扫描文本:样式扫描与处理工具】

http://fanqiang.chinaunix.net/program/other/2005-09-07/3621.shtml

http://www.tsnc.edu.cn/default/tsnc_wgrj/doc/awk.htm

http://www.aslibra.com/doc/awk.htm [这篇未整理完,非常赞]

1 awk简介 

awk是三个人名的缩写,他们是:Aho、(Peter)Weinberg和(Brain)Kernighan。正是这三个人创造了awk---一个优秀的 样式扫描与处理工具。awk是一种编程语言,用于在linux/unix下对 文本和数据进行处理。数据可以来自标准输入、一个或多个文件,或其它命令的输出。它 支持用户自定义函数和动态正则表达式等先进功能,是linux/unix下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。

awk的处理文本和数据的方式是这样的,它逐行扫描文件,从第一行到最后一行,寻找匹配的特定模式的行,并在这些行上进行你想要的操作。如果没有指定处理动作,则把匹配的行显示到标准输出(屏幕),如果没有指定模式,则所有被操作所指定的行都被处理。

1.1awk的工作流程 :

执行awk时, 它会反复进行下列四步骤.
 
1,自动从指定的数据文件中读取一个数据行.
2,自动更新(Update)相关的内建变量之值. 如 : NF, NR, $0...
3,依次执行程序中 所有 的 Pattern { Actions } 指令.
4,当执行完程序中所有 Pattern { Actions } 时, 若数据文件中还有未读取的数据, 则反复执行步骤1到步骤4.
awk会自动重复进行上述4个步骤, 使用者不须于程序中编写这个循环 (Loop).

2 awk的调用方式 

awk提供了适应多种需要的不同解决方案,它们是: 

2.1 awk命令行

例1:显示文本文件mydoc匹配(含有)字符串"sun"的所有行。 

$awk '/sun/{print}' mydoc 
由于显示整个记录(全行)是awk的缺省动作,因此可以省略action项
$awk '/sun/' mydoc 
例2:下面的示例显示了内置变量和内置函数length()的使用: 

该命令行将显示文本myfile中所有超过80个字符的行号,在这里,用$0表示整个记录(行),同时,内置变量NR不使用标志符'$'。 

$awk 'length($0)>80 {print NR}' myfile 
例3:作为一个较为实际的例子,我们假设要对UNIX中的用户进行安全性检查,方法是考察/etc下的passwd文件,检查其中的passwd字段(第二字段)是否为"*",如不为"*",则表示该用户没有设置密码,显示出这些用户名(第一字段)。我们可以用如下语句实现: 
$ awk -F: '$2!="x" {printf("%s no password!\n",$1)}' /etc/passwd
例4:如何把一行竖排的数据转换成横排?
awk '{printf("%s,",$1)}' filename

2.2 使用-f选项调用awk程序

awk允许将一段awk程序写入如下awk脚本文件,注意这里就是把上述的单引号之间的pattern {action}内容写入文本,注意,原命令行中是:$ awk 'length($2)>length($1){print $1 "," $2}' fileName:

length($2)>length($1){print $1 "," $2}

然后在awk命令行中用-f选项调用并执行这段程序。

$awk -f awkScript fileName


3 awk的语法

3.1 awk命令格式

与其它UNIX命令一样,awk拥有自己的语法: 

awk [ -F re] [parameter...] ['prog'] [-f progfile][in_file...] 
参数说明: 

3.1.1 -F re:

允许awk更改其字段分隔符。 如 "-F :"表示以冒号":"来进行分割。不指定时默认以空格来分割

可以同时使用多个域分隔符(用正则匹配),这时应该把分隔符写成放到方括号中,如$awk -F'[:\t]' '{print $1,$3}' test,表示以空格、冒号和tab作为分隔符。

3.1.2 parameter: 

该参数帮助为不同的变量赋值。 

3.1.3 'prog'= 'pattern {action}' 

程序段可以由多个组成为:pattern {action} pattern {action}...

awk的程序语句段。这个语句段必须用单拓号:'和'括起,以防被shell解释。这个程序语句段的标准形式为: 'pattern {action}' 

pattern模式:

pattern参数可以是egrep正则表达式中的任何一个,它可以 使用语法/re/再加上一些样式匹配技巧构成。与sed类似,你也可以使用","分开两样式以选择某个范围。

pattern可以是以下任意一个:

/正则表达式/:使用通配符的扩展集,例如/[Nn]ice/
关系表达式:关系运算符进行操作,如length($2)>length($1)选择第二个字段比第一个字段长的行。
模式匹配表达式:用运算符~(匹配)和~!(不匹配),如"banana" ~ /an/ 为真
范围模板:模式,模式:指定一个行的范围。该语法不能包括BEGIN和END模式。如awk '/root/,/mysql/' test将显示root第一次出现到mysql第一次出现之间的所有行
BEGIN:让用户指定在第一条输入记录被处理之前所发生的动作,通常可在这里设置全局变量。
END:让用户在最后一条输入记录被读取之后发生的动作。

【注1】范围模板匹配从第一个模板的第一次出现到第二个模板的第一次出现之间所有行。如果有一个模板没出现,则匹配到开头或末尾。如$ awk '/root/,/mysql/' test将显示root第一次出现到mysql第一次出现之间的所有行。

例如,显示第一个匹配Sun或sun的行与第一个匹配Moon或moon的行之间的行,并显示到标准输出上。;

$awk '/[Ss]un/,/[Mm]oon/ {print}' myfile 

【注2】模式匹配awk 提供的 ~ (match) 及 !~(not match) 二个关系运算符.

其用法与涵义如下:
若 A 为一字符串, B 为一正则表达式(Regular Expression)
A ~ B 判断 字符串A 中是否 包含 能匹配(match)B表达式的子字符串.
A !~ B 判断 字符串A 中是否 不包含 能匹配(match)B表达式的子字符串.


【awk的常规表达式元字符 】
\ 换码序列 
^ 在字符串的开头开始匹配 
$ 在字符串的结尾开始匹配 
. 与任何单个字符串匹配 
[ABC] 与[]内的任一字符匹配 
[A-Ca-c] 与A-C及a-c范围内的字符匹配(按字母表顺序) 
[^ABC] 与除[]内的所有字符以外的任一字符匹配 
Desk|Chair 与Desk和Chair中的任一个匹配 
[ABC][DEF] 关联。与A、B、C中的任一字符匹配,且其后要跟D、E、F中的任一个字符。 
* 与A、B或C中任一个出现0次或多次的字符相匹配 
+ 与A、B或C中任何一个出现1次或多次的字符相匹配 
? 与一个空串或A、B或C在任何一个字符相匹配 
(Blue|Black)berry 合并常规表达式,与Blueberry或Blackberry相匹配 

action操作

action参数总是被大括号包围,它由一系统awk语句组成,各语句之间用";"分隔。awk解释它们,并在pattern给定的样式匹配的记录上执行其操作。与shell类似,你也可以使用“#”作为注释符,它使“#”到行尾的内容成为注释,在解释执行时,它们将被忽略。

action由一人或多个命令、函数、表达式组成,之间由换行符或分号隔开,并位于大括号内。主要有四部份:

变量或数组赋值
输出命令
内置函数
控制流命令

你可以省略pattern和action之一,但不能两者同时省略,当省略pattern时没有样式匹配,表示对所有行(记录)均执行操作,省略action时执行缺省的操作——在标准输出上显示。 

3.1.4 -f progfile:

允许awk调用并执行progfile指定有程序文件。progfile是一个文本文件,他必须符合awk的语法。 

3.1.5 in_file:

awk的输入文件,awk允许对多个输入文件进行处理。值得注意的是awk不修改输入文件。如果未指定输入文件,awk将接受标准输入,并将结果显示在标准输出上。awk支持输入输出重定向。


3.2 awk的记录、字段与内置变量

awk环境变量

变量	描述
$n	当前记录的第n个字段,字段间由FS分隔。
$0	完整的输入记录。
ARGC	命令行参数的数目。
ARGIND	命令行中当前文件的位置(从0开始算)。
ARGV	包含命令行参数的数组。
CONVFMT	数字转换格式(默认值为%.6g)
ENVIRON	环境变量关联数组。
ERRNO	最后一个系统错误的描述。
FIELDWIDTHS	字段宽度列表(用空格键分隔)。
FILENAME	当前文件名。
FNR	同NR,但相对于当前文件。
FS	字段分隔符(默认是任何空格)。
IGNORECASE	如果为真,则进行忽略大小写的匹配。
NF	当前记录中的字段数。
NR	当前记录数。
OFMT	数字的输出格式(默认值是%.6g)。
OFS	输出字段分隔符(默认值是一个空格)。
ORS	输出记录分隔符(默认值是一个换行符)。
RLENGTH	由match函数所匹配的字符串的长度。
RS	记录分隔符(默认是一个换行符)。
RSTART	由match函数所匹配的字符串的第一个位置。
SUBSEP	数组下标分隔符(默认值是\034)。

awk处理的工作与数据库的处理方式有相同之处,其相同处之一就是 awk支持对记录和字段的处理,其中对字段的处理是grep和sed不能实现的,这也是awk优于二者的原因之一。在awk中, 缺省的情况下总是将文本文件中的一行视为一个记录,而将一行中的某一部分作为记录中的一个字段。为了操作这些不同的字段, awk借用shell的方法,用$1,$2,$3...这样的方式来顺序地表示行(记录)中的不同字段。特殊地,awk用$0表示整个行(记录)。不同的字段之间是用称作分隔符的字符分隔开的。系统默认的分隔符是空格。awk允许在命令行中用 -F re的形式来改变这个分隔符。事实上,awk用一个内置的变量 FS来记忆这个分隔符。awk中有好几个这样的内置变量,例如,记录分隔符变量RS、当前工作的记录数NR等等,本文后面的附表列出了全部的内置变量。这些内置的变量可以在awk程序中引用或修改,例如,你可以利用NR变量在模式匹配中指定工作范围,也可以通过修改记录分隔符RS让一个特殊字符而不是换行符作为记录的分隔符。 
例如,文本文件1.txt:

(1,1)%(1,2)%(1,3)%(1,4)%(1,5)%(1,6)%(1,7)%(1,8)%(1,9)
(2,1)%(2,2)%(2,3)%(2,4)%(2,5)%(2,6)%(2,7)%(2,8)%(2,9)
(3,1)%(3,2)%(3,3)%(3,4)%(3,5)%(3,6)%(3,7)%(3,8)%(3,9)
(4,1)%(4,2)%(4,3)%(4,4)%(4,5)%(4,6)%(4,7)%(4,8)%(4,9)
(5,1)%(5,2)%(5,3)%(5,4)%(5,5)%(5,6)%(5,7)%(5,8)%(5,9)
(6,1)%(6,2)%(6,3)%(6,4)%(6,5)%(6,6)%(6,7)%(6,8)%(6,9)
(7,1)%(7,2)%(7,3)%(7,4)%(7,5)%(7,6)%(7,7)%(7,8)%(7,9)
(8,1)%(8,2)%(8,3)%(8,4)%(8,5)%(8,6)%(8,7)%(8,8)%(8,9)
(9,1)%(9,2)%(9,3)%(9,4)%(9,5)%(9,6)%(9,7)%(9,8)%(9,9)
显示文本文件1.txt中第3行到第6行中以字符%分隔的第一字段,第三字段和第七字段
$ awk -F % 'NR==3,NR==6{print $1 $3 $7}' ./1.txt
(3,1)(3,3)(3,7)
(4,1)(4,3)(4,7)
(5,1)(5,3)(5,7)
(6,1)(6,3)(6,7)

3.3 awk的内置函数 

awk的内置函数 

【说明:表中v项表示第一个支持变量的工具(下同):A=awk,N=nawk,P=POSIX awk,G=gawk 】

V 函数 用途或返回值 
------------------------------------------------ 
N gsub(reg,string,target) 每次常规表达式reg匹配时替换target中的string 
N index(search,string) 返回string中search串的位置 
A length(string) 求串string中的字符个数 
N match(string,reg) 返回常规表达式reg匹配的string中的位置 
N printf(format,variable) 格式化输出,按format提供的格式输出变量variable。 
N split(string,store,delim) 根据分界符delim,分解string为store的数组元素 
N sprintf(format,variable) 返回一个包含基于format的格式化数据,variables是要放到串中的数据 
G strftime(format,timestamp) 返回一个基于format的日期或者时间串,timestmp是systime()函数返回的时间 
N sub(reg,string,target) 第一次当常规表达式reg匹配,替换target串中的字符串 
A substr(string,position,len) 返回一个以position开始len个字符的子串 
P totower(string) 返回string中对应的小写字符 
P toupper(string) 返回string中对应的大写字符 
A atan(x,y) x的余切(弧度) 
N cos(x) x的余弦(弧度) 
A exp(x) e的x幂 
A int(x) x的整数部分 
A log(x) x的自然对数值 
N rand() 0-1之间的随机数 
N sin(x) x的正弦(弧度) 
A sqrt(x) x的平方根 
A srand(x) 初始化随机数发生器。如果忽略x,则使用system() 
G system() 返回自1970年1月1日以来经过的时间(按秒计算)

awk之所以成为一种优秀的程序设计语言的原因之一是它吸收了某些优秀的程序设计语言(例如C)语言的许多优点。这些优点之一就是内置函数的使用, awk定义并支持了一系列的内置函数,由于这些函数的使用,使得awk提供的功能更为完善和强大,例如,awk使用了一系列的字符串处理内置函数(这些函数看起来与C语言的字符串处理函数相似,其使用方式与C语言中的函数也相差无几),正是由于这些内置函数的使用,使awk处理字符串的功能更加强大。本文后面的附录中列有一般的awk所提供的内置函数,这些内置函数也许与你的awk版本有些出入,因此,在使用之前,最好参考一下你的系统中的联机帮助。 
作为内置函数的一个例子,我们将在这里介绍awk的printf函数,这个函数使得awk与c语言的输出相一致。实际上,awk中有许多引用形式都是从C语言借用过来的。如果你熟悉C语言,你也许会记得其中的printf函数,它提供的强大格式输出功能曾经带我们许多的方便。幸运的是,我们在awk中又和它重逢了。awk中printf几乎与C语言中一模一样,如果你熟悉C语言的话,你完全可以照C语言的模式使用awk中的printf。因此在这里,我们只给出一个例子,如果你不熟悉的话,请随便找一本C语言的入门书翻翻。 

例:显示文件1.txt中的行号和第3字段: 
$ awk -F %  '{printf"%03d%s\n",NR,$3}' 1.txt
001(1,3)
002(2,3)
003(3,3)
004(4,3)
005(5,3)
006(6,3)
007(7,3)
008(8,3)
009(9,3)

3.4 awk的变量 

3.4.1 awk内置的变量

在awk程序中引用内置变量不需要使用标志符"$"(回忆一下前面讲过的NR的使用)。

内建变量的使用。变量列表在前面已列出,现在举个例子说明一下。$ awk -F: '{IGNORECASE=1; $1 == "MARY"{print NR,$1,$2,$NF}'test,把IGNORECASE设为1代表忽略大小写,打印第一个域是mary的记录数、第一个域、第二个域和最后一个域。

3.4.2 awk自定义变量

awk允许用户在awk程序语句中定义并调用自已的变量。在awk中引用自定义变量必须在它前面加上标志符"$"。与C语言不同的是,awk中不需要对变量进行初始化,awk根据其在awk中第一次出现的形式和上下文确定其具体的数据类型。当变量类型不确定时,awk默认其为字符串类型。这里有一个技巧:如果你要让你的awk程序知道你所使用的变量的明确类型,你应当在在程序中给它赋初值。

赋值格式:Variable = expression,如$ awk '$1 ~/test/{count = $2 + $3; print count}' test,上式的作用是,awk先扫描第一个域,一旦test匹配,就把第二个域的值加上第三个域的值,并把结果赋值给变量count,最后打印出来。

awk可以在命令行中给变量赋值,然后将这个变量传输给awk脚本。如$ awk -F: -f awkscript month=4 year=2004 test,上式的month和year都是自定义变量,分别被赋值为4和2004。在awk脚本中,这些变量使用起来就象是在脚本中建立的一样。注意,如果参数前面出现test,那么在BEGIN语句中的变量就不能被使用。

3.5 运算与判断

awk算术运算符 

运算符 用途 
------------------ 
x^y x的y次幂 
x**y 同上 
x%y 计算x/y的余数(求模) 
x+y x加y 
x-y x减y 
x*y x乘y 
x/y x除y 
-y 负y(y的开关符号);也称一目减 
++y y加1后使用y(前置加) 
y++ 使用y值后加1(后缀加) 
--y y减1后使用y(前置减) 
y-- 使用后y减1(后缀减) 
x=y 将y的值赋给x 
x+=y 将x+y的值赋给x 
x-=y 将x-y的值赋给x 
x*=y 将x*y的值赋给x 
x/=y 将x/y的值赋给x x%=y 将x%y的值赋给x 
x^=y 将x^y的值赋给x 
x**=y 将x**y的值赋给x 

awk允许的测试: 
操作符 含义
x==y x等于y 
x!=y x不等于y 
x>y x大于y 
x>=y x大于或等于y 
x<y x小于y 
x<=y x小于或等于y? 
x~re x匹配正则表达式re? 
x!~re x不匹配正则表达式re? 

比较表达式:conditional expression1 ? expression2: expression3,

例如:

$ awk '{max = {$1 > $3} ? $1: $3: print max}' test。如果第一个域大于第三个域,$1就赋值给max,否则$3就赋值给max。
$ awk '$1 + $2 < 100' test。如果第一和第二个域相加大于100,则打印这些行。
$ awk '$1 > 5 && $2 < 10' test,如果第一个域大于5,并且第二个域小于10,则打印这些行。

3.6 awk的流程控制 

awk提供的完备的流程控制语句类似于C语言。

3.6.1、BEGIN和END: 

在awk中两个特别的表达式,BEGIN和END,这两者都可用于pattern中(参考前面的awk语法),提供BEGIN和END的作用是给程序赋予初始状态和在程序结束之后执行一些扫尾的工作。任何在BEGIN之后列出的操作(在{}内)将在awk开始扫描输入之前执行,而END之后列出的操作将在扫描完全部的输入之后执行。因此,通常使用BEGIN来显示变量和预置(初始化)变量,使用END来输出最终结果。 

BEGIN模块后紧跟着动作块,这个动作块在awk处理任何输入文件之前执行。所以它可以在没有任何输入的情况下进行测试。它通常用来改变内建变量的值,如OFS,RS和FS等,以及打印标题。如:$ awk 'BEGIN{FS=":"; OFS="\t"; ORS="\n\n"}{print $1,$2,$3}’ test。上式表示,在处理输入文件以前,域分隔符(FS)被设为冒号,输出文件分隔符(OFS)被设置为制表符,输出记录分隔符(ORS)被设置为两个换行符。$ awk 'BEGIN{print "TITLE TEST"}‘只打印标题。

END不匹配任何的输入文件,但是执行动作块中的所有动作,它在整个输入文件处理完成后被执行。如$ awk 'END{print "The number of records is" NR}' test,上式将打印所有被处理的记录数。

3.6.2、流程控制语句 

awk提供了完备的流程控制语句,其用法与C语言类似。下面我们一一加以说明: 

1、if...else语句: 

awk分支结构允许嵌套

if (expression){
	statement; statement; ...
}
else{
        statement; statement; ...
}

例如:

$ awk '{if ($1 <$2) print $2 "too high"}' test。如果第一个域小于第二个域则打印。
$ awk '{if ($1 < $2) {count++; print "ok"}}' test.如果第一个域小于第二个域,则count加一,并打印ok。

2、while语句 

while(expression) 
{
	statement; statement; ...
}
变量的初始值为1,若i小于可等于NF(记录中域的个数),则执行打印语句,且i增加1。直到i的值大于NF.
$ awk '{ i = 1; while ( i <= NF ) { print NF,$i; i++}}' test。
$ awk '{for (i = 1; i<NF; i++) print NF,$i}' test。

3、do-while语句 

do 
{ 
语句 
}while(条件判断语句)

4、for语句 

for(初始表达式;终止条件;步长表达式) 
{语句} 

5、break,continue,exit语句

在awk的 while、do-while和for语句中允许使用break,continue语句来控制流程走向,也允许使用exit这样的语句来退出。break中断当前正在执行的循环并跳到循环外执行下一条语句。continue从当前位置跳到循环开始处执行。对于exit的执行有两种情况:当exit语句不在END中时,任何操作中的exit命令表现得如同到了文件尾,所有模式或操作执行将停止,END模式中的操作被执行。而出现在END中的exit将导致程序终止。 

breadkcontinue语句。break用于在满足条件的情况下跳出循环;continue用于在满足条件的情况下忽略后面的语句,直接返回循环的顶端。如:

{for ( x=3; x<=NF; x++) 
            if ($x<0){print "Bottomed out!"; break}}
{for ( x=3; x<=NF; x++)
            if ($x==0){print "Get next item"; continue}}
next语句从输入文件中读取一行,然后从头开始执行awk脚本。如:
{if ($1 ~/test/){next}
    else {print}
}
exit语句用于结束awk程序,但不会略过END块。退出状态为0代表成功,非零值表示出错。

6、下标与数组

awk中的数组的下标可以是数字和字母,称为关联数组。
用变量作为数组下标。如:

$ awk {name[x++]=$2};END{for(i=0;i<NR;i++) print i,name[i]}' test。

数组name中的下标是一个自定义变量x,awk初始化x的值为0,在每次使用后增加1。第二个域的值被赋给name数组的各个元素。在END模块中,for循环被用于循环整个数组,从下标为0的元素开始,打印那些存储在数组中的值。因为下标是关健字,所以它不一定从0开始,可以从任何值开始。
special for循环用于读取关联数组中的元素。格式如下:

{for (item in arrayname){
         print arrayname[item]
         }
}
打印有值的数组元素。打印的顺序是随机的:

$ awk '/^tom/{name[NR]=$1}; END{for(i in name){print name[i]}}' test。

用字符串作为下标。如:count["test"]
用域值作为数组的下标。一种新的for循环方式:

for (index_value in array) statement

如以下语句将打印$1中字符串出现的次数。它首先以第一个域作数组count的下标,第一个域变化,索引就变化。

$ awk '{count[$1]++} END{for(name in count) print name,count[name]}' test

delete函数用于删除数组元素。如下分配给数组line的是第一个域的值,所有记录处理完成后,special for循环将删除每一个元素:

$ awk '{line[x++]=$1} END{for(x in line) delete(line[x])}' test


3.7 awk中的自定义函数 

awk函数的定义方法如下: 
function 函数名(参数表){ 
函数体 
} 
在gawk中允许将function省略为func,但其它版本的awk不允许。函数名必须是一个合法的标志符,参数表中可以不提供参数(但在调用函数时函数名后的一对括号仍然是不可缺少的),也可以提供一个或多个参数。与C语言相似,awk的参数也是通过值来传递的。 在awk中调用函数比较简单,其方法与C语言相似,但awk比C语言更为灵活,它不执行参数有效性检查。换句话说,在你调用函数时,可以列出比函数预计(函数定义中规定)的多或少的参数,多余的参数会被awk所忽略,而不足的参数,awk将它们置为缺省值0或空字符串,具体置为何值,将取决于参数的使用方式。 

例:下面的例子演示了函数的使用。在这个示例中,定义了一个名为print_header的函数,该函数调用了两个参数FileName和PageNum,FileName参数传给函数当前使用的文件名,PageNum参数是当前页的页号。这个函数的功能是打印(显示)出当前文件的文件名,和当前页的页号。完成这个功能后,这个函数将返回下一页的页号。 
nawk 
>'BEGIN{pageno=1;file=FILENAME 
>pageno=print_header(file,pageno);#调用函数print_header 
>printf("当前页页号是:%d\n",pageno); 
>} 


>#定义函数print_header 
>function print_header(FileName,PageNum){ 
>printf("%s %d\n",FileName,PageNum); 
>PageNum++;return PageNUm; 
>} 
>}' myfile 


4 几个例子

4.1运行方式范例

4.1.1 文本数据

为便于解释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"分别表示"组装"及"包装"部门.

4.1.2 示例1【awk命令行运行方式】

[ 范例 :] 以文件 emp.dat 为例, 计算每人应发工资并打印报表.
[ 分析 :] awk 会自行一次读入一列数据, 故程序中仅需告诉awk 如何处理所读入的数据行.

$ awk '{ print $2, $3 * $4 }' emp.dat
Jenny 21000
Dan 23650
Max 27170
John 27500
Linda 19950

4.1.3 示例2【awk脚本运行方式】

[ 范例 :] 承上例,组装部门员工调薪5%,(组装部门员工之ID以"A"开头),所有员工最后之薪资率若仍低于100, 则以100计,编写awk程序打印新的员工薪资率报表.
[分析 ] : 这个程序须先判断所读入的数据行是否合于指定条件, 再进行某些动作.awk中 Pattern { Actions } 的语法已涵盖这种 " if (条件) { 动作} "的架构. 

如果是awk命令行,则执行:

$ awk '$1 ~ /^A.*/ { $3 *= 1.05 } $3<100 { $3 = 100 } { printf("%s %8s %d\n", $1, $2, $3)}' emp.dat
其中单引号之间由三组构成:‘pattern {action}pattern {action}pattern {action}',现在如果要写入awk脚本文件,只需将单引号中的内容写入。

编写如下之程序, 并取名 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的工作程序是: 

1,从数据文件中每次读入一个数据行, 依序执行完程序中所有的 Pattern{ Action }指令:
2,第一个Pattern{ Action }指令是:$1~/^A.*/ { $3 *= 1.05 }

用来判断该笔数据行的第一栏是否包含以"A"开头的子字符串. 其中 /^A.*/ 是一个Regular Expression, 用以表示任何以"A"开头的字符串. (有关 Regular Expression 之用法 参考 附录 E ).
3,第二个Pattern{ Action }指令是:$3 < 100 { $3 = 100 }

若第三栏的数据内容(表薪资率)小于100, 则调整为100.
4,第三个Pattern{ Action }指令是:{printf("%s %8s %d\n",$1,$2,$3)}

省略了Pattern(无条件执行Actions), 故所有数据行调整后的数据都将被印出.
5,再从数据文件中读进下一笔记录继续进行处理.

4.2 数组使用范例

4.2.1 文本数据

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

4.2.2 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."]++ .

4.2.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记录该下标之值且执行{....}中之指令. 藉由这个方式便可取出数组中储存的信息.

4.2.4 示例1

[ 范例 : ] 统计各科修课人数,并印出结果.
建立如下程序,并取名为 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
      O.S. 2
      A.I. 1
 Algorithm 2
      D.S. 1
  Graphics 2
  Discrete 3
     Arch. 2
[ 说 明 : ]
这程序包含二个Pattern { Actions }指令.
第一个Pattern { Actions }指令是:{ for( i=2; i <= NF; i++) Number[$i]++ }
第二个Pattern { Actions }指令是: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]++ 之后
i=2时 $i="O.S." Number["O.S."]的值从默认的0,变成了1 ;
i=3时 $i="Arch." Number["Arch."]的值从默认的0,变成了1 ;
同理,i=4时 $i="Discrete" Number["Discrete"]的值从默认的0,变成了1 ;
第二个 Pattern { Actions }指令中END 为awk之保留字, 为 Pattern 的一种.END 成立(其值为true)的条件是: "awk处理完所有数据, 即将离开程序时. "平常读入数据行时, END并不成立, 故其后的Actions 并不被执行;唯有当awk读完所有数据时, 该Actions才会被执行 ( 注意, 不管数据行有多少笔, END仅在最后才成立, 故该Actions仅被执行一次.)BEGIN 与 END 有点类似, 是awk中另一个保留的Pattern,唯一不同的是: "以 BEGIN 为 Pattern 的 Actions 于程序一开始执行时, 被执行一次."。NF 为awk的内建变量, 用以表示awk正处理的数据行中, 所包含的字段个数.

4.3 awk 程序中使用 Shell 命令范例

awk程序中允许呼叫Shell指令. 并提供管道解决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 中表示管道的符号. awk 把 | 之前的字符串"who"当成Shell上的命令, 并将该命令送往Shell执行, 执行的结果(原先应于屏幕印出者)则藉由pipe送进awk程序中.
getline为awk所提供的输入指令.getline 一次读取一行数据, 若读取成功则return 1, 若读取失败则return -1, 若遇到文件结束(EOF), 则return 0;

4.4 awk 程序的应用实例

本节将示范一个统计上班到达时间及迟到次数的程序.
这程序每日被执行时将读入二个文件:1,员工当日到班时间的数据文件 ( 如下列之 arr.dat )2,存放员工当月迟到累计次数的文件.
当程序执行执完毕后将更新第二个文件的数据(迟到次数), 并打印当日的报表.这程序将分成下列数小节逐步完成, 其大纲如下:

[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中如何读取文件数据 >
 

4.4.1 文本数据

某公司其员工到勤时间档如下, 取名为 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

4.4.2 重定向输出到文件

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, 其内容如下 :

 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 无法帮您开启一个以空字符串为文件名的文件).
因此在编辑awk程序时, 须格外留心. 因为若敲错变量名称,awk在编译程序时会认为是一新的变量, 并不会察觉. 因此往往会造成运行时错误.
BEGIN 为awk的保留字, 是 Pattern 的一种.
以 BEGIN 为 Pattern 的 Actions 于awk程序刚被执行尚未读取数据文件时被执行一次, 此后便不再被执行.
读者或许觉得本程序中的I/O重定向符号应使用 " >>" (append)而非 " >".
本程序中若使用 ">" 将数据重导到 today_rpt1, awk 第一次执行该指令时会产生一个新档 today_rpt1, 其后再执行该指令时则把数据追加到today_rpt1文件末, 并非每执行一次就重开一个新文件.
若采用">>"其差异仅在第一次执行该指令时, 若已存在today_rpt1则 awk 将直接把数据append在原文件之末尾. 这一点, 与UNIX中的用法不同.

4.4.3 awk 中如何利用系统资源

awk程序中很容易使用系统资源. 这包括在程序中途调用 Shell 命令来处理程序中的部分数据; 或在调用 Shell 命令后将其产生的结果交回 awk 程序(不需将结果暂存于某个文件). 这一过程是借助 awk 所提供的管道 (虽然有些类似 Unix 中的管道, 但特性有些不同),及一个从 awk 中呼叫 Unix 的 Shell 命令的语法来达成的.
[例 :] 承上题, 将数据按员工ID排序后再输出到文件 today_rpt2 , 并于表头附加执行时的日期.
[ 分 析 : ]
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() 二个.

在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" 的执行次数是 "一次".
在 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 -k 1 >>today_rpt2"}
执行如下命令:
awk -f reformat2.awk arr.dat
执行后, 系统会自动将 sort 后的数据追加( Append; 因为使用 " >>") 到文件 today_rpt2末端. today_rpt2 内容如下 :
 Today is  09月 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
[ 说 明 : ]
awk程序由三个主要部分构成 :
[ i.] Pattern { Action} 指令
[ ii.] 函数主体. 例如 : function double( x ){ return 2*x } (参考第11节 Recursive Program )
[ iii.] Comment ( 以 # 开头识别之 )
awk 的输入指令 getline, 每次读取一列数据. 若getline之后
未接任何变量, 则所读入之资料将以$0 记录, 否则以所指定的变量储存之.
[ 以本例而言] :
执行 "date" | getline 后, $0 之值为 "2007年 09月 21日 星期五 14:28:02 CST",当 $0 之值被更新时, awk将自动更新相关的内建变量, 如: $1,$2,..,NF.故 $2 之值将为"09月", $3之值将为"21日".
(有少数旧版的awk不允许即使用者自行更新(update)$0的值,或者更新$0时,它不会自动更新 $1,$2,..NF. 这情况下, 可改用gawk或nawk. 否则使用者也可自行以awk字符串函数split()来分隔$0上的数据)
本程序中 printf() 指令会被执行12次( 因为有arr.dat中有12行数据), 但读者不用担心数据被重复sort了12次. 当awk结束该程序时才会 close 这个 pipe , 此时才将这12行数据一次送往系统,并呼叫 "sort -k 1 >> today_rpt2" 处理之.
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中.

4.4.4 执行 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,
#!/bin/sh
# 注意以下的 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
将产生运行时错误(无法打开文件).
但某些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 mydump.awk  #(未接任何数据文件文件名)

$ ./mydisplay  #(未接任何数据文件文件名)
将会发现: 此后键入的任何数据将逐行复印一份于屏幕上. 这情况不是机器当机 ! 是因为awk程序正处于执行中. 它正按程序指示, 将读取数据并重新dump一次; 只因执行时未指定数据文件文件名, 故awk 便以stdin(键盘上的输入)为数据来源. 读者可利用这个特点, 设计可与awk即时聊天的程序.

4.4.5 如何打印一行中$3之后的所有参数

注意printf不打印换行符,print会打印。
ps ux |awk '{for(i=3;i<=NF;i++) printf $i" ";printf "\n"}'
或者
ps ux |cut -d " " -f 3- 
cut -d " " -f 3- input_filename > output_filename





.... 


你可能感兴趣的:(linux-awk【行扫描文本:样式扫描与处理工具】)