Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13369] ファイルの概要

このコミットは、Go言語のEmacsモード (go-mode.el) において、replace-string および replace-regexp 関数の使用方法を、XEmacsとの互換性を高めるように変更するものです。具体的には、これらの関数を直接使用する代わりに、Emacs Lispプログラム内で推奨される search-forward/re-search-forwardreplace-match の組み合わせに置き換えることで、より堅牢で互換性の高いコードを実現しています。

コミット

commit cbcf358d296d86e15b842bb3b70af2deb36b8edc
Author: Mats Lidell <mats.lidell@cag.se>
Date:   Thu Jun 21 13:01:54 2012 -0400

    misc/emacs: Replace replace-{string|regexp} for XEmacs compatible code
    
    Use code to be used in lisp programs as suggested in the doc strings for
    replace-{string|regexp}. Bonus: This code works for XEmacs.
    
    R=golang-dev, sameer, jmeurin
    CC=golang-dev
    https://golang.org/cl/6296073

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cbcf358d296d86e15b842bb3b70af2deb36b8edc

元コミット内容

misc/emacs: Replace replace-{string|regexp} for XEmacs compatible code

このコミットは、replace-string および replace-regexp 関数をXEmacs互換のコードに置き換えるものです。 replace-{string|regexp} のドキュメント文字列で推奨されているLispプログラムで使用されるコードを使用します。これにより、XEmacsでも動作するようになります。

変更の背景

この変更の背景には、Emacs Lispの関数 replace-string および replace-regexp の挙動が、異なるEmacsディストリビューション(特にGNU EmacsとXEmacs)間で微妙に異なる、あるいは特定のバージョンで非推奨となる可能性があったことが挙げられます。コミットメッセージには「Bonus: This code works for XEmacs.」と明記されており、XEmacsでの互換性問題が主な動機であったことが示唆されます。

Emacs Lispでは、文字列や正規表現の置換を行う際に、対話的な使用とプログラム内での使用で推奨されるパターンが異なる場合があります。replace-stringreplace-regexp は、ユーザーが対話的に置換を行う際に便利ですが、Lispプログラム内で特定のロジックに基づいて置換を行う場合には、より細かく制御できる search-forward/re-search-forwardreplace-match の組み合わせが推奨されることがあります。これは、置換対象の文字列が見つからなかった場合の挙動や、正規表現のグループ参照の扱いなどにおいて、より予測可能で堅牢なコードを書くためです。

このコミットは、go-mode.el がEmacs環境で広く利用されることを考慮し、特定のEmacsディストリビューションに依存しない、より汎用的なコードベースを目指した改善の一環と言えます。

前提知識の解説

このコミットを理解するためには、以下のEmacs Lispの概念と関数に関する知識が必要です。

  1. Emacs Lisp (Elisp): Emacsエディタの拡張言語であり、Emacsのほとんどの機能がElispで書かれています。ユーザーはElispを使ってEmacsをカスタマイズしたり、新しい機能を追加したりできます。

  2. バッファ (Buffer): Emacsで開かれているテキストの単位です。ファイルの内容はバッファに読み込まれ、編集が行われます。

  3. ポイント (Point): バッファ内の現在のカーソル位置を指します。

  4. リージョン (Region): バッファ内でマークされたテキストの範囲です。

  5. point-minpoint-max: それぞれバッファの先頭と末尾のポイント位置を返します。これらはバッファ全体を対象とする操作でよく使われます。

  6. with-current-buffer: 指定されたバッファを一時的に現在のバッファとして設定し、その中でLisp式を実行するためのマクロです。これにより、特定のバッファに対して操作を行うことができます。

  7. replace-string: バッファまたはリージョン内の指定された文字列を別の文字列に置換する関数です。対話的な使用に適しています。

  8. replace-regexp: バッファまたはリージョン内の正規表現にマッチする文字列を別の文字列に置換する関数です。これも対話的な使用に適しています。

  9. goto-char: ポイントを指定された位置に移動させる関数です。

  10. beginning-of-buffer: ポイントをバッファの先頭に移動させる関数です。goto-char (point-min) と同等です。

  11. search-forward: ポイントから前方に指定された文字列を検索する関数です。見つかった場合、ポイントはマッチの直後に移動し、t を返します。見つからなかった場合、nil を返します。

  12. re-search-forward: ポイントから前方に指定された正規表現を検索する関数です。search-forward と同様に動作しますが、正規表現を使用します。正規表現にグループが含まれる場合、match-stringreplace-match でそれらのグループを参照できます。

  13. replace-match: 直前の search-forward または re-search-forward で見つかったマッチを置換する関数です。正規表現のグループを参照して置換文字列を構築することも可能です。この関数は、プログラム内で検索と置換を細かく制御する際に非常に有用です。

  14. if: 条件分岐を行うための特殊形式です。

  15. file-name-nondirectory: ファイルパスからディレクトリ部分を除いたファイル名のみを返す関数です。

