2013年11月23日土曜日

zip とか tar の中身を grep するツール

zipやtar などのアーカイブの中身を、いちいち展開することなくそのまま検索でき、 かつ、ShiftJis とか EUC など日本語コードの違いも、よしなに扱う grepライクな python スクリプトを作ってみた。
(ググって探せば、先人の作ったもっといいものが見つかるような気もするけど)

jzgrep.py

スクリプトのソースは ここ
動かすには、python2.6 以上が必要。でも、python3 では動かない。
動作確認は、linux と os-x10.6 でしか行なっていない。

使い方は、$ python jzgrep.py --help | less を参照。
$ python jzgrep.py --help
Usage: jzgrep.py [OPTIONS] PATTERN [FILE ...]
  PATTERN  -- 正規表現パターン (Python の `re' モジュールを使っています)
  FILE...  -- PATTERN で検索するファイル。省略時は stdin。
  OPTIONS
       --help -- このメッセージを表示して終了する
       -n     -- 行番号を表示
       -l     -- PATTERN に一致する行を含むファイル名だけを出力
       -H     -- 出力する行の先頭にファイル名を付ける。
                 (検索するファイルが複数の場合は、これがデフォルト)
       -h     -- 出力する行の先頭にファイル名を付けない
                 (検索するファイルが一つの場合は、これがデフォルト)
       -i     -- 大文字小文字を区別しない (re.IGNORECASE)
       -s     -- 警告メッセージを出さない (e.g [Warning]: Cannot decode ...)

対応しているファイル・タイプは、
  • 普通のテキストファイル
  • gzip, bzip2
  • 暗号化していない zip 類 (たぶん epub とか jar も)
  • tar とその圧縮ファイル( tar、 tar.gz 及び tar.bz2)
また、tar や zip の中の tar や zip 、さらにその中の tar や zip、 さらに… も再帰的に検索できる筈。

日本語エンコーディングは、今のところ
utf-8, cp932, euc-jp, iso2022_jp
の4種類だけに対応している。
(他の文字コードについては、ソース中の _encodings 変数や matchtext 関数を、 少しだけ書き換えれば対応できるかもしれない)

但し、jzgrep.py はとても遅い!
たぶん、異なる日本語コードの照合処理について、こんな感じで、
for enc in ("iso2022_jp", "utf-8", "euc-jp", "cp932")
  try:
    m = re.search(PATTERN, TEXTLINE.decode(enc))
    if m: return m
  except UnicodeError: pass
return None
総当りでチェックしているからだと思う。
それに、ほんのたまにだけど、 間違って余計なものにヒットしちゃうこともある。かな?

2013年11月4日月曜日

青空文庫の複数テキストを合本して kindle-paperwhite で読む

AozoraEpub3 という素晴らしいソフトのおかげで、青空文庫の小説を、kindle で自由に読めるようになった。
青空からテキストデータの zip をダウンロードして、それを、 AozoraEpub3で epub3 に変換し、さらに、 kindlegen で mobi に変換。あとは、usb ケーブルで kindle に転送すれだけでいい。
AozoraEpub3の作者の方には本当に感謝致します。

ただ、欲を言を言わせて貰うと、 短編やシリーズ物など、複数の小説データを合本する機能が欲しい。
まあ、TODOには優先度高として挙げられているので、 AozoraEpub3の将来のバージョンで合本の機能が追加されるのを待てばいいわけだが、 何でもかんでも人任せというのも情けない。

というわけで、 青空文庫の複数のテキストファイルを結合するスクリプトを作ってみた。
(本当は、epub レベルで結合するのが理想なんだけど、 epub3の仕様は、僕には難しすぎて無理…)

ソースコード (bindao.py)

ソースは ここ。 実行するには、python2.7 以上が必要。でも、python3 では動きません。
os-x 10.6.8 + kindle-pw でしか試していないけど、 たぶん、linux でも動作すると思う。

合本するロジックは、 「青空文庫の作品を合本して Epub 化する試み」 の方法を参考にさせて頂き、プラス、
  • 目次を階層化するために、個別作品の見出しレベルを変更
    合本上では、個別の構成作品の表題を大見出しに設定するので、 個別作品自体に大見出しがあれば、重複を避けるために、それぞれ、 大見出しを中見出しに、中見出しを小見出しに変換する。ただし、 個別作品に小見出しがあればこの変換はしない。

  • Zip のまま処理する。
    個別作品のzipアーカイブをいちいち展開する手間を省く。

  • 合本ファイルとしての、出力も zip 形式にする。
    個別作品のzip内に含まれる画像データ等を、 出力のzipファイルにも自動で埋め込ませる。

の3つの機能を施した。

使い方

ターミナルアプリなどから、shell上で、
$ python bindao.py 結合する青空形式のzipファイル....
を実行すると、
Top>
というプロンプトが出る。help と打つと、各コマンドの説明が表示されるが、 その中でも、よく使うコマンドは、
  • title 合本のタイトル
  • author 合本の著者名
  • bindzip
の3つ。
titleコマンドで合本の表題を、authorコマンドで著者名を設定し、最後に、 bindzip と打てば、bind.zip というファイルが作成される。
より詳しくは、$ python bindao.py -h を参照。

実際の例

青空文庫の小栗虫太郎の短編4本を合本してみた時のログ。
  1. まずは、青空文庫から zip ファイルをダウンロードする。
     $ curl -O http://www.aozora.gr.jp/cards/000125/files/4841_ruby_16998.zip
     $ curl -O http://www.aozora.gr.jp/cards/000125/files/666_ruby_2185.zip
     $ curl -O http://www.aozora.gr.jp/cards/000125/files/45230_ruby_26898.zip
     $ curl -O http://www.aozora.gr.jp/cards/000125/files/667_ruby_441.zip
    
  2. ダウンロードした zip を、bindao.py で結合する。
    (この場合は、-noauthor というオプションを指定しないと、 個別作品の表題の次にも著者名が書かれてしまう)
     $ python bindao.py -noauthor 4841_ruby_16998.zip 666_ruby_2185.zip \
                                   45230_ruby_26898.zip 667_ruby_441.zip
       Top> title 小栗虫太郎短編集 1
       Top> author 小栗虫太郎
       Top> show all
         合本の表題: 小栗虫太郎短編集 1
         合本の著者: 小栗虫太郎
         合本の副題: -未設定-
         構成ファイル(1): 4841_ruby_16998.zip 内の goko_satsujinjiken.txt
         ................................................................
         ................................................................
       Top> bindzip
         OK: bind.zip を出力しました
       Top> quit
      
  3. bind.zip の中身を確認
     $ unzip -l bind.zip 
        ------  ----     ----    ----
        258407  10-30-13 15:21   bind.txt
          9286  03-04-12 18:40   fig666_02.png
           674  03-04-12 18:39   fig666_01.png
           296  07-24-07 13:52   fig45230_01.png
           255  07-24-07 13:52   fig45230_02.png
          9834  02-27-06 05:33   fig45230_03.png
         12804  02-24-11 17:54   fig667_01.png
     $
    
  4. AozoraEpub3 を使って、bind.zip を epub ファイルに変換する
    $ java -jar AozoraEpub3.jar
    • 端末設定 で kindle-PW を選択
    • 表題: 本文内 「表題→著者名」を選択し、「先頭が発行者」のチェックを外す。
    • 目次設定の「目次(NCX)階層化」のチェックをいれる
    • 他はデフォルトのままで bind.zipをドラッグアンドドロップ
    • 「変換」をクリック
  5. kindlegen で mobi に変換
      $ kindlegen '[小栗虫太郎] 小栗虫太郎短編集 1.epub' -o oguri.mobi
    
  6. kindle を usb 接続して mobi ファイルをコピーする。
    osx の場合は、/Volumes/Kindle にマウントされるので、
    $ cp oguri.mobi /Volumes/Kindle/documents/
最後に、ファインダーなどで kindle device をアンマウントすることを忘れないように。

ありゃ、部分的に辞書が引けない場合がある!

AozoraEpub3 + bindao.py を使って、 小栗の他にも、久生十蘭、甲賀三郎、浜尾四郎など、 古き良き時代の探偵小説をかたっぱしから合本して、秋の読書を堪能してたんだけど、 なんだか部分的にkindleの辞書が引けない本があることに気づいた。
調べたい単語をロングタッチして、デジタル大辞泉を引いても、
「お探しの言葉が見つかりませんでした」
と出るばかり。でも、同じ言葉を同じ作品の別の段落で引くと、 ちゃんと辞書が引けたりする。
bindao.py のバグかとも思ったのだが、 AozoraEpub3単独で変換しても直らない。かといって、 この症状をググってみても全然引っかからない。 同じような問題で悩んでいる人は誰もいないようだ。もしかしたら、 僕の kindle-pw だけで起こる、固有の問題なのかもしれない。

で、いろいろ調べてみた。
なんとなく、文章量の長い段落でこの現象が起きているみたいなので、 epubファイル内本文のxhtmlファイルを開き、 問題の生じる段落で適当に句点の後(。の後)に改行をいれてみると、 辞書が引けるようになった。
でもこれだと、kindleで見た時に、改行が半角空白で表示されてしまう。
改行の代わりに、‌ という特殊シンボル(自分でも意味がわかっていない)を使っても駄目。
その他いろいろ試行錯誤の結果、 <span /> という、たぶん、意味のない空タグを、 句読点の後に入れてやると直ることがわかった。余分な半角空白も入らず、 かつ、どこでも辞書引きがOKになる。ただ mobi ファイルが太ってしまうが…

想像だけど、空白も改行もタグも途中に入らない日本語の長い段落というのは、 xhtml上では、長いワードだと認識されるのではないだろうか。 その異常に長いワードをkindleが解析する時に、 僕の環境に固有の何か他の事象が重なると、このような不具合が起きるのではないかと思う。

で、これも、 python のスクリプトでまとめてみた。
ソースコードは ここ
使い方は、AozoraEpub3 で epub を作成し、kindlegen を実行する前に、 shellからこのスクリプトを実行すればいい。
例えば、AozoraEpub3 が出力するファイル名を source.epub とすると、
 $ python aoepub3_hack.py source.epub dist.epub 
 $ kindlegen dist.epub
これで、空の span タグだらけの、変な dist.mobi が出来上がる。
(他のオプションなどは、 $ python aoepub3_hack.py -h を参照)

しばらく使ってみて、今のところ特に問題はない。
ただ、こんな解決方法はどう考えても醜い。本によっては、 <span /> タグを入れている事が原因で、 何か変な副作用が起きるかもしれない。
こんなことやらなくても、何か、css 関連の設定ですっきりと解決、 なんてことになればいいのだけど。

2013年2月1日金曜日

emacs24 elisp 新機能

M-x view-emacs-news をざっと眺めてみて、特に lisp 関連で気になった事柄、 のメモ

レキシカルスコープ

emacs24からは、lexical-binding というファイルローカルな変数に t をセットすれば、レキシカルスコープ環境で、 lispプログラムを評価してくれるらしい。
つまり、ソースファイルの先頭に、
;; -*- coding: utf-8; lexical-binding: t -*-
とか書いとけば、common-lisp や schemeのように、普通にクロージャーが書けるということだ。
試してみた。
$ cat lextest.el
;; -*- coding: utf-8; lexical-binding: t -*-
;; カウンター生成関数
(defun make-counter (&optional init)
  (let ((count (or init 0)))
    #'(lambda (&optional com) 
        (case com
          ((peek :peek)  count)
          ((reset :reset) (setq count (or init 0)) count)
          (t (setq count (1+ count)) count)))))
$
この、lextest.el を M-x load-file で読み込み、 *scratch* バッファーで動作確認。
(setq my-counter (make-counter 100))
 => (closure ...)
(funcall my-counter)
 => 101
(funcall my-counter)
 => 102
(funcall my-counter :peek)
 => 102
(funcall my-counter)
 => 103
(funcall my-counter :reset)
 => 100
(funcall my-counter)
 => 101
いちいち、funcall で 関数シンボルを呼び出す以外は、 scheme と同じように書けるのが嬉しい。
では、ソースファイル中の、 ある一部の関数だけレキシカルバインディングしたい時はどうするのか?
安易に、
(let ((lexical-binding t)) ..... )
とやっても駄目だった。
emacs24からは、eval が拡張されており、`M-x describe-function` では、
    (eval FORM &optional LEXICAL)
        Evaluate FORM and return its value.
        If LEXICAL is t, evaluate using lexical scoping.
とある。第2引数に t を渡してやればレキシカルスコープでの評価になるらしい。
先の make-counter を、この eval を使って定義してみると、
(eval
 '(defun make-counter (&optional init)
    (let ((count (or init 0)))
      #'(lambda (&optional com) 
          (case com
            ((peek :peek) count)
            ((reset :reset) (setq count (or init 0)) count)
            (t (setq count (1+ count)) count)))))
 t)
何か変なコードだけど、一応これでちゃんと動作した。
上のように書けば、従来のダイナミックスコープなソースコードの中に、 部分的にレキシカルスコープなコードを割りこませることもできそうだ。でも、 他にもっといい方法がありそうなもんだが?

letrec マクロ

たぶん、レキシカルスコープ導入の関係で追加されたのだと思う。
よくある相互参照再帰のパターンで確認してみた。
;; -*- lexical-binding: t -*-
(defun foo (n)
  (letrec ((even-p #'(lambda (x)
                       (or (= x 0) (funcall odd-p (- x 1)))))
           (odd-p  #'(lambda (x)
                       (and (not (= x 0)) (funcall even-p (- x 1)))))
           )
    (cond ((funcall even-p n) "EVEN")
          ((funcall odd-p  n) "ODD")
          (t "????"))))
(mapcar 'foo '(100 101 102 103)) 
=> ("EVEN" "ODD" "EVEN" "ODD")
上のコードは、スコープに関係なく動作する。 でも、letrec の部分をletに置き換えてみると、 ダイナミックスコープな環境では動作してしまうが、 lexical-binding な環境ではエラーになる。
実はこのあたり、ダイナミックスコープでも letrec みたいにしないと駄目なんじゃないか?と僕はずっと思っていた。 だから、今までは、
 (let ((even-p nil) (odd-p nil))
   (setq even-p #'(lambda ....  ))
   (setq odd-p #'(lambda ....  ))
   .....)
てな感じの汚いコードを書いていた。このあたりの理解がまだまだあやふやだ....

pcase マクロ

emacs24から導入された、ML形式のパターンマッチング構文らしいのだが、 MLって知らないし、`M-x describe-function pcase` を 読んでもよくわからない。
で、ググってみた。
ここ に、pcase を使ったサンプルがたくさん書いてある。
このサンプルと、`describe-function pcase` のドキュメントとを両方眺めると、なんだか少しだけわかったような気になる。
要は、
(pcase EXP
  (UPATTERN1 CODE1 ...) 
  (UPATTERN2 CODE2 ...) 
  ...)
のフォームをとり、EXPに対して UPATTERNのパターンマッチングをかけ、 照合すれば 右辺の CODE... を実行する。

そして、(UPATTERN CODE ...) の所に (SYM code...) と書くと、 何でもマッチし、かつ、SYMはEXPに束縛され code... 部で使うことができる。

SYMにバッククオートを付けて、(`SYM code..) と書くと、 (eq SYM EXP) でないと照合しない。

