武蔵の日記 RSSフィード

2005 | 08 | 09 | 10 | 11 | 12 |
2006 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 11 |
2012 | 03 | 04 | 06 | 07 | 08 | 09 | 11 | 12 |
2013 | 01 |

January 18(Sun), 2009

Emacs Lispのイディオム

| 17:45 |  Emacs Lispのイディオムを含むブックマーク  Emacs Lispのイディオムのブックマークコメント

Emacs Lisp Idiomsを勉強をかねて訳しました。興味がある方はご覧ください。一部見出しレベルが異なりますが、はてなだとこれが限界みたいっす。。。

Emacs lisp Idioms (by Xah Lee, 2008-06)

このページではテキスト処理に関する基本的な Emacs-lisp プログラミングイディオムを集めています。

このページは二部構成で Interactive セクションと、Batchセクションの二つのセクションに分かれています。Interactiveセクションでは実際にファイルを編集しているときに呼び出すコマンドを書く際のイディオムを扱います。例えば、カーソル位置の単語をGoogleで検索する、カレントリージョンの特定の単語を置換する、XMLテンプレートを挿入する、既存のプログラミングプロジェクトの関数名をリネームするなどです。Batchセクションでは、unixシェルツールやPerlを用いて行うようなバッチスタイルのテキスト処理を扱います。例えば、与えられたファイルやディレクトリの中を検索して置換する、複数のファイルでXML正当性検査を行うディレクトリにあるtagファイルを探し、レポートを作成するです。

このページで紹介されているイディオムは、ビギナーの elisp プログラマーにとって適切な、基本的なタスクしか含まれておらず、頻繁に必要となるケースしか扱っていません。

このページに書いてあることを理解するためには、最初にカーソル位置を取得する、カーソルを移動する、テキスト検索をする、テキストを挿入・削除するといった基本的なemacsの機能を知っておく必要があります。

Interactive関数イディオム

これがユーザー定義関数の典型的なひな形になります:

(defun my-function ()
  "... do this returns that ..."
  (interactive)
  (let (localVar1 localVar2 ...)
    (setq localVar1 ...)
    (setq localVar2 ...)
    ...
    ;; do something ...
  )
)
テキストを取得する
与えられた初期位置・終了位置のテキストを取得する
; get the string from buffer
(setq myStr (buffer-substring myStartPos myEndPos))
(setq myStr (buffer-substring-no-properties myStartPos myEndPos))

Emacs文字列シンタックス色づけ、active button、ハイパーテキストなどのためにプロパティーを持つことができます。buffer-substring-no-properties関数はこれらのプロパティーを持たないプレーンな文字列を返します。しかし、文字列引数に取る多くの関数プロパティー付きの文字列引数に取ることもできます。

カーソル位置の単語・カーソル行を取得する

カーソル位置の単語・行・文・url・ファイルネームなどの取得は次のようになります:

