awk语言快速入门笔记

awk语言

第一章:快速入门

开始

Awk 是一种使用方便且表现力很强的编程语言, 它可以应用在多种不同的计算与数据处理任务中。
假设emp.data文件如下:
Job 4.00 0
Dan 3.78 0
Kathy 4.00 10
Mark 5.00 20
Mary 5.50 22
Susie 4.25 18

例如:判断雇员工作时长大于零的人薪酬。

awk '$3 > 0 { print $1, $2 * $3 }' emp.data

awk由一个单的的 模式-动作 组成 , 模式$3 > 0,动作 { print $1, $2 * $3 }

awk '{print $1, $3}' emp.data
awk '$3==0 {print $1}' emp.data

awk程序结构:

pattern { action }
pattern { action }

运行awk程序
运行一个awk程序有多种方式,如下
awk ‘program’ input files

awk '$3==0 {print $1}' file1 file2

也可以这样,交互式:

awk '$3==0 {print $1}'

程序比较长的时候,放一个文件中:
awk -f progfile optional list of iles (选项 -f 告诉awk从文件中提取程序。 )

简单的输出

awk 的数据只有两种类型: 数值与由字符组成的字符串。每次读取一行,将行分解为一个一个字段(默认将字段看作是 非空白字符组成的序列)。当前输入行的第一个自动叫作$1,第二个是$2,以此类推,一整行为$0。每行的字段数可能不一样。

{ print $0} # 打印整行
{ print $1, $3 } # 打印某个字段

NF,字段的数量

awk计算当前输入行的字段数量,并将它存储在一个内建的变量中,即NF。
{ print NF, $1 $NF}
将打印每一个输入行的字段数量,第一个字段,及最后一个字段

打印行号,NR

{ print NR, $0 }  # 打印行号加整行

将文本放入输出中

{ print "total pay for", $1, "is", $2*$3 }

更精美的输出

格式化输出printf

printf(format, value1, value2, …, valuen )

{ printf("total pay for %s is $%.2f\n", $1, $2*$3) }
{ printf("%-8s $%6.2f\n", $1, $2*$3) }

输出排序

awk '{ printf("%6.2f %s\n", $2*$3, $0) }' emp.data | sort -n

选择

通过比较进行选择

$2 >= 5 { print $0 }
$2 * $3 > 50 { print("$5.2f for %s\n", $2*$3, $1)}

通过文本内容选择

$1 == "Susie"

模式组合

$2 >=4 || $3 >= 20

数据验证

数据验证本质上是否定的。

NF != 3 { print $0, "number of fields is not equal to 3" }
$2 < 3.35 { print $0, "rate is below minimum wage" }
$2 > 10 { print $0, "rate exceeds $10 per hour" }
$3 < 0 { print $0, "negative hours worked" }
$3 > 60 { print $0, "too many hours worked" }

beging 与 end

BEGIN { print "NAME" RATE HOSRS"; print "" }
      { print }

注意: 同一行多个语句用分号隔开;print “” 打印一个空行,后面print打印当前行。

用awk计算

计算

$3 > 15 { emp = emp + 1}
END     { print emp, "employees worked more than 15 hours" }

当awk变量作为数值使用时,默认初始值为0,不需要初始化。

计算总和与平均数

END {print NR, "employees"}

# 利用NR来计算平均报酬
{ pay = pay + $2 * $3 }       # 累加所有雇员的报酬
END { print NR, "employees" 
      print "total pay is", pay 
      print "average pay is", pay / NR
     }
# 这个程序有个可能的问题,即NR可能为0的情况。

操作文本

# 搜索每小时工资最高的雇员:
$2 > maxrate { maxrate = $2; maxemp = $1 }
END { print "highest hourly rate: ", maxrate, "for", maxemp }

如出现多个相同的最高每小时工资,则打印第一个

字符串拼接

    { names = names $1 " "}
END {print names}

这个程序里面names不需要显示初始化。

打印最后一行

     { last = $0 }
END  { print last }

内建函数

{ print $1, length($1) }

行,单词与字符计数

{ nc = nc + length($0) +1  # 每个行尾加1是包含换行符,因为$0不包含换行符
  nw = nw + NF
}
END { print NR, "lines,", nw, "words,", nc, "characters" }

流程控制

if-else

例子:计算每小时工资多于$6.00的雇员总报酬与平均报酬。判断0的情况

$2 > 6 {n = n+1; pay = pay+$2*$3 }
END    {if (n>0)
         print n, "employes, total pay is ", pay,
                   "average pay is", pay/n
         else
            print "no employees are paid more than $6/hour"
            }

while语句

一个while含有一个条件判断与一个循环体。当条件为真是,循环体执行。
例子:一笔钱在一个特定的利率下,其价值如何随着投资时间的增长而增加,价值公式是
value = amount(1+rate)[^years]

# interest1 - compute compound interest
# input: amount rate years
# output: compounded value at the end of each year
{i=1
 while (i<=$3) {
   printf("\t%.2f\n", $1*(1+$2)^i)
   i = i + 1
   } 
 }

for语句

大多数循环体包括初始化,测试,增值,而for语句对此进行压缩成一行。

# interest2 - compute compound interest
# input: amount rate years
# output: compounded value at the end of each year
{ for (i=1; i<=$3; i=i+1)
  printf("\t%.2f\n",$1*(1+$2)^i)
  }

数组

例子:按行逆序显示输入数据。

# reverse - print input in reverse order by line
{ line[NR]=$0 } # remember each input line
END { i = NR    # print lines in reverse order
  while (i>0){
    print line[i]
    i = i - 1
  }
}

for 循环等价实现

# reverse - print input in reverse order by line
{ line[NR]=$0 }    # remember each input line
END { for (i=NR; i>0; i=i-1)
      print line[i]
}

实用"一行"手册

# 输入行的总行数
END { print NR }

# 打印第10行
NR == 10

# 打印每行的最后一个字段
{ print $NF }

# 打印最后一行的最后一个字段
{ field = $NF }
END { print field}

# 打印字段数多于4个的行
NF > 4

# 打印最后一个字段值大于4的行
$NF > 4

# 打印所有行的字段数的总和
{ nf = nf + NF }
END {print nf}

# 打印包含Beth的行的数量
/Beth/ { nlines = nlines + 1 }
END { print nlines }

# 打印具有最大值的第一个字段,及包含它的行(假设$1总是正的)
$1 > max { max=$1; maxline=$0 }
END { print max, maxline }

# 打印至少包含一个字段的行
NF > 0

# 打印长度超过80个字符的行
length($0) > 80

# 在每一行的前面加上他的字段数
{ print NF, $0}

# 打印每行的第1与第2个字段,但顺序相反
{ print $2, $1 }

# 交换每行的第1与第2个字段,并打印该行
{ temp=$1; $1=$2; $2=temp; print }

# 将每一行的第一个字段用行号代替
{ $1=NR; print }

# 打印删除了第2个字段后的行
{ $2=""; print }

# 将每行的字段按逆序打印
{ for (i=NF; i>0; i=i-1) printf("%s ", $i) printf("\n") }

# 打印每行的所有字段值之和
{ sum =0 
  for(i=1; i<=NF; i=i+1) sum=sum+$1
  print sum
}

# 将所有行的所有字段值累加起来
{ for (i=1; i<NF; i=i+1) sum=sum+$i}
  END { print sum}
  
# 将每一行的每一个字段用它的绝对值替换
{ for(i=1; i<=NF; i=i+1) if ($i<0) $i=-$i  
  print
}

第二章:AWK语言

在一些语句中,模式可以不存在;也有语句动作及花括号也不存在。如果程序经过awk检查没有问题,即可每次读取一行,按顺序检查每一个模式。对每个与当前行匹配的模式,对应动作就会执行。一个缺失的模式匹配每个输入行。因此每个不带有模式的动作对每个输入行都会执行。
只含有模式而没有动作的语句,会打印每个匹配模式的输入行。

输入文件countries

# cat countries.data (每个字段之间用tab分割,最后一个字段之间是空格)
USSR    8649    275     Asia
Canada  3852    25      North America
China   3705    1032    Asia
USA     3615    237     North America
Brazil  3286    134     South America
India   1267    746     Asia
Mexico  762     78      North America
France  211     55      Europe
Japan   144     120     Asia
Germany 96      61      Europe
England 94      56      Europe

模式

模式控制着动作的执行:模式匹配是则执行相应的动作。

模式汇总说明
1. BEGING{ statements }在输入被读取前,statements执行一次
2.END { statements }当所有输入读取完后,statements执行一次
3.expression{statements}每碰到一个expression为真,statements就执行,expression为真是值非零或非空
4./regular expression/{statements}当碰到这样一个输入行时, statements 就执行: 输入行含有一段字符串, 而该字符串可以被 regular expression 匹配.
5.compound pattern {statements}一个复合模式将表达式用 &&(AND),||(OR), !(NOT), 以及括号组合起来; 当 compound pattern 为真时, statements 执行.
6.pattern1, pattern2 {statements}一个范围模式匹配多个输入行,这些行从匹配pattern1 开始,到pattern2结束,对其中每行执行statements

BEGIN与END

这两个模式不匹配任何输入行。也不能与其他模式组合。如果有多个则按照顺序执行。
BEGING常用更改输入行被分割为自动的默认方式。分割字符由一个内建变量FS 控制。默认是空格或制表符分割。

# print countries with column headers and totals 
BEGIN { FS = "\t" # make tab the field separator
printf("%10s %6s %5s %s\n\n",
"COUNTRY", "AREA", "POP", "CONTINENT")
}
{ printf("%10s %6d %5d %s\n", $1, $2, $3, $4)
area = area + $2
pop = pop + $3
}
END { printf("\n%10s %6d %5d\n", "TOTAL", area, pop) }
输出结果
COUNTRY AREA POP CONTINENT
USSR 8649 275 Asia
Canada 3852 25 North America
China 3705 1032 Asia
USA 3615 237 North America
Brazil 3286 134 South America
India 1267 746 Asia
Mexico 762 78 North America
France 211 55 Europe
Japan 144 120 Asia
Germany 96 61 Europe
England 94 56 Europe
TOTAL 25681 2819

字符串匹配模式

字符串匹配模式
1. /regexpr
如果当前输入行包含能够被regexpr匹配的子字符串,则被匹配
2.expression ~ /regexpr/
如果 expression的字符串值包含能被regexpr匹配的子字符串,则被匹配
3.expression !~ /regexpr/
如expression的字符串值不包含能被regexpr匹配的子字符串,则被匹配
~ 意思是被…匹配。!~ 意思是 不被…匹配。
*** 注意: /Asia/ 是 $0 ~ /Asia/ 的简写形式。***

正则表达式

正则表达式
1. 正则表达式的元字符包括:
\ ^ $ . [ ] | () * + ?
2. 一个基本的正则表达式包括下面几种:
一个不是元字符的字符, 例如 A, 这个正则表达式匹配的就是它本身
一个匹配特殊符号的转义字符: \t 匹配一个制表符
一个被引用的元字符, 例如 *, 按字面意义匹配元字符
^ 匹配一行的开始
$ 匹配一行的结束
. 匹配任意一个字符
一个字符类: [ABC] 匹配字符 A, B 或 C
字符类可能包含缩写形式: [A-Za-z] 匹配单个字母
一个互补的字符类: [^0-9] 匹配任意一个不是数字的字符
3. 这些运算符将正则表达式组合起来:
选择: A|B匹配A或B
拼接: AB 匹配后面紧跟着 B 的 A
闭包: A* 匹配 0 个或多个 A
正闭包: A+ 匹配一个或多个 A
零或一: A? 匹配空字符串或 A
括号: 被 ® 匹配的字符串, 与 r 所匹配的字符串相同

如果某个字符前面有 \,就是该字符是被引用的。
如下:
^C 匹配以字符 C 开始的字符串;
C$ 匹配以字符 C 结束的字符串;
^C$ 匹配只含有单个字符 C 的字符串;
^.$ 匹配有且仅有一个字符的字符串;
^…$ 匹配有且仅有 3 个字符的字符串;
… 匹配任意 3 个字符;
.$ 匹配以句点结束的字符串.

1 匹配以 A, B, 或 C 开始的字符串;
[ABC] 匹配以任意一个字符 (除了 A, B, 或 C) 开始的字符串;
[^ABC] 匹配任意一个字符, 除了 A, B, 或 C;
[a-z]$ 匹配任意一个有且仅有一个字符的字符串, 且该字符不能是小写字母

B* 匹配空字符串, 或 B, BB, 等等.
AB*C 匹配 AC, 或 ABC, ABBC, 等等.
AB+C 匹配 ABC, 或 ABBC, ABBBC, 等等.
AB?C 匹配 AC 或 ABC
[A-Z]+ 匹配由一个或多个大写字母组成的字符串.
(AB)+C 匹配 ABC, ABABC, ABABABC, 等等.

正则表达式中,选择运算符 | 优先级最低,然后是拼接运算,最后是重复运算符 *,+,与?
与算术表达式规则一样,优先级搞的运算符优先处理。
一些例子

/^[0-9]+$/
匹配含有且只含有数字的输入行.
/^[0-9][0-9][0-9]$/
输入行有且仅有 3 个数字.
/^(\+|-)?[0-9]+\.?[0-9]*$/
十进制小数, 符号与小数部分是可选的.
/^[+-]?[0-9]+[.]?[0-9]*$/
也是匹配十进制小数, 带有可选的符号与小数部分.
/^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)([eE][+-]?[0-9]+)?$/
浮点数, 符号与指数部分是可选的.
/^[A-Za-z][A-Za-z0-9]*$/
一个字母, 后面再跟着任意多个字母或数字 (比如 awk 的变量名).
/^[A-Za-z]$|^[A-Za-z][0-9]$/
一个字母, 又或者是一个后面跟着一个数字的字母 (比如 Basic 的变量名).
/^[A-Za-z][0-9]?$/
同样是一个字母, 又或者是一个后面跟着一个数字的字母.

正则表达式

表达式匹配
c非元字符c
\c转义序列或字面意义上的c
^字符串的开始
$字符串的结束
.任意一个字符
[c_1c_2…]任意一个在 c1c2… 中的字符.
[^c_1c_2…]任意一个不在 c1c2… 中的字符.
[c_1-c_2…]任意一个在范围内的字符, 范围由 c1 开始, 由 c2 结束.
[^c_1-c_2…]任意一个不在范围内的字符, 范围由 c1 开始, 由 c2 结束.
r_1|r_2任意一个被 r1 或 r2 匹配的字符串.
(r_1)(r_2)任意一个字串 xy, 其中 r1 匹配 x, 而 r2 匹配 y; 如果当中不含有选择运算符, 那么括号是可以省略的
®*零个或连续多个能被 r 匹配的字符串.
®+一个或连续多个能被 r 匹配的字符串.
®?零个或一个能被 r 匹配的字符串. 在这里括号可以省略.
®任意一个能被 r 匹配的字符串.

如: $4 == “Asia” || $ == “Europe” ==> 4   / ( A s i a ∣ E r u o p e ) 4 ~ /^(Asia|Eruope) 4 /(AsiaEruope)/
FNR == 1, FNR ==5 { print FILENAME ": " $0 } ==> FNR <= 5 { print FILENAME ": " $0 } # 打印前5行,前面有文件名
FNR 表示当前输入文件中到目前为止读取到的行数, FILENAME 表示当前输入文件名 它们都是内建变量。

模式总结

模式例子匹配
BEGINGBEGING输入被读取之前
ENDEND素有输入被读取完之后
expression$3 < 100第3个字段小于100的行
streing-matching/Asia/含有Asia的行
compound$3 <100 && $4 == “Asia”第3个字段小于100且第4个字段是Asia的行
rangeNR == 10, NR ==20输入的第10行到第20行

动作

动作中的语句可以包括:

  • expression,包括常量,变量,赋值,函数调用等等。
  • print expression-list
  • printf(format, expression-list)
  • if (expression) statements
  • if (expression) statemetns else statements
  • while (expression) statements
  • for (expression; expression; expression) statements
  • for (expression in array) statements
  • do statements while (expression)
  • break
  • continue

内建变量

变量意义默认值
ARGC命令行参数的个数-
ARGV命令行参数数组-
FILENAME当前输入文件名-
FNR当前输入文件的记录个数-
FS控制着输入行的字段分割符" "
NF当前记录的字段个数-
NR到目前为止读的记录数量-
OFMT数值的输出格式“%.6g”
OFS输出字段分割符" "
ORS输出的记录的分割符“\n”
RLENGTH被函数 match 匹配的字符串的长度-
RS控制着输入行的记录分割符“\n”
RSTART被函数 match 匹配的字符串的开始
SUBSEP下标分割符“\034”
# 将一个新的字符串赋值给字段:
BEGING   { FS=OFS="\t" }
$4 == "North America" { $4="NA" }
$4 == "South America" { $4="SA" }
{ print }

$(NF-1) # 倒数第2个字段
$NF - 1 # 最后一个字段减 1 后的值
$(NF+1) # 不存在的字段,值初始就是 空字符串
# 也可以创建第5个字段
BEGIN { FS=OFS="\t" }
{ $5=1000*$3/$2 ; print }

逻辑运算符

expr1&&expr2 如果expr1的值为假,则expr2就不执行,expr3 || expr4 如果expr3的值为真,则expr4就不执行。

条件表达式

expr1 ? expr2 : expr3
如expr1为真(非零uo非空)则expr执行,否则expr1为假,则expr3。

{ print ($1 !=0 ? 1/$1 : "$1 is zero,line " NR) } # 打印$1的倒数,如$1为0则警告

赋值运算符

var = expr 
$4 == "Asia" { pop=pop + $3; n=n + 1 }
END          { print "Total population of the", n, "Aisa contries is ", pop, "million". }

$3 > maxpop { maxpop=$3; country=$1 }
END         { print "country with largest population:", country, maxpop }

内建算术函数

randint = int(n*rand()) + 1  # 返回一个1到n之间的一个整数
x = int(x + 0.5)  # 将正数x四舍五入为最接近它的整数
函数返回值
atan2(y,x)y/x 的反正切值, 定义域在 −π 到 π 之间
cos(x)x 的余弦值, x 以弧度为单位
exp(x)x 的指数函数, ex
int(x)x 的整数部分; 当 x 大于 0 时, 向 0 取整
log(x)x 的自然对数 (以 e 为底)
rand()返回一个随机数 r, 0 ≤ r < 1
sin(x)x 的正弦值, x 以弧度为单位.
sqrt(x)x 的方根
srand(x)x 是 rand() 的新的随机数种子

