Wordbank - 英和辞書用コマンドラインインタフェース

Common Lisp で何かプログラムを作ろうと思い立ち、Wordbank というものを作りました。これはコマンドプロンプトに入力した英単語の意味を表示するだけのプログラムですが、辞書で引いた単語の履歴を覚えておいて、後で復習できるような目的で作成しています(履歴機能は未実装)。こだわったところは、SLIME 上でコンパイルしたり Quicklisp でロードできるようにしつつ、出来上がった Lisp のソースコードをそのままシェルからスクリプト実行できるようにした点です。複数ファイルから構成される場合にスクリプト実行する方法はもう少し工夫する必要がありそうです。

問題の辞書ですが、デ辞蔵 REST版API という Web サービス があったので利用させて頂きました。現状、API の動作確認用プロトタイプという段階であり、クエリを投げて結果を表示することしかできませんが、引き続き履歴機能の実装と検索機能の拡充を進めていく予定です。

ちなみに HTTP リクエストには Drakma、検索結果からの情報抽出には CL-PPCRE という正規表現ライブラリを利用しました。Common Lisp では package.lisp にパッケージ情報やら asdf ファイルに依存関係やらを書くことが多いようですが、CL-Project というスケルトン生成ツールはユニットテストの設定もしてくれるので便利でした。同様のツールで Quickproject がありますが、こちらはユニットテストの設定はしないようです。

get-output-stream-string

get-output-stream-string は指定したストリームに出力された文字を出力順のまま文字列として返す関数である。関数が実行される際に、ストリーム中のすべての文字はクリアされる。Hyperspec の get-output-stream-string によると、 make-string-output-stream 以外で生成したストリームに関しての動作は未定義とされている。Practical Common Lisp のサンプルプログラムに with-output-to-string で生成したストリームから get-output-stream-string で文字列として取り出しているコードがあるが、言語仕様上はよろしくないようだ。

Common Lisp (SBCL) でスクリプトを書く方法

Common Lisp (SBCL) でスクリプトを書くいい方法が見つからなかったので調べてみた。重要なのは次の三点。

  • Quicklisp をロードしたイメージファイルの生成
  • シェルスクリプト shebang からの exec sbcl ハック
  • scripted main はシェルスクリプトに与えた $0 から判定する

順を追って説明します。

Quicklisp をロードしたイメージファイルの生成

スクリプト実行するたびに Quicklisp を実行しているとロードに時間がかかるので、あらかじめ Quicklisp をロードした状態のイメージファイル (以下の例では sbcl-base.core という名前のイメージファイル) を生成し、スクリプト実行時に指定する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sbcl [~/dev/tmp]
This is SBCL 1.2.1, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
* (sb-ext:save-lisp-and-die "sbcl-base.core" :executable t)
[undoing binding stack and other enclosing state... done]
[saving current Lisp image into sbcl-base.core:
writing 5744 bytes from the read-only space at 0x0x20000000
writing 3120 bytes from the static space at 0x0x20100000
writing 53903360 bytes from the dynamic space at 0x0x1000000000
done]

イメージファイルを指定して sbcl を起動するには —core オプションに指定すればよい。

シェルスクリプト shebang からの exec sbcl ハック

sbcl を直接 shebang で実行するのではなく /bin/sh 経由で sbcl に exec する。--core オプションは先ほど生成した Quicklisp をロード済みのイメージファイルで起動するために指定している。

1
2
3
4
5
#!/bin/sh
#|
exec sbcl --core sbcl-base.core --script $0 $0 "$@"
|#
;;; code comes here

このとき、最初の $0 はこのファイル自身をスクリプト実行するために与えている。2 番目の $0 は後述する scripted main であることを判定するために使う。最後の "$@" は残りの全引数を sbcl に与えている。

2 行目の #|# で始まるためシェルスクリプトからはコメント行に解釈される。一方、Common Lisp からは #| から |# まではコメントであると見なされるためスクリプト実行時は無視される。ただし、最初の shebang の行のみは Common Lisp の文法では解釈できないため、.sbclrc に読み飛ばす設定をする。