これらの関数と概念を理解することで、コミットで行われた変更の意図と技術的な詳細を深く把握することができます。特に、replace-string/replace-regexpsearch-forward/re-search-forward + replace-match の使い分けは、Emacs Lispプログラミングにおける重要なパターンです。

技術的詳細

このコミットの技術的詳細な変更点は、go-mode.el 内の2つの関数、gofmt-apply-patchgofmt-process-errors における文字列置換ロジックの修正にあります。

gofmt-apply-patch 関数における変更

この関数は、gofmt コマンドによって生成されたパッチを現在のバッファに適用する役割を担っています。変更前は、パッチファイルのヘッダーにある一時ファイルパス (^--- /tmp/gofmt[0-9]*) を実際のファイル名に置換するために replace-regexp を使用していました。

変更前:

(replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- " filename)
                  nil (point-min) (point-max))

このコードは、バッファ全体 (point-min から point-max) に対して正規表現置換を実行します。nil は対話的な確認をしないことを意味します。

変更後:

(goto-char (point-min))
(if (re-search-forward "^--- \\(/tmp/gofmt[0-9]*\\)" nil t)
  (replace-match filename nil nil nil 1))

変更後は、以下の手順で置換を行います。

  1. (goto-char (point-min)): まず、バッファの先頭にポイントを移動させます。これは、検索をバッファの最初から開始するためです。
  2. (re-search-forward "^--- \\(/tmp/gofmt[0-9]*\\)" nil t): 正規表現 ^--- \(/tmp/gofmt[0-9]*\) を前方検索します。
    • ^: 行の先頭にマッチします。
    • --- : リテラル文字列 --- にマッチします。
    • \(/tmp/gofmt[0-9]*\): これが正規表現のグループ1です。/tmp/gofmt に続く数字の列にマッチします。この部分が replace-match で参照されます。
    • nil: 検索の終了位置を指定しません(バッファの最後まで検索します)。
    • t: エラーが発生した場合にエラーを発生させず、nil を返します。
  3. (if ...): re-search-forward がマッチを見つけた場合(t を返す場合)に、replace-match が実行されます。
  4. (replace-match filename nil nil nil 1): 直前の re-search-forward で見つかったマッチを置換します。
    • filename: 置換後の文字列です。これは、gofmt-apply-patch 関数に渡された実際のファイル名です。
    • nil: FIXME: no-undo 引数。通常は nil
    • nil: FIXME: literal 引数。通常は nil
    • nil: FIXME: string 引数。通常は nil
    • 1: これは非常に重要で、正規表現のグループ1を置換対象とすることを意味します。つまり、^--- /tmp/gofmt[0-9]* 全体ではなく、正規表現の括弧で囲まれた部分 (/tmp/gofmt[0-9]*) のみが filename で置換されます。これにより、--- の部分はそのまま残り、--- actual-filename の形式が維持されます。

gofmt-process-errors 関数における変更

この関数は、gofmt の標準エラー出力(エラーメッセージ)を処理し、Emacsのコンパイルモードで理解できる形式に変換します。ここでも、一時的な入力タグ (gofmt-stdin-tag) を実際のファイル名に置換するために replace-string を使用していました。

変更前:

(beginning-of-buffer)
(insert "gofmt errors:\n")
(replace-string gofmt-stdin-tag (file-name-nondirectory filename) nil (point-min) (point-max))

変更後:

(goto-char (point-min))
(insert "gofmt errors:\n")
(if (search-forward gofmt-stdin-tag nil t)
  (replace-match (file-name-nondirectory filename) nil t))

変更後は、以下の手順で置換を行います。

  1. (goto-char (point-min)): バッファの先頭にポイントを移動させます。
  2. (insert "gofmt errors:\n"): エラーバッファの先頭に "gofmt errors:\n" というヘッダーを挿入します。
  3. (search-forward gofmt-stdin-tag nil t): gofmt-stdin-tag というリテラル文字列を前方検索します。
    • nil: 検索の終了位置を指定しません。
    • t: エラーが発生した場合にエラーを発生させず、nil を返します。
  4. (if ...): search-forward がマッチを見つけた場合(t を返す場合)に、replace-match が実行されます。
  5. (replace-match (file-name-nondirectory filename) nil t): 直前の search-forward で見つかったマッチを置換します。
    • (file-name-nondirectory filename): 置換後の文字列です。これは、元のファイル名からディレクトリ部分を除いたものです。
    • nil: FIXME: no-undo 引数。
    • t: FIXME: literal 引数。これは、置換文字列をリテラルとして扱うことを意味します。