字符串运算符

{ print NR ":" $0 }

用作正则表达式的字符串

BEGIN { digits = "^[0-9]+$" }
$2 ~ digits  # 第2个字段有且仅有数字的行打印出来

# 具有有效浮点数的行打印出来
BEGING {
  sign = "[+-]?"
  decimal = "[0-9]+[.]?[0-9]"
  fraction = "[.][0-9]+"
  exponent = "([eE]" sign "[0-9]+)?"
  number = "^" sign "("decimal "|" fraction ")" exponent "$"
}
$0 ~ number

内建字符串函数

函数描述
gsub(r,s)将 $0 中所有出现的 r 替换为 s, 返回替换发生的次数.
gsub(r,s,t )将字符串 t 中所有出现的 r 替换为 s, 返回替换发生的次数
index(s,t)返回字符串 t 在 s 中第一次出现的位置, 如果 t 没有出现的话, 返回 0.
length(s)返回 s 包含的字符个数
match(s,r)测试 s 是否包含能被 r 匹配的子串, 返回子串的起始位置或 0; 设置RSTART 与 RLENGTH
split(s,a)用 FS 将 s 分割到数组 a 中, 返回字段的个数
split(s,a,fs)用 fs 分割 s 到数组 a 中, 返回字段的个数
sprintf(fmt,expr-list)根据格式字符串 fmt 返回格式化后的 expr-list
sub(r,s)将$0 的最左最长的, 能被 r 匹配的子字符串替换为 s, 返回替换发生的次数.
sub(r,s,t)把 t 的最左最长的, 能被 r 匹配的子字符串替换为 s, 返回替换发生的次数.
substr(s,p)返回 s 中从位置 p 开始的后缀.
substr(s,p,n)返回 s 中从位置 p 开始的, 长度为 n 的子字符串.

r 表示一个正则表达式 (或者是一个字符串, 或者是被一对斜杠包围了的) 。 s 与 t 是字符串表达式, n与 p 是整数

数值或字符串

number “” # 将空字符串拼接到number可以将它强制转换成字符串
string + 0 # 给字符串加上零可以把它强制转换成数值。

流程控制语句

if-else形式:
if (expression)
statements1
else
statements2

while形式:
while (expression)
statements

{ i=1
  while (i<=NF) {
    print $i
    i++
    }
  }

for形式:
for (expression1; expression2;expression3)
statements
等价于
expression1
while(expression2){
statements
expression3
}

{ for(i=1; i<=NF; i++)
  print $i
}

do形式:
do
statements
while (expression)

# 流程控制语句
1. {statements}
语句组
2. if (expression)statements
如果expression为真,执行statements
3. if (expression) statements1 else statements2
如果 expression 为真, 执行 statements1, 否则执行 statements2
4. while (expression) statements
如果 expression 为真, 执行 statements; 然后重复前面的过程
5. for (expression1;expression2;expression3 ) statements
等价于 expression1; while (expression2) { statements; expression3}
6. for (variable in array) statements
轮流地将 variable 设置为 array 的每一个下标, 并执行 statements
7. do statements while (expression)
执行 statements; 如果 expression 为真就重复
8. break
马上离开最内层的, 包围 break 的 while, for 或 do
9. continue 开始最内层的, 包围 continue 的 while, for, 或 do 的下一次循环
10. next
开始输入主循环的下一次迭代
11. exit
12. exit expression
马上执行 END 动作; 如果已经在 END 动作内, 那就退出程序. 将 expression 作为程序的
退出状态返回.

空语句

单独一个分号表示一个空语句。

BEGING {FS="\t"}
  {for(i=1; i<=NF && $i !=""; i++)
  ;
  if(i<=NF)
    print
  }

数组

数组元素的默认初始值为0或空字符串 “”。

x[NR]=$0

# 如逆序打印
  { x[NR]=$0 }
END { for (i=NR;i>0;i--) print x[i] }

# 将Asia与Europe的人口数量累加到pop数组中。
/Asia/    { pop["Asia"] += $3 }
/Europe/  { pop["Europe"] += $3 }
END  { print "Asia population is", pop["Asia"], "million." 
       print "European population is", pop["Europe"], "million."
}

# 数组的下标可以是任意表达式,如
BEGIN { FS="\t" }
      { pop[$4]+=$3 }
END   { for (name in pop) print name, pop[name] }

# 删除 delete,如循环删除数组pop的所有元素
for(i in pop)
  delete pop[i]

多维数组

awk不直接支持多维数组,它用一维数组来近似模拟多维数组。下标之间用一个分隔符分开。

for(i=1; i<=10; i++)
    for(j=1; j<=10; j++)
        arr[i, j] =0
下标具有形式:1,1  1,2等等。
字符串具有形式 1 SUBSEP 1, 1 SUBSEP 2 等等。内建变量SUBSEP分隔,默认值是"\034".
测试一个多维下标是否是某个数组的成员:
if((i,j) in arr)
遍历这样一个数组:
for(k in arr)。
如单独访问某个下标成分: split(k, x, SUBSEP)

用户自定义函数

定义函数的语句具有形式:
function name(parameter-list) {
statements
}
举例来说, 这个函数计算参数的最大值:
function max(m, n) {
return m > n ? m : n
}
举例来说, max 函数可以像这样调用:
{ print max($1, max($2, $3)) } # print maxinum of $1, $2, $3
function max(m, n) {
return m > n ? m : n
}
调用函数时, 函数名与左括号之间不能留有空白. 函数操作的是变量的拷贝, 而不是变量本身.然而, 当数组
作为函数的参数时, 函数接收到的参数就不是数组的拷贝, 所以函数可以改变数组的元素, 或往数组增添新的值 (这叫作 “按引用传递”). 函数名不能当作参数使用.

输出

print 与 printf 语句可以用来产生输出. print 用于产生简单的输出; printf 用于产生格式化的输出. 来自 print 与 printf 的输出可以被重定向到文件, 管道与终端. 这两个语句可以混合使用, 输出按照它们产生的顺序出现.

1. print
将 $0 打印到标准输出
2. print expression, expression, …
打印各个 expression, expression 之间由 OFS 分开, 由 ORS 终止
3. print expression,expression,… > filename
输出至文件 filename
4. print expression,expression,… >> filename
累加输出到文件 filename, 不覆盖之前的内容
5. print expression,expression,… | command
输出作为命令 command 标准输入
6. printf(format,expression,expression,…)
7. printf(format,expression,expression,…) > filename
8. printf(format,expression,expression,…) >> filename
9. printf(format,expression,expression,…) | command
printf 类似于 print, 但是第 1 个参数规定了输出的格式
10. close(filename), close( command)
断开 print 与 filename (或 command) 之间的连接
11. system(command)
执行 command; 函数的返回值是 command 的退出状态
printf 的参数列表不需要被包围在一对括号中. 但是如果 print 或 printf 的参数
列表中, 有一个表达式含有关系运算符, 那么或者表达式, 或者参数列表, 必须用一对括
号包围. 在非 Unix 系统上可能不提供管道与 system

输出分隔符

输出字段分割符与输出记录分隔符存储在内建变量 OFS 与 ORS 中. 初始情况下, OFS 与ORS 分别被设置成一个空格符与一个换行符, 但它们的值可以在任何时候改变.
BEGIN { OFS = “:”; ORS = “\n\n” }
{ print $1, $2 }

printf 格式说明符的例子

fmt$1printf(fmt, $1)
%c97a
%d97.597
%5d97.597
%e97.59.750000e+01
%f97.597.500000
%7.2f97.597.50
%g97.597.5
%.6g97.597.5
%o97141
%06o97000141
%x9761
%sJanuary|January|
%10sJanuary| January|
%-10sJanuary|January |
%.3sJanuary|Jan|
%10.3sJanuary| Jan|
%-10.3sJanuary|Jan |
%%January%

输出到管道

语句: print | command

# print continents and populations, sorted by population
BEGIN { FS="\t" }
  { pop[$4]+=$3 }
END  { for(c in pop) 
  print("%15s\t%6d\n", c, pop[c]) | "sort -t'\t' +1rn" }  
输出的是
Asia2173
Europe172
North America340
South America134

输入

常见的是把输入数据放在一个文件中, 例如文件data, 然后再键入
awk ‘program’ data
如果没有指定输入文件, awk 就从它的标准输入读取数据;

输入分隔符

内建变量 FS 的默认值是 " ", 也就是一个空格符。

BEGIN { FS = ",[ \t]*|[ \t]+" }

多行记录

默认的记录分隔符可以通过向内建变量 RS 赋予新值来改变, 但是必须按照某种受限的方式来进行。 处理多行记录的通常方式是使用:BEGIN { RS = “”; FS = “\n” }

getline函数

函数 getline 可以从当前输入行, 或文件, 或管道, 读取输入。
表达式:getline <“file”
从文件 file 读取输入. 它不会对 NR 与 FNR 产生影响, 但是会执行字段分割, 并且设置 NF.
表达式:getline x <“file”
从 file 读取下一条记录, 存到变量 x 中. 记录不会被分割成字段, 变量 NF, NR, 与 FNR 都不会
被修改

表达式被设置的变量
getline$0, NF, NR, FNR
getline varvar, NR, FNR
getline <file$0, NF
getline var <filevar
cmd | getline$0, NF
cmd | getline varvar
# include - replace # include "f" by contents of file f
/^#include/ {
  gsub(/"/, "", $2)
  while (getline x < $2 > 0)
    print x
  next
}
{ print }

# 登录用户的人数
while ("who" | getline )
  n++
# 当前日期
"date" | getline d
# 注意规避错误
while (getline < "file") ... # 危险的,因为getline文件不存在返回-1,非零为真
应该改成
while (getline < "file" > 0)...  # 这是可以的,即getline返回1才执行

命令行参数

命令行参数可以通常 awk 的内建数组 ARGV 来访问, 内建变量 ARGC 的值是参数的个数再
加 1.
*对于命令行 awk -f progfile a v=1 b
ARGC 的值是 4, ARGV[0] 含有 awk, ARGV[1] 含有 a, ARGV[2] 含有 v=1, ARGV[3] 含有 b.
但是如awk在命令行中出现,则不会被当作参数,-f filename 或 -F同意如此。
awk -F ‘\t’ ‘$3 > 100’ countries # ARGV的值是2,ARGV[1]是countries

# 另一个用命令行参数的例子是seq,它可以产生一个整数序列:
# seq - print sequences of integers
# input: arguments q, p q, or p q r; q >= p; r > 0
# output: integers 1 to q, p to q, or p to q in steps of r
BEGIN {
if (ARGC == 2)
for (i = 1; i <= ARGV[1]; i++)
print i
else if (ARGC == 3)
for (i = ARGV[1]; i <= ARGV[2]; i++)
print i
else if (ARGC == 4)
for (i = ARGV[1]; i <= ARGV[2]; i += ARGV[3])
print i
}
命令
awk -f seq 10
awk -f seq 1 10
awk -f seq 1 10 1
都是生成 1 到 10 这十个整数

与其他程序的交互

system函数

内建函数 system(expression) 用于执行命令, 命令由 expression 给出, system 的返回值
就是命令的退出状态.

