第12章 拼写检查

本章背景:讲解UNIX拼写检查工具的演变:从最早管道原型 → spell → ispell/aspell → 完整的awk实现拼写检查器。 核心思想:UNIX工具哲学,用小工具管道组合完成任务;awk强大的文本处理,不到200行实现拼写检查。 用到命令:tr sort uniq comm;shell脚本参数处理;awk函数、关联数组、getline、match/sub/gsub、ARGV/ARGC/FILENAME/FNR内置变量。

12.1 spell 程序

作用

读取文件,输出字典中找不到的单词列表(疑似拼写错误)。

⚠注意:POSIX没有标准化spell,不是所有系统自带。

简单应用示例

脚本里嵌入拼写检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#! /bin/sh -
file=/tmp/report.$$
echo "Type in the problem, finish with Control-D."
# 读取键盘输入,写入$file(写文件)
cat > $file
while true
do
    printf "[E]dit, Spell [C]heck, [S]end, or [A]bort: "
    read choice
    case $choice in
        [Ee]*) ${EDITOR:-vi} $file ;;
        [Cc]*) spell $file ;;   #调用spell检查
        [Aa]*) exit 0 ;;
        [Ss]*) break ;;
    esac
done

12.2 最初的UNIX拼写检查原型(管道版本)

1975年,最早spell原型完全由UNIX管道组合实现,没有写C程序。

1
2
3
4
5
6
prepare filename |
tr A-Z a-z |            #大写转小写
tr -c a-z '\n' |        #非字母替换成换行,拆分出单词
sort |                  #单词排序
uniq |                  #去重
comm -13 dictionary -   #输出:输入里存在、字典里不存在的单词

工具知识点:comm

前提:两个输入必须预先排序 语法:comm [-123] file1 file2,其中-代表读取标准输入 输出三列:

  1. 只在file1存在
  2. 只在file2存在
  3. 两个文件都存在
选项 作用
-1 不显示第1列
-2 不显示第2列
-3 不显示第3列

示例 comm -13 dictionary -

  • -1:屏蔽仅字典有的;-3屏蔽两边共有的;
  • 只剩下 只出现在管道输入(待检查文件) 的单词,就是疑似错拼单词。

prepare是预处理工具,作用剔除标记(roff/html标记),简单场景直接用cat。

comm 命令详解

核心基础

comm file1 file2

  • 要求:file1、file2 两个文件必须提前排序(sort),否则结果完全乱掉。
  • 输出固定3列,用tab制表符隔开:
列号 含义
第1列 仅在 file1 里面出现的行
第2列 仅在 file2 里面出现的行
第3列 file1 和 file2 两者都有的行

⚠重点:-1 -2 -3 选项,数字代表【列编号】,不是文件编号! -1 → 不输出第1列 -2 → 不输出第2列 -3 → 不输出第3列

不是控制文件file1/file2;是控制输出结果里的三列要不要打印出来。

举个简单手工例子

文件A(file1)

1
2
3
apple
banana
cat

文件B(file2)

1
2
3
banana
dog
egg

两个都已经排好序。

执行:

1
comm A B

输出(tab分隔三列):

1
2
3
4
5
apple
        banana
cat
    dog
    egg

拆解输出:

  1. apple → 第1列:只在A(file1)
  2. banana →第3列:A、B都有
  3. cat →第1列:只在A(file1)
  4. dog →第2列:只在B(file2)
  5. egg →第2列:只在B(file2)

测试选项

1
2
3
4
comm -1 A B   # 不打印第1列(删掉只属于file1的行)
comm -2 A B   # 不打印第2列(删掉只属于file2的行)
comm -3 A B   # 不打印第3列(删掉两边共有的行)
comm -13 A B  # 同时:关闭第1列 + 关闭第3列

comm -13 A B

  • -1:关掉第1列(仅file1的行不输出)
  • -3:关掉第3列(两边共有的行不输出) 只剩下第2列:只存在于file2的行。

输出结果:

1
2
dog
egg

符号 - 的含义

comm 的两个参数位置 comm file1 file2,其中任意一个位置写横杠 -,代表:这个位置不从磁盘读文件,改为读取【标准输入(stdin,管道过来的数据)】。

回到书上拼写检查的管道命令:

1
comm -13 dictionary -