;; grab a thing at point. The "thing" is a semantic unit. It can be a
;; word, symbol, line, sentence, filename, url and others.
;; grab the current word
(setq myStr (thing-at-point 'word))
;; grab the current word with hyphens or underscore
(setq myStr (thing-at-point 'symbol))
;; grab the current line
(setq myStr (thing-at-point 'line))
;; grab the start and end positions of a word (or any other thing)
(setq myBoundaries (bounds-of-thing-at-point 'word))

取得したいものが「シンボル」の場合、「シンボル」がダッシュ(-)やアンダーバー(_)という文字を伴ったアルファベットの列を意味することに気をつけてください。例えば、phpリファレンス引きコマンドを作成していて、カーソルが"print_r($y);"のpの位置にあった場合、取得したいのはprintではなくprint_rです。「シンボル」の正確な意味はそのモードのシンタックス・テーブルに依存します。

以下に示すのが、アクティブなリージョンがない場合に「シンボル」を取得するphpリファレンス引きコマンドの例です:

(defun php-lookup ()
  "Look up current word in PHP ref site in a browser.\n
  If a region is active (a phrase), lookup that phrase."
  (interactive)
  (let (myword myurl)
    (setq myword
          (if (and transient-mark-mode mark-active)
              (buffer-substring-no-properties (region-beginning) (region-end))
            (thing-at-point 'symbol)))
    (setq myurl
          (concat "http://us.php.net/" myword))
    (browse-url myurl)))
マッチしたペアーの間のテキストを取得する

「<>」、「()」、「""」などで囲まれた範囲を取得します。

肝になるのはskip-chars-backwardとskip-chars-forward関数です。以下に示す例では、p1はカーソルの左側の「"」に設定され、p2はカーソル右側の「"」に設定されます。

(defun select-inside-quotes ()
  "Select text between double straight quotes on each side of cursor."
  (interactive)
  (let (p1 p2)
    (skip-chars-backward "^\"")
    (setq p1 (point))
    (skip-chars-forward "^\"")
    (setq p2 (point))

    (goto-char p1)
    (push-mark p2)
    (setq mark-active t)
  )
)

もし「()」で囲まれたテキストを取得したのであれば、「"^\""」を「"^("」と「"^)"」に変更してください。「<>」の場合も同様です。このコードはネストしたペアーを考慮していないことに気をつけてください。

リージョンを処理する

ここでは現在のリージョンのテキストを処理するためのイディオムを紹介します。

定義する関数に二つのパラメータ(お約束としては「start」・「end」とします)を持たせます。そうして「(interactive "r")」を用いると、現在のリージョンの開始と終了位置が二つのパラメータにセットされます。例えばこのようになります:

(defun remove-hard-wrap-region (start end)
  "Replace newline chars in region by single spaces."
  (interactive "r")
  (let ((fill-column 90002000))
    (fill-region start end)))
アクティブなリージョン、あるいはカーソル上の単語を処理する

現在のリージョン、もしそれがなければ、カーソル上の単語を処理するイディオムは、以下のようになります:

(defun down-case-word-or-region ()
  "Lower case the current word or text selection."
(interactive)
(let (pos1 pos2)
  (if (and transient-mark-mode mark-active)
      (setq pos1 (region-beginning)
            pos2 (region-end))
    (setq pos1 (car (bounds-of-thing-at-point 'word))
          pos2 (cdr (bounds-of-thing-at-point 'word))))
  (downcase-region pos1 pos2)
  )
)

ユーザーからの入力を促す

ユーザーからの入力を引数とする

定義する関数引数としてユーザーに入力を促すためのイディオムは次のようになります:

(defun query-friends-phone (name)
  "..."
  (interactive "sEnter friend's name: ")
  (message "Name: %s" name)
)

「(interactive "sEnter friend's name:")」が行っていることは、ユーザーに情報を入力するように促していることで、それは文字列として受け取られ、定義する関数パラメータの値となります。

「(interactive)」は別な種類の入力も受け付けます。次にあげるのは「(interactive)」を用いたいくつかの基本的な例です:

  • 「(interactive)」は定義する関数を interactive に利用できるようにする。つまり execute-extended-command(M-x) を用いて使えるようにする。
  • 「(interactive "s")」はユーザーに入力を促す。入力された情報は文字列して受け取られ、定義する関数引数となる。
  • 「(interactive "n")」はユーザーに入力を促す。入力された情報は数字として受け取られ、定義する関数引数となる。
ユーザーに問い合わせを行う

「(interactive ...)」は定義する関数パラメータに値をセットするには役立ちます。しかし、時には関数の途中でユーザーに入力を促す必要があります。例えば、「このファイルを編集しますか?」です。この場合、「y-or-n-p」関数を用います。このようになります:

(if (y-or-n-p "Do it?")
    (progn
      ;; code to do something here
    )
  (progn
    ;; code if user answered no.
  )
)

「y-or-n-p」はユーザーに y か n の入力を求めます。また「yes-and-no-p」を用いて、 yes あるいは no のどちらかを答えてもらうこともできます。この「yes-and-no-p」は例えばファイルを削除するときの確認などで用います。

ユーザーからの入力を得るためのより一般的な方法が必要ならば、「read-from-minibuffer」があります。この関数は例えばキーワード補完や入力ヒストリーのような機能を用いたいときに役立ちます。

文字列を処理する

Perlのようなスクリプト言語では、文字列を処理する命令が数多くあります。elispでは、少数しかありません。というのも、elispはずっと強力なバッファーデータタイプを提供し、バッファー上のテキストを処理する関数を文字通り何千も持つからです。文字列やテキストがある時に、文字列の切り出しや、文字数のカウントをし、それを一時的なバッファーに挿入する以上のことを行う必要があります。次に例を載せます:

;; suppose mystr is a var whose value is a string
(setq mystr "some string here you need to process")
(setq mystrNew
      (with-temp-buffer
        (insert mystr)
        ;; manipulate the string here
        (buffer-string) ; get result
        ))

テキストの検索と置換

文字列の検索と置換はテキスト処理を特徴付けるものです。これらを行う方法を以下に示します:

;; idiom for string replacement in current buffer;
;; use search-forward-regexp if you need regexp
  (goto-char (point-min))
  (while (search-forward "myStr1" nil t) (replace-match "myReplaceStr1"))

  (goto-char (point-min))
  (while (search-forward "myStr2" nil t) (replace-match "myReplaceStr2"))
  ;; repeat for other string pairs

dired でマークされたファイルに定義した関数を適応する

dired でマークしたファイルに定義した関数を用いるには、「dired-get-marked-files」をこのようにして用います:

;; idiom for processing a list of files in dired's marked files

;; suppose myProcessFile is your function that takes a file path
;; and do some processing on the file

(defun dired-myProcessFile ()
  "apply myProcessFile function to marked files in dired."
  (interactive)
  (require 'dired)
  (mapc 'myProcessFile (dired-get-marked-files))
)

Batch スタイルテキスト処理のイディオム

ファイルを開き、処理し、保存、もしくは閉じる

ファイルを開き、処理し、保存、もしくは閉じる方法は次のようになります。

;; open a file, process it, save, close it
(defun my-process-file (fpath)
  "process the file at fullpath fpath ..."
  (let (mybuffer)
    (setq mybuffer (find-file fpath))
    (goto-char (point-min)) ;; in case buffer already open
    ;; do something
    (save-buffer)
    (kill-buffer mybuffer)))

何百ものファイルを処理するときは、emacsに元に戻るための情報や fontification を保持する必要はありません。この方が効率的になります:

(defun my-process-file (fpath)
  "process the file at fullpath fpath ..."
  (let ()
    ;; create temp buffer without undo record. first space is necessary
    (set-buffer (get-buffer-create " myTemp"))
    (insert-file-contents fpath nil nil nil t)
    ;; process it ...
    (kill-buffer " myTemp")))

シェルコマンドを呼び出す

;; idiom for calling a shell command
(shell-command "cp /somepath/myfile.txt  /somepath")

;; idiom for calling a shell command and get its output
(shell-command-to-string "ls")

shell-command と shell-command-to-string のどちらも以降の処理を行う前にシェルプロセスが終了するのを待ちます。終了するのを待たない場合は、 start-process や start-process-shell-command を用いてください。

ディレクトリを走査する

次の例では、 my-process-file はフルパスつきのファイル名を入力として受け取ります。「find-lisp-find-files」関数は、正規表現で指定されたファイルのフルパスのリストを生成します。「mapc」はこの手続きをリストの要素全てに適応します。*1

;; idiom for traversing a directory
(require 'find-lisp)
(mapc 'my-process-file (find-lisp-find-files "~/web/emacs/" "\\.html$"))

バッチモードでelispを走らせる

「--scriptオプションを用いれば、OSコマンドラインインターフェースシェル)で elisp プログラムを走らせることができます。例えば、

emacs --script process_log.el

Emacsは他にいくつかオプションを持ち、elisp スクリプトをどのように走らせるかを制御できます。主要なオプションは次のようになります:

フルオプション *意味
--no-site-file Do not load the site wide "site-start.el"
--no-init-file Do not load your init files "~/.emacs" or "default.el".
--batch Run emacs in batch mode, use it together with "--load" to specify a lisp file.
--script <file path> Run emacs like "--batch" with "--load" set to "<file path>".
--load="<elisp file path>" Execute the elisp file at "<elisp file path>".
--user=<user name> Load user <user name>'s emacs init file (the ".emacs").

バッチモードで走らせる elisp スクリプトを書いているときは、それが独立しているようにしてください。つまり、作成するスクリプトEmacs初期化ファイル上の関数を呼び出していないこと、呼び出す場合はそのスクリプトが必要とするライブラリーを「require」や「load」を用いて読み込むこと、 PerlPython スクリプトのようにスクリプト上で必要となるロードパスの設定を行っていること(つまり「(add-to-list 'load-path <library path>)」)に注意してください。

適切にスクリプトを作成したら、「emacs --script <elisp file path>」を実行してください。

もし作成した elisp プログラムEmacs の起動ファイル(.emacs)で定義された関数を必要とする場合は、「(load <emacs init file path>)」を用いて明示的にその関数をロードすべきです。あるいは、その関数を読み込む起動オプションを追加すべきです。例えばこのように: 「--user=xah」(必要となる関数スクリプト内で定義するのが最善ではありますが)。

使っている EmacsMac 上の Carbon Emacs や Aquamacs であれば、 Emacs プログラムへのパスは単に「emacs」ではなく、このようになります: 「/Applications/Emacs.app/Contents/MacOS/Emacs」。次に示すのはこのような場合の起動コマンドの例です:

/Applications/Emacs.app/Contents/MacOS/Emacs --no-site-file --batch --load=process_log.el

*1Gauche での apply として用いているようだ。