$1 == "#include" { gsub(/"/, "", $2); system("cat " $2); next }
{ print }

如果第一个字段是 #include, 那么双引号就会被移除, 然后 Unix 命令 cat 打印以第 2 个字段
命名的文件. 其他输入行被原样复制.

awk制作shell命令

一个简单的例子,假设写一个命令filed1用于打印输入行的第一个字段。
awk ‘{ print $1 }’ $*
chmod +x field1

# 这程序可以处理标准输入, 也可处理文件名参数列表, 并且字段的顺序与数量都可以是任意的
# field - print named fields of each input line
# usage: field n n n ... file file file ...
awk '
BEGIN {
for (i = 1; ARGV[i] ~ /^[0-9]+$/; i++) { # collect numbers
fld[++nf] = ARGV[i]
ARGV[i] = ""
}
if (i >= ARGC) # no file names so force stdin
ARGV[ARGC++] = "-"
}
{ for (i = 1; i <= nf; i++)
printf("%s%s", $fld[i], i < nf ? " " : "\n")
}
' $*

第三章:数据处理

数据转换与归约

Awk 最常用的一个功能是把数据从一种形式转换成另一种形式,另一个常用的功能是从一个大数据集中提取相关的数据, 通常伴随着汇总信息的重新格式化与准备。

列求和

# sum1 - print column sums
# input: rows of numbers
# output: sum of each column
# missing entries are treated as zeros
{for (i=1; i<=NF; i++)
  sum[i] += $i
  if (NF > maxfld)
    maxfld = NF
}
END { for (i=1; i<=maxfld; i++){
  printf("%g", sum[i])
  if(i<maxfld)
    printf("\t")
  else
    printf("\n")
  }
}

# 程序同样的事情,但会检查每行字段数是否与第一行相同
# sum2 - print column sums
# check that each line has the same number of fields
# as line one
NR==1 { nfld=NF }
    { for(i=1; i<=NF; i++)
          sum[i] += $i
      if(NF != nfld)
          print "line " NR " has " NF " entries, not " nfld
    }
END { for(i=1; i<nfld; i++)
         printf("%g%s", sum[i], i<nfld ? "\t" : "\n") # 列于列间插入制表符,最后一列后插入一个换行符
     }

# 判断字段是否是数值
# sum3 - print sums of numeric columns
# input: rows of integers and strings
# output: sums of numeric columns
# assumes every line has same layout
NR==1 { nlfd = NF
       for (i=1; i<=NF; i++)
           numcol[i] = isnum($i)
       }
       { for(i=1; i<NF; i++)
           if(numcol[i])
               sum[i] += $i
       }
END    { for(i=1; i<=nfld; i++) {
            if(numcol[i])
                printf("%g", sum[i])
            else
                printf("--")
            printf(i < nfld ? "\t" : "\n")
          }
       }       

计算百分比与分位数

# 分析,第一次遍历时数值存储在数组中,第二次遍历时计算百分比
# percent
# input: a column of nonnegative numbers
# output: each number and its percentage of the total
{ x[NR] = $i; sum += $1 }
END { if(sum !=0)
    for(i=1; i<=NR; i++)
        printf("%10.2f %5.1f\n", x[i], 100*x[i]/sum)
}

# 计算成绩,显示直方图
# histogram
# input: numbers between 0 and 100
# output: histogram of deciles
{ x[int($1/10)]++ }
END { for(i=0; i<10; i++)
    printf("%2d-%2d: %3d %s\n",
        10*i, 10*i+9, x[i], rep(x[i],"*"))
    printf("100:    %3d %s\n", x[10], rep(x[10],"*"))
}
function rep(n,s,   t) { # return string of n s's
    while(n-->0)
        t = t s
    retrun t
}
# 在管道线上第一个程序速记生成200个0到100的整数,并送给histogram
awk '
# generate random integers
BEGIN { for (i=1;i<=200;i++)
          print int(101*rand())
      }
' | 
awk -f histgoram
它的输出是
0 - 9: 20 ********************
10 - 19: 18 ******************
20 - 29: 20 ********************
30 - 39: 16 ****************
40 - 49: 23 ***********************
50 - 59: 17 *****************
60 - 69: 22 **********************
70 - 79: 20 ********************
80 - 89: 20 ********************
90 - 99: 22 **********************
100: 2 **

带逗号的数

# 12, 345.67 相加
# sumcomma - add up numbers containing commas
{ gsub(/,/, ""); sum += $0 }   # 逗号替换成空字符,及删除逗号
END { print sum }

# 给数字添加逗号,判断的是小数点左边开始,每三个数字后面跟一个逗号
# addcomma - put commas in numbers
# input: a number per line
# output: the input number followed by
# the number with commas and two decimal places
{ printf("%-12s %20s\n", $0, addcomma($0)) }
function addcomma(x, num){
    if(x<0)
        return "-" addcomma(-x)
    num = sprintf("%.2f", x)  # num is ddddddd.dd
    while(num ~ /[0-9][0-9][0-9][0-9]/)
        sub(/[0-9][0-9][0-9][,.]/, ",&", num)
    return num
}
# 注意&的用法,通过文本替换sub在每三个数字前面插入一个逗号

这是某些测试数据的输出:

源数字改变的数字
00.00
-1-1.00
-12.34-12.34
1234512,345.00
-1234567.89-1,234,567.89
-123.-123.00
-123456-123,456.00

字段固定的输入

假设每行的前 6 个字符包含一个日期, 日期的形式是 mmddyy, 如果我们想让它们按照日期排序, 最简单的办法是先把日期转换成 yymmdd 的形式:
# date convert - convert mmddyy into yymmdd in $1
{ $1 = substr($1,5,2) substr($1,1,2) substr($1,3,2); print }
=============================
如果输入是按照月份排序的, 就像这样
013042 mary's birthday
032772 mark's birthday
052470 anniversary
061209 mother's birthday
110175 elizabeth's birthday
那么程序的输出是
420130 mary's birthday
720327 mark's birthday
700524 anniversary
090612 mother's birthday
751101 elizabeth's birthday

程序的交叉引用检查

nm 的典型输出是:
file.o:
00000c80 T _addroot
00000b30 T _checkdev
00000a3c T _checkdupl
U _chown
U _client
U _close
funmount.o:
00000000 T _funmount
U cerror

# nm.format - add filename to each nm output line
NF == 1 { file = $1 }
NF == 2 { print file, $1, $2 }
NF == 3 { print file, $2, $3 }

nm.format 的输入, 结果是
file.o: T _addroot
file.o: T _checkdev
file.o: T _checkdupl
file.o: U _chown
file.o: U _client
file.o: U _close
funmount.o: T _funmount
funmount.o: U cerror

格式化输出

支票样式:

1026
Aug 31, 2015
Pay to Mary R. Worth-------------------------------- $123.45
the sum of one hundred twenty three dollars and 45 cents exactly
打印支票的 awk 程序: 
# prchecks - print formatted checks
# input: number \t amount \t payee
# output: eight lines of text for preprinted check forms
BEGIN {
FS = "\t"
dashes = sp45 = sprintf("%45s", " ")
gsub(/ /, "-", dashes) # to protect the payee
"date" | getline date # get today's date
split(date, d, " ")
date = d[2] " " d[3] ", " d[6]
initnum() # set up tables for number conversion
}
NF != 3 || $2 >= 1000000 { # illegal data
printf("\nline %d illegal:\n%s\n\nVOID\nVOID\n\n\n", NR, $0)
next # no check printed
}
{ printf("\n") # nothing on line 1
printf("%s%s\n", sp45, $1) # number, indented 45 spaces
printf("%s%s\n", sp45, date) # date, indented 45 spaces
amt = sprintf("%.2f", $2) # formatted amount
printf("Pay to %45.45s $%s\n", $3 dashes, amt) # line 4
printf("the sum of %s\n", numtowords(amt)) # line 5
printf("\n\n\n") # lines 6, 7 and 8
}
function numtowords(n, cents, dols) { # n has 2 decimal places
cents = substr(n, length(n)-1, 2)
dols = substr(n, 1, length(n)-3)
if (dols == 0)
return "zero dollars and " cents " cents exactly"
return intowords(dols) " dollars and " cents " cents exactly"
}
function intowords(n) {
n = int(n)
if (n >= 1000)
return intowords(n/1000) " thousand " intowords(n%1000)
if (n >= 100)
return intowords(n/100) " hundred " intowords(n%100)
if (n >= 20)
return tens[int(n/10)] " " intowords(n%10)
return nums[n]
}
function initnum() {
  split("one two three four five six seven eight nine " \
     "ten eleven twelve thirteen fourteen fifteen " \
     "sixteen seventeen eighteen nineteen", nums, " ")
  split("ten twenty thirty forty fifty sixty " \
       "seventy eighty ninety", tens, " ")
}

函数 numtowords 与 intowords 把数字转换成对应的单词, 转换过程非常直接。

数据验证

对称的分隔符

下面这个程序与列求和程序非常相似, 但没有求和
操作: 
# colcheck - check consistency of columns
# input: rows of numbers and strings
# output: lines whose format differs from first line
NR == 1 {
nfld = NF
for (i = 1; i <= NF; i++)
type[i] = isnum($i)
}
{ if (NF != nfld)
printf("line %d has %d fields instead of %d\n",
NR, NF, nfld)
for (i = 1; i <= NF; i++)
if (isnum($i) != type[i])
printf("field %d in line %d differs from line 1\n",
i, NR)
}
function isnum(n) { return n ~ /^[+-]?[0-9]+$/ }

可以用于检查分隔符是否按照正确的顺序出现, 程序虽小, 但却是检查
程序的典型代表:
# p12check - check input for alternating .P1/.P2 delimiters
/^\.P1/ { if (p != 0)
print ".P1 after .P1, line", NR
p = 1
}
/^\.P2/ { if (p != 1)
print ".P2 with no preceding .P1, line", NR
p = 0
}
END { if (p != 0) print "missing .P2 at end" }
如果分隔符按照正确的顺序出现, 那么变量 p 就会按照 0 1 0 1 0 ... 1 0 的规律变化, 否则, 一条错误消息被打印出来, 消息含有发生错误时, 当前输入行所在的行号

密码文件检查

例子:Unix 系统中的密码文件含有授权用户的用户名及其相关信息, 密码文件的每一行都由 7 个字段组成:(root:qyxRi2uhuVjrg:0:2::/:)
# passwd - check password file
BEGIN {
FS = ":" }
NF != 7 {
printf("line %d, does not have 7 fields: %s\n", NR, $0) }
$1 ~ /[^A-Za-z0-9]/ {
printf("line %d, nonalphanumeric user id: %s\n", NR, $0) }
$2 == "" {
printf("line %d, no password: %s\n", NR, $0) }
$3 ~ /[^0-9]/ {
printf("line %d, nonnumeric user id: %s\n", NR, $0) }
$4 ~ /[^0-9]/ {
printf("line %d, nonnumeric group id: %s\n", NR, $0) }
$6 !~ /^\// {
printf("line %d, invalid login directory: %s\n", NR, $0) }

打包与拆分

# 打包(bundle)为每一行加上文件名前缀, 文件名可以通过内建变量 FILENAME 得到
# bundle - combine multiple files into one
{ print FILENAME, $0 }
对应的拆分unbundle
# unbundle - unpack a bundle into separate files
$1 != prev { close(prev); prev=$1 }
    { print substr($0, index($0, " ") + 1) >$1 }

多行记录

由空行分隔的记录

例子:
Adam Smith
1234 Wall St., Apt. 5C
New York, NY 10021
212 555-4321

David W. Copperfield
221 Dickens Lane
Monterey, CA 93940
408 555-0041
work phone 408 555-6532
Mary, birthday January 30

Canadian Consulate
555 Fifth Ave
New York, NY
212 586-2400
若记录分隔符 RS 被设置成空值(RS=“”), 则每一个行块都被当成一个记录, 于是
BEGIN { RS = “”}
/New York/ # 打印所有的, 含有 New York 的记录, 而不管这个记录有多少行:
Adam Smith
1234 Wall St., Apt. 5C
New York, NY 10021
212 555-4321
Canadian Consulate
555 Fifth Ave
New York, NY
212 586-2400

这样的话, 则输出记录之间是不会有空白行的, 输入格式并不会被保留下来. 为了解决这个问题, 最简单的办法是把输出记录分隔符 ORS 设置成 \n\n:
BEGIN { RS = “”; ORS = “\n\n” }
/New York/

BEGIN { RS = “”; FS = “\n” }
$1 ~ /Smith$/ { print $1, $4 } # name, phone
程序的输出是:
Adam Smith 212 555-4321

不管 FS 的值是什么, 换行符总是多行记录的字段分隔符之一. 如果 RS 被设置成 “”,则默认的字段分隔符就是空格符, 制表符, 以及换行符; 如果 FS 是 \n, 则换行符就是唯一的字段分隔符.

处理多行记录

下面的程序 pipeline
按照姓氏对输入进行排序:
# pipeline to sort address list by last names
awk '
BEGIN { RS = ""; FS = "\n" }
{ printf("%s!!#", x[split($1, x, " ")])
for (i = 1; i <= NF; i++)
printf("%s%s", $i, i < NF ? "!!#" : "\n")
}
' |
sort |
awk '
BEGIN { FS = "!!#" }
{ for (i = 2; i <= NF; i++)
printf("%s\n", $i)
printf("\n")
}
'

带有头部和尾部的记录

如下面的一个记录:
accountant
Adam Smith
1234 Wall St., Apt. 5C
New York, NY 10021

doctor - ophthalmologist
Dr. Will Seymour
798 Maple Blvd.
Berkeley Heights, NJ 07922

lawyer
David W. Copperfield
221 Dickens Lane
Monterey, CA 93940

doctor - pediatrician
Dr. Susan Mark
600 Mountain Avenue
Murray Hill, NJ 07974
/^doctor/, /^$/  # 匹配doctor开始,以空白行结束,/^$/匹配一个空白行

这样:可用一个变量控制行的打印。如当前输入行包含期望的头部信息,则p为1,随后尾部p置为0(也是p的初始值)。即p为1才会打印。
/^doctor/ { p = 1; next }
p == 1
/^$/ { p = 0; next } 

名称-值

如下支票簿:
check 1021
to Champagne Unlimited
amount 123.10
date 1/1/87

deposit
amount 500.00
date 1/1/87
check 1022
date 1/2/87

amount 45.10
to Getwell Drug Store
tax medical
check 1023
amount 125.00
to International Travel
date 1/3/87

amount 50.00
to Carnegie Hall
date 1/3/87
check 1024
tax charitable contribution

to American Express
check 1025
amount 75.75
date 1/5/87
如要计算存款与支票的总额, 只需要扫描存款项与支票项即可: 
# check1 - print total deposits and checks
/^check/ { ck = 1; next }
/^deposit/ { dep = 1; next }
/^amount/ { amt = $2; next }
/^$/ { addup() }
END { addup()
printf("deposits $%.2f, checks $%.2f\n",
deposits, checks)
}
function addup() {
if (ck)
checks += amt
else if (dep)
deposits += amt
ck = dep = amt = 0
}
输出是:
deposits $500.00, checks $418.95

另一种方法:函数 field(s) 在当前记录中搜索名字是 s 的条目, 如果找到, 就把该项的值返回
# check2 - print total deposits and checks
BEGIN { RS = ""; FS = "\n" }
/(^|\n)deposit/ { deposits += field("amount"); next }
/(^|\n)check/ { checks += field("amount"); next }
END { printf("deposits $%.2f, checks $%.2f\n",
deposits, checks)
}
function field(name, i,f) {
for (i = 1; i <= NF; i++) {
split($i, f, "\t")
if (f[1] == name)
return f[2]
}
printf("error: no field %s in record\n%s\n", name, $0)
}

# 程序以一种更加紧凑的方式打印支票信息: 
1/1/87 1021 $123.10 Champagne Unlimited
1/2/87 1022 $45.10 Getwell Drug Store
1/3/87 1023 $125.00 International Travel
1/3/87 1024 $50.00 Carnegie Hall
1/5/87 1025 $75.75 American Express
程序的代码是
# check3 - print check information
BEGIN { RS = ""; FS = "\n" }
/(^|\n)check/ {
for (i = 1; i <= NF; i++) {
split($i, f, "\t")
val[f[1]] = f[2]
}
printf("%8s %5d %8s %s\n",
val["date"],
val["check"],
sprintf("$%.2f", val["amount"]),
val["to"])
for (i in val)
delete val[i]
}

第四章:报表与数据库

使用 awk 从文件中提取信息, 并生成报表, 重点在表格数据。基本思想是把文件组看成是关系数据库, 这样做的好处是可以用名字 (而不是数字) 标识字段。

报表生成

Awk 可以从文件中挑选数据, 并将挑选到的数据格式化成报表。
用三个步骤的过程来生成报表: 准备, 排序, 格式化.。
准备步骤包括选择数据 ,对数据进行一些运算得到期望的信息 ; 想要特定的顺序就要排序;格式化由第2个awk程序完成,根据已排序的数据生成报表。

一个简单的报表

用前面第二章的countries进行生成报表:

CONTINENTCOUNTRYPOPULATIONAREAPOP. DEN.
AsiaJapan120144833.3
AsiaIndia7461267588.8
AsiaChina10323705278.5
AsiaUSSR275864931.8
EuropeGermany6196635.4
EuropeEngland5694595.7
EuropeFrance55211260.7
North AmericaMexico78762102.4
North AmericaUSA237361565.6
North AmericaCanada2538526.5
South AmericaBrazil134328640.8
生成报表的前两个阶段由程序 prep1 完成, 当文件 countries 作为输入时, prep1 提取并计算相关的信息, 并对其进行排序:
# prep1 - prepare countries by continent and pop. den.
BEGIN { FS = "\t" }
{ printf("%s:%s:%d:%d:%.1f\n",
$4, $1, $3, $2, 1000*$3/$2) | "sort -t: +0 -1 +4rn"
}
# 程序 prep1 把输出直接输送给 sort 命令, 参数 -t 告诉 sort 把冒号作为字段分隔符, +0 -1表示把第 1 个字段作为排序的主键, 参数 +4rn 表示把第 5 个字段作为次要主键, 按照数值的逆序进行排序

已完成三个步骤中的前两个: 准备与排序, 现在所要做的是把数据格式化成我们想要的报表格式, 程序 form1 做的正是这个工作: 
# form1 - format countries data by continent, pop. den.
BEGIN { FS = ":"
printf("%-15s %-10s %10s %7s %12s\n",
"CONTINENT", "COUNTRY", "POPULATION",
"AREA", "POP. DEN.")
}
{ printf("%-15s %-10s %7d %10d %10.1f\n",
$1, $2, $3, $4, $5)
}
可通过键入:awk -f prep1 countries | awk -f form1

# 程序 prep2:
# prep2 - prepare countries by continent, inverse pop. den.
BEGIN { FS = "\t"}
{ den = 1000*$3/$2
printf("%-15s:%12.8f:%s:%d:%d:%.1f\n",
$4, 1/den, $1, $3, $2, den) | "sort"
}
# 程序 form2:
# form2 - format countries by continent, pop. den.
BEGIN { FS = ":"
printf("%-15s %-10s %10s %7s %12s\n",
"CONTINENT", "COUNTRY", "POPULATION",
"AREA", "POP. DEN.")
}
{ if ($1 != prev) {
print ""
prev = $1
} else
$1 = ""
printf("%-15s %-10s %7d %10d %10.1f\n",
$1, $2, $3, $4, $5)
}
通过 awk -f prep1 countries | awk -f form2
# 格式化程序 form2 是一个 “control-break” 程序, 变量 prev 跟踪大洲的名字, 只有当大洲名字变化时才会打印出来

更复杂的报表

假设我们需要为每一个大洲作一个汇总, 以及计算每一个国家占总人口与总面积的比重, 我们需要新增一个标题, 以及更多的列表头:

prep3 从文件 countries中准备并排序必要的信息:
# prep3 - prepare countries data for form3
BEGIN { FS = "\t" }
pass == 1 {
area[$4] += $2
areatot += $2
pop[$4] += $3
poptot += $3
}
pass == 2 {
den = 1000*$3/$2
printf("%s:%s:%s:%f:%d:%f:%f:%d:%d\n",
$4, $1, $3, 100*$3/poptot, $2, 100*$2/areatot,
den, pop[$4], area[$4]) | "sort -t: +0 -1 +6rn"
}
程序需要遍历输入数据两次, 第一次遍历累加每个大洲的面积与人口数, 并分别保存到数组
area 与 pop 中, 同时计算总面积与总人口数, 分别保存在变量 areatot 与 poptot 中. 第二次遍历对每个国家的统计结果进行格式化, 并输送给 sort. 两次遍历通过变量 pass 控制, 其值可以通过命令行设置:  
awk -f prep3 pass=1 countries pass=2 countries

格式化程序 form3 的源代码是:
# form3 - format countries report number 3
BEGIN {
FS = ":"; date = "January 1, 1988"
hfmt = "%36s %8s %12s %7s %12s\n"
tfmt = "%33s %10s %10s %9s\n"
TOTfmt = " TOTAL for %-13s%7d%11.1f%11d%10.1f\n"
printf("%-18s %-40s %19s\n\n", "Report No. 3",
"POPULATION, AREA, POPULATION DENSITY", date)
printf(" %-14s %-14s %-23s %-14s %-11s\n\n",
"CONTINENT", "COUNTRY", "POPULATION", "AREA", "POP. DEN.")
printf(hfmt, "Millions ", "Pct. of", "Thousands ",
"Pct. of", "People per")
printf(hfmt, "of People", "Total ", "of Sq. Mi.",
"Total ", "Sq. Mi. ")
printf(hfmt, "---------", "-------", "----------",
"-------", "----------")
}
{ if ($1 != prev) { # new continent
if (NR > 1)
totalprint()
prev = $1 # first entry for continent
poptot = $8; poppct = $4
  areatot = $9; areapct = $6
} else { # next entry for continent
$1 = ""
poppct += $4; areapct += $6
}
printf(" %-15s%-10s %6d %10.1f %10d %9.1f %10.1f\n",
$1, $2, $3, $4, $5, $6, $7)
gpop += $3; gpoppct += $4
garea += $5; gareapct += $6
}
END {
totalprint()
printf(" GRAND TOTAL %20d %10.1f %10d %9.1f\n",
gpop, gpoppct, garea, gareapct)
printf(tfmt, "=====", "======", "=====", "======")
}
function totalprint() { # print totals for previous continent
printf(tfmt, "----", "-----", "-----", "-----")
printf(TOTfmt, prev, poptot, poppct, areatot, areapct)
printf(tfmt, "====", "=====", "=====", "=====")
}
最后通过: 
awk -f prep3 pass=1 countries pass=2 countries | awk -f form3
可以得到优美的表格。

# 程序 form4 非常类似于 form3, 但是它没有用于控制列宽度的魔数, 相反, 它生成了一些 tbl 命令与表格数据, 不同列的数据之间用制表符分隔, 剩下的工作由 tbl 完成. 
# form4 - format countries data for tbl input
BEGIN {
FS = ":"; OFS = "\t"; date = "January 1, 1988"
print ".TS\ncenter;"
print "l c s s s r s\nl\nl l c s c s c\nl l c c c c c."
printf("%s\t%s\t%s\n\n", "Report No. 3",
"POPULATION, AREA, POPULATION DENSITY", date)
print "CONTINENT", "COUNTRY", "POPULATION",
"AREA", "POP. DEN."
print "", "", "Millions", "Pct. of", "Thousands",
"Pct. of", "People per"
print "", "", "of People", "Total", "of Sq. Mi.",
"Total", "Sq. Mi."
print "\t\t_\t_\t_\t_\t_"
print ".T&\nl l n n n n n."
}
{ if ($1 != prev) { # new continent
if (NR > 1)
totalprint()
prev = $1
poptot = $8; poppct = $4
areatot = $9; areapct = $6
} else { # next entry for current continent
$1 = ""
poppct += $4; areapct += $6
}
printf("%s\t%s\t%d\t%.1f\t%d\t%.1f\t%.1f\n",
$1, $2, $3, $4, $5, $6, $7)
gpop += $3; gpoppct += $4
garea += $5; gareapct += $6
}
END {
totalprint()
print ".T&\nl s n n n n n."
printf("GRAND TOTAL\t\t%d\t%.1f\t%d\t%.1f\n",
gpop, gpoppct, garea, gareapct)
print "", "=", "=", "=", "=", "="
print ".TE"
}
function totalprint() { # print totals for previous continent
print ".T&\nl s n n n n n."
print "", "_", "_", "_", "_", "_"
printf(" TOTAL for %s\t%d\t%.1f\t%d\t%.1f\n",
prev, poptot, poppct, areatot, areapct)
print "", "=", "=", "=", "=", "="
print ".T&\nl l n n n n n."
}

# 从一个小程序开始: 这个程序以左对齐的方式在列中打印文本条目, 其列宽度为所在列的最大值; 如果是数值则右对齐, 但是数值所占的域相对于本列最宽的项居中。
# table - simple table formatter
BEGIN {
FS = "\t"; blanks = sprintf("%100s", " ")
number = "^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)$"
}
{ row[NR] = $0
for (i = 1; i <= NF; i++) {
if ($i ~ number)
nwid[i] = max(nwid[i], length($i))
wid[i] = max(wid[i], length($i))
}
}
END {
for (r = 1; r <= NR; r++) {
n = split(row[r], d)
for (i = 1; i <= n; i++) {
sep = (i < n) ? " " : "\n"
if (d[i] ~ number)
printf("%" wid[i] "s%s", numjust(i,d[i]), sep)
else
printf("%-" wid[i] "s%s", d[i], sep)
}
}
}
function max(x, y) { return (x > y) ? x : y }
function numjust(n, s) { # position s in field n
return s substr(blanks, 1, int((wid[n]-nwid[n])/2))
}
第一次遍历记录数据与每列的最大宽度, 第二次遍历 (位于 END) 在适当的位置打印每一项. 对字母项进行左对齐比较容易: 我们使用 wid[i] (第 i 列的最大宽度) 为 printf 构造格式字符串,比如说, 如果列的最大宽度是 10, 则第 i 列的格式字符串就是 %-10s (假设该列是字母项).
如果是数字项, 则要多做点工作: 第 i 列的条目 v 需要右对齐, 就像这样:
    wid[i]    | 
   nwid[i]    |
        v
v 左边的空格数是 (wid[i]-nwid[i])/2, 所以 numjust 会在 v 的末尾拼接这么多的空格,然后再按照 %10s 的格式输出 (假设该列的最大宽度是 10).        

打包的查询与报表

如果某个查询经常被访问, 比较方便的做法是把它打包到一个命令中, 这样在下次执行的时候, 就可以少打点字.

比如就叫 info, 查询时只要输入
info Canada
info USA
等等

awk '
# info - print information about country
# usage: info country-name
BEGIN { FS = "\t" }
$1 ~ /'$1'/ {
printf("%s:\n", $1)
printf("\t%d million people\n", $3)
printf("\t%.3f million sq. mi.\n", $2/1000)
printf("\t%.1f people per sq. mi.\n", 1000*$3/$2)
}
' countries

# 修改 info: 参数通过 ARGV 传递进来, 而不是由 shell 进行替换.
awk '
BEGIN { FS = "\t"; pat = ARGV[1]; ARGV[1] = "-" }
$1 ~ pat {
printf("%s:\n", $1)
printf("\t%d million people\n", $3)
printf("\t%.3f million sq. mi.\n", $2/1000)
printf("\t%.1f people per sq. mi.\n", 1000*$3/$2)
}
' "$1" <countries
另一种解决办法是用var=text 替换掉ARGV:
awk '
BEGIN { FS = "\t" }
$1 ~ pat {
printf("%s:\n", $1)
printf("\t%d million people\n", $3)
printf("\t%.3f million sq. mi.\n", $2/1000)
printf("\t%.1f people per sq. mi.\n", 1000*$3/$2)
}
' pat="$1" <countries

格式信函

​ letter.text
​ |
参数值--------> form.gen-------> 格式信函
格式信函的文本内容存放在文件 letter.text 中, 文本中包含了许多参数, 只需要通过参数替换, 就可以生成不同内容的信函.

程序 form.gen 是格式信函生成程序:
# form.gen - generate form letters
# input: prototype file letter.text; data lines
# output: one form letter per data line
BEGIN {
FS = ":"
while (getline <"letter.text" > 0) # read form letter
form[++n] = $0
}
{ for (i = 1; i <= n; i++) { # read data lines
temp = form[i] # each line generates a letter
for (j = NF; j >= 1; j--)
gsub("#" j, $j, temp)
print temp
}
}
form.gen 的 BEGIN 从文件 letter.text 读取格式信函的文本, 并存放到数组 form 中,剩下的工作是读取输入参数, 调用 gsub 将文本中的 #n 替换成对应的参数, 被修改的只是存储在数组 form 中的信函副本, 文件 letter.text 的内容并没有发生改变. 注意程序是如何通过字符串拼接生成 gsub 的第一个参数

关系数据库系统

一个简单的关系型数据库系统, 这个系统的核心包括一个类 awk 查询语言 q, 一个数据字典 relfile, 以及一个查询处理程序 qawk, qawk 的作用是把 q 描述的查询翻译成 awk 程序.
这个系统通过以下三种方式将 awk 扩展成一个数据库语言:

  • 通过名字引用字段 (而不是数字).
  • 数据库可以分散在多个文件中, 而不是限定在一个文件上.
  • 可以交互性地生成一个查询序列.

假设数据库由一个单独的文件 countries 组成, 文件的每一行都包括四个字段, 字段的名字分别是 country, area, population, 以及 continent.
现在在数据库中新增一个文件 capitals, 文件的每一行都由两个字段组成,国家及其首都:字段之间用制表符分开。
USSR Moscow
Canada Ottawa
China Beijing
USA Washington
Brazil Brasilia
India New Delhi
Mexico Mexico City
France Paris
Japan Tokyo
Germany Bonn
England London

awk ' BEGIN { FS = "\t" }
FILENAME == "capitals" {
cap[$1] = $2
}
FILENAME == "countries" && $4 == "Asia" {
print $1, $3, cap[$1]
}
' capitals countries
如果我们只需要输入
$continent ~ /Asia/ { print $country, $population, $capital }

自然连接

一个 自然连接 (natural join) (或简称为 连接) 指的是一个运算, 它将两张表以公共属性为基础组合成一张表, 这张表包含了两张表所有的属性, 但是重复的属性会被移除.

# join - join file1 file2 on first field
# input: two sorted files, tab-separated fields
# output: natural join of lines with common first field
BEGIN {
OFS = sep = "\t"
file2 = ARGV[2]
ARGV[2] = "" # read file1 implicitly, file2 explicitly
eofstat = 1 # end of file status for file2
if ((ng = getgroup()) <= 0)
exit # file2 is empty
}
{ while (prefix($0) > prefix(gp[1]))
if ((ng = getgroup()) <= 0)
exit # file2 exhausted
if (prefix($0) == prefix(gp[1])) # 1st attributes in file1
for (i = 1; i <= ng; i++) # and file2 match
print $0, suffix(gp[i]) # print joined line
}
function getgroup() { # put equal prefix group into gp[1..ng]
if (getone(file2, gp, 1) <= 0) # end of file
return 0
for (ng = 2; getone(file2, gp, ng) > 0; ng++)
if (prefix(gp[ng]) != prefix(gp[1])) {
unget(gp[ng]) # went too far
return ng-1
}
return ng-1
}
function getone(f, gp, n) { # get next line in gp[n]
if (eofstat <= 0) # eof or error has occurred
return 0
if (ungot) { # return lookahead line if it exists
gp[n] = ungotline
ungot = 0
return 1
}
return eofstat = (getline gp[n] <f)
}
function unget(s) { ungotline = s; ungot = 1 }
function prefix(s) { return substr(s, 1, index(s, sep) - 1) }
function suffix(s) { return substr(s, index(s, sep) + 1) }

relfile

relfile 的文件中 (“rel” 指的是 “relation”). 文件 relfile 包含了数据库中每张表的名字, 属性, 如果表格不存在, 那么文件还包含了构造表格的规则. 文件 relfile的内容是一系列的表格描述符, 表格描述符具有形式:

tablename
    attribute
    attribute
      ...
    !command
    ...

如果一张表不含构造命令, 则说明已经有一个文件以该表的名字命名, 且包含了该表的数据, 这样的表叫做 基表 (base table), 数据被插入到基表中, 并在基表中更新. 在文件relfile 中, 如果某张表在属性名之后出现了构造命令, 则称这张表是 导出表 (derived table),只有在必要的时候才会构造导出表.
如下例子,用下面的 relfile 来表示扩展了的国家数据库:
countries:
country
area
population
continent
capitals:
country
capital
cc:
country
area
population
continent
capital
!sort countries >temp.countries
!sort capitals >temp.capitals
!join temp.countries temp.capitals >cc
说明:两张基表 — countries 与 capitals, 一张导出表 cc, 导出表 cc 通过把基表排序并存放在临时文件中, 再对临时文件执行连接操作来生成, 也就是说, cc 通过执行下面语句生成。
sort countries >temp.countries
sort capitals >temp.capitals
join temp.countries temp.capitals >cc
一个 relfile 常常包含一张 全局关系表 (universal relation), 它是一张包含了所有属性的表, 是 relfile 的最后一张表, 这种做法保证至少有一张表包含了属性的所有可能的组合, 表格cc 就是数据库 countries-capitals 的全局关系表.

q,类awk查询语言

数据库查询语言 q 由单独一行的 awk 程序组成, 但字段名被属性名替代. 查询处理程序 qawk 按照下面的步骤来响应一个查询:

  1. 判断该查询所包含的属性集;
  2. 从 relfile 的第一行开始, 搜索第一张包含了查询中全部属性的表, 如果该表是基表, 则用它作为查询的输入, 如果是导出表, 则构造该表, 再用它作为查询的输入. (这意味着查询中可能出现的属性组合一定也出现在 relfile 的基表或导出表中);
  3. 通过把符号型的字段引用替换成对应的数值型字段引用, 将 q 查询转换成等价的 awk 程序,
    这个程序接下来会把步骤 2 决定的表作为输入数据.

q 查询: $continent ~ /Asia/ { print $country, $population }
查询处理程序把该查询转换成程序:$4 ~ /Asia/ { print $1, $3 }
并把文件 countries 作为输入数据。

q 查询: { print $country, $population, $capital }
包含属性 country, population 与 capital, 它们都是导出表 cc 的属性子集. 于是查询处理程序利用 relfile 列出的构造命令构造出表格 cc, 并将该查询转换成程序: { print $1, $3, $5 }
程序把刚构造而成的 cc 作为输入数据.

也可以用来作计算, 下面这个查询打印面积的平均值:
{ area += $area }; END { print area/NR }

qawk,q-to-awk翻译器

把 q 查询转换成等价的 awk 程序

  • 首先, qawk 读取文件 relfile, 将表名收集到数组 relname 中. 为了构造第 i 张表, 程序把必要的构造命令收集到数组 array 中, 从 cmd[i, 1] 开始存放. 它还把每张表的属性收集到2 维数组 attr 中, 元素 attr[ i,a] 存放的是第 i 张表中, 名为 a 的属性的索引.

  • 然后, qawk 读取一个查询, 判断它会用到哪些属性, 查询中的属性都是形式为 $name 的字符串. 利用函数 subset, 就可以找到第一张包含了查询中全部属性的表 Ti. 程序通过把属性的索引替换成原始形式来生成 awk 程序, 并执行必要的命令来生成表格 Ti, 再执行新生成的 awk程序, 把表格 Ti 作为输入数据.

这是 qawk 的源代码:
# qawk - awk relational database query processor
BEGIN { readrel("relfile") }
/./ { doquery($0) }
function readrel(f) {
while (getline <f > 0) # parse relfile
if ($0 ~ /^[A-Za-z]+ *:/) { # name:
gsub(/[^A-Za-z]+/, "", $0) # remove all but name
relname[++nrel] = $0
} else if ($0 ~ /^[ \t]*!/) # !command...
cmd[nrel, ++ncmd[nrel]] = substr($0,index($0,"!")+1)
else if ($0 ~ /^[ \t]*[A-Za-z]+[ \t]*$/) # attribute
attr[nrel, $1] = ++nattr[nrel]
else if ($0 !~ /^[ \t]*$/) # not white space
print "bad line in relfile:", $0
}
function doquery(s, i,j) {
for (i in qattr) # clean up for next query
delete qattr[i]
query = s # put $names in query into qattr, without $
while (match(s, /\$[A-Za-z]+/)) {
qattr[substr(s, RSTART+1, RLENGTH-1)] = 1
s = substr(s, RSTART+RLENGTH+1)
}
for (i = 1; i <= nrel && !subset(qattr, attr, i); )
i++
if (i > nrel) # didn't find a table with all attributes
missing(qattr)
else { # table i contains attributes in query
for (j in qattr) # create awk program
gsub("\\$" j, "$" attr[i,j], query)
for (j = 1; j <= ncmd[i]; j++) # create table i
if (system(cmd[i, j]) != 0) {
print "command failed, query skipped\n", cmd[i,j]
return
}
awkcmd = sprintf("awk -F'\t' '%s' %s", query, relname[i])
printf("query: %s\n", awkcmd) # for debugging
system(awkcmd)
}
}
function subset(q, a, r, i) { # is q a subset of a[r]?
for (i in q)
if (!((r,i) in a))
return 0
return 1
}
function missing(x, i) {
print "no table contains all of the following attributes:"
for (i in x)
print i
}

第五章:文本处理

生成随机文本

可以用内建函数 rand 生成随机数据, 每次调用该函数都会返回一个伪随机数. rand 每次都使用同一个种子数来生成随机数, 所以, 如果你想要得到一个不同的随机数序列, 就必须调用一次srand(), 它根据当前时间计算出一个种子数, 并用该种子数初始化 rand。

随机选择

rand 都会返回一个大于等于 0, 小于 1 的浮点数, 但是一般来说, 更通常的需求是返回一个 1 到 n 之间的随机整数, 我们可以用 rand 来实现:

# randint - return random integer x, 1 <= x <= n
function randint(n) {
return int(n * rand()) + 1
}

randint(n) 按比例调整 rand 的返回值, 调整后的值大于等于 0 并且小于 n, 将小数部分截去可以得到 0 至 n-1 的整数, 然后再加 1, 就是 1 到 n 之间的整数.

# 用 randint 随机选择一个字母
# randlet - generate random lower-case letter
function randlet() {
return substr("abcdefghijklmnopqrstuvwxyz", randint(26), 1)
}

# 利用 randint, 我们可以从 n 项的数组中随机选择一个元素:
print x[randint(n)]

# 函数 choose 从数组 A 的前 n 中随机选择 k 个元素, 并按照原来的顺序打印出来:
# choose - print in order k random elements from A[1]..A[n]
function choose(A, k, n, i) {
    for (i = 1; n > 0; i++)
        if (rand() < k/n--) {
            print A[i]
            k--
        }
}

# 写一个程序, 该程序生成 1 到 n 之间的 k 个互不相同的随机整数, 要求程序的时间复杂度与 k 成正比.
# print k distinct random integers between 1 and n
{ random($1, $2) }
function random(k, n, A, i, r) {
    for (i = n-k+1; i <= n; i++)
        ((r = randint(i)) in A) ? A[i] : A[r]
    for (i in A)
        print i
}
function randint(n) { return int(n*rand())+1 }

写一个随机生成四手桥牌的程序.在这里插入图片描述
程序生成从1 到52 的整数的一个随机排列, 排列结果存放到数组deck 中. 数组被均分成四段, 分别对每段中的13 个数排序, 每一段都表示一手牌: 数字52 对应黑桃A, 51 对应黑桃K, 1 对应梅花二.函数permute(k,n) 使用Floyd 算法从1 到n 的整数中随机生成一个长度为k 的排列. 函数sort(x,y) 使用插入排序, 对deck[x…y] 中的元素进行排序. 最后, 函数prhands 按照上面的风格, 格式化并输出每一手牌.

# bridge - generate random bridge hands
BEGIN { split(permute(52,52), deck) # generate a random deck
sort(1,13); sort(14,26); sort(27,39); sort(40,52) # sort hands
prhands() # format and print the four hands
}
function permute(k, n, i, p, r) { # generate a random permutation
srand(); p = " " # of k integers between 1 and n
for (i = n-k+1; i <= n; i++)
if (p ~ " " (r = int(i*rand())+1) " " )
sub(" " r " ", " " r " " i " ", p) # put i after r in p
else p = " " r p # put r at beginning of p
return p
}
function sort(left,right, i,j,t) { # sort hand in deck[left..right]
for (i = left+1; i <= right; i++)
for (j = i; j > left && deck[j-1] < deck[j]; j--) {
t = deck[j-1]; deck[j-1] = deck[j]; deck[j] = t
}
}
function prhands() { # print the four hands
b = sprintf("%20s", " "); b40 = sprintf("%40s", " ")
card = 1 # global index into deck
suits(13); print b " NORTH"
print b spds; print b hrts; print b dnds; print b clbs
suits(26) # create the west hand from deck[14..26]
ws = spds substr(b40, 1, 40 - length(spds))
wh = hrts substr(b40, 1, 40 - length(hrts))
wd = dnds substr(b40, 1, 40 - length(dnds))
wc = clbs substr(b40, 1, 40 - length(clbs))
suits(39); print " WEST" sprintf("%36s", " ") "EAST"
print ws spds; print wh hrts; print wd dnds; print wc clbs
suits(52); print b " SOUTH"
print b spds; print b hrts; print b dnds; print b clbs
}
function suits(j) { # collect suits of hand in deck[j-12..j]
for (spds = "S:"; deck[card] > 39 && card <= j; card++)
spds = spds " " fvcard(deck[card])
for (hrts = "H:"; deck[card] > 26 && card <= j; card++)
hrts = hrts " " fvcard(deck[card])
for (dnds = "D:"; deck[card] > 13 && card <= j; card++)
dnds = dnds " " fvcard(deck[card])
for (clbs = "C:"; card <= j; card++)
clbs = clbs " " fvcard(deck[card])
}
function fvcard(i) { # compute face value of card i
if (i % 13 == 0) return "A"
else if (i % 13 == 12) return "K"
else if (i % 13 == 11) return "Q"
else if (i % 13 == 10) return "J"
else return (i % 13) + 1
}

废话生成器

它根据已有的废话重新创建一个新的出来. 输入是一个句子集合:
A rolling stone:gathers no moss.
History:repeats itself.
He who lives by the sword:shall die by the sword.
A jack of all trades:is master of none.
Nature:abhors a vacuum.
Every man:has a price.
All’s well that:ends well.
冒号将主语和谓语分开. 废话生成器随机选择一个主语与另一个谓语作组合, 如果运气好的话, 可能会产生很有意思的格言警句:
A rolling stone repeats itself.
History abhors a vacuum.
nature repeats itself.
All’s well that gathers no moss.
He who lives by the sword has a price.

# cliche - generate an endless stream of cliches
# input: lines of form subject:predicate
# output: lines of random subject and random predicate
BEGIN { FS = ":" }
{ x[NR] = $1; y[NR] = $2 }
END { for (;;) print x[randint(NR)], y[randint(NR)] }
function randint(n) { return int(n * rand()) + 1 }

随机语句

上下文无关语法 (context-free grammar) 指的是一组规则, 这组规则定义了如何生成或分析一个语句集合. 每一条规则 (称为 产生式 (production)) 都具有形式:
A ----> B C D …
该产生式的意思是每一个 A 都可以被 “重写” 为 B C D … 产生式左边的符号 (A) 称为 非终结符(nonterminal), 它可以被进一步地扩展. 产生式右边的符号可以是非终结符 (可以是多个 A) 或终结符 (terminal), 终结符指的是不能被扩展的符号. 多个产生式可以共享同一个非终结符, 终结符与非终结符也可以在产生式的右边出现多次。

开发一个程序, 该程序根据语法生成语句, 每次生成都从一个指定的非终结符开始. 程序从文件中读取语法规则, 记录下每一个左部出现的次数, 左部所拥有的右部的个数, 以及它们各自的组成成分. 然后, 每输入一个非终结符, 就会为该非终结符生成一个随机语句.
程序使用三个数组来存放语法规则:

  • lhs[A] 给出了非终结符 A 的产生式个数,
  • rhscnt[A,i] 存放的是 A 的第 i 条产生式右部的符号个数,
  • rhslist[A, i, j] 存放的是 A 的第 i 条产生式右部的第 j 个符号. 对于前面提到的语法规则, 三个数组的内容分别是:
  • 在这里插入图片描述
# sentgen - random sentence generator
# input: grammar file; sequence of nonterminals
# output: a random sentence for each nonterminal
BEGIN { # read rules from grammar file
while (getline < "grammar" > 0)
if ($2 == "->") {
i = ++lhs[$1] # count lhs
rhscnt[$1, i] = NF-2 # how many in rhs
for (j = 3; j <= NF; j++) # record them
rhslist[$1, i, j-2] = $j
} else
print "illegal production: " $0
}
{ if ($1 in lhs) { # nonterminal to expand
gen($1)
printf("\n")
} else
print "unknown nonterminal: " $0
}
function gen(sym, i, j) {
if (sym in lhs) { # a nonterminal
i = int(lhs[sym] * rand()) + 1 # random production
for (j = 1; j <= rhscnt[sym, i]; j++) # expand rhs's
gen(rhslist[sym, i, j])
} else
printf("%s ", sym)
}

实现一个非递归的语句生成程序

用一个由用户管理的栈替换掉递归. 在对产生式的右部进行展开时, 程序按照相反的顺序把右部压入到栈中, 这样就可以按照正确的顺序产生输出.
# sentgen2 - random sentence generator (nonrecursive)
# input: grammar file; sequence of nonterminals
# output: random sentences generated by the grammar
BEGIN { # read rules from grammar file
while (getline < "grammar" > 0)
if ($2 == "->") {
i = ++lhs[$1] # count lhs
rhscnt[$1, i] = NF-2 # how many in rhs
for (j = 3; j <= NF; j++) # record them
rhslist[$1, i, j-2] = $j
} else
print "illegal production: " $0
}
{ if ($1 in lhs) { # nonterminal to expand
push($1)
gen()
printf("\n")
} else
print "unknown nonterminal: " $0
}
function gen( i, j) {
while (stp >= 1) {
sym = pop()
if (sym in lhs) { # a nonterminal
i = int(lhs[sym] * rand()) + 1 # random production
for (j = rhscnt[sym, i]; j >= 1; j--) # expand rhs's
push(rhslist[sym, i, j])
} else
printf("%s ", sym)
}
}
function push(s) { stack[++stp] = s }
function pop() { return stack[stp--] }

交互式的文本处理

技巧测试之运算

一个程序 arith 显示一系列加法运算问题, 比如:7 + 9 = ?
调用程序的命令行有两种形式:

  • awk -f arith # 没有参数最大值就是10.
  • awk -f arith n # 有参数限制每个问题的最大值。
# arith - addition drill
# usage: awk -f arith [ optional problem size ]
# output: queries of the form "i + j = ?"
BEGIN {
maxnum = ARGC > 1 ? ARGV[1] : 10 # default size is 10
ARGV[1] = "-" # read standard input subsequently
srand() # reset rand from time of day
do {
n1 = randint(maxnum)
n2 = randint(maxnum)
printf("%g + %g = ? ", n1, n2)
while ((input = getline) > 0)
if ($0 == n1 + n2) {
print "Right!"
break
} else if ($0 == "") {
print n1 + n2
break
} else
printf("wrong, try again: ")
} while (input > 0)
}
function randint(n) { return int(rand()*n)+1 }

技巧测试之测验

quiz 从题库中抽取特定的文件, 并用文件中的问题向用户提问.如测试化学元素的了解情况。

# quiz - present a quiz
# usage: awk -f quiz topicfile question-subj answer-subj
BEGIN {
FS = ":"
if (ARGC != 4)
error("usage: awk -f quiz topicfile question answer")
if (getline <ARGV[1] < 0) # 1st line is subj:subj:...
error("no such quiz as " ARGV[1])
for (q = 1; q <= NF; q++)
if ($q ~ ARGV[2])
break
for (a = 1; a <= NF; a++)
if ($a ~ ARGV[3])
break
if (q > NF || a > NF || q == a)
error("valid subjects are " $0)
while (getline <ARGV[1] > 0) # load the quiz
qa[++nq] = $0
ARGC = 2; ARGV[1] = "-" # now read standard input
srand()
do {
split(qa[int(rand()*nq + 1)], x)
printf("%s? ", x[q])
while ((input = getline) > 0)
if ($0 ~ "^(" x[a] ")$") {
print "Right!"
break
} else if ($0 == "") {
print x[a]
break
} else
printf("wrong, try again: ")
} while (input > 0)
}
function error(s) { printf("error: %s\n", s); exit }

文本处理

单词计数

# 分解出每一个单词, 把单词的出现次数记录在关联数组中. 
# wordfreq - print number of occurrences of each word
# input: text
# output: number-word pairs sorted by number
{ gsub(/[.,:;!?(){}]/, "") # remove punctuation
for (i = 1; i <= NF; i++)
count[$i]++
}
END { for (w in count)
print count[w], w | "sort -rn"
}

文本格式化

程序 fmt 把它的输入格式化成每行至多 60 个字符, 基本思路是通过移动单词, 尽可能地把每一行都塞满. 空行表示分段, 除此之外没有其他控制指令. 如果某些文本在创建时没有考虑到每行的长度, 就可以使用 fmt 对它们进行格式化.

# fmt - format
# input: text
# output: text formatted into lines of <= 60 characters
/./ { for (i = 1; i <= NF; i++) addword($i) }
/^$/ { printline(); print "" }
END { printline() }
function addword(w) {
if (length(line) + length(w) > 60)
printline()
line = line " " w
}
function printline() {
if (length(line) > 0) {
print substr(line, 2) # removes leading blank
line = ""
}
}

维护手稿的交叉引用

程序 xref 在文档中搜索以 .# 开始的行, 对该行的每一次出现, 程序都会递增数组 count中与该类别对应的元素的值, 然后打印一条 gsub 语句.
# xref - create numeric values for symbolic names
# input: text with definitions for symbolic names
# output: awk program to replace symbolic names by numbers
/^\.#/ { printf("{ gsub(/%s/, \"%d\") }\n", $2, ++count[$1]) }
END { printf("!/^[.]#/\n") }
对于文件 document, xref 输出的是第二个程序 xref.temp:
{ gsub(/_quotes_/, "1") }
{ gsub(/_alice_/, "1") }
{ gsub(/_huck_/, "2") }
!/^[.]#/
gsub 把符号名全局性地替换成数字编号, 最后一条语句忽略以 .# 开始的行, 从而删除掉符号名定义

第一个程序 xref 扫描原始文档并创建第二个程序 xref.temp, 实际的转换过程将由 xref.temp 完成. 假设手稿的原版是 document, 只需要键入
awk -f xref document > xref.temp
awk -f xref.temp document
就可以得到带有数字形式引用的文档. 第二个程序的输出可以被重定向到打印机或文本格式化程序.

制作KWIC索引

KWIC (Keyword-In-Context) 索引把一行内的每个单词都显示在单词所在行的上下文语境中, 在本质上, 它所提供的信息等价于 重要语汇索引 (concordance), 虽然形式上有点不同.

使用awk的话会更加方便, 只需要两个简短的awk程序, 程序之间再放置一个sort命令即可:
awk '
# kwic - generate kwic index
{ print $0
for (i = length($0); i > 0; i--) # compute length only once
if (substr($0,i,1) == " ")
# prefix space suffix ==> suffix tab prefix
print substr($0,i+1) "\t" substr($0,1,i-1)
} ' |
sort -f |
awk '
BEGIN { FS = "\t"; WID = 30 }
{ printf("%" WID "s %s\n", substr($2,length($2)-WID+1),
substr($1,1,WID))
} '
第一个程序首先打印每个输入行的副本, 然后, 为输入行内的每一个空格打印一行输出, 输出行由三部分构成: 当前输入行空格后的内容, 制表符, 当前输入行空格前的内容.
第二个 awk 程序对 sort 的输出进行重构与格式化. 它首先打印当前输入行制表符后面的
内容, 再是一个制表符, 最后是当前输入行制表符前面的内容.

制作索引

制作索引的过程由六个命令共同完成:

  • ix.sort1 先按索引字, 再按页码对输入进行排序
  • ix.collapse 合并同一个术语的页码
  • ix.rotate 生成索引字的旋转
  • ix.genkey 为了强制按照正确的顺序进行排序, 生成一个排序键
  • ix.sort2 按照排序键进行排序
  • ix.format 生成最终的输出
    这些命令逐渐地往最终的索引中添加 索引字-页码 对.
# 第一个排序命令把 索引字-页码 对当作输入数据,把相同的术语放在一起,并按照页码排序:
# ix.sort1 - sort by index term, then by page number
# input/output: lines of the form string tab number
# sort by string, then by number; discard duplicates
sort -t 'tab' +0 -1 +1n -2 -u
# sort 的命令行参数: -t'tab' 表示使用制表符作为字段分隔符; +0 -1 表示第一个排序键是第一个字段, 结果是按照字母排序; +1n -2 表示第二个排序键是第二个字段, 结果是按照数值排序; -u 表示丢弃重复条目. 

上面的输出作为ix.collapse的输入,ix.collapase把同一个术语的页码都放在同一行。

# ix.collapse - combine number lists for identical terms
# input: string tab num \n string tab num ...
# output: string tab num num ...
BEGIN { FS = OFS = "\t" }
$1 != prev {
if (NR > 1)
printf("\n")
prev = $1
printf("%s\t%s", $1, $2)
next
}
{ printf(" %s", $2) }
END { if (NR > 1) printf("\n") }

程序 ix.rotate 为索引字生成旋转, 例如根据 “string comparison” 生成“comparison, string”. 旋转操作与 KWIC 索引制作过程中出现的旋转大致相同, 虽然我们用了不同的方法来编写. 注意 for 循环中的赋值表达式.

# ix.rotate - generate rotations of index terms
# input: string tab num num ...
# output: rotations of string tab num num ...
BEGIN { FS = OFS = "\t" }
{ print $1, $2 # unrotated form
for (i = 1; (j = index(substr($1, i+1), " ")) > 0; ) {
i += j # find each blank, rotate around it
printf("%s, %s\t%s\n",
substr($1, i+1), substr($1, 1, i-1), $2)
}
}

程序ix.genkey对旋转后的索引字排序. 办法是为每一行加上一个前缀 (排序键), 这个前缀确保排序结果是正确的, 在后面的步骤中会把这些前缀移除.

# ix.genkey - generate sort key to force ordering
# input: string tab num num ...
# output: sort key tab string tab num num ...
BEGIN { FS = OFS = "\t" }
{ gsub(/~/, " ", $1) # tildes now become blanks
key = $1
# remove troff size and font change commands from key
gsub(/\\f.|\\f\(..|\\s[-+][0-9]/, "", key)
# keep blanks, letters, digits only
gsub(/[^a-zA-Z0-9 ]+/, "", key)
if (key ~ /^[^a-zA-Z]/) # force nonalpha to sort first
key = " " key # by prefixing a blank
print key, $1, $2
}

程序ix.sort2排序命令对输入按照字母顺序排序, 同之前的一样, 选项 -f 表示合并大小写字母, -d表示按照字典序排序

# ix.sort2 - sort by sort key
# input/output: sort-key tab string tab num num ...
sort -f -d

程序 ix.format 移除排序键, 把 […] 扩展成 troff 的字体设置命令, 并在每个术语之前加上一个格式化命令 .XX, 格式化程序可以利用这个命令控制文本的大小, 位置等.

# ix.format - remove key, restore size and font commands
# input: sort key tab string tab num num ...
# output: troff format, ready to print
BEGIN { FS = "\t" }
{ gsub(/ /, ", ", $3) # commas between page numbers
gsub(/\[/, "\\f(CW", $2) # set constant-width font
gsub(/\]/, "\\fP", $2) # restore previous font
print ".XX" # user-definable command
printf("%s %s\n", $2, $3) # actual index entry
}

概括起来, 索引构造过程由六个命令的流水线组成

  • sh ix.sort1 |
  • awk -f ix.collapse |
  • awk -f ix.rotate |
  • awk -f ix.genkey |
  • sh ix.sort2 |
  • awk -f ix.format

第六章:小语言

经常使用 awk 开发 “小语言” 的翻译器。“小语言” 指的是特定于某些应用领域的专用编程语言。
开发翻译器的原因主要有三点:

  • 首先, 它可以帮助你了解语言处理程序的工作流程
  • 实际工作中, 为了实现一个专用的编程语言, 通常需要投入大量的精力与财力, 在这之前有必要测试一下新语言的语法和语义
  • 希望编程语言能够在实际的工作发挥作用
    语言处理程序围绕下面这个模型构造而成:
源程序
目标程序
开始
分析器
符号表
合成器
结束

分析器 (analyzer) 是语言处理程序的前端, 它负责读取源程序 (source program) 并将其切分成一个个词法单元, 词法单元包括运算符, 操作数等.
分析器对源程序进行语法检查, 如果源程序含有语法错误, 它就会打印一条错误消息.
最后, 分析器把源程序转换成某种中间形式, 并传递给后端 (合成器).
合成器 (synthesizer) 再根据中间形式生成目标程序 (target program).
合成器在生成目标程序的过程中需要和符号表 (symbol table) 通信, 而符号表中的内容由分析器收集而来.

汇编程序于解释程序

汇编语言指令集

操作码指令意义
01get从输入读取一个数, 并存放到累加器中
02put把累加器的值写到输出
03ld M把地址为 M 的内存单元的值读取到累加器中
04st M把累加器的值存放到地址为 M 的内存单元中
05add M把地址为 M 的内存单元的值与累加器的值相加, 再把结果存放到累加器中
06sub M把地址为 M 的内存单元的值与累加器的值相减, 再把结果存放到累加器中
07jpos M如果累加器的值为正, 则跳转到内存地址 M
08jz M如果累加器的值为零, 则跳转到内存地址 M
09j M跳转到内存地址 M
10halt停止执行
const C伪操作符, 用于定义一个常量 C
一个简单的汇编语言程序, 它的功能是输出多个整数的和, 0 表示输入结束
# print sum of input numbers (terminated by zero)
      ld zero # initialize sum to zero
      st sum
loop get      # read a number
     jz  done # no more input if number is zero
     add sum  # add in accumulated sum
     st  sum  # store new value back in sum
     j   loop # go back and read another number
done ld  sum  # print sum
     put
     halt

zero const 0
sum const

一个 解释器用来模拟计算机执行机器语言程序时所表现出的行为. 解释器循环地从 mem 中读取指令, 把指令译码成操作符与操作数, 再模拟指令的执行. 变量 pc 用来模拟程序计数器 (program counter).

# asm - assembler and interpreter for simple computer
# usage: awk -f asm program-file data-files...
BEGIN {
srcfile = ARGV[1]
ARGV[1] = "" # remaining files are data
tempfile = "asm.temp"
n = split("const get put ld st add sub jpos jz j halt", x)
for (i = 1; i <= n; i++) # create table of op codes
op[x[i]] = i-1
# ASSEMBLER PASS 1
FS = "[ \t]+"
while (getline <srcfile > 0) {
sub(/#.*/, "") # strip comments
symtab[$1] = nextmem # remember label location
if ($2 != "") { # save op, addr if present
print $2 "\t" $3 >tempfile
nextmem++
}
}
close(tempfile)
# ASSEMBLER PASS 2
nextmem = 0
while (getline <tempfile > 0) {
if ($2 !~ /^[0-9]*$/) # if symbolic addr,
$2 = symtab[$2] # replace by numeric value
mem[nextmem++] = 1000 * op[$1] + $2 # pack into word
}
# INTERPRETER
for (pc = 0; pc >= 0; ) {
addr = mem[pc] % 1000
code = int(mem[pc++] / 1000)
if (code == op["get"]) { getline acc }
else if (code == op["put"]) { print acc }
else if (code == op["st"]) { mem[addr] = acc }
else if (code == op["ld"]) { acc = mem[addr] }
else if (code == op["add"]) { acc += mem[addr] }
else if (code == op["sub"]) { acc -= mem[addr] }
else if (code == op["jpos"]) { if (acc > 0) pc = addr }
else if (code == op["jz"]) { if (acc == 0) pc = addr }
else if (code == op["j"]) { pc = addr }
else if (code == op["halt"]) { pc = -1 }
else { pc = -1 }
}
}

绘图语言

输入数据:
label Annual Traffic Deaths, USA, 1925-1984
range 1920 5000 1990 60000
left ticks 10000 30000 50000
bottom ticks 1930 1940 1950 1960 1970 1980
1925 21800
1930 31050
1935 36369

1981 51500
1982 46000
1983 44600
1984 46200
展示图如下:
在这里插入图片描述

# graph - processor for a graph-drawing language
# input: data and specification of a graph
# output: data plotted in specified area
BEGIN { # set frame dimensions...
ht = 24; wid = 80 # height and width
ox = 6; oy = 2 # offset for x and y axes
number = "^[-+]?([0-9]+[.]?[0-9]*|[.][0-9]+)" \
"([eE][-+]?[0-9]+)?$"
}
$1 == "label" { # for bottom
sub(/^ *label */, "")
botlab = $0
next
}
$1 == "bottom" && $2 == "ticks" { # ticks for x-axis
for (i = 3; i <= NF; i++) bticks[++nb] = $i
next
}
$1 == "left" && $2 == "ticks" { # ticks for y-axis
for (i = 3; i <= NF; i++) lticks[++nl] = $i
next
}
$1 == "range" { # xmin ymin xmax ymax
xmin = $2; ymin = $3; xmax = $4; ymax = $5
next
}
$1 == "height" { ht = $2; next }
$1 == "width" { wid = $2; next }
$1 ~ number && $2 ~ number { # pair of numbers
nd++ # count number of data points
x[nd] = $1; y[nd] = $2
ch[nd] = $3 # optional plotting character
next
}
$1 ~ number && $2 !~ number { # single number
nd++ # count number of data points
x[nd] = nd; y[nd] = $1; ch[nd] = $2
next
}
END { # draw graph
if (xmin == "") { # no range was given
xmin = xmax = x[1] # so compute it
ymin = ymax = y[1]
for (i = 2; i <= nd; i++) {
if (x[i] < xmin) xmin = x[i]
if (x[i] > xmax) xmax = x[i]
if (y[i] < ymin) ymin = y[i]
if (y[i] > ymax) ymax = y[i]
}
}
frame(); ticks(); label(); data(); draw()
}
function frame() { # create frame for graph
for (i = ox; i < wid; i++) plot(i, oy, "-") # bottom
for (i = ox; i < wid; i++) plot(i, ht-1, "-") # top
for (i = oy; i < ht; i++) plot(ox, i, "|") # left
for (i = oy; i < ht; i++) plot(wid-1, i, "|") # right
}
function ticks( i) { # create tick marks for both axes
for (i = 1; i <= nb; i++) {
plot(xscale(bticks[i]), oy, "|")
splot(xscale(bticks[i])-1, 1, bticks[i])
}
for (i = 1; i <= nl; i++) {
plot(ox, yscale(lticks[i]), "-")
splot(0, yscale(lticks[i]), lticks[i])
}
}
function label() { # center label under x-axis
splot(int((wid + ox - length(botlab))/2), 0, botlab)
}
function data( i) { # create data points
for (i = 1; i <= nd; i++)
plot(xscale(x[i]),yscale(y[i]),ch[i]=="" ? "*" : ch[i])
}
function draw( i, j) { # print graph from array
for (i = ht-1; i >= 0; i--) {
for (j = 0; j < wid; j++)
printf((j,i) in array ? array[j,i] : " ")
printf("\n")
}
}
function xscale(x) { # scale x-value
return int((x-xmin)/(xmax-xmin) * (wid-1-ox) + ox + 0.5)
}
function yscale(y) { # scale y-value
return int((y-ymin)/(ymax-ymin) * (ht-1-oy) + oy + 0.5)
}
function plot(x, y, c) { # put character c in array
array[x,y] = c
}
function splot(x, y, s, i, n) { # put string s in array
n = length(s)
for (i = 0; i < n; i++)
array[x+i, y] = substr(s, i+1, 1)
}

sort生成器

# sortgen - generate sort command
# input: sequence of lines describing sorting options
# output: Unix sort command with appropriate arguments
BEGIN { key = 0 }
/no |not |n't / { print "error: can't do negatives:", $0; ok = 0 }
# rules for global options
{ ok = 0 }
/uniq|discard.*(iden|dupl)/ { uniq = " -u"; ok = 1 }
/separ.*tab|tab.*sep/ { sep = "t'\t'"; ok = 1 }
/separ/ { for (i = 1; i <= NF; i++)
if (length($i) == 1)
sep = "t'" $i "'"
ok = 1
}
/key/ { key++; dokey(); ok = 1 } # new key; must come in order
# rules for each key
/dict/ { dict[key] = "d"; ok = 1 }
/ignore.*(space|blank)/ { blank[key] = "b"; ok = 1 }
/fold|case/ { fold[key] = "f"; ok = 1 }
/num/ { num[key] = "n"; ok = 1 }
/rev|descend|decreas|down|oppos/ { rev[key] = "r"; ok = 1 }
/forward|ascend|increas|up|alpha/ { next } # this is sort's default
!ok { print "error: can't understand:", $0 }
END { # print flags for each key
cmd = "sort" uniq
flag = dict[0] blank[0] fold[0] rev[0] num[0] sep
if (flag) cmd = cmd " -" flag
for (i = 1; i <= key; i++)
if (pos[i] != "") {
flag = pos[i] dict[i] blank[i] fold[i] rev[i] num[i]
if (flag) cmd = cmd " +" flag
if (pos2[i]) cmd = cmd " -" pos2[i]
}
print cmd
}
function dokey( i) { # determine position of key
for (i = 1; i <= NF; i++)
if ($i ~ /^[0-9]+$/) {
pos[key] = $i - 1 # sort uses 0-origin
break
}
for (i++; i <= NF; i++)
if ($i ~ /^[0-9]+$/) {
pos2[key] = $i
break
}
if (pos[key] == "")
printf("error: invalid key specification: %s\n", $0)
if (pos2[key] == "")
pos2[key] = pos[key] + 1
}

逆波兰式计算器

逆” 指的是运算符跟在操作数的后面, “波兰” 是因为这种表示法是波兰数学家 Jan Lukasiewicz 发明的
中缀表达式
(1 + 2) * (3 - 4) / 5
写成逆波兰表示法变成
1 2 + 3 4 - * 5 /
逆波兰式不需要括号 — 如果提前知道运算符的操作数个数, 那么表达式就不会有歧义.

计算采用逆波兰式表示法表示的数学表达式, 所有的运算符与操作数都用空格分开. 如果某个字段是操作数, 就把它入栈; 如果是运算符, 就对栈顶的操作数执行对应的运算. 每当一个输入行结束时, 打印栈顶元素的值, 并把它弹出.

# calc1 - reverse-Polish calculator, version 1
# input: arithmetic expressions in reverse Polish
# output: values of expressions
{ for (i = 1; i <= NF; i++)
if ($i ~ /^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)$/) {
stack[++top] = $i
} else if ($i == "+" && top > 1) {
stack[top-1] += stack[top]; top--
} else if ($i == "-" && top > 1) {
stack[top-1] -= stack[top]; top--
} else if ($i == "*" && top > 1) {
stack[top-1] *= stack[top]; top--
} else if ($i == "/" && top > 1) {
stack[top-1] /= stack[top]; top--
} else if ($i == "^" && top > 1) {
stack[top-1] ^= stack[top]; top--
} else {
printf("error: cannot evaluate %s\n", $i)
top = 0
next
} 
if (top == 1)
printf("\t%.8g\n", stack[top--])
else if (top > 1) {
printf("error: too many operands\n")
top = 0
}
}

第二个逆波兰式计算器支持用户自定义变量, 并且可以调用数学函数. 变量名由字母开始, 后跟字母或数字, 语句 var= 表示把栈顶元素弹出, 并赋值给变量 var. 如果输入行以赋值语句结束, 则不会打印出任何值.

# calc2 - reverse-Polish calculator, version 2
# input: expressions in reverse Polish
# output: value of each expression
{ for (i = 1; i <= NF; i++)
if ($i ~ /^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)$/) {
stack[++top] = $i
} else if ($i == "+" && top > 1) {
stack[top-1] += stack[top]; top--
} else if ($i == "-" && top > 1) {
stack[top-1] -= stack[top]; top--
} else if ($i == "*" && top > 1) {
stack[top-1] *= stack[top]; top--
} else if ($i == "/" && top > 1) {
stack[top-1] /= stack[top]; top--
} else if ($i == "^" && top > 1) {
stack[top-1] ^= stack[top]; top--
} else if ($i == "sin" && top > 0) {
stack[top] = sin(stack[top])
} else if ($i == "cos" && top > 0) {
stack[top] = cos(stack[top])
} else if ($i == "atan2" && top > 1) {
stack[top-1] = atan2(stack[top-1],stack[top]); top--
} else if ($i == "log" && top > 0) {
stack[top] = log(stack[top])
} else if ($i == "exp" && top > 0) {
stack[top] = exp(stack[top])
} else if ($i == "sqrt" && top > 0) {
stack[top] = sqrt(stack[top])
} else if ($i == "int" && top > 0) {
stack[top] = int(stack[top])
} else if ($i in vars) {
stack[++top] = vars[$i]
} else if ($i ~ /^[a-zA-Z][a-zA-Z0-9]*=$/ && top > 0) {
vars[substr($i, 1, length($i)-1)] = stack[top--]
} else {
printf("error: cannot evaluate %s\n", $i)
top = 0
next
}
if (top == 1 && $NF !~ /\=$/)
printf("\t%.8g\n", stack[top--])
else if (top > 1) {
printf("error: too many operands\n")
top = 0
}
}

中缀计算器

每一个输入行都被当作一个单独的表达式, 表达式被求值并打印出来. 我们仍然要求所有的运算符, 操作数和括号都用空格分开. 变量 f 指向下一个待检验的字段 (运算符或操作数).

# calc3 - infix calculator
NF > 0 {
f = 1
e = expr()
if (f <= NF) printf("error at %s\n", $f)
else printf("\t%.8g\n", e)
}
function expr( e) { # term | term [+-] term
e = term()
while ($f == "+" || $f == "-")
e = $(f++) == "+" ? e + term() : e - term()
return e
}
function term( e) { # factor | factor [*/] factor
e = factor()
while ($f == "*" || $f == "/")
e = $(f++) == "*" ? e * factor() : e / factor()
return e
}
function factor( e) { # number | (expr)
if ($f ~ /^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)$/) {
return $(f++)
} else if ($f == "(") {
f++
e = expr()
if ($(f++) != ")")
printf("error: missing ) at %s\n", $f)
return e
} else {
printf("error: expected number or ( at %s\n", $f)
return 0
}
}
表达式 $(f++) 先取 $f 的值, 然后再递增 f, 请注意它与 $f++ 的区别, 后者是递增 $f 的值.