参数对应:

  1. 第一个参数:dictionary → file1 = 字典文件(磁盘上真实文件,已经sort排序)
  2. 第二个参数:- → file2 = 读取管道标准输入(前面管道输出的单词流)

这里: file1 = dictionary(字典)
file2 = 管道传过来待检查的单词

comm -13 dictionary -

  • -1:关闭【第1列】:只在file1(字典)里面有的单词 → 不打印
  • -3:关闭【第3列】:字典和待检查两边都有的单词 → 不打印
  • 剩下:第2列:只存在file2(待检查文本)、不在file1(字典)的单词 → 就是疑似拼错的单词!

表格对应书上示例

comm列 代表内容 ‑13操作
第1列 只在字典(file1)中的单词 ‑1,屏蔽不输出
第2列 只在待检查文本(file2)中的单词(错拼) 保留输出
第3列 字典、待检查文本两者都有的正确单词 ‑3,屏蔽不输出

一句话记忆:

  1. -1 -2 -3 的数字是输出的列号,不是文件号;
  2. 参数位置上的-横杠,代表该位置读取管道/标准输入,不是磁盘文件。

完整管道回顾

1
2
3
4
5
6
prepare filename |
tr A-Z a-z |
tr -c a-z '\n' |
sort |
uniq |
comm -13 dictionary -

管道前面一连串输出,送到comm的第二个参数位置(就是那个-),充当file2;磁盘文件dictionary充当file1。

12.3 改良的 ispell 与 aspell

原生UNIX spell支持私有用户字典:+字典文件

1
spell +/usr/local/lib/local.words myfile > myfile.errs
  • +后面接私有字典:存放专业术语、专有名词,这些单词不在系统字典,但属于正确单词。

spell 的坑

  1. 仅支持一个私有字典;
  2. 私有字典必须是已经排序好;
  3. 受locale环境影响,locale改变,排序逻辑变化,spell会异常,同一个文件报错数量会变化。

解决:执行spell前,设置LC_ALL=C保证字典和程序的locale一致。

ispell / aspell

两个流行开源拼写检查工具:

  • ispell:老牌交互拼写检查;
  • aspell:拼写纠错建议更好,目标替代ispell。

重要选项(两者通用)

选项 作用
-l 批处理,输出所有疑似错误单词(输出不排序、不去重)
-p file 指定个人私有字典

问题:原生输出未排序、重复,和传统spell行为不一致,需要shell脚本包装。

包装1:模拟spell,使用ispell

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /bin/sh
mydict=
case $1 in
    +?*)
        mydict=${1#+}    #去掉开头+
        mydict="-p $mydict"
        shift
        ;;
esac
cat "$@" | ispell -l $mydict | sort -u

逻辑:

  1. 如果第一个参数以+开头,提取字典文件名,转为ispell的‑p参数;
  2. cat "$@"读取全部输入文件;交给ispell;sort -u排序去重,模拟spell输出。
脚本解析

逐点拆解脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /bin/sh
mydict=
case $1 in
+?*)
    mydict=${1#+}    #去掉开头+
    mydict="-p $mydict"
    shift
        ;;
esac
cat "$@" | ispell -l $mydict | sort -u
  1. 关于参数 +/usr/local/lib/local.words

调用脚本的时候:

1
./myspell +/usr/local/lib/local.words test.txt
  • $1 就是第一个参数:+/usr/local/lib/local.words(加号和路径是一整个参数,不拆开)
  • $2 就是 test.txt

你的理解:$1 保存完整的 +/usr/local/lib/local.words,是正确。

  1. case $1 in +?*)

这是 shell 的模式匹配(不是正则),写在 case 里面。

  • +:字面字符,匹配参数以加号 + 开头
  • ?:shell glob,匹配任意1个字符
  • *:shell glob,匹配0个或者任意多个字符

合起来:+?*

匹配:以+开头,并且加号后面至少还要有一个字符。 也就是识别 +filename 这种格式;单独一个 + 不会匹配。

