count-words-example
¶におけるホワイトスペースバグ前節で説明した count-words-example
コマンドには2つのバグ、というよりは2つの現れ方を持つ1つのバグがあります。まず、あるテキストの途中で空白文字のみを含む範囲をマークすると、count-words-example
コマンドはその範囲に1つの単語が含まれていると表示します!次に、バッファの末尾、あるいは狭められたバッファのアクセス可能な部分で空白文字のみを含む範囲をマークすると、このコマンドは次のようなエラーメッセージを表示します:
Search failed: "\\w+\\W*"
GNU EmacsのInfoでこれを読んでいる場合は、これらのバグをご自身で検証できます。
まず、通常の方法で関数を評価してインストールしてください。
ご希望であれば、このキーバインドを評価して設定することも可能です:
(keymap-global-set "C-c =" 'count-words-example)
最初のテストを行うには、マークとポイントを次の行の先頭と末尾に合わせ、C-c =(C-c =をバインドしていない場合はM-x count-words-example)を入力してください:
one two three
Emacsは、リージョンに3単語あると正しく表示します。
テストを繰り返しますが、マークを行頭に置き、ポイントを単語 ‘one’ の直前に配置してください。再度、コマンド C-c =(または M-x count-words-example)を入力します。行頭の空白文字のみで構成されているため、Emacs はリージョンに単語がないと表示するはずです。しかし実際には、Emacs はリージョンに1つの単語があると表示します!
3回目のテストでは、サンプル行を*scratch*バッファの末尾にコピーし、行の末尾にスペースを数個入力してください。単語‘three’の直後にマークを置き、行末にポイントを置いてください。(行末はバッファの末尾になります。)先ほどと同様にC-c =(またはM-x count-words-example)と入力してください。ここでも、リージョンが行末の空白文字のみで構成されているため、Emacsはリージョンに単語がないことを通知するはずです。代わりに、Emacsは‘Search failed’というエラーメッセージを表示します。
その2つのバグは同じ問題に起因している。
バグの最初の現れ方を考えてみましょう。ここでは、コマンドが行頭の空白に1つの単語が含まれていることを示します。実際に起こることは次の通りです。M-x count-words-example コマンドがポイントを領域の先頭に移動させます。while はポイントの値が end
の値より小さいかどうかを判定しますが、実際には小さいです。その結果、正規表現検索が最初の単語を探して見つけ出します。検索は単語の後にポイントを残します。count には1が設定されます。while
のループが繰り返されますが、今度はポイントの値が end
の値より大きいため、ループが終了し、関数は領域内の単語数が1であるというメッセージを表示します。要するに、正規表現検索は、単語がマークされた領域の外にあるにもかかわらず、それを探し出して見つけるのです。
このバグの2つ目の現象では、バッファ末尾の領域が空白になっています。Emacsは ‘Search
failed’ と記述します。実際には、while ループ内の真偽判定が
true となるため、検索式が実行されます。しかし、バッファ内にこれ以上の単語が存在しないため、検索は失敗します。
このバグのどちらの挙動においても、検索範囲が領域外まで広がるか、あるいは広げようとします。
解決策は、探索範囲をその領域に限定することです――これはかなり単純な操作ですが、ご想像の通り、それほど単純ではありません。
これまで見てきたように、re-search-forward
関数は第1引数として検索パターンを受け取ります。しかし、この必須の第1引数に加えて、3つのオプション引数も指定可能です。オプションの第2引数は検索範囲を限定します。オプションの第3引数が
t の場合、検索に失敗した際にエラーを通知するのではなく、関数は nil を返します。オプションの第4引数は繰り返し回数です。(Emacs では、関数名である C-h f と入力し、続けて RET
と入力することで、その関数のドキュメントを確認できます。)
count-words-example の定義では、領域の終端の値が変数 end に保持され、これが関数の引数として渡されます。したがって、正規表現の検索式に引数として end を追加できます:
(re-search-forward "\\w+\\W*" end)
ただし、count-words-example
の定義にこの変更のみを加え、新しいバージョンの定義を空白文字列でテストすると、‘Search
failed’ というエラーメッセージが表示されます。
次のようなことが起こります。検索範囲がリージョンに限定され、そのリージョン内に単語構成文字が存在しないため、予想通り失敗します。失敗したため、エラーメッセージが返されます。しかし、この場合にエラーメッセージを受け取りたいわけではなく、「リージョンに単語が存在しません」というメッセージを受け取りたいのです。
この問題の解決策は、re-search-forward に第3引数として t を渡すことです。これにより、検索に失敗した際にエラーを通知するのではなく、関数が nil を返すようになります。
ただし、この変更を加えて試してみると、「Counting words in region ...」というメッセージが表示され……C-g (keyboard-quit)
と入力するまで、そのメッセージが表示され続けます。
次のようなことが起こります。検索は前述の通り領域内に限定されますが、予想通り領域内に単語構成文字が存在しないため、失敗します。その結果、re-search-forward 式は nil
を返します。それ以外の処理は一切行いません。特に、検索対象が見つかった際に副次的に行うポイントの移動は行いません。re-search-forward 式が nil
を返した後、while
ループ内の次の式が評価されます。この式はカウントをインクリメントします。その後、ループが繰り返されます。re-search-forward 式がポイントを移動させなかったため、ポイントの値は依然として end
の値よりも小さく、真偽判定は true となります。… そしてサイクルが繰り返されます …
count-words-example の定義には、検索に失敗した場合に while ループの真偽判定が false
となるよう、さらに別の修正を加える必要があります。言い換えれば、単語数変数をインクリメントする前に、真偽判定において満たされなければならない条件が2つあります。すなわち、point
が依然として領域内にあり、かつ検索式がカウント対象の単語を見つけていることです。
第1条件と第2条件がともに成立する必要があるため、領域判定式と検索式という2つの式をand特殊形式で結合し、次のようにtrue-or-false-testとしてwhileループ内に埋め込むことができます。
(and (< (point) end) (re-search-forward "\\w+\\W*" end t))
re-search-forward 式は、検索に成功した場合に t
を返し、副作用としてポイントを移動させます。その結果、単語が見つかるたびに、ポイントはリージョン内を移動します。検索式が別の単語を見つけられなかった場合、あるいはポイントがリージョンの末尾に達した場合、真偽値判定は
false となり、while ループが終了し、count-words-example 関数がいずれかのメッセージを表示します。
これらの最終的な変更を加えたことで、count-words-exampleはバグなく動作するようになりました(少なくとも、私が見つけた限りでは!)。実際の表示は以下の通りです:
;;; Final version: while
(defun count-words-example (beginning end)
"Print number of words in the region."
(interactive "r")
(message "Counting words in region ... ")
;;; 1. Set up appropriate conditions.
(save-excursion
(let ((count 0))
(goto-char beginning)
;;; 2. Run the while loop. (while (and (< (point) end) (re-search-forward "\\w+\\W*" end t)) (setq count (1+ count)))
;;; 3. Send a message to the user.
(cond ((zerop count)
(message
"The region does NOT have any words."))
((= 1 count)
(message
"The region has 1 word."))
(t
(message
"The region has %d words." count))))))