递归下降语法分析

递归下降语法分析器的关键部分在于它的一整套递归分析函数, 它们负责识别由非终结符生成的字符串. 每一个函数都按照产生式规则调用其他函数, 一直分析到终结符为止, 到这时, 输入中所含的词法单元都被读取出来并加以分类. 由于该方法的自顶向下与递归这两个特性, 所以被称为 “递归下降语法分析”

# awk.parser - recursive-descent translator for part of awk
# input: awk program (very restricted subset)
# output: C code to implement the awk program
BEGIN { program() }
function advance() { # lexical analyzer; returns next token
if (tok == "(eof)") return "(eof)"
while (length(line) == 0)
if (getline line == 0)
return tok = "(eof)"
sub(/^[ \t]+/, "", line) # remove white space
if (match(line, /^[A-Za-z_][A-Za-z_0-9]*/) || # identifier
match(line, /^-?([0-9]+\.?[0-9]*|\.[0-9]+)/) || # number
match(line, /^(<|<=|==|!=|>=|>)/) || # relational
match(line, /^./)) { # everything else
tok = substr(line, 1, RLENGTH)
line = substr(line, RLENGTH+1)
return tok
}
error("line " NR " incomprehensible at " line)
}
function gen(s) { # print s with nt leading tabs
printf("%s%s\n", substr("\t\t\t\t\t\t\t\t\t", 1, nt), s)
}
function eat(s) { # read next token if s == tok
if (tok != s) error("line " NR ": saw " tok ", expected " s)
advance()
}
function nl() { # absorb newlines and semicolons
while (tok == "\n" || tok == ";")
advance()
}
function error(s) { print "Error: " s | "cat 1>&2"; exit 1 }
function program() {
advance()
if (tok == "BEGIN") { eat("BEGIN"); statlist() }
pastats()
if (tok == "END") { eat("END"); statlist() }
if (tok != "(eof)") error("program continues after END")
}
function pastats() {
gen("while (getrec()) {"); nt++
while (tok != "END" && tok != "(eof)") pastat()
nt--; gen("}")
}
function pastat() { # pattern-action statement
if (tok == "{") # action only
statlist()
else { # pattern-action
gen("if (" pattern() ") {"); nt++
if (tok == "{") statlist()
else # default action is print $0
gen("print(field(0));")
nt--; gen("}")
}
}
function pattern() { return expr() }
function statlist() {
eat("{"); nl(); while (tok != "}") stat(); eat("}"); nl()
}
function stat() {
if (tok == "print") { eat("print"); gen("print(" exprlist() ");") }
else if (tok == "if") ifstat()
else if (tok == "while") whilestat()
else if (tok == "{") statlist()
else gen(simplestat() ";")
nl()
}
function ifstat() {
eat("if"); eat("("); gen("if (" expr() ") {"); eat(")"); nl(); nt++
stat()
if (tok == "else") { # optional else
eat("else")
nl(); nt--; gen("} else {"); nt++
stat()
}
nt--; gen("}")
}
function whilestat() {
eat("while"); eat("("); gen("while (" expr() ") {"); eat(")"); nl()
nt++; stat(); nt--; gen("}")
}
function simplestat( lhs) { # ident = expr | name(exprlist)
lhs = ident()
if (tok == "=") {
eat("=")
return "assign(" lhs ", " expr() ")"
} else return lhs
}
function exprlist( n, e) { # expr , expr , ...
e = expr() # has to be at least one
for (n = 1; tok == ","; n++) {
advance()
e = e ", " expr()
}
return e
}
function expr(e) { # rel | rel relop rel
e = rel()
while (tok ~ /<|<=|==|!=|>=|>/) {
op = tok
advance()
e = sprintf("eval(\"%s\", %s, %s)", op, e, rel())
}
return e
}
function rel(op, e) { # term | term [+-] term
e = term()
while (tok == "+" || tok == "-") {
op = tok
advance()
e = sprintf("eval(\"%s\", %s, %s)", op, e, term())
}
return e
}
function term(op, e) { # fact | fact [*/%] fact
e = fact()
while (tok == "*" || tok == "/" || tok == "%") {
op = tok
advance()
e = sprintf("eval(\"%s\", %s, %s)", op, e, fact())
}
return e
}
function fact( e) { # (expr) | $fact | ident | number
if (tok == "(") {
eat("("); e = expr(); eat(")")
return "(" e ")"
} else if (tok == "$") {
eat("$")
return "field(" fact() ")"
} else if (tok ~ /^[A-Za-z][A-Za-z0-9]*/) {
return ident()
} else if (tok ~ /^-?([0-9]+\.?[0-9]*|\.[0-9]+)/) {
e = tok
advance()
return "num((float)" e ")"
} else
error("unexpected " tok " at line " NR)
}
function ident( id, e) { # name | name[expr] | name(exprlist)
if (!match(tok, /^[A-Za-z_][A-Za-z_0-9]*/))
error("unexpected " tok " at line " NR)
id = tok
advance()
if (tok == "[") { # array
eat("["); e = expr(); eat("]")
return "array(" id ", " e ")"
} else if (tok == "(") { # function call
eat("(")
if (tok != ")") {
e = exprlist()
eat(")")
} else eat(")")
return id "(" e ")" # calls are statements
} else
return id # variable
}