これらの変更は、replace-stringreplace-regexp がバッファ全体を対象とするのに対し、search-forward/re-search-forwardreplace-match の組み合わせは、最初にマッチした箇所のみを置換するという点で異なります。このコミットでは、置換対象がバッファ内で一度しか出現しないことが前提とされているため、このアプローチが採用されています。これにより、より制御された、そしてXEmacsを含む様々なEmacs環境での互換性が向上した置換処理が実現されています。

コアとなるコードの変更箇所

misc/emacs/go-mode.el ファイルの以下の部分が変更されました。

--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -819,8 +819,9 @@ Replace the current buffer on success; display errors on failure.\"
   (require 'diff-mode)
   ;; apply all the patch hunks
   (with-current-buffer patchbuf
-    (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- " filename)
-                      nil (point-min) (point-max))\n
+    (goto-char (point-min))
+    (if (re-search-forward "^--- \\(/tmp/gofmt[0-9]*\\)" nil t)
+      (replace-match filename nil nil nil 1))\n
     (condition-case nil
         (while t
           (diff-hunk-next)\n
@@ -831,9 +832,10 @@ Replace the current buffer on success; display errors on failure.\"
 (defun gofmt-process-errors (filename errbuf)\n
   ;; Convert the gofmt stderr to something understood by the compilation mode.\n
   (with-current-buffer errbuf
-    (beginning-of-buffer)\n
+    (goto-char (point-min))\n
     (insert "gofmt errors:\\n")
-    (replace-string gofmt-stdin-tag (file-name-nondirectory filename) nil (point-min) (point-max))\n
+    (if (search-forward gofmt-stdin-tag nil t)
+      (replace-match (file-name-nondirectory filename) nil t))\n
     (display-buffer errbuf)\n
     (compilation-mode)))

コアとなるコードの解説

gofmt-apply-patch 関数内の変更

  • 変更前: (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- " filename) nil (point-min) (point-max))
    • この行は、バッファ全体 ((point-min) から (point-max)) に対して、正規表現 ^--- /tmp/gofmt[0-9]* にマッチする文字列を --- filename を結合した文字列に置換していました。nil は対話的な確認なしで置換を実行することを意味します。
  • 変更後:
    (goto-char (point-min))
    (if (re-search-forward "^--- \\(/tmp/gofmt[0-9]*\\)" nil t)
      (replace-match filename nil nil nil 1))
    
    • (goto-char (point-min)): バッファの先頭にポイントを移動させます。これは、re-search-forward が現在のポイントから検索を開始するためです。
    • (re-search-forward "^--- \\(/tmp/gofmt[0-9]*\\)" nil t): 正規表現 ^--- \(/tmp/gofmt[0-9]*\) を前方検索します。ここで、\(/tmp/gofmt[0-9]*\) の部分が正規表現のグループ1としてキャプチャされます。
    • (if ...): re-search-forward がマッチを見つけた場合にのみ、続く replace-match が実行されます。
    • (replace-match filename nil nil nil 1): 直前の検索でマッチした文字列を置換します。最後の 1 は、正規表現のグループ1(つまり、/tmp/gofmt[0-9]* の部分)のみを filename の値で置換することを指示しています。これにより、--- の部分はそのまま残り、--- <実際のファイル名> という形式が維持されます。

gofmt-process-errors 関数内の変更

  • 変更前: (replace-string gofmt-stdin-tag (file-name-nondirectory filename) nil (point-min) (point-max))
    • この行は、バッファ全体に対して、gofmt-stdin-tag という文字列を、filename からディレクトリ部分を除いたファイル名に置換していました。
  • 変更後:
    (goto-char (point-min))
    (insert "gofmt errors:\\n")
    (if (search-forward gofmt-stdin-tag nil t)
      (replace-match (file-name-nondirectory filename) nil t))
    
    • (goto-char (point-min)): バッファの先頭にポイントを移動させます。
    • (insert "gofmt errors:\\n"): エラーメッセージの前にヘッダーを挿入します。
    • (search-forward gofmt-stdin-tag nil t): gofmt-stdin-tag というリテラル文字列を前方検索します。
    • (if ...): search-forward がマッチを見つけた場合にのみ、続く replace-match が実行されます。
    • (replace-match (file-name-nondirectory filename) nil t): 直前の検索でマッチした文字列を、filename からディレクトリ部分を除いたファイル名で置換します。最後の t は、置換文字列をリテラルとして扱うことを意味します。

これらの変更は、replace-stringreplace-regexp がバッファ全体を対象とする一括置換であるのに対し、search-forward/re-search-forwardreplace-match の組み合わせは、検索して最初に見つかった箇所を置換するという、より制御されたアプローチを採用しています。このアプローチは、特にXEmacsのような異なるEmacsディストリビューション間での互換性を高めるために推奨されるプラクティスです。

関連リンク

参考にした情報源リンク