.sbclrc
1
2
3
4
5
(set-dispatch-macro-character #\# #\!
(lambda (stream character n)
(declare (ignore character n))
(read-line stream nil nil t)
nil))

scripted main はシェルスクリプトに与えた $0 から判定する

scripted main とはスクリプトとして実行された際のエントリポイントを表す造語。例えば、Python ならば

1
2
3
def main():
# ...
main() # call main function directly

のように実際の処理をべた書きしてしまうと、このスクリプトをモジュールとして外部に公開するときに、import するたびに main() が実行されてしまい困ることになる。そこで、

1
2
if __name__ == '__main__':
main() # scripted main

のように __name__ 変数でスクリプト実行かどうかを条件判定すれば、このモジュールをスタンドアロンなスクリプトとして実行するときは main() が実行され、外部から import される場合には main() は実行されずに必要な機能のみ読み込ませることが可能になる。

これと同じことを SBCL で実現するにはコマンド実行時のみコマンドライン引数がセットされる sb-ext:*posix-argv* を使い、(pathname-name *load-truename*) で与えられる文字列が見つかればスクリプト実行なので scripted main を実行、見つからなければ REPL や外部ファイルから load されたものとして scripted main は実行しない、というように見分ける作戦にする。

1
2
3
4
5
;; scripted main
(when (member (pathname-name *load-truename*)
sb-ext:*posix-argv*
:test #'(lambda (x y) (search x y :test #'equalp)))
(main))

しかし、普通に sbcl --script file <filename> のようにファイルを指定して実行しただけでは <filename> という文字列は sb-ext:*posix-argv* に含まれないという問題がある。そこで、sbcl を exec するときに --script $0 の後に重ねて $0 を指定することで、shebang から起動された場合は sb-ext:*posix-argv* の第二要素に必ずスクリプトのファイル名を入れることにする。(第一要素は sbcl になる)

以上をまとめるとスクリプト全体は次のようになる。

myscript.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/sh
#|
exec sbcl --core sbcl-base.core --script $0 $0 "$@"
|#
(let* ((*standard-output* (make-broadcast-stream))
(*error-output* *standard-output*))
(ql:quickload "split-sequence"))
(in-package :cl-user)
(defpackage :myscript
(:use #:cl #:split-sequence))
(in-package :myscript)
;;; code comes here
(defun main (argv)
(format t "~a~%" (first argv))
(format t "~s~%" (split-sequence #\, (first argv))))
;; scripted main
(when (member (pathname-name *load-truename*)
sb-ext:*posix-argv*
:test #'(lambda (x y) (search x y :test #'equalp)))
(main (cddr sb-ext:*posix-argv*)))

コマンドライン引数で渡した文字列を split-sequence を使って , でリストに分割して表示するだけのスクリプト。split-sequence は Quicklisp からロードしているが、ロード中のメッセージを抑制するために、*standard-output**error-output* の束縛を一時的に変更して端末に出力されないようにしている。

1
2
3
$ ./myscript.lisp "hello, world"
hello, world
("hello" " world")

参考

Writing Scripts with Common Lisp

Android Studio 0.8.1

Android Studio 0.8.1 でプロジェクト作成してみたが、[INSTALL_FAILED_OLDER_SDK] というエラーが起きてしまい実行することができなかった。AVD や SDK のバージョンをかえてみたり実機で実行してみたけど結果は同じ。同様のエラーがないか検索してみると、app/src/build.gradle の compileSdkVersion をアプリケーションを実行する AVD や実機のバージョンに合わせればよいらしいことがわかった。今回は Android 4.0.3 (API 15) の実機が手元にあったので、以下のような設定にした。同時に、Android SDK Manager で Android 4.0.3 (API 15) の SDK Platform もインストールしておいた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apply plugin: 'com.android.application'
android {
compileSdkVersion 15 // Changed this value from android-L to 15
buildToolsVersion '20.0.0'
defaultConfig {
applicationId "com.example.tanaka.myapplication"
minSdkVersion 15
targetSdkVersion 20
versionCode 1
versionName "1.0"
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}

Evil を eval-last-sexp に対応させる

Emacs では通常、Lisp の S 式の最後 (閉じ括弧の次) にカーソルを置いて C-x C-e を押すとその S 式が eval-last-sexp により評価される。しかし、Evil の normal 状態では、S 式が行末にある場合にカーソルが閉じ括弧をポイントするため、 eval-last-sexp により正しく S 式を指定して評価することができない。insert 状態では正しく指定できるが、評価するたびに insert 状態に入るのは勝手が悪すぎる。というわけで、調べてみると eval-last-sexp が内部で使っている preceding-sexp に defadvice する方法で解決できることがわかった。

normal-state interacts poorly with eval-last-sexp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defadvice preceding-sexp (around evil activate)
"In normal-state, last sexp ends at point."
(if (evil-normal-state-p)
(save-excursion
(unless (or (eobp) (eolp))
(forward-char))
ad-do-it)
ad-do-it))
(defadvice pp-last-sexp (around evil activate)
"In normal-state, last sexp ends at point."
(if (evil-normal-state-p)
(save-excursion
(unless (or (eobp) (eolp))
(forward-char))
ad-do-it)
ad-do-it))

Slime でも同様に slime-last-expression に対して defadvice することで対応できた。

1
2
3
4
5
6
7
8
(defadvice slime-last-expression (around evil activate)
"In normal-state, last sexp ends at point."
(if (evil-normal-state-p)
(save-excursion
(unless (or (eobp) (eolp))
(forward-char))
ad-do-it)
ad-do-it))

初期の Git コマンドのソースコード

Linus さんによる Git の最初のコミットとなる e83c5163 のソースコードを読んでみた。

init-db

.dircache という名前でディレクトリを作成し、その中に 00 ~ FF のディレクトリを作成する。Git はこのディレクトリをコンテンツ管理用の DB として扱い、ファイルのコンテンツから求めた SHA-1 ハッシュ値をファイル名としてこのディレクトリに保存する。保存するディレクトリは SHA-1 ハッシュ値の先頭 1 バイトから保存すべき 00 ~ FF のディレクトリを識別し、そのディレクトリの中に SHA-1 ハッシュ値の残りの 19 バイトからなるファイル名で保存する。このようにインデクシングする理由は、.dircache はそれが置かれているディレクトリ以下のすべてのファイルを管理するため、ファイル数が多くなった場合に対処しているのだろう (という予想)。

データの基本的なフォーマットは以下の通り。

  • blob データサイズ\0データ
  • tree データサイズ\0データ
  • commit データサイズ\0データ

“データ” の部分はそれぞれの形式毎に変わる。

update-cache

指定したファイルを blob データサイズ\0データ という形式に変換し、それを zlib で deflate した結果を .dircache/objects 以下の DB に保存する。このときのファイル名は deflate 後の結果から SHA-1 ハッシュ値を求めたものになる。

さらに、登録するファイル情報を .dircache/index にキャッシュエントリとして保存する。.dirchache/index の構造は単純で、1 つのキャッシュヘッダとそれに続くキャッシュエントリのリストから構成される。キャッシュエントリは fstat から求めたファイル情報と deflate 結果から求めた SHA-1 ハッシュ値を含んでおり、ファイル名でソートされている。また、キャッシュヘッダにもすべてのキャッシュエントリのリストから算出した SHA-1 ハッシュ値を保存する。

write-tree

キャッシュエントリを走査し、tree データサイズ\0ファイルモード ファイル名\0SHA-1... という形式のデータを生成する。update-cache でファイルを blob 形式で DB に保存したのと同様に、この tree 形式を zlib で deflate して、その結果から求めた SHA-1 ハッシュ値をファイル名として DB に保存する。このときのハッシュ値は commit-tree で使う。

commit-tree

引数で指定した SHA-1 ハッシュ値の tree を commit する。コミットメッセージは標準入力から取得する。形式は以下の通り。

commit データサイズ\0tree SHA-1\nparent SHA-1\nauthor 本名 <ユーザ名@email> 日時\ncommitter 本名 <ユーザ名@email> 日時\n\nコミットメッセージ

write-tree と同様に上記のデータを zlib で deflate して SHA-1 ハッシュ値からなるファイル名で DB に登録する。-p で親となる tree を指定することができ、通常は前回 commit したときの tree の SHA-1 ハッシュ値を指定する(初回 commit 時は指定しない)。この tree を逆順にたどっていくことにより履歴を巡ることが可能。

cat-file

DB に保存されているデータはすべて zlib で deflate されているためそのままでは読めない。cat-tree はこれを読める形式に変換するコマンドで、引数に指定した SHA-1 ハッシュ値の DB ファイルを inflate して一時ファイル (temp_git_file_XXXXXX) に書き出す。

read-tree

blob と commit 形式のファイルは cat-file で inflate すれば読めるが、tree 形式は SHA-1 ハッシュ値がバイナリ形式で含まれるためそのままでは読めない。read-tree は tree 形式のファイルをテキストとして読める形に変換して一時ファイルに書き出す。

show-diff

.dircache/index で管理しているすべてのキャッシュエントリについて、DB 内のファイルと現在のファイルの stat 情報に差分があれば diff を表示する。

使い方

1
2
$ git clone github.com/gitster/git.git
$ git checkout e83c5163

そのままでは make できなかったので以下のパッチをあてる。

次の実行例は DB を作成して README と Makefile を tree に登録して commit するまでの例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ ./init-db
defaulting to private storage area
$ ./update-cache README
$ ./update-cache Makefile
$ find .dircache -type f
.dircache/index
.dircache/objects/66/5025b11ce8fb16fadb7daebf77cb54a2ae39a1
.dircache/objects/90/9a87257113dd11a2c2749c059b4aa6d55ed9f7
$ ./write-tree
8f4d3dbaec34d144bfcf5a8f2d7e0573abc00230
$ ./commit-tree 8f4d3dbaec34d144bfcf5a8f2d7e0573abc00230
Committing initial tree 8f4d3dbaec34d144bfcf5a8f2d7e0573abc00230
first commit
1f3b88e43904b187069aa7a8b6ff8006743969c9
$ ./read-tree 8f4d3dbaec34d144bfcf5a8f2d7e0573abc00230
100644 Makefile (909a87257113dd11a2c2749c059b4aa6d55ed9f7)
100644 README (665025b11ce8fb16fadb7daebf77cb54a2ae39a1)
$ ./cat-file 665025b11ce8fb16fadb7daebf77cb54a2ae39a1
temp_git_file_OfINdU: blob
$ head temp_git_file_OfINdU
GIT - the stupid content tracker
"git" can mean anything, depending on your mood.
- random three-letter combination that is pronounceable, and not
actually used by any common UNIX command. The fact that it is a
mispronounciation of "get" may or may not be relevant.
- stupid. contemptible and despicable. simple. Take your pick from the
dictionary of slang.

Git コマンドの使い方は以下の記事が参考にさせていただきました。

Git の履歴を削除して新しいブランチを作成する方法

Git の履歴をすべて削除して新しいブランチからリポジトリ管理を始めたい場合、まずは以下のように git checkout コマンドに —orphan オプションをつけて空のブランチを作成する。

1
$ git checkout --orphan tmp

この状態で、いつものように Git 管理下におきたいファイルをステージングして commit する。何も commit していない状態だと git branch を実行しても tmp ブランチは表示されないようだ。

1
2
$ git add .
$ git commit -m "first commmit"

この時点で master と履歴が 1 つの tmp というブランチが存在することになる。あとは master ブランチを tmp ブランチで push して履歴をすべて削除した master ブランチに改変する。

1
$ push -f . tmp:master

別のリモートリポジトリに push

GitHub から clone したリポジトリを履歴を削除した上で BitBucket で新たに管理を開始したい場合などは、まず上記の手順で履歴を削除したリポジトリを作成した後、以下のように origin の url を変更する。

1
2
$ git checkout master # カレントブランチを master ブランチに戻す
$ git remote set_url origin git@bitbecket.com:username/repository.git

あとはリモートリポジトリに対して push すればよい。

1
$ git push origin master

最初に作成した orphan なリポジトリはもはや不要なので削除してよい。

1
$ git branch -d tmp

Atom Shell で Hello world

Atom Shell を使っておそらく本来の使われ方ではないプログラムを作成してみる。以下のような Node.js プログラムを普通に実行することができる模様。

hello.js
1
2
console.log("hello, world");
process.exit(0);

最後の process.exit(0) を書かないとプロセスが終了しない。 実行するときは引数に JavaScript ファイルを指定すればよい。

実行結果
1
2
$ ./out/Release/Atom.app/Contents/MacOS/Atom hello.js
hello, world

require でロードしたモジュールも普通に使うことができる。

cpus.js
1
2
3
var os = require('os');
console.log(os.cpus());
process.exit(0);
実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./out/Release/Atom.app/Contents/MacOS/Atom cpus.js
[ { model: 'Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz',
speed: 1700,
times: { user: 4692350, nice: 0, sys: 3321620, idle: 38701300, irq: 0 } },
{ model: 'Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz',
speed: 1700,
times: { user: 2480960, nice: 0, sys: 1154690, idle: 43077500, irq: 0 } },
{ model: 'Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz',
speed: 1700,
times: { user: 4196740, nice: 0, sys: 2200760, idle: 40315740, irq: 0 } },
{ model: 'Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz',
speed: 1700,
times: { user: 2333780, nice: 0, sys: 1036880, idle: 43342400, irq: 0 } } ]

本来のデスクトップアプリケーションを目的としたプログラムのチュートリアルは下記ドキュメントにある。

ちょっと試してみたけど、main.js から HTML の DOM を操作することはできないんだろうか。よくわからない…。

Atom Shell をビルドする

GitHub が最近オープンソースで公開した Atom というエディタが話題になっているが、それに使われているフレームワークが Atom Shell というものらしい。GitHub の README.md によると JavaScript や HTML や CSS といった Web テクノロジでクロスプラットフォームなデスクトップアプリケーションが作れるフレームワークなのだそうだ。以前から Microsoft Windows でも HTA (HTML Application) という HTML + JScript/VBScript でデスクトップアプリケーションが作れる技術があったが、あのノリなのかもしれない。もっとも HTA は Internet Explorer 限定だったけど。

試しにソースコードを clone してビルドしてみる。とはいってもドキュメントの通りに進めるだけだった。まずはソースツリーを clone する。

1
2
$ git clone https://github.com/atom/atom-shell.git
$ cd atom-shell

ビルドに必要なものはすべて bootstrap.py を実行することでそろえてくれるようだ。

1
$ ./script/bootstrap.py

あとは build.py を実行するだけ。

1
$ ./script/build.py

最後に test.py を実行して完了!

1
$ cd ./script/test.py

out/Release/Atom.app/Contents/MacOS/Atom に実行ファイルが生成される。

Python でファイルを読むとき

Python でファイルを読むときはいつも

1
2
3
4
5
6
f = open("file.txt")
line = f.readline()
while line:
print line
line = f.readline()
f.close()

のように書いていたけど、こんな書き方しなくても、

1
2
3
4
f = open("file.txt")
for line in f:
print line
f.close()

これでよかったんだ…。効率は良くないかもしれないけど、もっと短く書ける。

1
2
for line in open("file.txt"):
print line

でも Python 2.6 からは with 構文使うのがいいらしい。

1
2
3
with open("file.txt") as f:
for line in f:
print line

これなら with を抜けた後に自動的に f.close() されるようだ。まさに Lisp の with-open-file マクロだな。