第七章:算法实验

排序

插入排序非常简单, 但是只有在元素很少的情况下效率才足够高; 快速排序是最好的通用排序算法之一; 堆排序可以保证即使在最坏的情况下, 也可以拥有较高的效率.

插入排序

基本概念. 用插入排序给一堆卡片排序可以这样做: 每次从卡片堆里拿出一张, 然后把它插入到手上拿着的卡片的合适位置
实现. 下面的代码使用插入排序对数组 A[1], …, A[n] 进行升序排列. 第一个动作把输入数据读取到一个数组中, END 动作调用函数 isort 对数组进行排序, 最后输出排序结果:

# insertion sort
{ A[NR] = $0 }
END { isort(A, NR)
for (i = 1; i <= NR; i++)
print A[i]
}
# isort - sort A[1..n] by insertion
function isort(A, n, i, j, t) {
for (i = 2; i <= n; i++)
for (j = i; j > 1 && A[j-1] > A[j]; j--) {
# swap A[j-1] and A[j]
t = A[j-1]; A[j-1] = A[j]; A[j] = t
}
}

**测试. **应该如何测试 isort?为了测试程序的薄弱环节, 我们还需要构造一些特殊的测试用例, 用来测试边界与异常情况. 对排序来说, 典型的边界与异常情况包括:

  • 序列长度为 0
  • 序列长度为 1
  • 序列包含 n 个随机数
  • 序列包含 n 个已排序的数
  • 序列包含 n 个逆序排列的数
  • 序列包含 n 个相同的数

