13.1.1 count-words-example 中的空白字符错误

在前面的章节中描述的 count-words-example 命令有两个错误,或者更确切地说,有一个带有两个表现形式的错误。首先,如果你标记一个仅包含中间某些文本的空白区域,count-words-example 命令会告诉你该区域包含一个单词!其次,如果你标记一个仅包含位于缓冲区末尾或缩小缓冲区的可访问部分的空白字符的区域,该命令会显示一个错误消息,看起来像这样:

搜索失败:"\\w+\\W*"

如果你是在GNU Emacs的Info中阅读本文,你可以自行测试这些错误。

首先,按照通常的方式评估该函数以安装它。

如果愿意,也可以通过评估以下绑定来安装此键绑定:

(global-set-key "\C-c=" 'count-words-example)

进行第一个测试,将标记和点设置到以下行的开始和结束,然后键入 C-c =(或如果未绑定 C-c =,则为 M-x count-words-example):

    一个  两个  三

Emacs 将告诉你,该区域有三个单词,这是正确的。

重复测试,但将标记放在该行的开头,并将点放在单词 ‘一个’ 之前。再次输入命令 C-c =(或 M-x count-words-example)。Emacs 应该告诉你该区域没有单词,因为它仅由该行开头的空白字符组成。但是,Emacs却告诉你该区域有一个单词!

对于第三个测试,将示例行复制到 *scratch* 缓冲区的末尾,然后在该行的末尾输入多个空格。将标记放在单词 ‘’ 之后,将点放在行末。 (行末将是缓冲区的末尾。)像之前一样,输入 C-c =(或 M-x count-words-example)。再次,Emacs 应该告诉你该区域没有单词,因为它仅由该行末尾的空白字符组成。但是,相反,Emacs 显示一个错误消息,其中显示 ‘Search failed’。

这两个错误源于同一个问题。

考虑错误的第一个表现形式,在该表现形式中,该命令告诉你该行开头的空白字符包含一个单词。发生的情况是:M-x count-words-example 命令将点移动到区域的开头。while 测试点的值是否小于 end 的值,它是的。因此,正则表达式搜索寻找并找到第一个单词。它将点放在单词之后。 count 设置为一。while 循环重复; 但是这次点的值大于 end 的值,循环退出;函数显示一条消息,其中包含区域中的单词数为一。简而言之,尽管标记区域外的单词,但正则表达式搜索却寻找并找到了该单词。

在错误的第二个表现形式中,该区域是位于缓冲区末尾的空白字符。Emacs 显示 ‘Search failed’。发生的情况是,在 while 循环中的真假测试中,测试为真,因此执行搜索表达式。但由于缓冲区中没有更多的单词,搜索失败。

在错误的两个表现形式中,搜索都会扩展或尝试扩展到区域之外。

解决方案是限制搜索到该区域,这是一个相当简单的操作,但正如你可能期望的那样,它并不像你想象的那么简单。

正如我们所见,re-search-forward 函数将搜索模式作为其第一个参数。但除了这第一个,强制性的参数外,它还接受三个可选参数。可选的第二个参数限制了搜索。可选的第三个参数,如果是 t,则使函数在搜索失败时返回 nil 而不是引发错误。可选的第四个参数是重复计数。(在Emacs中,可以通过键入 C-h f,函数的名称,然后 RET 来查看函数的文档。)

count-words-example 的定义中,区域末尾的值由传递给函数的变量 end 持有。因此,我们可以将 end 添加为正则表达式搜索表达式的参数:

(re-search-forward "\\w+\\W*" end)

然而,如果你只对 count-words-example 定义进行这个更改,然后在一段空白区域上测试新版本的定义,你将收到一条错误消息,其中显示 ‘Search failed’。

发生的情况是:搜索限制为该区域,因为在该区域中没有单词构成字符,所以搜索失败,正如你所期望的那样。由于失败,我们收到了一条错误消息。但在这种情况下,我们不希望收到错误消息;我们希望收到消息“该区域不包含任何单词”。

解决此问题的方法是向 re-search-forward 提供第三个参数 t,这将导致函数在搜索失败时返回 nil 而不是引发错误。

然而,如果你进行此更改并尝试运行它,你将看到消息“计算区域内的单词数 ...”,等等,你将继续看到该消息...,直到你键入 C-gkeyboard-quit)。

发生的情况是:搜索限制为该区域,与之前一样失败,因为该区域中没有单词构成字符,正如预期的那样。因此,re-search-forward 表达式返回 nil。它仅仅返回 nil。特别地,如果找到了搜索目标,它不会移动点,这是它的副作用之一。在 re-search-forward 表达式返回 nil 后,while 循环中的下一个表达式将被评估。该表达式递增计数。然后循环重复。由于 re-search-forward 表达式没有移动点,所以 while 循环的真假测试测试为真,因为点的值仍然小于 end 的值。...循环重复...

count-words-example 定义需要进行另一个修改,以使 while 循环的真假测试在搜索失败时测试为假。换句话说,在增加单词计数变量之前,必须同时满足两个条件:点必须仍然在区域内,并且搜索表达式必须找到一个要计数的单词。

由于第一个条件和第二个条件必须一起为真,所以这两个表达式,区域测试和搜索表达式,可以用 and 特殊形式连接,并嵌入到 while 循环中作为真假测试,如下所示:

(and (< (point) end) (re-search-forward "\\w+\\W*" end t))

re-search-forward 表达式在搜索成功时返回 t,并作为副作用移动点。因此,随着找到单词,点通过区域移动。当搜索表达式未能找到另一个单词,或当点达到区域的末尾时,真假测试失败,while 循环退出,count-words-example 函数显示其消息之一。

在加入这些最终更改后,count-words-example 就没有错误了(或者至少,我没有找到错误!)。以下是它的最终版本:

;;; 最终版本: while
(defun count-words-example (beginning end)
  "打印区域内的单词数。"
  (interactive "r")
  (message "计算区域内的单词数 ... ")

;;; 1. 设置适当的条件。
  (save-excursion
    (let ((count 0))
      (goto-char beginning)

;;; 2. 运行 while 循环。
      (while (and (< (point) end)
                  (re-search-forward "\\w+\\W*" end t))
        (setq count (1+ count)))

;;; 3. 向用户发送消息。
      (cond ((zerop count)
             (message
              "该区域不包含任何单词。"))
            ((= 1 count)
             (message
              "该区域包含一个单词。"))
            (t
             (message
              "该区域包含 %d 个单词。" count))))))