リストに対しては、 (`(,v . ,rest) code ... ) と書けば、EXP の car部が v に、 cdr部が rest に束縛され、それらを code... 部で使うことができる。

という感じかな? 細かい所は依然として不明だけど。

試しに、pcase を使って、string-join みたいなものを作ってみた。
(defun string-join (lst &optional term)
  (pcase lst
    (`nil "")
    (`(,e . nil) e)
    (`(,e . ,rest) (concat e (or term ",") (string-join rest term)))
    ))
(string-join '()) ;=> ""
(string-join '("a")) ;=> "a"
(string-join '("a" "b")) ;=> "a,b"
(string-join '("a" "b" "c")) ;=> "a,b,c"
(string-join '("a" "b" "c" "d") "|") ;=> "a|b|c|d"
なんか、scheme の syntax-rules の雰囲気?
まだ漠然としかわかっていないのだが、ちゃんと理解すれば、 パーサを書く時とかマクロを書く時などに重宝しそうな気がする。

notifications.el

emacs24 の NEWSの説明には、
** notifications.el provides an implementation of the Desktop
Notifications API. It requires D-Bus for communication.
とある。
要は、mac の growlnotify のように、 簡単なメッセージをデスクトップ上に表示できる機能らしい。 但し、D-Bus が動いてないといけない。

試しに、ubuntuの emacs24.1で、
  (require 'notifications)
  (notifications-notify :title "Notifications Test" :body  "Hello!")
を実行させた結果。

関数、notifications-notify は notification id を返すのでそれを覚えておき、 再度その id で notifications-notify を実行させれば、同じ位置でメッセージを表示させることもできるみたいだ。 例えばこれ、
(require 'notifications)
(setq my-notification-id nil)
(defun my-notify (message)
  (interactive "sMessage: ")
  (setq my-notification-id
        (notifications-notify :title "Notifications-Notify"
                              :body message
                              :replaces-id my-notification-id
                              :timeout (* 10 1000))))
M-x my-notify で表示させた通知画面は、そのタイムアウトである10秒内に 再度実行させても同じ位置で表示される。

2010年5月7日金曜日

VA_ARGS マクロ

objective-c の NSLog について、デバッグ時のみログを出力し、 リリースビルドではおとなしくさせる方法を知りたく、ググってみた。
いろいろなサイトで紹介されており、 次のようなマクロを定義すれば出来るとある。

    #ifdef MYDEBUG 
    #define MYLOG(...) NSLog(__VA_ARGS__)
    #else
    #define MYLOG(...) 
    #endif
で、この、__VA_ARGS__ って一体何だ? と思い、本家 gccの ドキュメント を探ってみると、 にずばりそのものの記述があった。
どうやら、printf みたいなことをマクロでやるためのものらしい。
C言語なんて もう何十年も前に K&R を読んだっきり、 全く勉強していなかったけど、いつのまにか進歩している!
デバッグする上では、この MYLOG マクロで充分なんだけど、どうせなら 、 NSLog を書いたクラスやメソッドの場所がわかればいいなあと思い、さらに gccのページを探ってみると.....
みつけた!
__PRETTY_FUNCTION__ という変数が、それを使った場所のメソッド名や関数名を、 文字列として保持しているらしい。
じゃあ、ということで、メソッド名も出力してくれる、MYLOG2を書いてみた。

#define MYLOG2(fmt, ...) \
  NSLog([@"%s " stringByAppendingString:(fmt)], \
        __PRETTY_FUNCTION__, ##__VA_ARGS__)

__VA_ARGS__ の前の ## は、 VA_ARGS が空の時に、 ',' の展開をさせないためのおまじないだ。

以下が、そのサンプルと実行結果。
# サンプルソース
$ cat test-mylog.m
// -*-mode:objc; coding:utf-8;  -*-
#import <Foundation/Foundation.h>

#define MYLOG2(fmt, ...) \
  NSLog([@"%s " stringByAppendingString:(fmt)], \
        __PRETTY_FUNCTION__, ##__VA_ARGS__)

@interface MyTest : NSObject {
  NSString *message; 
}
@property (readwrite, copy) NSString *message;
@end

@implementation MyTest
- (id) init {
  self = [super init];
  if(self){message = nil;}
  return self;
}
- (void) dealloc {
  MYLOG2(@"dealloc!"); // Test for Empty VA_ARGS
  [self setMessage:nil];
  [super dealloc];
}
-(NSString *)message{ return message;}
-(void)setMessage:(NSString *)m {
  NSString *fmt = @"message will change from %@ to %@.";
  if( ! m )fmt = @"message '%@' will release.";
  MYLOG2(fmt, message, m);
  m = [m copy]; [message release]; message = m;
  MYLOG2(@"message did change to %@", message);
}
@end

int main(int argc, char *argv[]){
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  MyTest *test = [[[MyTest alloc] init] autorelease];
  [test setMessage:@"Hello"];

  [pool release];
  return 0;
}

# コンパイル
$ gcc test-mylog.m -framework Foundation 

# 実行
$ ./a.out
 -[MyTest setMessage:] message will change from (null) to Hello.
 -[MyTest setMessage:] message did change to Hello
 -[MyTest dealloc] dealloc!
 -[MyTest setMessage:] message 'Hello' will release.
 -[MyTest setMessage:] message did change to (null)
$
ちゃんと、[ クラス名 セレクター ] が表示される。

調子に乗って、MYLOG2と同じやり方で、alert の簡易マクロを書いてみた。

#define MYALERT_OK(title, ...) \
  ([[NSAlert alertWithMessageText:(title) \
                    defaultButton:@"OK"  \
                  alternateButton:nil otherButton:nil \
        informativeTextWithFormat:__VA_ARGS__ ] runModal])

#define MYALERT_CANCEL_OK(title, ...) \
  ([[NSAlert alertWithMessageText:(title) \
                    defaultButton:@"CANCEL" \
                  alternateButton:@"OK" otherButton:nil \
        informativeTextWithFormat:__VA_ARGS__ ] runModal])

このマクロは、こんな感じで使える。
if( ! [[NSFileManager defaultManager] fileExistsAtPath:fileName] ){
  int choice = 
    MYALERT_CANCEL_OK(@"Confirm", @"Create new file:%@ ?",fileName);
  if(choice == NSAlertDefaultReturn){
    MYALERT_OK(@"CANCELED!",@"");
    ..................;
  }else{
    ..................;
  }
}
これなら、javascript 並みにたやすく alert デバッグができそうだ。

ところが、この MYLOG2 と MYALERT_CANCEL_OK をいろいろと使っているうち に、うまくいかない場合を見つけてしまった。
例えば、
   MYLOG2([NSString stringWithFormat:@"%@ %%s",
         @"Program name is"], argv[0]);
と書くと、これは、
    error: syntax error before ‘)’ token
    error: syntax error before ‘]’ token
のコンパイルエラーになってしまう。
でも、最初の引数を、次のように括弧で囲むとエラーは無くなる。
  MYLOG2( ([NSString stringWithFormat:@"%@ %%s",
        @"Program name is"]), argv[0]);

どうやら、マクロの実引数に、stringWithFormat: のような VA_ARGS形式のセレクターを使うと、そのままでは、 ',' が変な風に解釈されてしまい、 コンパイルエラーになってしまうみたいだ。 でも、その実引数が、仮引数の ... 部分にマッチする場合は何の問題もない。
とりあえずの解決策は、( ) で囲め、ということか。
このあたりはさっぱりわからない。でも、マクロは楽しい。

マクロを使って、こんなことも出来てしまった。
$cat test-indexset.m
// -*-mode:objc; coding:utf-8;  -*-
#import <Foundation/Foundation.h>

/*
 * Ruby の NSIndexSet.each{|val| ...} 相当のつもり。
 * VA_ARGS を使っているが、実際には、3引数のマクロとして使う
*/
#define MYEACH_INDEXSET(idx,idxvalue, ...) \
{ \
  NSIndexSet *___tmpidx___ = (idx); \
  NSUInteger idxvalue = [___tmpidx___ firstIndex]; \
  while( idxvalue != NSNotFound ){ \
    __VA_ARGS__ \
    idxvalue = [___tmpidx___ indexGreaterThanIndex:idxvalue]; \
  } \
}

int main(int argc, char *argv[]){
  NSAutoreleasePool *pool;
  pool = [[NSAutoreleasePool alloc] init];
  
  NSArray *strList =
    [NSArray arrayWithObjects:@"zero",@"one",@"two"
             ,@"three",@"four",@"five", nil];
  NSMutableIndexSet *idxSet =
    [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0,4)];
  [idxSet addIndex:10];[idxSet addIndex:5];
  MYEACH_INDEXSET(idxSet, val,
                  {
                    NSString *s = @"?";
                    if( val < [strList count]){
                      s = [strList objectAtIndex:val];
                    }
                    NSLog(@"strList[%d]=%@",val,s);
                  });
  [pool release];
  return 0;
}

$ gcc  test-indexset.m -framework Foundation

$ ./a.out
   strList[0]=zero
   strList[1]=one
   strList[2]=two
   strList[3]=three
   strList[5]=five
   strList[10]=?
$
僕としては、ruby のイテレータもどきを、 objective-c で実装したつもりだ。
マクロの実引数に { } で囲んだ文を指定して、さらに、展開も文なんだけど、 とりあえずサンプルは動作している。
でも、こんなの、たぶん問題だらけなんだろうなあ....
確か、昔読んだCの落とし穴本に、 「マクロは文ではない」と書いてあったと思う。

2010年2月22日月曜日

cocoa emacs のインストール(その2)

インラインパッチ が新しくなっていたので、cocoa emacs を再インストールした。

インストール

インストールは、 MacEmacs JP Project サイトにある、
「IMEパッチ適用 Emacs 23.1.92 では...」
の所に書いてあるコマンドを、そのまま実行するだけで完了。

.emacs.el も、 今までと同じ内容 で問題なく動作した。

使ってみて

漢字変換中に C-iC-o を使った時のカーソル変化が、 前のバージョンよりもわかりやすくなり、日本語の入力がとても使いやすくなった。

ただ、do-applescript がおかしな動作をする。
僕の環境が悪いのかもしれないが、
(do-applescript "display dialog \"Hello\"")
を実行すると、何故かemacsが、
Fatal error (10) Emacs[3125:10b] 
   *** CFMessagePort: bootstrap_register(): failed 1100 (0x44c) 
   'Permission denied', port = 0x3803, 
    name = 'org.gnu.Emacs.ServiceProvider'
Abort trap
というエラーメッセージを吐いて、落ちてしまうのだ。

この現象が起きるのは、default-input-method が MacOSX の場合に限るようなので、とりあえずの回避策として、 do-applescriptを、以下の用に再定義してみた。
(when (string-match "23\\.1\\.92" emacs-version)
  (defun do-applescript (script)
    (let ((default-input-method "japanese"))
      (ns-do-applescript script))))
ns-do-applescriptが動く時だけ、 input-method を japanese に変更しただけだが、 これでなんとか動くようになる。
もともと、do-applescript は、 ns-do-applescript のエイリアスなので、再定義しても大丈夫だろう。

2009年11月4日水曜日

cocoa emacs のインストール

leopardに、cocoa emacs (emacs23.1) をインストールした時のメモ。

INSTALL

インストールそのものは、 MacEmacs JP Project の 「IMEパッチの適用」に書いてあるコマンドを、そのまま実行すればうまくいく。
$ tar xvfz inline_patch-20090617.tar.gz
$ tar xvfz emacs-23.1.tar.gz
$ cd emacs-23.1
$ patch -p 0 < ../inline_patch-20090617/emacs-inline.patch
$ ./configure --with-ns --without-x
$ make bootstrap
$ make install
$ open nextstep/Emacs.app
ただし、これらの作業を、emacsのシェルモードで行うと、
 Error: charsets directory does not exist.
 ethiopic.el: Error: Failure in loading charset map: MULE-ethiopic
のエラーが出てコンパイルに失敗する。
たぶん、EMACS関連の環境変数が邪魔になっているのだろうと思い、 configure実行の前に、
$ unset EMACSAPP EMACSDATA EMACSPATH EMACSDOC INSIDE_EMACS EMACSLOADPATH
を実行すれば解決。もっとも、ターミナルで作業する分には問題ない。

フォント設定

フォントは、 ここ とか ここ を参考にして、意味もわからずに見様見真似で設定してみた。
こんな感じだ。
(create-fontset-from-fontset-spec
  "-*-*-medium-r-normal--14-*-*-*-*-*-fontset-hiramaru14" nil t)
(create-fontset-from-fontset-spec
  "-*-*-medium-r-normal--16-*-*-*-*-*-fontset-hiramaru16" nil t)
(create-fontset-from-fontset-spec
  "-*-*-medium-r-normal--20-*-*-*-*-*-fontset-hiramaru20" nil t)
(mapc
 #'(lambda (fontset)
     (set-fontset-font fontset 'japanese-jisx0208
                       '("Hiragino Maru Gothic Pro" . "iso10646-1"))
     (set-fontset-font fontset 'katakana-jisx0201
                       '("Hiragino Maru Gothic Pro" . "iso10646-1"))
     (set-fontset-font fontset 'japanese-jisx0212
                       '("Hiragino Maru Gothic Pro" . "iso10646-1"))
     ) (list "fontset-hiramaru20" "fontset-hiramaru16" "fontset-hiramaru14"))
(let (
      ;; (my-fontset "fontset-hiramaru14") ;; ちっちゃいフォント
      (my-fontset "fontset-hiramaru20") ;; でっかいフォント
     )
  (set-default-font my-fontset)
  (add-to-list 'default-frame-alist `(font . ,my-fontset)))
いちおう、これで、英数文字と日本語文字の幅が1対2になり、 carbon の時と同等にきれいに表示されるようになった。

IMの設定

.emacs.el に以下を追加すると、 かなキーでモードラインに「あU:」とかが表示されるようになる。
(set-language-environment "Japanese")
(setq default-input-method "MacOSX")
ただ、「ことえり」の場合は、 C-x o などでバッファーを切り替えた時に、 モードラインの表示が現在の変換モードと一致しなくなる。
そこで、コマンドループのフック( post-command-hook )に、 IMの更新をするコードを追加してみた。
(load "cl")
(add-hook
 'post-command-hook
 (lexical-let ((previous-buffer nil))
   #'(lambda ()
       (unless (eq (current-buffer) previous-buffer)
         ;; (message "Change IM %S -> %S" previous-buffer (current-buffer))
         (if (bufferp previous-buffer) (mac-handle-input-method-change))
         (setq previous-buffer (current-buffer))))))
無理矢理っぽいコードだが、とりあえずこれで、 バッファー切り替えでも違和感なく使えるようになった。

DocView

emacs の中でPDFがみられるという、 doc-view を試してみた。 でも、ディスク内のあらゆるPDFを試してもうまく表示されない。
emacs-23.1/lisp/doc-view.el をみてみると、Ghostscript と、 xpdf または teTeX が必要と書いてある。
さっそく port でインストール。
$sudo port install ghostscript
$sudo port install ghostscript-fonts-hiragino
$sudo port install xpdf
これでdoc-viewが動き、PDFが表示できるようになる。
でも、macでは、全てのpdfをdocviewで表示できるわけではないらしい。 見れるファイルもあるし見れないのもある。

.emacs.el

cocoa emacs用に、僕が使っている .emacs.el の抜粋。
いろいろなサイトに載っていた設定例の寄せ集めだが、 これでも、試行錯誤の連続で、落ち着くまでには結構苦労した。
(if (>= emacs-major-version 23)
    ;; cocoa emacsの設定
    (progn
      ;; Command キーをMetaキーにする
      (setq ns-command-modifier (quote meta))
      ;; (setq ns-alternate-modifier (quote super))
      (setq ns-alternate-modifier nil)

      ;; IM 設定
      (set-language-environment "Japanese")
      (setq default-input-method "MacOSX")
      (prefer-coding-system  'utf-8-unix)
      ;; minibufferは英数モードで始める
      (add-hook 'minibuffer-setup-hook 'mac-change-language-to-us)
      ;; buffer切り替えの時にも、IM状態をアップデート
      (load "cl")
      (add-hook
       'post-command-hook
       (lexical-let ((previous-buffer nil))
         #'(lambda ()
             (unless (eq (current-buffer) previous-buffer)
               ;; (message "Change IM %S -> %S" previous-buffer (current-buffer))
               (if (bufferp previous-buffer) (mac-handle-input-method-change))
               (setq previous-buffer (current-buffer))))))

      (define-key global-map [ns-drag-file] 'ns-find-file) ;; find-file

      ;; Set text scale key 
      (global-set-key "\C-c+" #'(lambda () (interactive) (text-scale-increase 1)))
      (global-set-key "\C-c-" #'(lambda () (interactive) (text-scale-decrease 1)))
      (global-set-key "\C-c0" #'(lambda () (interactive) (text-scale-increase 0)))

      ;; font 設定
      ;; (setq fixed-width-use-QuickDraw-for-ascii t)
      (setq mac-allow-anti-aliasing t)

      (create-fontset-from-fontset-spec
       "-*-*-medium-r-normal--14-*-*-*-*-*-fontset-hiramaru14" nil t)
      (create-fontset-from-fontset-spec
       "-*-*-medium-r-normal--16-*-*-*-*-*-fontset-hiramaru16" nil t)
      (create-fontset-from-fontset-spec
       "-*-*-medium-r-normal--20-*-*-*-*-*-fontset-hiramaru20" nil t)
      (mapc
       #'(lambda (fontset)
           (set-fontset-font fontset 'japanese-jisx0208
                             '("Hiragino Maru Gothic Pro" . "iso10646-1"))
           (set-fontset-font fontset 'katakana-jisx0201
                             '("Hiragino Maru Gothic Pro" . "iso10646-1"))
           (set-fontset-font fontset 'japanese-jisx0212
                             '("Hiragino Maru Gothic Pro" . "iso10646-1"))
           ) (list "fontset-hiramaru20" "fontset-hiramaru16" "fontset-hiramaru14"))
      
      (create-fontset-from-fontset-spec
       "-*-*-medium-r-normal--14-*-*-*-*-*-fontset-hirakaku14" nil t)
      (create-fontset-from-fontset-spec
       "-*-*-medium-r-normal--16-*-*-*-*-*-fontset-hirakaku16" nil t)
      (create-fontset-from-fontset-spec
       "-*-*-medium-r-normal--20-*-*-*-*-*-fontset-hirakaku20" nil t)
      (mapc
       #'(lambda (fontset)
           (set-fontset-font fontset 'japanese-jisx0208
                             '("Hiragino Kaku Gothic Pro" . "iso10646-1"))
           (set-fontset-font fontset 'katakana-jisx0201
                             '("Hiragino Kaku Gothic Pro" . "iso10646-1"))
           (set-fontset-font fontset 'japanese-jisx0212
                             '("Hiragino Kaku Gothic Pro" . "iso10646-1"))
           ) (list "fontset-hirakaku20" "fontset-hirakaku16" "fontset-hirakaku14"))

      (setq face-font-rescale-alist
            '(("^-apple-hiragino.*" . 1.2)
              (".*osaka-bold.*" . 1.2)
              (".*osaka-medium.*" . 1.2)
              (".*courier-bold-.*-mac-roman" . 1.0)
              (".*monaco cy-bold-.*-mac-cyrillic" . 0.9)
              (".*monaco-bold-.*-mac-roman" . 0.9)
              ("-cdac$" . 1.3)))

      (let ((my-fontset "fontset-hirakaku20")
            ;; (my-fontset "fontset-hiramaru20")
            )
        (set-default-font my-fontset)
        (add-to-list 'default-frame-alist `(font . ,my-fontset)))
      )
  
  ;; carbon emacs (emacs 22) の設定
  (progn
    (require 'carbon-font)
    ;; ...................................................
    ;; ...................................................
    ))

2009年10月19日月曜日

emacsから翻訳サイトをアクセス

emacsからSafariを起動し、 googleやexciteの翻訳ページをアクセスするコマンド。 たぶん、leopardでしか動作しないと思う。

browse-translate-site.el
(defun my-escape-url (str &optional encoding)
  "URL-encode a string. ENCODING default is 'utf-8"
  (mapconcat
   (lambda (ch)
     (if (or (and (<= ?A ch) (<= ch ?Z)) (and (<= ?a ch) (<= ch ?z))
             (memq ch (list ?_ ?. ?-))) (char-to-string ch)
       (format "%%%02X" ch)))
   (encode-coding-string str (if encoding encoding 'utf-8)) ""))
(defun escape-js-string (str)
  "Escape string to javascript string literal"
  (apply #'concat
         (mapcar
          #'(lambda (c)
              (cond
               ((= c ?\\ ) "\\\\") ((= c ?\" ) "\\\"") 
               ((= c ?\' ) "\\'")  ((= c ?\t ) "\\t")
               ((= c ?\012) "\\n") ((= c ?\015) "\\r")
               ((= c ?\010) "\\b") ((= c ?\014) "\\f")
               ((< c 32) (format "\\u%0.4x" c))
               (t (char-to-string c)))) str)))
(cond
 ((and (<= emacs-major-version 22)
       (string-match
        ".*:1:14" (format "%s" (getenv "__CF_USER_TEXT_ENCODING"))))
  ;; emacs22 and CFUserTextEncodingが MacJapanese の場合
  (defun applescript-string-literal (str)
    "do-applescriptに渡す文字列リテラルを作成"
    (let ((reslst '()))
      (mapc
       '(lambda (ch)
          (cond ((= ch ?\\) (setq reslst (cons 128 reslst)))
                ((= ch ?\") (setq reslst (cons 34 reslst)))
                ((consp (car reslst))
                 (setcar reslst (cons ch (car reslst))))
                (t (setq reslst (cons (list ch) reslst)))))
       str ;; (append str nil)
       )
      (if (null reslst) "\"\""
        (mapconcat
         '(lambda (x)
            (if (consp x)
                (concat "\"" (reverse x) "\"")
              (format "ascii character %d" x))
            ) (reverse reslst) " & ")) 
      )))
 (t ;; emacs23 or CFUserTextEncodingがMacJapanese以外の場合(暫定)
  (defun applescript-string-literal (str)
    "do-applescriptに渡す文字列リテラルを作成"
    (concat "\""
            (replace-regexp-in-string ;; convert " => \"
             "\\\"" "\\\\\""         
             (replace-regexp-in-string ;; convert \ => \\
              "\\\\" "\\\\\\\\"
              str)) "\""))))

(defun safari-script-open ()
  "on openWithWait(theUrl, timeoutSec) \n\
     tell application \"Safari\" \n\
       open location \"about:blank\" \n\
       activate \n\
       delay 0.5 \n\
       do JavaScript \"location.href='\" & theUrl & \"';\" in document 1 \n\
       repeat timeoutSec times \n\
         delay 1 \n\
         if ((URL of document 1 as text) does not start with \"about:\") then \n\
           set state to do JavaScript \"document.readyState\" in document 1 \n\
           if (state = \"complete\") then return true \n\
         end if \n\
       end repeat \n\
       return false \n\
     end tell \n\
   end openWithWait \n\
\n\
   on waitForLoad(timeoutSec) \n\
     tell application \"Safari\" \n\
       delay 0.5 \n\
       repeat timeoutSec times \n\
         delay 1 \n\
         if not my isMarkDoc() then \n\
           set state to do JavaScript \"document.readyState\" in document 1 \n\
           if (state = \"complete\") then return true \n\
         else \n\
           -- log (\"markd\") \n\
         end if \n\
       end repeat \n\
       return false \n\
     end tell \n\
   end waitForLoad \n\
   on markDoc() \n\
     tell application \"Safari\" \n\
       do JavaScript \"document['(@_@)'] = '@_@';\" in document 1 \n\
     end tell \n\
   end markDoc \n\
   on isMarkDoc() \n\
     tell application \"Safari\" \n\
       set scp to do JavaScript \"document['(@_@)'];\" in document 1 \n\
       -- log (\"scp=\" & scp) \n\
       return scp = \"@_@\" \n\
     end tell \n\
   end isMarkDoc ")

(defun translate-with-excite (text is-waei)
  "TEXTをexciteで翻訳する。IS-WAEIが t ならば和英、nilならば英和"
  (do-applescript
   (concat
    (safari-script-open) "\n"
    (encode-coding-string
     (format "\
   tell application \"Safari\" \n\
     set theUrl to \"http://www.excite.co.jp/world/\" \n\
     if (my openWithWait(theUrl, 40)) then \n\
       tell document 1 \n\
         set orgsent to %s as unicode text \n\
         do javascript \"document.forms.world.before.value = '\" & orgsent & \"';\" \n\
         do javascript \"document.forms.world.wb_lp.selectedIndex = %s;\" \n\
         my markDoc() \n\
         do javascript \"document.forms.world.submit();\" \n\
         if (my waitForLoad(30)) then \n\
           set aftval to do javaScript \"document.getElementById('after').value\" \n\
           set the clipboard to aftval \n\
         else \n\
           display dialog \"Cannot submit\" \n\
         end if \n\
       end tell \n\
     else \n\
       display dialog \"Cannot open \" & theUrl \n\
     end if \n\
   end tell \n"
             (applescript-string-literal (escape-js-string text))
             (if is-waei 1 0))
     (if (>= emacs-major-version 23) 'utf-8 'sjis-mac) )))
  )

(defun translate-with-google (text from-lang to-lang)
  "Translate TEXT FROM-LANG into TO-LANG with google\n\
   from-lang, to-lang:    en,ja ...\n\
   The translation result is copied into the clipboard. "
  (let ((url
         (format
          "http://translate.google.co.jp/translate_t?prev=hp&hl=ja&js=y&text=%s&sl=%s&tl=%s"
          (my-escape-url text 'utf-8)
          from-lang to-lang)))
    (do-applescript
     (concat
      (safari-script-open) "\n"
      (format "\
   tell application \"Safari\" \n\
     set theUrl to \"%s\" \n\
     if (my openWithWait(theUrl, 30)) then \n\
       tell document 1 \n\
         set rval to do javaScript \"document.getElementById('result_box').innerText\" \n\
         set the clipboard to rval \n\
       end tell \n\
     else \n\
       display dialog \"Cannot open \" & theUrl \n\
     end if \n\
   end tell \n" url)))))

(defvar browse-translate-japanese-hist nil)
(defun browse-translate-japanese (s e)
  "Translate region text into japanese or english\n\
   If 8-bit character is included in the region then translates into English,\n\
  otherwise translates into Japanese. \n\
   The translation result is copied into the clipboard. \n\
   Translate engine is google or excite, default is excite"
  (interactive "r")
  (save-excursion
    (let ((text (buffer-substring-no-properties s e)))
      ;; (let ((waei-p (multibyte-string-p text)))
      (let ((waei-p (string-match "[^\000-\177]"
                                  (encode-coding-string text 'utf-8))))
        (let ((engine
               (read-from-minibuffer
                (format "%s using x(excite) or g(google) > "
                        (if waei-p "To ENGLISH" "To JAPANESE" ))
                (if (consp browse-translate-japanese-hist)
                    (car browse-translate-japanese-hist) "x")
                nil nil 'browse-translate-japanese-hist)))
          (cond ((member engine '("g" "G" "google")) ;; use google
                 (translate-with-google
                  text (if waei-p "ja" "en") (if waei-p "en" "ja")))
                ((member engine '("x" "X" "e" "excite")) ;; use excite.co.jp
                 (translate-with-excite text waei-p))
                (t (message "Unknown translate engine"))))))))

インストール

  1. 上のソースを、browse-translate-site.el というファイルにセーブ
  2. browse-translate-site.el をどこか、load-pathが切ってあるフォルダーに移動。
  3. (ex. $ sudo mv browse-translate-site.el /Applications/Emacs.app/Contents/Resources/site-lisp/ )
  4. .emacs.el に以下を追加
  5. (autoload 'browse-translate-japanese "browse-translate-site"
              "Translate region text into japanese or english" t nil)
    (global-set-key "\C-ch" 'browse-translate-japanese)
    
  6. emacs再起動

使い方

  • emacsのバッファーでリージョンを指定し、Control-C と h を叩く。
  • ミニバッファーに何か聞いてくるので、 excite翻訳なら x を、google翻訳なら g を指定。
  • Safariが開き、exciteなりgoogleの翻訳結果が表示されるはず。 (exciteの時はちょっと時間がかかる)

翻訳結果はクリップボードにコピーされるので、必要なら、 emacsに戻って結果をペーストすることもできる。また、英訳するか和訳するかは、 リージョンに8bit文字があれば英訳、なければ和訳するように判断させている。

elispの中で使っているapplescriptは、 Safariでページロードを待つ処理 を参照。
__CF_USER_TEXT_ENCODING を、 標準の ユーザID:1:14 以外に設定していると、日本語が変になって動かないかもしれない。

追記  (2009年 11月4日 水曜日)
cocoa emacs (emacs23.1) 用に修正