主要有两种办法来实现测试与评价过程的自动化, 每一种都有它各自的优点.
第一种称为“批处理模式”: 编写一个程序来运行事先计划好的测试集, 并运用上面提到排序算法.

# batch test of sorting routines
BEGIN {
print " 0 elements"
isort(A, 0); check(A, 0)
print " 1 element"
genid(A, 1); isort(A, 1); check(A, 1)
n = 10
print " " n " random integers"
genrand(A, n); isort(A, n); check(A, n)
print " " n " sorted integers"
gensort(A, n); isort(A, n); check(A, n)
print " " n " reverse-sorted integers"
genrev(A, n); isort(A, n); check(A, n)
print " " n " identical integers"
genid(A, n); isort(A, n); check(A, n)
}
function isort(A,n, i,j,t) {
for (i = 2; i <= n; i++)
for (j = i; j > 1 && A[j-1] > A[j]; j--) {
# swap A[j-1] and A[j]
t = A[j-1]; A[j-1] = A[j]; A[j] = t
}
}
# test-generation and sorting routines...
function check(A,n, i) {
for (i = 1; i < n; i++)
if (A[i] > A[i+1])
printf("array is not sorted, element %d\n", i)
}
function genrand(A,n, i) { # put n random integers in A
for (i = 1; i <= n; i++)
A[i] = int(n*rand())
}
function gensort(A,n, i) { # put n sorted integers in A
for (i = 1; i <= n; i++)
A[i] = i
}
function genrev(A,n, i) { # put n reverse-sorted integers
for (i = 1; i <= n; i++) # in A
A[i] = n+1-i
}
function genid(A,n, i) { # put n identical integers in A
for (i = 1; i <= n; i++)
A[i] = 1
}

第二种方法相对来说没那么方便, 基本思想是构建一个框架程序.利用该框架可以很容易以交互性的方式来完成测试.
程序的基本组织是一系列的正则表达式, 它们负责扫描输入数据, 判断数据类型和使用的排序算法. 如果某个输入数据不与任何一个模式相匹配, 程序就会输出一条错误消息, 并演示正确的使用方法 .

