lispを(なるべく)書かないコマンド作成講座(菅原泰樹)
「Emacsのトラノマキ」連載第三回「lispを(なるべく)書かないコマンド作成講座」
* はじめに
今回は今までと趣向を変えて自分用のコマンドの作り方を紹介しようと思います.elispをごりごり書かないとコマンドを作れないと思っている人も多いかと思いますが,そんなことはないです.書かなくてもなんとかなるもんです.そんな訳でなるべくelispを書かずにコマンドを定義する方法を紹介していきます.
elispを書かずにコマンドを定義するには以下の方法があります.
- キーボードマクロを活用する
- 外部プログラムを使う
- Emacsに手を入れる
Emacsに手を入れるのは最後の手段なので,キーボードマクロと外部プログラムを使う方法を見ていきましょう.
* キーボードマクロ
Emacsにはキーボードマクロというものがあります.Excelなどのキーボードマクロと大体同じようなものです.すでに使っている人も多いかもしれません.筆者のまわりの人の使い方を見ていると一発取りで,一個だけキーボードマクロを登録して使っている人が多いのですが,実は登録したマクロを編集したり名前を付けたりすることができます.さらに.emacsに作ったキーボードマクロを保存なんてこともできます.
** マクロを使ってみる
さっそく使ってみましょう.マクロの登録は"<F3>"または"C-x ("で開始,"<F4>"はたは "C-x )"で終了です.
まず"<F3>"を押してください.ミニバッファに"Defining kbd macro..."と表示され,モードラインに"Def"と表示されているはずです.この状態で"C-e"としてから"hoge"と入力して"<F4>"を押します.続けて書くと"<F3> C-e hoge <F4>"ですね.
次に登録したマクロを実行してみます."<F4>"または"C-x e"を押してください.行の最後に"hoge"という文字が挿入されます.このように文字の入力だけではなく,キーボードからの入力全てをマクロとして登録することができます.
** 分割してマクロを登録する
マクロは便利なのですが,登録時に気を付けないといけないことがあります.それは「"C-g"を押すとマクロの登録がキャンセルされる」ということです.大きなマクロを登録していて,不意に"C-g"を押してしまうと想像を絶する悲しみに包まれます.
そんな悲しみに包まれたくない場合,登録したマクロに新しい入力を追記する機能を活用します."C-u C-u <F3>"と入力してください.ミニバッファに"Appending to kbd macro..."と表示され,以前登録したマクロの続きを登録できるようになります.
ここで"..hoge"と入力し"<F4>"を押すとマクロに今回の入力が追記されます.結果として"C-e hoge..hoge"という入力のマクロが作成されます.
追記を用して分割して登録するようにすれば,"C-g"を押してしまっても少しは安心できると思います.
** マクロに名前を付ける,保存する
複数のマクロを使いたい場合はマクロに名前を付けてあげます.名前を付けるには"C-x C-k n"と入力します.すると以下のように質問されるので"hoge"等の適当な名前を付けてあげます.
================
Name for last kbd macro:
================
あとは"M-x hoge"のようにすればマクロを実行できます.
名前を付けたマクロを保存するには.emacsを開いて"M-x insert-kbd-macro"とします.すると以下のように質問されるので,保存したいマクロの名前を入力します.
================
Name for last kbd macro:
================
入力すると以下のような設定が.emacsに追加されるのでそのまま保存します.
================
(fset 'hoge
(lambda (&optional arg) "Keyboard macro." (interactive "p")
(kmacro-exec-ring-item (quote ("^Ehoge..hoge" 0 "%d")) arg)))
================
これで次にEmacsを起動したときも"M-x hoge"でマクロを実行できます.
** マクロの編集
登録したマクロを後から編集したい場合は,"C-x C-k e"と入力します.すると以下のように質問されます.
================
Keyboard macro to edit (C-x e, M-x, C-h l, or keys):
================
直前に登録したマクロを編集したい場合は"<RET>"を入力します.名前を付けたマクロを編集したい場合は"M-x"を入力してから,マクロ名を入力します.
マクロの編集モードになると以下のような画面になります.
================
;; Keyboard Macro Editor. Press C-c C-c to finish; press C-x k RET to cancel.
;; Original keys: C-e hoge..hoge
Command: hoge
Key: none
Counter: 0
Format: "%d"
Macro:
C-e ;; move-end-of-line
hoge..hoge ;; self-insert-command * 10
================
ここで"Macro:"以下の部分をそれっぽい文字列で編集することでマクロの編集が行なえます.編集が終わったら"C-c C-c"と入力して保存しましょう.キャンセルするときは"C-x k"でバッファを消してしまえば大丈夫です.
たとえば以下のように修正すると,"C-e C-b hoge..hoge"というマクロになります.
================
C-e ;; move-end-of-line
C-b
hoge..hoge ;; self-insert-command * 10
================
.emacsにマクロを保存している場合は"insert-kbd-macro"をやりなおして設定を書き換えるのを忘れないようにしてくださいね.
** 複雑なマクロ
最後にちょっと複雑なマクロを作ってみましょう.こんなマクロを作ってみます.
いまいる行に書いてあるファイル名を開いて,そのファイルの文字コードをshift-jisに変更する.
変更した後,元のバッファに戻って一行したに移動する.
さて,前準備としてファイル名の一覧を作っておきましょう.*scratch*バッファに以下のようなファイル名一覧を書いておきます.
================
~/tmp/foo.txt
~/tmp/bar.txt
~/tmp/baz.txt
================
次に"~/foo.txt"の先頭にカーソルを移動させておきます.
そうしたらマクロ登録の開始です.以下のマクロを登録します.
================
C-x r SPC a
C-SPC C-e M-w
C-x C-f C-y RET
C-x RET f shift_jis RET
C-x r j a
C-n C-a
================
これだけだと何のことだかさっぱりわからないですね.それぞれの意味を説明して,マクロの紹介を終わります.
- "C-x r SPC a" ::
今のバッファと位置を"a"という名前のレジスタに保存
- "C-SPC C-e M-w" ::
今の行にあるファイル名をコピー
- "C-x C-f C-y RET" ::
コピーしたファイル名のファイルを開く
- "C-x RET f shift_jis RET" ::
ファイルの文字コードをshift_jisに変更
- "C-x r j a" ::
"a"という名前のレジスタに保存したバッファと位置に移動.つまりファイル名一覧に戻る.
- "C-n C-a" ::
ファイル名一覧の次の行の先頭に移動
** コラム: レジスタ
複雑なマクロの紹介でレジスタという言葉が出てきました.このレジスタという機能はあまり日の目をみることがないんですが,マクロを作るときには大活躍するのでここで紹介しておきます.
レジスタとは簡単に言えば「編集中に使える変数」といったところです.aからzまでの名前に位置(Emacsの用語で言うならマーカ)や,文字列を保存することができます.マクロで複雑なことをしようとすると複数の文字列や位置を保存する必要があるので,このレジスタがうってつけなわけです.
レジスタを操作するには操作用コマンドのあとにレジスタ名を入力します.たとえば"C-x r SPC a"はレジスタ"a"に位置を保存,"C-x r SPC z"はレジスタ"z"に位置を保存です.
以下にレジスタ操作コマンドを表にしておきます.
| C-x r SPC | 今のバッファと位置を保存 |
| C-x r j | 保存したバッファと位置に移動 |
| C-x r s | 選択範囲の文字列を保存 |
| C-x r n | 数値0を保存 |
| C-u 3 C-x r n | 数値3を保存 |
| C-x r + | 保存した数値を1増やす |
| C-x r i | 保存した文字列や数値を挿入. |
| | 挿入した文字列の反対側へ移動するには C-x C-x |
* 外部プログラムを実行する
次に,編集中に外部プログラムを実行するコマンドを紹介します.elispが書けなくてもフィルタとして使える外部プログラムを使えば結構いろいろできてしまいます.
** shell-command -- プログラムを実行
"M-x shell-command"とするとプログラムを実行できます."M-!"がショートカットです.."C-u M-x shell-command"とすることでその結果を今のバッファに挿入することもできます.
たとえば"C-u M-! ls"とすると,lsした結果が今のバッファに挿入されます.catならファイルの中身を挿入です.
shell-command は外部プログラムの実行にシェルを介しているのでパイプを使うことも可能です.たとえば"C-u M-! ls | grep hoge"とするとlsをgrepでフィルタした結果がバッファに挿入されます.
最後に"&"を付けるとEmacsが非同期で実行してくれます.たとえば"M-! ls&"とするとlsを非同期で実行します.
** shell-command-on-region -- 範囲を指定してプログラムを実行
"shell-command"はプログラムを実行するだけでしたが,"shell-command-on-region"は選択した範囲を標準入力としてプログラムを実行します.ショートカットは"M-|"です."C-u M-x shell-command-on-region"とすることでプログラムの結果で選択範囲を置き換えます."shell-command-on-region"も"shell-command"と同様に,パイプを使うことができます.
たとえばバッファに以下のような文書が書いてあったとします.
================
foo 1
bar 2
baz 3
foobar 4
================
これらを選択してから"M-| wc"とすると,範囲内の文字数をwcでカウントした結果がミニバッファに以下のように表示されます.
================
4 8 27
================
"C-u M-| grep foo"とすると選択した範囲が以下のようにfooを含むものだけになります.
================
foo 1
foobar 4
================
** compile -- 非同期でのコマンドの実行とエラー行へのジャンプ
みなさん良く使っていると思われる"M-x compile"ですが,実はこのコマンド,コンパイルする以外にも使えます.
このコマンドがやっていることが何かというと
1. 外部プログラムを非同期で実行する
2. 「ファイル名:行番号:内容」の書式があったときにファイルにジャンプする
ということです.この2番目が重要で,この書式で出力されればgrepでもcatでも,どんなプログラムの出力も受けつけてくれます.
ためしてみましょう.まず"M-x lgrep"で適当なgrepの結果を出力します.
================
-*- mode: grep; default-directory: "~/src/emacs/src/" -*-
lgrep -i -n regexp *.[ch] /dev/null
alloc.c:5074: shrink_regexp_cache ();
ccl.c:2349:Each element looks like (REGEXP . CCL-CODE),
ccl.c:2351:When a font whose name matches REGEXP is used for displaying a character,
coding.c:7557: doesn't compile new regexps. */
dired.c:106:extern Lisp_Object Vcompletion_regexp_list;
================
このバッファを"C-x C-w"で"grep.txt"として保存します.保存したら"M-x compile"として以下のように入力します.
================
Compile command: cat grep.txt
================
するとgrepの結果がコンパイルしたときと同じように色付きで表示されます.あとは"M-n", "M-p"を使ったりしてファイルにジャンプすることができます.
結果をファイルに保存しているので必要ない行を手で編集することもできますし,保存しておいた結果を次の日にあらためて見直すといった使いかたもできます.
* 外部プログラムを実行するコマンドを作る
ここからは,ちょっとしたelispを書いて外部プログラムを実行するコマンドを書く方法を紹介していきます.といってもあまり構える必要はありません.覚えるのはユーザの入力の受け取り方,バッファの簡単な操作方法,コマンドの実行方法だけです.
ちなみに,ここで説明しているコマンドの作り方は正しいelispの書き方からは程遠いものです.ちゃんと勉強したい方には,入門として以下がおすすめです.
- Emacsに付いてくる「Emacs Lisp Intro」
- 広瀬雄二さんの「優しい Emacs-Lisp 講座」
http://www.gentei.org/~yuuji/elisp/
** コマンドの作り方
まずはコマンドの作り方からです.Emacsのコマンドは先頭にinteractiveというおまじないが書いてある関数です."M-x my-command-test"で実行できるコマンドは以下のように作ります.
================
(defun my-command-test ()
(interactive)
(message "hoge1")
(insert "hoge2"))
================
ここで"message","insert"という関数を使っています."message"はミニバッファに文字列を出力する関数で,"insert"は今いるバッファ文字列を挿入する関数です.ここで一緒に覚えてしまいましょう.
コマンドの作り方の基本はこれだけです.簡単ですね.
*** ユーザの入力を受けとる
ユーザの入力を受けとるには"read-string"や"read-file-name"を使います."read-string"ではただの文字列を,"read-file-name"ではファイル名を入力できます.
入力された文字列の変換のために,一緒に"expand-file-name"と"format"も覚えてしまいます."expand-file-name"はファイル名の"~"の展開をしてくれます."format"はCなどの"printf"とほぼ同じで文字列の書式付けをします.
これらの使い方は以下のようになります.
================
(defun my-read-test ()
(interactive)
(insert (read-string "String: ") "\n")
(insert (read-file-name "File Name: ") "\n")
(insert (format "|%10s:%-10s|" "left" "right") "\n")
(insert (expand-file-name "~/") "\n"))
================
*** ローカル変数を宣言する
ローカル変数を宣言するには"let"です."my-read-test"を"let"を使って書き換えてみます."expand-file-name"と"format"も入力された文字に対して行なうようにしました.
"let"の最初の引数で変数の一覧を宣言します.ローカル変数の有効範囲は"let"で囲まれている部分です.
================
(defun my-read-test ()
(interactive)
(let (str file)
(setq str (read-string "String: "))
(setq file (read-file-name "File Name: "))
(insert str "\n")
(insert file "\n")
(insert (format "|%10s|" str) "\n")
(insert (expand-file-name file) "\n")))
================
*** 選択範囲を取得する
選択した範囲を取得するには"region-beginning","region-end"を使います."region-beginning"では選択した範囲の開始点が,"region-end"では選択した範囲の終了点が取得できます.選択した範囲の文字列をミニバッファに表示するコマンドを作ってみましょう.
================
(defun my-region-test ()
(interactive)
(message "%s"
(buffer-substring
(region-beginning) (region-end))))
================
*** バッファの操作
elispを書く上でバッファの操作は避けて通れません.必須のものだけ簡単に一覧しておきます.
- switch-to-buffer :: 引数のバッファを表示する
- display-buffer :: 引数のバッファを別ウィンドウに表示する
- get-buffer-create :: 引数の名前のバッファを取得する.なければ作成する
- with-current-buffer :: 今いるバッファを一時的に引数のバッファに変更する
- save-excursion :: 囲まれた範囲を実行後にカーソル位置を元に戻す
- erase-buffer :: 今いるバッファの内容を全て削除する
- kill-buffer :: 引数のバッファを削除する
- buffer-substring :: 今いるバッファの引数の範囲の文字列を取得する
- delete-region :: 今いるバッファの引数の範囲の文字列を削除する
簡単なサンプルを作ってみましょう.選択範囲の文字列を*hoge*というバッファに出力して,別のウィンドウに表示してみます.
================
(defun my-buffer-test ()
(interactive)
(let (str buf)
;; str に選択範囲の文字列を設定
(setq str (buffer-substring
(region-beginning) (region-end)))
;; buf に*hoge*バッファを設定
;; なければ自動的に作られる
(setq buf (get-buffer-create "*hoge*"))
;; ↓↓今いるバッファを一時的に*hoge*に変更
(with-current-buffer buf
;; *hoge*バッファの中身を削除
(erase-buffer)
;; strを*hoge*バッファに挿入
(insert str))
;; ↑↑ここで今いるバッファが元のバッファにもどる
;; *hoge*バッファを別のウィンドウに表示
(display-buffer buf)))
================
** プロセスを起動する
これで外部プログラムを起動するコマンドを作るための準備がととのいました.あとは起動用の関数を呼んであげるだけです.外部プログラムを呼ぶには"call-process","call-process-region"を使います.
*** call-process -- プロセスを起動する
プロセスをただ起動するだけの場合,"call-process"を使います."call-process"の定義は以下のようになっています.
(call-process PROGRAM INFILE BUFFER DISPLAY ARGS...)
PROGRAM, BUFFER, ARGS引数の使い方だけ覚えてください.それぞれ以下の意味です.
- PROGRAM :: 起動する外部プログラム
- BUFFER :: 結果を出力するバッファ."t"なら今いるバッファ,"nil"なら結果は破棄される.
- ARGS :: プログラムに渡す引数.可変長引数.
サンプルとしてディレクトリを指定してlsした結果を*ls*バッファに出力するコマンドを作ってみます.以外と簡単でしょ?
================
(defun my-ls ()
(interactive)
(let (dir buf)
;; ディレクトリの入力
(setq dir (expand-file-name (read-file-name "Dir: ")))
(setq buf (get-buffer-create "*ls*"))
(with-current-buffer buf
(erase-buffer))
;; lsを実行して*ls*バッファに出力
(call-process "ls" nil buf nil "-la" dir)
;; *ls*バッファの表示
(display-buffer buf)))
================
*** call-process-region -- 選択範囲を標準入力としてプロセスを起動する
フィルタを実装するには"call-process-region"を使って選択範囲を渡すコマンドを作ります."call-process-region"の定義は以下のようになっています.
(call-process-region START END PROGRAM DELETE BUFFER DISPLAY
&rest ARGS)
PROGRAM, BUFFER, ARGS引数は"call-process"と同じです.START, END, DELETE引数の意味はそれぞれ以下通りです.
- START, END :: プロセスに渡す範囲
- DELETE :: "t"なら範囲を削除して,結果に置き換える
サンプルとしてastyleを使って選択範囲をフォーマットするコマンドを作ってみます.
================
(defun my-astyle ()
(interactive)
;; カーソル位置を保存
(save-excursion
(call-process-region
(region-beginning) (region-end)
"astyle"
t ;; 選択範囲の置き換え
t ;; 今いるバッファに出力
nil
"--style=gnu")))
================
* 最後に
今回は自分用のコマンドをお手軽に作る方法を紹介しました.どうでしょう?自分用のコマンドが作れそうな気分になってもらえたでしょうか?せっかくEmacsを使っているのに標準のコマンドや誰かが作ったコマンドを利用しているだけではもったいないです.自分で拡張してこそのEmacsです.この記事を参考にいろいろ試してみて下さい.
また,後半ではelispの書き方をほんの触りだけ書いてみました.これをきっかけにelisperが一人でも増えたら嬉しいなぁ.と筆者は思っています.