sed awk 第二版学习(十一)—— 交互式拼写检查器 spellcheck.awk
目录
1. 脚本代码
2. 执行情况
3. 代码详解
(1)BEGIN 过程
(2)主过程
(3)END 过程
(4)支持函数
4. 附加说明
这是一个基于 UNIX spell 程序的名为 spellcheck 的 awk 脚本,功能是将 spell 发现的每个单词都显示给用户,并询问是否要修改这个单词。可以在每次遇到这个单词时进行修改,或者可以一次将所有的拼写错误都改掉。用户还可以选择添加单词,这些单词以后将由 spell 从本地字典文件中找到。
要让该脚本正常工作需要安装 spell 程序包及其字典。下面的命令安装 aspell 程序和英文字典:
yum -y install aspell aspell-en
1. 脚本代码
spellcheck.awk 脚本文件内容如下:
# spellcheck.awk -- 交互式拼写检查器
#
# 作者:Dale Dougherty
#
# 用法:awk -f spellcheck.awk [+dict] file
# 用 spellcheck 作为 shell 程序的名字
# SPELLDICT = "dict"
# SPELLFILE = "file"
# BEGIN 操作完成下面的任务:
# 1) 处理命令行参数
# 2) 创建临时文件名
# 3) 执行 spell 程序来创建单词列表文件
# 4) 显示用户响应列表
BEGIN {
# 处理命令行参数
# 至少两个参数 -- awk 和文件名
if (ARGC > 1) {
# 如果多于两个参数,第二个参数为 dict
if (ARGC > 2) {
# 测试如果字典被指定为“+”,将 ARGV[1] 赋给 SPELLDICT
if (ARGV[1] ~ /^\+.*/)
SPELLDICT = ARGV[1]
else
SPELLDICT = "+" ARGV[1]
# 将 ARGV[2] 赋给 SPELLFILE
SPELLFILE = ARGV[2]
# 删除参数,这样 awk 将不把它们当作文件打开
delete ARGV[1]
delete ARGV[2]
}
# 不多于两个参数
else {
# 将文件 ARGV[1] 赋给 SPELLFILE
SPELLFILE = ARGV[1]
# 测试本地字典文件是否存在
if (! system ("test -r dict")) {
# 如果存在,询问是否使用它
printf ("Use local dict file? (y/n)")
getline reply < "-"
# 如果回答是,使用“dict”
if (reply ~ /[yY](es)?/){
SPELLDICT = "+dict"
}
}
}
} # 处理参数个数大于 1 的过程结束
# 如果参数个数不大于 1,那么打印 shell 命令的用法
else {
print "Usage: spellcheck [+dict] file"
exit 1
}
# 处理命令行参数的过程结束
# 创建临时文件名,每个以 sp_ 开头
wordlist = "sp_wordlist"
spellsource = "sp_input"
spellout = "sp_out"
# 将 SPELLFILE 复制到临时输入文件
system("cp " SPELLFILE " " spellsource)
# 现在运行 spell 程序,将输出发送到 wordlist
print "Running spell checker ..."
if (SPELLDICT)
SPELLCMD = "spell " SPELLDICT " "
else
SPELLCMD = "spell "
system(SPELLCMD spellsource " > " wordlist )
# 测试单词列表,看是否有拼错的单词出现
if ( system("test -s " wordlist ) ) {
# 如果单词列表为空(或拼写命令失败),退出
print "No misspelled words found."
system("rm " spellsource " " wordlist)
exit
}
# 将单词列表文件赋给 ARGV[1] 使得 awk 能读取它
ARGV[1] = wordlist
# 显示用户响应列表
responseList = "Responses: \n\tChange each occurrence,"
responseList = responseList "\n\tGlobal change,"
responseList = responseList "\n\tAdd to Dict,"
responseList = responseList "\n\tHelp,"
responseList = responseList "\n\tQuit"
responseList = responseList "\n\tCR to ignore: "
printf("%s", responseList)
} # BEGIN 过程结束
# 主过程,处理单词列表的每行,目的是显示拼错的单词并提示用户进行适当的处理。
{
# 设置拼错的单词
misspelling = $1
response = 1
++word
# 打印拼错的单词并提示用户响应
while (response !~ /(^[cCgGaAhHqQ])|^$/ ) {
printf("\n%d - Found %s (C/G/A/H/Q/):", word, misspelling)
getline response < "-"
}
# 现在处理用户的响应
# CR - 回车表示忽略当前的单词
# 帮助
if (response ~ /[Hh](elp)?/) {
# 显示响应列表并再给出提示
printf("%s", responseList)
printf("\n%d - Found %s (C/G/A/Q/):", word, misspelling)
getline response < "-"
}
# 退出
if (response ~ /[Qq](uit)?/) exit
# 添加到字典
if ( response ~ /[Aa](dd)?/) {
dict[++dictEntry] = misspelling
}
# 对每个出现的都进行修改
if ( response ~ /[cC](hange)?/) {
# 读取收集的文件的每一行
newspelling = ""; changes = ""
while( (getline < spellsource) > 0){
# 调用函数显示拼错单词的行,并提示用户对每个进行更正
make_change($0)
# 所有行都被加入到临时输出文件
print > spellout
}
# 读完所有的行,关闭临时输入和临时输出文件
close(spellout)
close(spellsource)
# 如果做了修改
if (changes){
# 显示被修改的行
for (j = 1; j <= changes; ++j)
print changedLines[j]
printf ("%d lines changed. ", changes)
# 在保存前执行确认
confirm_changes()
}
}
# 全局修改
if ( response ~ /[gG](lobal)?/) {
# 调用函数来提示更正并显示被修改的行,在保存之前让用户确认所有的修改
make_global_change()
}
} # 主过程结束
# END 过程使所有的修改成为永久性的。它覆盖原始文件并将单词添加到字典中,还删除临时文件。
END {
# 如果到达这时只读取了一个记录,没有做修改,那么退出。
if (NR <= 1) exit
# 用户必须对保存更正的文件给出确认
while (saveAnswer !~ /([yY](es)?)|([nN]o?)/ ) {
printf "Save corrections in %s (y/n)? ", SPELLFILE
getline saveAnswer < "-"
}
# 如果答案是,那么将临时输入文件转移到 SPELLFILE 中,保存旧的 SPELLFILE 以防万一
if (saveAnswer ~ /^[yY]/) {
system("cp " SPELLFILE " " SPELLFILE ".orig")
system("mv " spellsource " " SPELLFILE)
}
# 如果答案为不,那么删除临时输入文件
if (saveAnswer ~ /^[nN]/)
system("rm " spellsource)
# 如果单词已经被添加到字典中,那么提示用户确认保存在当前字典中。
if (dictEntry) {
printf "Make changes to dictionary (y/n)? "
getline response < "-"
if (response ~ /^[yY]/){
# 如果没有定义字典,那么使用“dict”
if (! SPELLDICT) SPELLDICT = "dict"
# 遍历数组并将单词添加到字典中
sub(/^\+/, "", SPELLDICT)
for ( item in dict )
print dict[item] >> SPELLDICT
close(SPELLDICT)
# 排序字典文件
system("sort " SPELLDICT "> tmp_dict")
system("mv " "tmp_dict " SPELLDICT)
}
}
# 删除单词列表
system("rm sp_wordlist")
} # END 过程结束
# 函数的定义
# make_change -- 提示用户对当前输入行中的拼写错误进行更正。
# 调用它自己找到字符串中的其它错误
# stringToChange -- 初始为 $0,因此和 $0 的子串不匹配
# len -- 从 $0 开始到被匹配字符串的末尾的长度
# 假定已经定义了 misspelling。
function make_change (stringToChange, len, # 参数
line, OKmakechange, printstring, carets) # 局部变量
{
# 在 stringToChange 中匹配拼错的单词,否则什么也不做
if ( match(stringToChange, misspelling) ) {
# 显示匹配的行
printstring = $0
gsub(/\t/, " ", printstring)
print printstring
carets = "^"
for (i = 1; i < RLENGTH; ++i)
carets = carets "^"
if (len)
FMT = "%" len+RSTART+RLENGTH-2 "s\n"
else
FMT = "%" RSTART+RLENGTH-1 "s\n"
printf(FMT, carets)
# 如果没有定义,提示用户更正
if (! newspelling) {
printf "Change to:"
getline newspelling < "-"
}
# 回车表示忽略
# 如果用户输入更正并确认
while (newspelling && ! OKmakechange) {
printf ("Change %s to %s? (y/n):", misspelling, newspelling)
getline OKmakechange < "-"
madechg = ""
# 测试响应
if (OKmakechange ~ /[yY](es)?/ ) {
# 做修改(只在第一次遇到时)
madechg = sub(misspelling, newspelling, stringToChange)
}
else if ( OKmakechange ~ /[nN]o?/ ) {
# 提供重新更正的机会
printf "Change to:"
getline newspelling < "-"
OKmakechange = ""
}
} # while 循环结束
# 如果 len 为真,则处理 $0 的子串
if (len) {
# 对它进行汇编
line = substr($0,1,len-1)
$0 = line stringToChange
}
else {
$0 = stringToChange
if (madechg) ++changes
}
# 将修改过的行放入数组中以备显示
if (madechg)
changedLines[changes] = ">" $0
# 创建子串使得可以和其它情况相匹配
len += RSTART + RLENGTH
part1 = substr($0, 1, len-1)
part2 = substr($0, len)
# 余下的部分中,是否存在拼错的单词
make_change(part2, len)
} # if 结构结束
} # 函数 make_change() 结束
# make_global_change -- 提示用户对所有拼错的行进行全局修改,没有参数
# 假定已经定义了 misspelling。
function make_global_change( newspelling, OKmakechange, changes)
{
# 提示用户对当前拼错的单词进行更正
printf "Globally change to:"
getline newspelling < "-"
# 回车表示忽略
# 如果有回答,确认
while (newspelling && ! OKmakechange) {
printf ("Globally change %s to %s? (y/n):", misspelling, newspelling)
getline OKmakechange < "-"
# 测试响应并做修改
if (OKmakechange ~ /[yY](es)?/ ) {
# 打开文件,读取所有的行
while( (getline < spellsource) > 0){
# 如果找到匹配,用 gsub 做修改并打印每个被修改的行。
if ($0 ~ misspelling) {
madechg = gsub(misspelling, newspelling)
print ">", $0
changes += 1 # 计算修改的行数
}
# 将所有行写入临时输出文件
print > spellout
} # 读取文件的 while 循环结束
# 关闭临时文件
close(spellout)
close(spellsource)
# 报告修改的数量
printf ("%d lines changed. ", changes)
# 保存修改前执行确认函数
confirm_changes()
} # if (OKmakechange ~ y) 结束
# 如果更正没有被确认,提示输入新的单词
else if ( OKmakechange ~ /[nN]o?/ ){
printf "Globally change to:"
getline newspelling < "-"
OKmakechange = ""
}
} # 提示用户进行更正的 while 循环结束
} # 函数 make_global_change() 结束
# confirm_changes -- 在保存修改之前确认
function confirm_changes( savechanges) {
# 在保存修改之前提示用户确认
while (! savechanges ) {
printf ("Save changes? (y/n)")
getline savechanges < "-"
}
# 如果确认,用输出代替输入
if (savechanges ~ /[yY](es)?/)
system("mv " spellout " " spellsource)
}
2. 执行情况
# 待检查的文件 ch00 中有五行内容
$ cat ch00
SparcStation
languauge
nawk
Utlitities
tar xvf filename
# 执行脚本调用 spell 程序检查文件中单词的拼写错误,并执行响应的操作
$ awk -f spellcheck.awk ch00
Running spell checker ...
Responses:
Change each occurrence,
Global change,
Add to Dict,
Help,
Quit
CR to ignore:
1 - Found SparcStation (C/G/A/H/Q/):a
2 - Found Utlitities (C/G/A/H/Q/):c
Utlitities
^^^^^^^^^^
Change to:utilities
Change Utlitities to utilities? (y/n):y
>utilities
1 lines changed. Save changes? (y/n)y
3 - Found filename (C/G/A/H/Q/):
4 - Found languauge (C/G/A/H/Q/):g
Globally change to:language
Globally change languauge to language? (y/n):y
> language
1 lines changed. Save changes? (y/n)y
5 - Found nawk (C/G/A/H/Q/):a
6 - Found xvf (C/G/A/H/Q/):c
tar xvf filename
^^^
Change to:
>tar xvf filename
1 lines changed. Save changes? (y/n)y
Save corrections in ch00 (y/n)? y
Make changes to dictionary (y/n)? y
# 修改后的文件内容
$ cat ch00
SparcStation
language
nawk
utilities
tar xvf filename
# 添加(或创建)到当前目录下字典文件中的单词
$ cat dict
SparcStation
nawk
用由 spell 找到的有拼写错误的单词列表,spellcheck 可以提醒用户修改它们。在显示第一个单词之前,首先显示一个可执行操作的响应列表。输入响应“a”后跟一个回车键表示将这个单词加入列表,该列表用于更新字典,如本例中的 SparcStation 和 nawk。输入“c”将修改出现的每个错误,这时的响应使用户看到包含拼写错误的行并进行修改。在用户修改完所有错误后,显示修改的行,用户将被询问是否保存修改,如本例中的 Utlitities。输入“g”完成全局修改,在提示用户输入正确的拼写和确认输入后完成修改,将每个受影响的行显示出来且在前面加一个“>”,然后在保存修改前询问用户的确认,如本例中的 languauge。
因为不能确定 xvf 是否是拼写错误,所以用户可以输入“c”来观察这行。在确认没有拼错之后,用户键入回车以忽略这个单词。也可以在出现拼错单词后直接回车以忽略该单词,如本例中的 filename。
当单处理完 spell 返回的所有单词后,或者用户在这之前退出,将提示用户保存修改的文件和字典。
3. 代码详解
spellcheck.awk 脚本可以分为以下四部分:
- BEGIN 过程,处理命令行参数并执行 spell 命令来建立一个拼错单词列表。
- 主过程,一次从列表中读取一个单词并提示用户输入正确的单词。
- END 过程,保存(覆盖)文件,同时也将拼错单词列表以外的单词扩充到当前字典中。
- 支持函数,调用它用于修改文件。
(1)BEGIN 过程
BEGIN 过程的第一部分处理命令行参数。它检测如果 ARGC 比 1 大,则程序继续。也就是说,除了“awk”之外,必须给出文件名,这个文件名指定了 spell 要分析的文档。一个可选的字典文件名可以被作为第二个参数。尽管 spellcheck 命令并不支持 spell 的任何隐含选项,但 spellcheck 程序遵循 spell 的命令行接口。如果没有给出字典,那么程序执行 test 命令以确定文件 dict 是否存在。如果存在,提示用户确定利用这个文件作为字典文件。一旦处理了这些参数,将从 ARGV 数组中删除它们,这将防止它们被解释成文件名参数。
BEGIN 过程的第二部分建立了一些临时文件,因为不想直接在原始文件上工作。在这个程序的末尾,用户将选择保存或放弃在临时文件上所做的工作。临时文件都是以“sp_”开始且在退出程序前被删除。
这个过程的第三部分执行 spell 程序并建立一个单词列表。要测试这个文件是否存在并且在访问之前确保文件中包含内容。如果由于一些原因 spell 程序失败了,或者没有发现拼错的单词,文件 wordlist 会是空的。如果这个文件存在,则将这个文件名作为 ARGV 数组的第二个元素。这是一个不常用但可行的为 awk 将要访问的输入文件提供文件名的办法。注意,当 awk 被调用时,这个文件并不存在!在命令行中指定的文档文件名,不再存在于 ARGV 数组中。本例不是利用 awk 的主输入循环来读取这个文档文件,而是利用一个 while 循环来读取文件以发现并更正拼写错误。
BEGIN 过程的最后一部分的任务是当显示一个拼错的单词时,定义和显示用户能做出的响应的列表。这个列表在程序开始运行时显示一次,以及当用户在主菜单中选择帮助时显示。将这个列表赋给一个变量,可以使得必要时在程序的不同点访问它,以避免重复定义。
(2)主过程
主过程显示一个拼错的单词和提示用户输入适当的回答,其中的核心操作由两个用户自定义函数来处理。这个过程对每个拼错的单词都执行。
wordlist 中的每个输入行的第一个字段,包含着拼错的单词并赋给 misspelling。在一个 while 循环中将拼错的单词显示给用户并提示用户作出响应。下面的正则表达式用于测试 response 的值:
while (response !~ /(^[cCgGaAhHqQ])|^$/)
用户只能通过输入任意指定的字符或键入回车键(一个空行)退出这个循环。利用正则表达式测试用户输入有助于编写简单灵活的程序,如这里用户可以输入一个小写或大写字母 c 或以其开头的单词(如 Change)。
主过程的余下部分由条件语句组成,用于测试用户指定的响应并执行相应的操作。“help”的作用是再次显示响应列表和重新显示提示。“quit”的操作是 exit,用于退出主程序并转到 END 过程。如果用户输入“add”,拼错的单词将被加入到数组 dict 中,并被添加到本地字典中。
“Change”和“Global”响应使程序真正开始工作。当用户输入“c”或“change”时,将显示文档中的一个拼错的单词,然后提示用户做修改,这将在文档中每个出现拼写错误的地方发生。当用户输入“g”或“global”时,提示用户立即修改,并且将一次修改该单词所有的错误,不提示用户确认每个修改。这个工作主要由 make_change() 和 make_global_change() 两个函数来处理。回车表示忽略拼错的单词并得到列表中的下一个单词。这是主输入循环的默认操作,因此不需要为它设置条件。
(3)END 过程
END 过程的目的是允许用户确认对文档或字典的任何永久性修改。下列情况之一将进入 END 过程:
- spell 命令失败或没有发现任何拼错的单词。
- 拼错单词列表已取尽。
- 用户输入“quit”作为提示的响应。
END 过程以一个条件语句开始来测试记录的个数是否小于或等于 1。当 spell 程序没有产生单词列表或当用户看到第一个记录之后输入“quit”时将产生这种情况。这种情况 END 过程将当作没有可保存的工作而退出。
接着创建一个 while 循环来询问用户将所做的修改保存到文档。这需要用户对提示回答“y”或“n”。如果回答是“y”,临时输入文件将代替原始文档文件;如果回答是“n”,临时文件被删除。不接受其它的响应。
下一步测试数组 dict 中是否有内容。它的元素是要添加到字典中的单词。如果用户同意将它们到字典中,这些单词将添加到上面所定义的当前字典中,否则添加到本地 dict 文件中。因为被 spell 读取的字典必须排序,因此将执行一个 sort 命令对送到临时文件的输出进行排序,这个临时文件将在后面的处理中覆盖原文件。
(4)支持函数
这里有三个支持函数,其中两个用于完成大多数修改工作,第三个函数用于确定用户想要保存的所做的修改。当用户想在文档中“改变每个错误”时,主过程利用一个 while 循环每次读取文档的一行(这行成为 $0)。通过调用 make_change() 函数来确定这行中是否包含拼错的单词。如果有,显示这行并提示用户输入相应单词的正确拼写。
如果在当前行没有找到拼错的单词,什么都不做。如果找到了,这个函数显示包含拼错单词的行并询问用户是否要改正。在显示当前行的下面有一排 ^ 符号用于标识拼错的单词。当前输入行被复制到 printstring 中,因为有必要改变这行以便显示。如果这个行包含一些制表符,在这个行的副本中每个制表符都用一个空格代替。这就解决了当制表符出现时如何对齐 ^ 符号的问题(当计算一行的长度时,一个制表符作为一个单独的字符计算,但实际上他显示时占用了更多的空格,通常是 5 到 8 个字符长)。
当显示出这行后,函数提示用户输入正确的单词,然后接着显示用户输入的内容并请求确认。如果确认是正确的单词,则调用 sub() 函数进行修改。如果没有确认,讲给用户另外一个机会输入正确的单词。
sub() 函数只修改一行中第一次出现的单词,要想让用户对每次修改做出确认,须要提取余下的部分中拼错的单词,而且不管第一次出现的单词是否被修改。为了完成这些功能,将函数 make_change() 设计成递归函数,它调用自己以在同一行上查找符合条件的其它单词。换句话说,当 make_change() 第一次被调用时,它查找整个 $0 并提取这一行中的第一个拼错的单词。然后它将这个行分为两部分 —— 第一部分包含到第一次出现的单词末尾的所有字符;第二部分包含这行的其它所有字符。然后它调用自己来提取第二部分中的拼错的单词。当递归调用时,这个函数使用了两个参数:
make_change(part2, len)
第一个参数是要修改的字符串,当从主程序中调用时,它初始为 $0,但以后各次都是 $0 余下的部分。第二个参数的 len 或第一部分的长度,用来提取子串并在最后将两部分重新组合在一起。
函数 make_change() 还用于将被改变的行收集到一个数组中:
# 将被改变的行放入数组中以便显示
if (madechg)
changedLines[changes] = ">" $0
如果 sub() 执行成功,变量 madechg 将得到一个值。$0(两个部分已经重新组合在一起)被赋给一个数组元素。当读取文档中的所有行之后,主程序循环访问这个数组并显示所有被修改的行。然后调用函数 confirm_changes() 询问是否保存这些修改。它将用临时输出文件覆盖临时输入文件,保持修改了拼错单词后的完整结果。
如果用户决定做“全局修改”,则调用函数 make_global_change() 来完成。这个函数和 make_change() 函数类似,但更简单,因为有现成的函数实现对每行的全局修改。
这个函数提示用户输入正确的单词。设置 while 循环用于读取文档的所有行并使用函数 gsub() 实现修改。所有修改一次完成,不提示用户确认。当所有的行被读取后,函数显示已经修改的行并在保存它们之前调用函数 confirm_changes() 得到用户对这些修改的确认。
函数 confirm_changes() 被主过程和 make_global_change() 函数调用,用来对产生的修改进行确认。设计这个函数是为了防止代码重复。它的目的就是当用文档文件的新版本(spellout)替换老版本(spellsource)时通知用户这些变化。
4. 附加说明
spell 一个明显的问题是程序返回的拼写错误单词的顺序与单词在文件中出现的顺序可能不一致,这是一个已知问题。而且看代码可知,这个脚本的逻辑是用户选择了几次“c”,就会遍历几遍原文件,而原文件有多少行,就会执行多少遍 make_change() 函数,make_change() 还是个递归函数,性能可想而知:
if ( response ~ /[cC](hange)?/) {
# 读取收集的文件的每一行
newspelling = ""; changes = ""
while( (getline < spellsource) > 0){
# 调用函数显示拼错单词的行,并提示用户对每个进行更正
make_change($0)
# 所有行都被加入到临时输出文件
print > spellout
}
除了可能的性能问题,仔细查看代码并做测试,会发现这个脚本还有 bug,如当原始文件只有一行时不会保存修改等。这本书是二十多年前写的,后来其实有更好的可用程序,如 aspell 就提供了更为全面的功能,如下图所示:
因此权且将该脚本当作一个熟悉 awk 编程的练习。所有工作都由 awk 编程语言完成,包括 10 个 UNIX 命令,在 awk 中利用一致的语法和相同的结构来处理。当一部分工作在 shell 中完成而另一部分工作在 awk 中完成时,可能引起混淆,例如必须注意 if 条件在语法上的区别和如何引用变量。awk 的现代版本提供了可替代的 shell 执行命令及与用户连接的功能。