# interactive test framework for sort routines
/^[0-9]+.*rand/ { n = $1; genrand(A, n); dump(A, n); next }
/^[0-9]+.*id/ { n = $1; genid(A, n); dump(A, n); next }
/^[0-9]+.*sort/ { n = $1; gensort(A, n); dump(A, n); next }
/^[0-9]+.*rev/ { n = $1; genrev(A, n); dump(A, n); next }
/^data/ { # use data right from this line
for (i = 2; i <= NF; i++)
A[i-1] = $i
n = NF - 1
next
}
/q.*sort/ { qsort(A, 1, n); check(A, n); dump(A, n); next }
/h.*sort/ { hsort(A, n); check(A, n); dump(A, n); next }
/i.*sort/ { isort(A, n); check(A, n); dump(A, n); next }
/./ { print "data ... | N [rand|id|sort|rev]; [qhi]sort" }
function dump(A, n) { # print A[1]..A[n]
for (i = 1; i <= n; i++)
printf(" %s", A[i])
printf("\n")
}
# test-generation and sorting routines ...
...

性能. isort 所执行的操作次数取决于 n 的值, 即待排序的元素个数, 以及它们原来的排列顺序. 插入排序是 平方级 算法, 也就是说, 在最坏的情况下, 随着元素个数的增加, 算法的运行时间将以二次方的速度增长.
总得来说, 插入排序适用于元素个数较少的情况, 当元素个数过多时, 该算法的性能就会快速下降, 除非输入数据基本有序.

程序用于组织测试, 以及为坐标图准备数据. 同样, 它的功能相当于一个微型编程语言, 可以灵活地指定参数

# test framework for sort performance evaluation
# input: lines with sort name, type of data, sizes...
# output: name, type, size, comparisons, exchanges, c+e
{ for (i = 3; i <= NF; i++)
test($1, $2, $i)
}
function test(sort, data, n) {
comp = exch = 0
if (data ~ /rand/)
genrand(A, n)
else if (data ~ /id/)
genid(A, n)
else if (data ~ /rev/)
genrev(A, n)
else
print "illegal type of data in", $0
if (sort ~ /q.*sort/)
qsort(A, 1, n)
else if (sort ~ /h.*sort/)
hsort(A, n)
else if (sort ~ /i.*sort/)
isort(A, n)
else print "illegal type of sort in", $0
print sort, data, n, comp, exch, comp+exch
}
# test-generation and sorting routines ...

输出数据的每一行都包括排序算法名称, 测试集类型, 测试集大小, 以及操作执行的次数. 输出数据被输送给绘图程序 grap, 它是我们在第 六 章讨论的绘图程序的原始版本

快速排序

**基本概念. **为了对一个元素序列进行排序, 快速排序算法把序列划分成两个子序列, 然后再递归地对子序列进行排序.
实现.

# quicksort
{ A[NR] = $0 }
END { qsort(A, 1, NR)
for (i = 1; i <= NR; i++)
print A[i]
}
# qsort - sort A[left..right] by quicksort
function qsort(A,left,right, i,last) {
if (left >= right) # do nothing if array contains
return # less than two elements
swap(A, left, left + int((right-left+1)*rand()))
last = left # A[left] is now partition element
for (i = left+1; i <= right; i++)
if (A[i] < A[left])
swap(A, ++last, i)
swap(A, left, last)
qsort(A, left, last-1)
qsort(A, last+1, right)
}
function swap(A,i,j, t) {
t = A[i]; A[i] = A[j]; A[j] = t
}

**性能. **qsort 所执行的操作次数取决于数组划分时的均匀程度. 如果数组划分每次都很平均, 那么程序的运行时间与 n log n 成正比. 于是, 如果数据规模变为原来的两倍, 那么程序的运行时间只会在原来两倍的基础上再稍微多出一点

堆排序

基本概念. 优先级队列 (priority queue) 是一种数据结构, 用于存储和检索元素.
它有两种基本操作: 往队列中插入一个新元素, 以及从队列中提取最大的元素. 这表明优先级队列可以用来排序: 首先把所有的元素插入到队列中, 然后每次抽取一个元素. 因为每次移除的都是最大的元素, 所以元素是以降序地方式从队列中抽取出来. 这种排序方法叫作堆排序.
堆排序使用一种称为 堆 (heap) 的数据结构来维护优先级队列, 我们可以把堆想像成一棵二叉树, 但是带有两条额外的性质:

  1. 树是高度平衡的: 叶子节点最多只在两个不同的层次上出现, 另外, 位于最底层 (距离根节点
    最远的层次) 的叶子尽量靠左排列;
  2. 树是部分有序的: 每个节点的值大于或等于它的孩子节点.

堆具有两个非常重要的特性.

  • 第一个特性是, 如果有 n 个节点, 那么所有的从根节点到叶子节点的路径长度都不会大于 log2 n.
  • 第二个特性是, 具有最大值的元素总是在根节点 (这个位置称为 “堆顶”)

**实现. **堆排序由两个阶段组成: 构造堆, 以及按顺序从堆中提取元素.

# heapsort
{ A[NR] = $0 }
END { hsort(A, NR)
for (i = 1; i <= NR; i++)
{ print A[i] }
}
function hsort(A,n, i) {
for (i = int(n/2); i >= 1; i--) # phase 1
{ heapify(A, i, n) }
for (i = n; i > 1; i--) { # phase 2
{ swap(A, 1, i) }
{ heapify(A, 1, i-1) }
}
}
function heapify(A,left,right, p,c) {
for (p = left; (c = 2*p) <= right; p = c) {
if (c < right && A[c+1] > A[c])
{ c++ }
if (A[p] < A[c])
{ swap(A, c, p) }
}
}
function swap(A,i,j, t) {
t = A[i]; A[i] = A[j]; A[j] = t
}

**性能. **hsort 总的操作次数正比于 n log n, 即使是在最坏的情况下也是如此.

剖析

对程序进行剖析, 也就是计算每条语句的执行次数. 剖析器可以打印某个程序中每条语句的执行次数.

makeprof 通过在代码中插入计数与打印语句, 为 awk程序构造剖析版本. 当剖析版的程序运行时, 它会计算每条语句的执行次数, 并把计数结果写到文件 prof.cnts 中.