你理解的:加号开头后面跟任意字符 ✔,? 保证不能只有一个孤零零的加号。

  1. ${1#+} —— 参数展开,不是命令替换!!重点区分 ,${1#+} 等价于 ${$1#+} 是错误!不能写美元符号在大括号里面的数字上,大括号内直接写数字代表位置参数。

分清两个极易搞混符号

  1. 命令替换:$(...) 美元括号,执行命令拿输出。
  2. 参数/变量展开:${var#pattern} 大括号,对变量字符串做删除,不启动任何子进程、不执行外部命令。

语法:${变量#模式}

#:从字符串开头,删除最短匹配的模式。

例子:

1
2
var="+/usr/local/lib/local.words"
echo ${var#+}

${var#+}:删掉变量开头的字面字符 +,得到结果: /usr/local/lib/local.words

回到代码:

1
mydict=${1#+}

$1 值是 +/usr/local/lib/local.words ${1#+} → 删除开头的 +,得到纯路径 /usr/local/lib/local.words。

然后:

1
mydict="-p $mydict"

变量变成:mydict="-p /usr/local/lib/local.words",后面直接给 ispell 使用。

对比记忆

写法 类型 作用
$(cmd) 命令替换 运行cmd,捕获输出结果
${var#xxx} 变量展开 对变量字符串头部删除匹配文本,不执行命令
  1. shift

匹配成功执行 shift;

shift:把位置参数左移一位。原来的$2变成新$1。

调用示例:./myspell +dict.txt a.txt b.txt

  • shift 执行前:$1=+dict.txt $2=a.txt $3=b.txt
  • shift 执行后:$1=a.txt $2=b.txt>

把带+的字典参数消耗掉,剩下的都是待检查的文件名。

5 cat "$@"

  • $@:全部位置参数;加双引号 "$@",每一个参数作为独立一项,文件名带空格也不会错乱。

shift之后,$@里面就是用户传入待检查的全部文件列表。 cat "$@" 读取所有这些文件的内容,输出到管道。

把完整调用走一遍

调用命令:

1
./myspell +/usr/local/lib/local.words note.txt
  1. $1=+/usr/local/lib/local.words,case匹配 +?*
  2. mydict=${1#+} → mydict=/usr/local/lib/local.words
  3. mydict="-p $mydict" → mydict="-p /usr/local/lib/local.words"
  4. shift:参数移位;现在 $1=note.txt
  5. cat "$@" → cat note.txt,读取文件内容输出管道
  6. 管道交给:ispell -l -p /usr/local/lib/local.words
  7. 输出交给 sort -u 排序去重。

如果调用脚本不带 +xxx 参数:case不匹配,mydict保持为空。 那管道就变成:cat "$@" | ispell -l | sort -u,不传递 -p 私有字典。

思考题

1
2
arg="+hello.world"
echo ${arg#+}

输出结果是什么?

答案 hello.world

关键小结

  1. +?*) case glob模式:匹配以+开头,加号后面至少有一个字符的参数。
  2. ${1#+}:变量展开,不是命令替换,删除字符串开头的+。
  3. shift:把已经处理完的字典参数扔掉,剩下的就是待检查文件名。
  4. "$@":全部剩余文件名,传给cat读取。
  5. 原生 spell +dict file 的接口,通过脚本翻译成 ispell -l -p dict 的接口。>

这就是shell包装脚本的价值:把旧程序的参数格式,翻译成新程序能识别的格式。

包装2:模拟spell,使用aspell

注意:aspell的‑p要求字典是编译二进制,不能直接用普通文本字典。 变通思路:aspell输出错词后,用fgrep -v -f 私有字典,把私有字典内单词从报错列表剔除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /bin/sh
mydict=cat
case $1 in
    +?*)
        mydict=${1#+}
        mydict="fgrep -v -f $mydict"
        shift
        ;;
esac
cat "$@" | aspell -l --mode=none | sort -u | eval $mydict
  • --mode=none:关闭html/tex标记过滤;
  • eval $mydict:没有私有字典就执行cat原样输出;有私有字典执行fgrep -v -f file,剔除私有字典内单词。

12.4 awk实现拼写检查程序 spell.awk

目标:纯awk实现拼写检查,规避原生spell缺陷:字典不需要预先排序,不受locale排序影响。

设计目标摘要

  1. 输入文本,输出不在字典中的疑似错误单词;
  2. 支持系统字典、用户追加私有字典;
  3. 字典无需排序;
  4. 大小写不敏感查找,输出保留原文大小写;
  5. 忽略标点、数字;支持缩写撇号 ';
  6. 输出排序去重;
  7. -verbose:输出 文件名:行号:单词,方便定位;
  8. -strip:后缀剥离(比如jumping → jump,处理单词变形,减少误报)。

调用示例:

1
2
3
4
5
#简单模式,输出疑似错单词
awk -f spell.awk testfile

# verbose模式,带文件名行号
awk -f spell.awk -v Verbose=1 testfile

输出示例(普通模式)

1
2
3
4
5
6
deroff
eqn
ier
nx
tbl
thier

verbose模式输出

1
2
3
4
5
6
testfile:7:eqn
testfile:7:tbl
testfile:11:deroff
testfile:12:nx
testfile:19:ier
testfile:19:thier

整体程序框架(三部分,awk标准模式)

1
2
3
BEGIN {initialize()}          #程序启动,初始化、加载字典、解析选项
{ spell_check_line() }        #每读入一行,调用函数处理
END {report_exceptions()}     #全部读完,输出报告

核心函数拆解

1. initialize()

做全部初始化工作:

  1. NonWordChars:正则,匹配非单词字符,后续用来剔除标点符号;
  2. get_dictionaries() 获取字典文件列表;
  3. scan_options()解析命令行选项(‑strip、‑verbose);
  4. load_dictionaries()读取字典文件;
  5. load_suffixes()加载后缀剥离规则;
  6. order_suffixes()后缀规则按长度从长到短排序(优先匹配长后缀)。

2. get_dictionaries()

字典来源优先级:

  1. awk变量Dictionaries(‑v Dictionaries="a.txt b.txt");
  2. 环境变量DICTIONARIES;
  3. 以上都没有,使用系统默认字典。

技巧:字典文件名作为数组下标 DictionaryFiles["/usr/dict/words"]++

  • 自动去重:同一个字典多次传入,数组key只会存一份;
  • 遍历直接用for(key in DictionaryFiles)。

3. load_dictionaries()

1
2
3
4
5
6
7
8
9
function load_dictionaries(    file, word)
{
    for (file in DictionaryFiles)
    {
        while ((getline word < file) > 0)
            Dictionary[tolower(word)]++   #全部转小写存入关联数组
        close(file)
    }
}
  • Dictionary关联数组:key=小写单词,value随便(只用来判断key是否存在);
  • 判断单词是否合法:if(lc_word in Dictionary)。
awk getline word < file 详解

awk getline word < file详解

1
2
3
while ((getline word < file) > 0)
    Dictionary[tolower(word)]++
close(file)

你的理解大体正确:
getline 用来读取磁盘文件file的一行文本,读到的行存入变量word。
读到文件末尾,就返回0,循环退出;发生错误返回负数。

getline 变量 < 文件 语法

1
getline word < file
  1. 打开磁盘上的 file 文件;
  2. 读取一行,去掉末尾换行符,放到变量 word;
  3. 返回值(非常重要)
返回值 含义
>0(等于1) 成功读到一行;继续循环
0 读到文件末尾 EOF,没有更多行
<0(-1) 出错:文件不存在、没有权限等

while条件

1
while ((getline word < file) > 0)
  • 只要返回值大于0(成功读到一行),就执行循环体;
  • 返回0(文件读完) → while条件不成立,跳出循环;
  • 如果文件打不开返回 -1,也直接跳出循环。

外面两层括号 ((... )>0):内层括号把getline作为整体,拿它的返回值和0比较。

完整流程走一遍

假设字典文件words内容:

1
2
3
Apple
Banana
Cat
1
2
3
4
5
6
for (file in DictionaryFiles)   # file="/usr/dict/words"
{
    while ((getline word < file) > 0)
        Dictionary[tolower(word)]++
    close(file)
}
  1. getline word < file →读到Apple → word="Apple",返回1; tolower(word)变成apple;Dictionary["apple"]++,数组key保存单词。
  2. 再次循环:getline读下一行 Banana → word=“Banana”,存入Dictionary["banana"]
  3. 再次循环:getline读Cat →存入Dictionary["cat"]
  4. 再次执行getline word < file:已经到文件结尾,返回0;0>0为假,退出while循环。
  5. close(file):❗必须关闭文件。>

awk不会自动关闭getline < file打开的文件;不close,打开文件句柄会耗尽,多次调用会bug。

重点区分两套完全不一样的读取(极易混淆)

①普通awk自动读取(主输入流,不用写getline)

1
awk '{print $0}' test.txt

awk自动循环读test.txt每一行,存入$0,这是awk默认工作模式。

②getline var < filename 手动读取磁盘文件(本案例)

这是手动主动打开另外一个磁盘文件,和awk主输入文件无关!
在这个load_dictionaries函数里面:awk正在处理待检查文本;同时用getline额外读取字典磁盘文件,加载单词表。

  • 读到的内容放到自定义变量word,不会修改 $0 $1 NF 这些字段!

⚠关键点:word是自定义变量,不是$0。getline var这种形式,不会更新$0、NF、NR、FNR。

易错点

  1. getline word < file,< file这里的file是awk变量,不要加引号。
    • file存文件名字符串;
    • 不能写 getline word < "file",那会打开名叫字面字符串file的文件。
  2. 用完一定要 close(file);反复循环打开大量文件,不关闭会报“too many open files”。
  3. 判断条件是 >0,不要写 !=0。因为出错返回负数,-1 !=0也成立,会进入循环,程序异常。
1
2
#不推荐,文件打不开也会跑循环
while ((getline word < file) !=0)

标准写法:while ((getline word < file) > 0),只处理成功读到行的情况。

简短总结

  1. getline word < file:打开file,读取一行放到变量word;返回1成功,0到文件末尾,负数出错。
  2. while ((getline word < file) >0):循环读取全部行,读到文件末尾退出循环。
  3. 这是手动读取额外磁盘文件,和awk主输入流无关,不改变$0。
  4. 读取完成务必close(file)关闭文件。

4. scan_options()解析选项,修改ARGV/ARGC

awk内置变量:

  • ARGV[]:命令行参数数组;ARGC参数总个数。

技巧:把选项对应的ARGV[k]设置为空字符串,awk就不会把它当做待读取的输入文件; 最后清理,去掉数组末尾空参数,避免awk错误读取标准输入。

识别选项:

  • -strip:开启后缀剥离;
  • -verbose:开启带行号输出;
  • =xxx.sfx:后缀规则文件。

5. load_suffixes() 加载后缀剥离规则文件

后缀规则示例片段english.sfx

1
2
3
ing$            # jumping → jump
ed$     "e      # breaded → bread
ies$    ie y    # flies → fly / ties → tie

逻辑:

  1. 读行;去掉注释#、行首尾空白,跳过空行;
  2. split()切割,正则存入Suffixes[],替换文本存入Replacement[]。

如果用户没有提供后缀文件,程序内置一套英文默认后缀。

6. order_suffixes() 冒泡排序后缀规则

⚠重要:后缀必须长的优先匹配。例如不能先匹配短的s$,再匹配长的ings$。

  • 将后缀正则从数组key转为有序数组OrderedSuffix[1...N];
  • 冒泡排序,按字符串长度降序排列;
  • swap(a,i,j)辅助函数交换数组元素。

7. spell_check_line() 处理每一行文本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function spell_check_line(    k, word)
{
    gsub(NonWordChars, "")    #删除全部非单词字符
    for (k = 1; k <= NF; k++)
    {
        word = $k
        sub("^'+", "", word)    #去除单词开头的撇号
        sub("'+$", "", word)    #去除单词结尾的撇号
        if (word != "")
            spell_check_word(word)
    }
}
  1. gsub把非单词字符全部清空,一行文本被分割成字段$1 $2 … $NF;
  2. 剔除单词首尾缩写撇号;
  3. 调用spell_check_word()做单词检查。
gsub、sub、NF详解

awk:gsub、sub、NF、正则 ^'+

  1. NF 内置变量

NF = Number of Fields,当前这一行的字段总数

awk 默认按空白(空格、tab)切割一行文本,切割之后:

  • $1:第 1 个字段
  • $2:第 2 个字段
  • …
  • $NF:最后一个字段
  • NF:一共有多少个字段。
  1. sub() 与 gsub()

两者都是字符串替换函数。

函数 全称 行为
sub(regex, replacement [,target]) substitute 只替换【第一个】匹配到的内容
gsub(regex, replacement [,target]) global substitute 全局,全部匹配都替换;g=global

语法:

1
2
sub(正则, 替换成什么, 待处理变量)
gsub(正则, 替换成什么, 待处理变量)

例子 1 gsub(代码第一行)

gsub(NonWordChars, "")
  • 没有写第三个参数,默认操作$0当前整行;
  • 把所有匹配NonWordChars正则的字符,全部替换为空字符串(直接删掉)。

作用:把一行里面所有非单词字符全部清除。

例子 2 sub

1
sub("^'+", "", word)
  • 在变量word上操作;
  • 找到匹配正则的内容,只替换第一个匹配,替换成空(删除)。
1
sub("^'+", "", word)
  • 在变量word上操作;
  • 找到匹配正则的内容,只替换第一个匹配,替换成空(删除)。
  1. 正则表达式 "^'+ 拆解
1
"^'+

表格

符号 含义
^ 正则锚点:字符串开头
' 字面的撇号字符(单引号)
+ 量词:前面的字符,出现 1 次或者多次

^'+ = 字符串开头,连续一个或多个撇号 '。

示例:

  • 单词 '''hello:开头有 3 个撇号 → 匹配成功,全部删掉 → 得到hello
  • don't:撇号在中间,不是开头,不会匹配,保留 don't

8. spell_check_word(word) 单词检查核心

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function spell_check_word(word,    key, lc_word, location, w, wordlist)
{
    lc_word = tolower(word)
    if (lc_word in Dictionary)    #字典存在,正确单词直接返回
        return
    else
    {
        if (Strip)                 #开启后缀剥离
        {
            strip_suffixes(lc_word, wordlist)
            for (w in wordlist)
                if (w in Dictionary) #剥离后缀后词根在字典,判定正确
                    return
        }
        #走到这里:疑似拼写错误
        location = Verbose ? (FILENAME ":" FNR ":") : ""
        Exception[lc_word] = location word
    }
}
  • FILENAME:当前正在处理的文件名;FNR:当前行号;
  • Exception关联数组:保存疑似错误单词,供END阶段输出。

9. strip_suffixes() 后缀剥离

输入一个小写单词,按已经排好序(长优先)的后缀正则逐个match()匹配:

  1. 匹配到后缀:截取得到词根;
  2. 根据规则生成候选词根/变形,存入wordlist[];
  3. 匹配到第一条后缀立刻break,不再尝试更短后缀。

返回wordlist给上层,上层判断:如果任意候选词根存在字典,该单词视为正确。

10. report_exceptions() END块输出结果

1
2
3
4
5
6
7
function report_exceptions(    key, sortpipe)
{
    sortpipe = Verbose ? "sort -f -t: -u -k1,1 -k2n,2 -k3" : "sort -f -u -k1"
    for (key in Exception)
        print Exception[key] | sortpipe
    close(sortpipe)
}

awk支持把print输出直接管道交给外部sort命令,不用自己实现复杂排序。

  • verbose模式:按文件名、数字行号排序;普通模式按单词忽略大小写排序;
  • close(sortpipe):必须关闭管道,否则程序不会结束。

spell.awk关键知识点汇总

  1. awk关联数组做哈希字典:key in array快速判断存在;
  2. getline < file读取字典文件;
  3. match() substr()字符串截取;sub gsub split()文本处理;
  4. awk内置变量:ARGV ARGC FILENAME FNR;
  5. awk可以直接把print输出管道交给外部shell命令;
  6. 后缀匹配:长后缀优先匹配,需要手动排序;
  7. 工程思想:排序交给外部sort,不要在awk内部重写复杂排序逻辑。

性能与工程思考

  1. 190行awk,实现完整拼写检查;速度接近C写的工具;
  2. 字典、后缀规则全部是普通文本,方便编辑;
  3. 不做标记去除(html/roff),遵循UNIX哲学:单独写工具预处理输入,本工具只负责拼写检查;
  4. awk的profile工具pgawk,可以生成性能分析报告,定位程序热点代码。

12.5 小结

  1. 最早UNIX拼写检查完全依靠管道工具组合实现,展现小工具组合的强大。
  2. spell存在locale、字典必须排序的坑;ispell/aspell批处理输出需要shell脚本包装,模拟传统spell。
  3. awk版本拼写检查:利用关联数组实现哈希字典,规避字典排序、locale问题;支持后缀剥离减少误报。
  4. UNIX工具思想:复杂工作交给成熟外部工具(sort),自己只做业务逻辑,不重复造轮子。
  5. shell脚本常见用途:包装已有程序,微调输入输出,适配自己需要的接口。