2009年9月21日月曜日

emacsからスクリプトエディターを開く

emacsで開いているapplescriptソースコードを、 ScriptEditor上で開くemacs lisp コマンド。

applescript-browse

(defun applescript-browse-url (script &optional action)
  "Convert script to applescript URL\n\
   action --- \"new\"(default) or \"insert\" or \"append\"\n\
   ex. (applescript-browse-url \"say time string of(current date)\")"
  (format "applescript://com.apple.scripteditor?action=%s&script=%s"
          (if action action "new") (url-hexify-string script)))

(defun applescript-browse (pfx)
  "バッファーの内容をApplescript URL protocolを使って、スクリプトエディター上に開く。\n\
  生成されたURLはキルリングに追加される。\n\
  リージョンを対象とする場合は、C-u で実行する。"
  (interactive "P")
  (save-excursion
    (let ((url
           (applescript-browse-url 
            (encode-coding-string
             (my-trim-string
              (apply (function buffer-substring-no-properties)
                     (if (equal pfx '(4))
                         (list (region-beginning) (region-end))
                       (list (point-min) (point-max)))))
             'utf-8))))
      (kill-new url)
      (browse-url url)
    )))

(defun my-trim-string (str)
  "stringの両端の空白を取り除く"
  (replace-regexp-in-string
   "^[ \\\t\\\r\\\n]+" ""
   (replace-regexp-in-string "[ \\\t\\\r\\\n]+$" "" str)))

使い方

M-x applescript-browseで、カレントバッファーの全内容が、 スクリプトエディターで開かれる。
リージョンを対象とする場合は、 C-u M-x applescript-browse で実行する。
また、applescript:// で始まるURLをキルリングに追加しているので、 applescript-browseを実行後、C-yでそれを貼付ける事ができる。

仕組みは、Applescript URL protocol supportというのを使って、 単にbrowse-urlしているだけだ。
URL protocol supportについては、 Script NoteさんAppleScript/URL PROTOCOL SUPPORT に、とてもわかりやすい説明がある。

2009年9月12日土曜日

do-applescript 専用の文字列リテラル関数

Carbon emacsには、emacsから applescriptを実行する関数として do-applescript というのがある。とても便利なのだが、applescript に文字列リテラルを渡す時のエスケープ処理が面倒なので、 専用の関数 applescript-string-literal を作ってみることにした。
つまり、
(do-applescript "display dialog \"any-string\"")
の赤色の部分を作成する関数だ。
(do-applescript (format "display dialog %s" 
                    (applescript-string-literal "any-string")))
のように使う。

最初は、単にバックスラッシュ使って、 ダブルクォートとバックスラッシュ自身をエスケープするだけでいいと思っていたのだが、 leopardの文字列処理はそう甘くはなかった。

どこが甘くなかったのか.....
例えば、
(do-applescript "display dialog \"Hello\"")
はちゃんと、Helloというメッセージダイアログを表示するのだが、 Helloを、"Hello" にかえて、
(do-applescript "display dialog \"\\\"Hello\\\"\"")
を実行すると、何故かエラーになってしまう。 バックスラッシュ(\)によるダブルクォート(") のエスケープ処理が効いていないみたいだ。

これは、バックスラッシュと円記号 にまつわるややこしい問題なのかと思い、試しに
(do-applescript "display dialog \"Hello\\\"")
を実行。すると、 Hello\ ではなく、 Hello と表示された。
スクリプトエディターで、\のコードを確認すると、
ASCII NUMBER "\\" は 128
ASCII NUMBER "¥" は 92
と評価される。(実際には¥はoption-\で入力)
ということは、 do-applescriptで送るバックスラッシュコード(asciiの92) が、applescript では半角の¥記号と認識され、 それで文字列リテラルのエスケープが効かないのか?
確かに、applescriptの ascii character 128 を使って、
(do-applescript "display dialog \"Hello\" & ascii character 128")
を実行すれば、正しく Hello\ と表示される。
めんどくさいけど、 バックスラッシュを ascii character 128 に、 ダブルクォートを ascii character 34 にして、それらと文字列リテラルを & でくっつければいいらしい。

でも、以前は確かこんな症状は起きなかったような気がする。 僕自身が何か変な環境設定でも行ったのだろうか....
調べてみると、環境変数 __CF_USER_TEXT_ENCODING が関係していることがわかった。この環境変数を UTFエンコードを表す 0x08000100 に設定して、
(例 : export __CF_USER_TEXT_ENCODING=`printf 0x%X $UID`":0x08000100:14")
emacsを再起動すると、
(do-applescript "display dialog \"\\\"Hello\\\"\"")
も正常に動作し、これまでの問題は全て解決する。しかし今度は日本語が化けてしまう。 デフォルトの $UID:1:14では shift_jisでエンコードすれば問題なかったのに......

結局、 __CF_USER_TEXT_ENCODING が ShiftJisの場合は、文字列リテラルと ascii character 文の結合で処理し、 それ以外の場合は単純なエスケープ処理を施すように applescript-string-literal 関数を実装することにした。

applescript-string-literalソースコード
(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に渡す文字列リテラルを作成\n\
        バックスラッシュとダブルクォーテーションをそれぞれ\n\
        ascii character 128 と ascii character 34に変換\n\
        ex. (applescript-string-literal \"\\\"abc\\\"\")\n\
        => \"ascii character 34 & \\\"abc\\\" & ascii character 34\""
    (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)) "\""))))

この applescript-string-literal を使って、
(do-applescript
  (format "display dialog %s"
     (applescript-string-literal "Hello \"backslash\"-\\")))
を実行すると、めでたく Hello "backslash"-\ が表示される。

__CF_USER_TEXT_ENCODINGがデフォルトのShiftJisの状態なら、 日本語だって、
(do-applescript
  (format "display dialog %s"
    (encode-coding-string
      (applescript-string-literal "バックスラッシュ \"\\\"を表示")
      'shift_jis)))
ちゃんと バックスラッシュ"\"を表示 のダイアログが表示される。

.... でも、本当は何かもっとスマートな方法があるんだろなあ....

追記  (2009年 11月4日 水曜日)
cocoa emacs (emacs23.1) の場合は、ascii characterへの変換は必要なく、 日本語もエンコードせずにそのまま送ればいいようだ。

2009年9月3日木曜日

schemeメモ - quoteで作ったリスト

単純に次ぎのコード、
(define (foo) '(a b c))
(foo) 
  ==> (a b c)
fooはどってことのない関数だが、実は、この書き方はとても危ない。

プログラムのどこかで、fooの出力を破壊的に変更してしまうと、
(reverse! (foo)) 
  ==> (c b a)
(foo) 
  ==> (a)
もはや、関数fooは (a b c)を返さなくなる。

vectorも同じだ。
(define (foo) '#(a b c))
(vector-set! (foo) 0 'aa)
(foo)
  ==> #(aa b c)

準クォートを使ったこれもだめだ
(define (foo x) `(,x a b c))
(foo 1)
  ==> (1 a b c)
(reverse! (foo 2))
  ==> (c b a 2)
(foo 1)
  ==> (1 a 2) 

しかしこれは大丈夫 (少なくともgoshでは)
(define (foo x) `(a b c ,x))
(foo 1)
  ==> (a b c 1)
(reverse! (foo 2))
  ==> (2 c b a)
(foo 1)
  ==> (a b c 1) 

無難な書き方はこうかな?
(define (foo x) (append `(,x a b c) '()))
(foo 1)
  ==> (1 a b c )
(reverse! (foo 2))
  ==> (c b a 2)
(foo 1)
  ==> (1 a b c )

まとめ
  • クォートで作ったリストやベクターは、静的にアロケートされている可能性がある
  • クォートで作ったリストやベクターに対し、直接に破壊操作を行わない。
  • 知らない関数の出力を破壊する時は、 前もってコピーしたものを破壊するのが無難。

しかしこれに慣れると、他の言語で同様なことを書くのに躊躇してしまいそうだ。
例えば、rubyだと
def foo ; return [1,2,3] ; end
foo.reverse!
  => [3, 2, 1]
foo
  => [1, 2, 3]
で全然問題ないのに、
def foo ; return [1,2,3].clone ; end 
と書いてしまうとか......

schemeメモ - appendの最後の引数

append の最後の引数はコピーされない!

R5RS の append の説明には、
The resulting list is always newly allocated, except that it shares structure with the last list argument.
と書いてある。荒っぽく訳すと、
appendは、いつも新規にアロケートしたリストを結果として返す。 ただし例外があり、最後の引数は構造を共有する。
ということは、(append list ... listN) の listN はコピーされないということか。 今までこんなことも知らずにコードを書いていたとは..... (^^;

実験してみた

> (define lst '(a b c))
> (eq? lst (cdr (append '(a) lst)))
  ==> #t  
確かに、最後の引数(a b c) は共有され、新規のアロケートはされていない。

もちろんこれもアロケートされない
> (eq? lst (append lst))
  ==> #t

とにかく最後の引数に () を置けば、みんなアロケートされる
> (eq? lst (cdr (append '(a) lst ())))
  ==> #f

単なるリストのコピーをしたければ
> (eq? lst (append lst ()))
  ==> #f
> (equal? lst (append lst ()))
  ==> #t

2009年9月1日火曜日

include マクロ

includeというマクロを書いてみた。 別ファイルに書かれてあるS式を、ソースの中に埋め込むマクロだ。 loadと似ているが、呼び出した場所の環境で評価される点が違う。

include.scm

;; ファイルに書かれているS式を埋め込むマクロ。
;; 使い方:  (include ファイル名)
;; ただし、ファイル名には文字列かトップレベルの変数しか指定できない
(defmacro include (fname)
  (let ((read-all
         (lambda (port)
           (let lp ((e (read port)) (result (list)))
             (if (eof-object? e) (reverse! result)
                 (lp (read port) (cons e result)))))))
    `(begin ,@(call-with-input-file 
                (eval fname (interaction-environment))
                (lambda (p) (read-all p))))))

適当なサンプルで動作確認

$ cat tmp2.scm    # 埋め込むファイル
  (display 
     (format "a=~A b=~A (foo a b) = ~A\n" a b (foo a b)))
  (foo a b)

$ gosh
  gosh> (load "./include.scm")
  gosh> (define file "tmp2.scm")
  gosh> (define foo +) (define a 10) (define b 20) ;; toplevel設定
  gosh> (let ((foo *) (a 11)) (load file))    ;; loadの実行結果
     =>  a=10 b=20 (foo a b) = 30
         #t
  gosh> (let ((foo *) (a 11)) (include file)) ;; includeの実行結果
     => a=11 b=20 (foo a b) = 220
        220
  gosh> ^D

変数 foo,a,bについて、 loadの場合はトップレベルの定義で評価されているが、 includeでは、(let ...) バインドの値で評価されているのが確認できる。

gosh,chezでの defmacro

僕は、scheme処理系として、gosh , kawa, petite-chez-scheme の三つを使っているのだが、 このうち、goshと petite にはデフォルトでdefmacroがない。
マクロ定義のソースファイルを処理系で同じにしたいので、goshとpetiteでの defmacroの定義方法を調べてみた。

goshでは define-macroを使って簡単にdefmacroらしきものが定義できた。
  (define-macro (defmacro name arglst . bodies)
    `(define-macro ,(cons name arglst) ,@bodies))

petiteには、define-syntax系の健全なマクロしかない。 syntax-caseというのを使えば何でも定義できそうだが、 syntax-case構文はなんだかとっても難しそうで僕の理解範疇を超えている。
幸いにも、petiteのダウンロードパッケージの中に、 examples/compat.ss というファイルがあり、この中に、 syntax-caseで定義したdefine-macroとdefmacroのソースをみつけた。 これをそのまま使えばなんとかなりそうだ。