# makeprof - prepare profiling version of an awk program
# usage: awk -f makeprof awkprog >awkprog.p
# running awk -f awkprog.p data creates a
# file prof.cnts of statement counts for awkprog
{ if ($0 ~ /{/) sub(/{/, "{ _LBcnt[" ++_numLB "]++; ")
print
}
END { printf("END { for (i = 1; i <= %d; i++)\n", _numLB)
printf("\t\t print _LBcnt[i] > \"prof.cnts\"\n}\n")
}

程序 printprof 从 prof.cnts 中获取语句的执行次数, 并添加到原版程序中.

# printprof - print profiling counts
# usage: awk -f printprof awkprog
# prints awkprog with statement counts from prof.cnts
BEGIN { while (getline < "prof.cnts" > 0) cnt[++i] = $1 }
/{/ { printf("%5d", cnt[++j]) }
{ printf("\t%s\n", $0) }

键入并执行命令
awk -f makeprof heapsort > heapsort.p
构造出的剖析版 heapsort.p 看起来就像:

# heapsort
{ _LBcnt[1]++; A[NR] = $0 }
END { _LBcnt[2]++; hsort(A, NR)
for (i = 1; i <= NR; i++)
{ _LBcnt[3]++; print A[i] }
}
function hsort(A,n, i) { _LBcnt[4]++;
for (i = int(n/2); i >= 1; i--) # phase 1
{ _LBcnt[5]++; heapify(A, i, n) }
for (i = n; i > 1; i--) { _LBcnt[6]++; # phase 2
{ _LBcnt[7]++; swap(A, 1, i) }
{ _LBcnt[8]++; heapify(A, 1, i-1) }
}
}
function heapify(A,left,right, p,c) { _LBcnt[9]++;
for (p = left; (c = 2*p) <= right; p = c) { _LBcnt[10]++;
if (c < right && A[c+1] > A[c])
{ _LBcnt[11]++; c++ }
if (A[p] < A[c])
{ _LBcnt[12]++; swap(A, c, p) }
}
}
function swap(A,i,j, t) { _LBcnt[13]++;
t = A[i]; A[i] = A[j]; A[j] = t
}
END { for (i = 1; i <= 13; i++)
print _LBcnt[i] > "prof.cnts"
}

假设把 100 个随机数作为输入数据, 运行 heapsort.p, 程序运行结束后, 键入并执行命令
awk -f printprof heapsort
就可以得到带有语句执行次数的原版程序 (每条语句的执行次数仅对本次运行有效).
命令的执行结果是:

# heapsort
100 { A[NR] = $0 }
1 END { hsort(A, NR)
for (i = 1; i <= NR; i++)
100 { print A[i] }
}
1 function hsort(A,n, i) {
for (i = int(n/2); i >= 1; i--) # phase 1
50 { heapify(A, i, n) }
99 for (i = n; i > 1; i--) { # phase 2
99 { swap(A, 1, i) }
99 { heapify(A, 1, i-1) }
}
}
149 function heapify(A,left,right, p,c) {
526 for (p = left; (c = 2*p) <= right; p = c) {
if (c < right && A[c+1] > A[c])
228 { c++ }
if (A[p] < A[c])
473 { swap(A, c, p) }
}
}
572 function swap(A,i,j, t) {
t = A[i]; A[i] = A[j]; A[j] = t
}

拓扑排序

找到一个顺序, 该顺序满足一个约束条件集合, 集合中的每个条件都具有形式 “x 必须在 y 之前出现”. 由约束条件集合所表示的偏序得到该集合上的一个线序, 这个操作叫作拓扑排序.

如果存在一条边, 从 x 指向 y, 那么 x 就是 y 的 前驱 (predecessor), y 是 x 的 后继 (successor).
假设约束条件用 前驱-后继 对来表示, 其中每个输入行的 x 和 y 表示一条从节点 x 指向 y 的边.

拓扑排序的目标是对图中的节点进行排序, 使得所有前驱都在它们的后继之前出现. 当且仅当图中不含有 环 (cycle, 是由多条边组成的序列, 当节点沿着这个序列前进时, 最终会回到节点的原来位置) 时, 这样的顺序才存在.

广度优先拓扑排序

tsort 的前三条语句从输入中读取 前驱-后继 对, 并构造一个后继节点列表, 就像:

nodepcntscntslist
a01h
b21g
c02f, h
d21i
e11d
f12b, g
g20
h22d, e
i11b

数组 pcnt 和 scnt 记录每个节点的前驱节点与后继节点个数, slist[x,i] 给出了节点 x的第 i 个后继节点的名字. 如果某个元素原来不在数组 pcnt 中, 那么程序的第一行就会为它创建一个元素.

# tsort - topological sort of a graph
# input: predecessor-successor pairs
# output: linear order, predecessors first
{ if (!($1 in pcnt))
pcnt[$1] = 0 # put $1 in pcnt
pcnt[$2]++ # count predecessors of $2
slist[$1, ++scnt[$1]] = $2 # add $2 to successors of $1
}
END { for (node in pcnt) {
nodecnt++
if (pcnt[node] == 0) # if it has no predecessors
q[++back] = node # queue node
}
for (front = 1; front <= back; front++) {
printf(" %s", node = q[front])
for (i = 1; i <= scnt[node]; i++)
if (--pcnt[slist[node, i]] == 0)
# queue s if it has no more predecessors
q[++back] = slist[node, i]
}
if (back != nodecnt)
print "\nerror: input contains a cycle"
printf("\n")
}

深度优先搜索

下面的函数判断某个图中是否存在节点 node可到达的环, 图用后继节点列表来表示.

# dfs - depth-first search for cycles
function dfs(node, i, s) {
visited[node] = 1
for (i = 1; i <= scnt[node]; i++)
if (visited[s = slist[node, i]] == 0)
dfs(s)
else if (visited[s] == 1)
print "cycle with back edge (" node ", " s ")"
visited[node] = 2
}

这个函数使用数组 visited 来判断某个节点是否被访问过. 开始时, visited[x] 等于 0.
在第一次进入节点 x 时, dfs 把 visited[x] 设置为 1, 最后一次离开节点 x 时, 把 visited[x]设置为 2. 在遍历过程中, dfs 利用 visited 来判断节点 y 是否是当前节点的祖先 (如果是的话, 说明它之前被访问过), 以及判断 y 之前是否被访问过. 如果 y 是当前节点的祖先, 那么visited[y] 等于 1; 如果 y 之前被访问过, 那么 visited[y] 的值就等于 2.

深度优先拓扑排序

给定一个 前驱-后续 对序列作为输入数据, 程序rtsort 输出图的逆序拓扑排序, 它对每一个没有前驱的节点应用深度优先搜索.

# rtsort - reverse topological sort
# input: predecessor-successor pairs
# output: linear order, successors first
{ if (!($1 in pcnt))
pcnt[$1] = 0 # put $1 in pcnt
pcnt[$2]++ # count predecessors of $2
slist[$1, ++scnt[$1]] = $2 # add $2 to successors of $1
}
END { for (node in pcnt) {
nodecnt++
if (pcnt[node] == 0)
rtsort(node)
}
if (pncnt != nodecnt)
print "error: input contains a cycle"
printf("\n")
}
function rtsort(node, i, s) {
visited[node] = 1
for (i = 1; i <= scnt[node]; i++)
if (visited[s = slist[node, i]] == 0)
rtsort(s)
else if (visited[s] == 1)
printf("error: nodes %s and %s are in a cycle\n",
s, node)
visited[node] = 2
printf(" %s", node)
pncnt++ # count nodes printed
}

Make:文件更新程序

我们假设依赖关系和构造命令存放在一个文件里, 文件名是 makefile, 里面包含了一系列规则, 每条规则都具有形式:
name: t1 t2 … tn
command
规则的第一行是依赖关系, 意思是程序或文件 name 依赖于目标 t1, t2, …, tn, 其中 ti 是文件名或另一个 name. 依赖关系的下面是一行或多行 command, 这些 command 是用来生成 name 的命令. 下面显示的是某个小程序的 makefile 文件, 程序由两个 C 文件和一个 yacc 语法文件组成 (yacc 是一个典型的用于程序开发的应用程序):
prog: a.o b.o c.o
cc a.o b.o c.o -ly -o prog
a.o: prog.h a.c
cc -c prog.h a.c
b.o: prog.h b.c
cc -c prog.h b.c
c.o: c.c
cc -c c.c
c.c: c.y
yacc c.y
mv y.tab.c c.c
print:
pr prog.h a.c b.c c.y

makefile 对应的依赖图是:

prog
a.o
a.c
prog.h
b.o
b.c
c.o
c.c
c.y
# make - maintain dependencies
BEGIN {
while (getline <"makefile" > 0)
if ($0 ~ /^[A-Za-z]/) { # $1: $2 $3 ...
sub(/:/, "")
if (++names[nm = $1] > 1)
error(nm " is multiply defined")
for (i = 2; i <= NF; i++) # remember targets
slist[nm, ++scnt[nm]] = $i
} else if ($0 ~ /^\t/) # remember cmd for
cmd[nm] = cmd[nm] $0 "\n" # current name
else if (NF > 0)
error("illegal line in makefile: " $0)
ages() # compute initial ages
if (ARGV[1] in names) {
if (update(ARGV[1]) == 0)
print ARGV[1] " is up to date"
} else
error(ARGV[1] " is not in makefile")
}
function ages( f,n,t) {
for (t = 1; ("ls -t" | getline f) > 0; t++)
age[f] = t # all existing files get an age
close("ls -t")
for (n in names)
if (!(n in age)) # if n has not been created
age[n] = 9999 # make n really old
}
function update(n, changed,i,s) {
if (!(n in age)) error(n " does not exist")
if (!(n in names)) return 0
changed = 0
visited[n] = 1
for (i = 1; i <= scnt[n]; i++) {
if (visited[s = slist[n, i]] == 0) update(s)
else if (visited[s] == 1)
error(s " and " n " are circularly defined")
if (age[s] <= age[n]) changed++
}
visited[n] = 2
if (changed || scnt[n] == 0) {
printf("%s", cmd[n])
system(cmd[n]) # execute cmd associated with n
ages() # recompute all ages
age[n] = 0 # make n very new
return 1
}
return 0
}
function error(s) { print "error: " s; exit }

AWK总结

在句法规则上, 如果某个成分被一对中括号 […] 包围, 则表示它们是可选的.
命令行

awk [-Fs] 'program' optional list of filenames
awk [-Fs] -f progfile optional list of filenames

参数 -Fs 把字段分隔符 FS 设置成 s, 如果没有提供文件名, awk 就从标准输入读取数据. 文件名的形式可以是 var=text, 在这种情况下, 相当于把 text 赋值给变量 var, 当这个参数被当作一个文件而被访问时, 执行赋值操作.

AWK 程序
一个 awk 程序由一系列的 模式–动作 语句和函数定义组成. 一个 模式–动作 语句具有形式:
pattern { action }
如果某个动作省略了模式, 则默认匹配所有输入行; 如果某个模式省略了动作, 则默认打印匹配行.
一个函数定义具有形式:
function name(parameter-list) { statement }
模式–动作 语句和函数定义由换行符或分号分隔, 并且这两个字符可以混合使用.

模式

  • BEGIN
  • END
  • expression
  • /regular expression/
  • pattern && pattern
  • pattern || pattern
  • !pattern
  • (pattern)
  • pattern, pattern

最后一个模式是范围模式, 它不能作为其他模式的组成部分. 类似地, BEGIN 和 END 也不能和其他模式结合.

动作
一个动作由一系列的语句组成, 这些语句包括:

  • break
  • continue
  • delete array-element
  • do statement while (expression)
  • exit[expression]
  • expression
  • if (expression) statement [else statement]
  • input-output statement
  • for (expression; expression ; expression) statement
  • for (variable in array) statement
  • next
  • return [expression]
  • while (expression) statement
  • { statement }

一个单独的分号表示空语句. 在一个 if-else 语句中, 如果第一个 statement 和 else 出现在同一行, 那么它必须以分号结尾, 或者用花括号包围起来. 类似地, 在 do 语句中, 如果 statement和 while 出现在同一行, 那么它必须以分号结尾, 或者用花括号包围起来.

程序格式
语句通过换行符或 (和) 分号隔开. 空行可以出现在任何语句, 模式–动作 语句, 或函数定义的前面或后面. 空格与制表符可以插入到运算符或操作数的周围. 一条长语句可以通过反斜杠延续到下一行. 另外, 如果一条语句在逗号, 左花括号, &&, ||, do, else, if 或 for 的右括号后断行, 则不需要反斜杠. 由 # 开始的注释可以出现在任意一行的末尾.

输入输出

  • close(expr) 关闭由 expr 指示的文件或管道
  • getline 把 $0 设置成下一条记录; 同时设置 NF, NR, FNR
  • getline <file 把 $0 设置成文件 file 的下一条记录; 同时设置 NF
  • getline var 把 var 设置成下一条记录; 同时设置 NR, FNR
  • getline var <file 把 var 设置成文件 file 的下一条记录.
  • print 打印当前记录
  • print expr-list 打印 expr-list 所表示的表达式
  • print expr-list >file 把表达式输出到文件 file 中
  • printf fmt, expr-list 格式化并输出
  • printf fmt, expr-list > file 格式化并输出到文件 file 中
  • system(cmd-line) 执行命令 cmd-line, 返回命令的退出状态

print 后面的 expr-list, 以及 printf 后面的 fmt, expr-list 可以用括号括起来. 在 print和 printf 中, >>file 表示把输出追加到文件 file 的末尾, | command 表示把输出写到一个管道中. 类似的, command | getline 表示把命令 command 的输出以管道的方式输送给getline. 函数 getline 在遇到文件末尾时返回 0, 出错时返回 -1.

printf 格式转换
printf 与 sprintf 识别以下格式转换命令:

  • %c ASCII 字符
  • %d 十进制数
  • %e [-]d.ddddddE[±]dd
  • %f [-]ddd.dddddd
  • %g 等效于 %e 或 %f, 选择转换后长度较短的那个, 无意义的零会被删除
  • %o 无符号八进制数
  • %s 字符串
  • %x 无符号十六进制数
  • %% 打印一个百分号 %, 不会有参数被转换

% 与控制字符之间可以出现额外的参数:

  • - 表达式在它所处的域中左对齐
  • width 当需要时, 把域的宽度填充到该值, 前导的 0 表示用 0 填充
  • .prec 字符串最大宽度, 或小数点后保留的位数

内建变量
下面列出的内建变量可以使用在任意一个表达式中:

  • ARGC 命令行参数的个数
  • ARGV 命令行参数组成的数组 (ARGV[0…ARGC-1])
  • FILENAME 当前输入文件的文件名
  • FNR 当前输入文件已读取的记录个数
  • FS 输入数据的字段分隔符 (默认是空格)
  • NF 当前输入记录的字段个数
  • NR 从程序开始到现在, 读取到的记录个数
  • OFMT 数字的输出格式 (默认是 “%.6g”)
  • OFS 输出字段分隔符 (默认是空格)
  • ORS 输出记录分隔符 (默认是换行符)
  • RLENGTH 被函数 match 中的正则表达式匹配的字符串的长度
  • RS 输入数据的记录分隔符 (默认是换行符)
  • RSTART 被函数 match 匹配的字符串在原字符串中的开始位置
  • SUBSEP 具有形式 [i,j,…] 的数组下标的分隔符 (默认是 “\034”)

ARGC 和 ARGV 包含被执行的程序的名字 (通常是 awk), 但是不包括出现在命令行中的 awk 程序 或选项. RLENGTH 同时也是 match 的返回值.

内建字符串函数
在下面列出的字符串函数中, s 和 t 表示字符串, r 表示正则表达式, i 和 n 表示整数.
sub 和 gsub 的替换字符串中的 & 会被匹配的字符串替换掉, 而 & 表示一个字面意义上的&.

  • gsub(r, s, t) 全局地把 t 中被 r 匹配的每一个子字符串替换为 s, 返回替换发生的次数; 如果省略 t, 则默认使用 $0
  • index(s, t) 返回 t 在 s 中的开始位置, 如果 s 不包含 t, 则返回 0
  • length(s) 返回 s 的长度
  • match(s, r) 返回s中匹配 r 的子字符串的起始位置, 如不存在可匹配的子字符串, 则返回0, 调用该函数会同时设置RSTART 与 RLENGTH
  • split(s, a, fs) 按照 fs, 把 s 切分到数组 a 中, 返回分割后的字段的个数; 如果省略 fs, 则默认使用 FS
  • sprintf(fmt, expr-list ) 返回格式化了的 expr-list ( 根据 fmt 进行格式化)
  • sub(r, s, t) 类似于 gsub, 但是它只替换第一个被匹配的子字符串
  • substr(s, i, n) 返回 s 中, 从 i 开始的, 长度为 n 的子字符串, 如果省略 n, 则返回 s 中从 i 开始的后缀

内建算术函数

  • atan2(y, x) y/x 的反正切值, 弧度制, 定义域从 −π 到 π
  • cos(x) 余弦 (弧度制)
  • exp(x) 指数 ex
  • int(x) 取整
  • log(x) 自然对数
  • rand() 返回一个伪随机数 r (0 ⩽ r < 1 )
  • sin(x) 正弦 (弧度制)
  • sqrt(x) 平方根
  • srand(x) 设置随机数种子, 如果省略 x, 则默认使用当天的时间

表达式运算符 (按优先级递增排列)
表达式可以通过下列运算符进行组合:

  • = += -= *= /= %= ^= 赋值

  • ?: 条件表达式

  • || 逻辑或

  • && 逻辑与

  • in 数组成员运算符

  • ~ !~ 正则表达式匹配运算符, 与否定匹配运算符

  • < <= > >= != == 关系运算符字符串拼接 (没有显式的运算符)

  • /+ - 加, 减

  • /* / % 乘, 除, 取模

  • /+ - ! 单目加, 单目减, 逻辑非

  • ˆ 指数运算符

  • ++ – 自增, 自减 (包括前缀形式与后缀形式)

  • $ 字段

所有的运算符都是左结合的, 除了赋值运算符, ?: 和 ˆ, 它们是右结合的. 任意一个表达式都可以用括号括起来.

正则表达式
正则表达式的元字符包括:

  • \ ^ $ . [ ] | ( ) * + ?

下面的表格总结了正则表达式及其所匹配的字符串:

  • c 匹配一个非元字符 c
  • \c 匹配一个转义序列, 或一个字面上的字符 c
  • ˆ 匹配一个字符串的开始
  • $ 匹配一个字符串的结束
  • . 匹配任意一个字符
  • [abc…] 字符类: 匹配 abc… 中的任意一个字符
  • [ˆabc…] 字符类: 匹配任意一个不在 abc… 中的字符
  • r1|r2 选择: 匹配一个能被 r1 或 r2 匹配的字符串
  • (r1)(r2) 拼接: 匹配字符串 xy, 其中 x 被 r1 匹配, y 被 r2 匹配
  • ®* 匹配 0 个或多个连续出现的被 r 匹配的字符串
  • ®+ 匹配 1 个或多个连续出现的被 r 匹配的字符串
  • ®? 匹配 0 个或 1 个被 r 匹配的字符串
  • ® 组合: 匹配的字符串与 r 所匹配的字符串相同

运算符按优先级升序排列. 只要没有违反优先级规则, 就可以省略多余的括号.

转义序列
在字符串与正则表达式中, 转义序列具有特殊的含义.

  • \b 退格
  • \f 换页
  • \n 换行
  • \r 回车
  • \t 制表
  • \ddd 八进制数, ddd 是 1 到 3 个数字, 每个数字的值在 0 到 7 之间
  • \c 其他字面上的字符 c, 比如 " 表示 ", \ 表示 \

限制
任意一个特定的 awk 实现都会强加一些限制条件, 下面列出了一些典型值:

  • 100 个字段
  • 每条输入记录 3000 个字符
  • 每条输出记录 3000 个字符
  • 每个字段 1024 个字符
  • 每个 printf 字符串 3000 个字符
  • 字面字符串 400 个字符
  • 字符类 400 个字符
  • 15 个打开文件
  • 1 个管道
  • 双精度浮点数

数值的限制与本地系统所能表示的数值范围有关, 比如某个机器所能表示的数值范围是 10−38 到1038, 超过这个范围的数值只拥有字符串形式

初始化, 比较和强制类型转换
每一个变量或字段, 在任意时刻都可能是字符串, 或数值, 或两者都是. 当变量通过赋值语句来获取一个值时: var = expr

它的类型也会被设置成表达式的类型 (“赋值” 包括 +=, -=, 等等). 算术表达式的类型是数值, 拼接是字符串类型, 以此类推. 如果赋值语句只是一个简单的复制, 比如 v1 = v2, 那么 v1 就会被设置成 v2 的类型.

比较时, 如果两个操作数的类型都是数值, 那么比较操作就会按照数值比较来进行. 否则的话, 操作数被强制转换成字符串 (如果原来不是字符串的话), 此时比较操作就按照字符串比较来进行. 通过某些手段, 可以把任意一个表达式强制转换成数值类型, 比如 expr + 0
转换成字符串可以这样做 (也就是和空字符作拼接操作): expr “”
字符串的数值形式的值, 指的是该字符串的数值前缀转换成数值后所得到的值.

未初始化的变量的值是数值 0 或空字符串 “”. 因此, 如果 x 没有被初始化过, 则条件判断 if (x) …
就会失败, 同样, 下面这些条件判断:
if (!x) …
if (x == 0) …
if (x == “”) …
都会成功, 但是注意: if (x == “0”) … 的比较结果为假.

如果可能的话, 字段的类型可以通过上下文环境来判断, 比如, $1++ 该表达式意味着: 如果有必要, 就把 $1 强制转换成数值类型, 而==$1 = $1 “,” $2== 意味着: 如果有必要, 就把 $1 和 $2 的类型强制转换成字符串.

如果无法根据上下文来判断变量的类型, 比如,if ($1 == $2) …
这时候就得根据输入数据来决定变量的类型. 所有字段的类型都是字符串, 但是, 如果字段包含了一个机器可识别的数, 那么它也会被当作数值类型.

显式为空的字段具有字符串值 “”, 它们不是数值类型. 该结论也适用于不存在的字段 (也就是超出 NF 的部分) 和空白行的 $0.

对字段成立的结论, 同样适用于由 split 创建的数组元素.
当在表达式中提到一个不存在的变量时, 就会创建该变量, 其初始值是 0 和 “”. 因此, 如果元素 arr[i] 当前不存在, 语句:
if (arr[i] == “”) …
就会导致元素 arr[i] 被创建, 且初始值为 “”, 这就使得 if 的条件判断结果为真.
测试语句 if (i in arr) … 判断元素 arr[i] 是否存在, 但是不会带来创建新元素的副作用.

  • SUBSEP 具有形式 [i,j,…] 的数组下标的分隔符 (默认是 “\034”)

ARGC 和 ARGV 包含被执行的程序的名字 (通常是 awk), 但是不包括出现在命令行中的 awk 程序 或选项. RLENGTH 同时也是 match 的返回值.

内建字符串函数
在下面列出的字符串函数中, s 和 t 表示字符串, r 表示正则表达式, i 和 n 表示整数.
sub 和 gsub 的替换字符串中的 & 会被匹配的字符串替换掉, 而 & 表示一个字面意义上的&.

  • gsub(r, s, t) 全局地把 t 中被 r 匹配的每一个子字符串替换为 s, 返回替换发生的次数; 如果省略 t, 则默认使用 $0
  • index(s, t) 返回 t 在 s 中的开始位置, 如果 s 不包含 t, 则返回 0
  • length(s) 返回 s 的长度
  • match(s, r) 返回s中匹配 r 的子字符串的起始位置, 如不存在可匹配的子字符串, 则返回0, 调用该函数会同时设置RSTART 与 RLENGTH
  • split(s, a, fs) 按照 fs, 把 s 切分到数组 a 中, 返回分割后的字段的个数; 如果省略 fs, 则默认使用 FS
  • sprintf(fmt, expr-list ) 返回格式化了的 expr-list ( 根据 fmt 进行格式化)
  • sub(r, s, t) 类似于 gsub, 但是它只替换第一个被匹配的子字符串
  • substr(s, i, n) 返回 s 中, 从 i 开始的, 长度为 n 的子字符串, 如果省略 n, 则返回 s 中从 i 开始的后缀

内建算术函数

  • atan2(y, x) y/x 的反正切值, 弧度制, 定义域从 −π 到 π
  • cos(x) 余弦 (弧度制)
  • exp(x) 指数 ex
  • int(x) 取整
  • log(x) 自然对数
  • rand() 返回一个伪随机数 r (0 ⩽ r < 1 )
  • sin(x) 正弦 (弧度制)
  • sqrt(x) 平方根
  • srand(x) 设置随机数种子, 如果省略 x, 则默认使用当天的时间

表达式运算符 (按优先级递增排列)
表达式可以通过下列运算符进行组合:

  • = += -= *= /= %= ^= 赋值

  • ?: 条件表达式

  • || 逻辑或

  • && 逻辑与

  • in 数组成员运算符

  • ~ !~ 正则表达式匹配运算符, 与否定匹配运算符

  • < <= > >= != == 关系运算符字符串拼接 (没有显式的运算符)

  • /+ - 加, 减

  • /* / % 乘, 除, 取模

  • /+ - ! 单目加, 单目减, 逻辑非

  • ˆ 指数运算符

  • ++ – 自增, 自减 (包括前缀形式与后缀形式)

  • $ 字段

所有的运算符都是左结合的, 除了赋值运算符, ?: 和 ˆ, 它们是右结合的. 任意一个表达式都可以用括号括起来.

正则表达式
正则表达式的元字符包括:

  • \ ^ $ . [ ] | ( ) * + ?

下面的表格总结了正则表达式及其所匹配的字符串:

  • c 匹配一个非元字符 c
  • \c 匹配一个转义序列, 或一个字面上的字符 c
  • ˆ 匹配一个字符串的开始
  • $ 匹配一个字符串的结束
  • . 匹配任意一个字符
  • [abc…] 字符类: 匹配 abc… 中的任意一个字符
  • [ˆabc…] 字符类: 匹配任意一个不在 abc… 中的字符
  • r1|r2 选择: 匹配一个能被 r1 或 r2 匹配的字符串
  • (r1)(r2) 拼接: 匹配字符串 xy, 其中 x 被 r1 匹配, y 被 r2 匹配
  • ®* 匹配 0 个或多个连续出现的被 r 匹配的字符串
  • ®+ 匹配 1 个或多个连续出现的被 r 匹配的字符串
  • ®? 匹配 0 个或 1 个被 r 匹配的字符串
  • ® 组合: 匹配的字符串与 r 所匹配的字符串相同

运算符按优先级升序排列. 只要没有违反优先级规则, 就可以省略多余的括号.

转义序列
在字符串与正则表达式中, 转义序列具有特殊的含义.

  • \b 退格
  • \f 换页
  • \n 换行
  • \r 回车
  • \t 制表
  • \ddd 八进制数, ddd 是 1 到 3 个数字, 每个数字的值在 0 到 7 之间
  • \c 其他字面上的字符 c, 比如 " 表示 ", \ 表示 \

限制
任意一个特定的 awk 实现都会强加一些限制条件, 下面列出了一些典型值:

  • 100 个字段
  • 每条输入记录 3000 个字符
  • 每条输出记录 3000 个字符
  • 每个字段 1024 个字符
  • 每个 printf 字符串 3000 个字符
  • 字面字符串 400 个字符
  • 字符类 400 个字符
  • 15 个打开文件
  • 1 个管道
  • 双精度浮点数

数值的限制与本地系统所能表示的数值范围有关, 比如某个机器所能表示的数值范围是 10−38 到1038, 超过这个范围的数值只拥有字符串形式

初始化, 比较和强制类型转换
每一个变量或字段, 在任意时刻都可能是字符串, 或数值, 或两者都是. 当变量通过赋值语句来获取一个值时: var = expr

它的类型也会被设置成表达式的类型 (“赋值” 包括 +=, -=, 等等). 算术表达式的类型是数值, 拼接是字符串类型, 以此类推. 如果赋值语句只是一个简单的复制, 比如 v1 = v2, 那么 v1 就会被设置成 v2 的类型.

比较时, 如果两个操作数的类型都是数值, 那么比较操作就会按照数值比较来进行. 否则的话, 操作数被强制转换成字符串 (如果原来不是字符串的话), 此时比较操作就按照字符串比较来进行. 通过某些手段, 可以把任意一个表达式强制转换成数值类型, 比如 expr + 0
转换成字符串可以这样做 (也就是和空字符作拼接操作): expr “”
字符串的数值形式的值, 指的是该字符串的数值前缀转换成数值后所得到的值.

未初始化的变量的值是数值 0 或空字符串 “”. 因此, 如果 x 没有被初始化过, 则条件判断 if (x) …
就会失败, 同样, 下面这些条件判断:
if (!x) …
if (x == 0) …
if (x == “”) …
都会成功, 但是注意: if (x == “0”) … 的比较结果为假.

如果可能的话, 字段的类型可以通过上下文环境来判断, 比如, $1++ 该表达式意味着: 如果有必要, 就把 $1 强制转换成数值类型, 而==$1 = $1 “,” $2== 意味着: 如果有必要, 就把 $1 和 $2 的类型强制转换成字符串.

如果无法根据上下文来判断变量的类型, 比如,if ($1 == $2) …
这时候就得根据输入数据来决定变量的类型. 所有字段的类型都是字符串, 但是, 如果字段包含了一个机器可识别的数, 那么它也会被当作数值类型.

显式为空的字段具有字符串值 “”, 它们不是数值类型. 该结论也适用于不存在的字段 (也就是超出 NF 的部分) 和空白行的 $0.

对字段成立的结论, 同样适用于由 split 创建的数组元素.
当在表达式中提到一个不存在的变量时, 就会创建该变量, 其初始值是 0 和 “”. 因此, 如果元素 arr[i] 当前不存在, 语句:
if (arr[i] == “”) …
就会导致元素 arr[i] 被创建, 且初始值为 “”, 这就使得 if 的条件判断结果为真.
测试语句 if (i in arr) … 判断元素 arr[i] 是否存在, 但是不会带来创建新元素的副作用.


  1. ABC ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值