coding()ハンドラの真価と広がる世界

昨日実装したcoding()ハンドラは、少し大袈裟かもしれないけど、実は自分の中では長年の夢だった。


on coding(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
try
"require \"jcode\";$KCODE=\"u\";/\\{.*\\}/=~" & quoted form of msg & ";puts($&);" --rubyコード
do shell script "ruby -e " & quoted form of result --シェルコード
on error
--do shell script262,144バイト制限エラーでも、日本語環境なら救われる
--http://developer.apple.com/jp/technotes/tn2002/tn2065.html
msg's items 1 thru -24 as text --日本語環境以外ではNG
end try
end try
end coding

RubyPerlのハッシュ、JavaScriptPythonPHP連想配列など多くのスクリプト言語は、その呼び名は違うけど、任意のキーで値にアクセス可能な配列を基本機能として備えている。キーの指定は文字列で可能で、変数に代入された文字列でもアクセスできる。キーや値へのアクセスは可能な限り自由。その追加・変更・削除も自由。ハッシュや連想配列に自由にアクセスできてこそ、使いやすい言語環境といえる。
また、ハッシュや連想配列は、多くの言語でシリアライズXMLバイトコードなどに変換してデータとして保存できること)可能だ。そして、多くの言語がJSONの読み書き(デシリアライズシリアライズ)をサポートしている。つまり、JSONを使えば、異なる言語間でもデータの受け渡しが簡単に行えるのだ。そんな環境になっている。

AppleScriptもしかり、似たような機能にレコードがある。ところが、AppleScriptのレコードには自由がない...。

  • 任意のキーでアクセスできるのだが、そのキーは文字列ではない。:コロンで区切られた左側のキーコードとして、コード中で解析されて、キーとして認識される必要がある。だから変数に代入した文字列は、簡単にはキーとして使えない。
  • しかも、一旦登録したレコードのキーは、変更はできるが、削除する方法がない...。
  • また、レコードはファイルに保存できるが、それはAppleScript独自の形式である。人間が見ても意味不明。他の言語がAppleScriptをサポートする訳もなく、AppleScriptのレコードを他の言語に渡すのは非常に面倒だった。
  • 一方でAppleScriptは、他の言語からのデータは上手に受け取る。do shell script や Safari経由のdo JavaScriptは、あちらの世界の処理結果を数値・文字列・リスト・レコードなど最適な形式で返ってくる。

つまり、来るもの拒まず、出るもの出さず。あるいは、人のものもオレのモノ、オレのモノもオレのモノ(ジャイアン)。そんな状態。

  • AppleScriptから他の言語環境にデータを渡すには、do shell script か do JavaScriptでコード生成するしか方法がない。
  • なのにJSONさえサポートされておらず、AppleScript独自のレコード形式でしか書き出せない。(意味ない)
  • そもそもAppleScriptのレコードが使いやすければ、そうそう他の言語に処理を依頼する必要性はないのだが...。
  • 素のAppleScriptでは、リスト・レコードを操作する機能が貧弱過ぎるのだ。

不便を補う工夫:変数の値をキーにしたレコード値へのアクセス

  • レコードに欠けている最も重要な機能だ。
  • 何はともあれ、スクリプト言語の基本と言えるこの機能は絶対に欲しい。
  • これがないと、何をやるにしてもスーパーウルトラ冗長になってしまう。
  • で、今までどうしていたか、というと...Run Scriptを使っていた。
  • Run Scriptは、それに続く文字列をAppleScriptとして解釈して、実行して、結果を返す。
  • なので以下のようにすると、a_keyに代入した"b"を利用してレコードにアクセスできるのだ。


set a_key to "b"
run script "{a:1,b:2,c:3}'s" & a_key

  • ところで、上記のままでは {a:1,b:2,c:3} 固定のレコードしか扱えない。
  • coding()ハンドラを手に入れた今なら簡単にレコードをコード化できるが、以前は以下のようなハンドラを作って凌いでいた。


on for_key2(a_record, a_key) try
((path to temporary items) as text) & "__value_for_key_record__.tmp"
set f to open for access file (result) with write permission
write a_record to f
end try
close access f
set res to run script "(read file (((path to temporary items) as text) & \"__value_for_key_record__.tmp\") as record)'s |" & a_key & "|"
--do shell script "rm ${TMPDIR}TemporaryItems/__value_for_key_record__.tmp" --削除を有効にすると遅い(2倍かかる)
res
end for_key2

  • レコードの内容をRun Scriptの世界に渡すため、
  • 一旦ファイルとして保存、
  • それをRun Scriptでレコードとして読み出している。
  • 消し忘れても再起動で削除される一時フォルダを利用したり、
  • パスワードなど、重要なデータを扱う時は直ぐに削除する努力をしたり、
  • 今思えば、相当涙ぐましい努力をしながら、キー値アクセスをしている。
coding()ハンドラ以降
  • coding()ハンドラを使えば、たった1行で済む!


on for_key(a_record, a_key) run script coding(a_record) & "'s |" & a_key & "|"
end for_key

  • シンプルかつ分かり易い、おまけに処理も速い!
  • ついでに、set_key()ハンドラも追加しておいた。
  • レコードに、キーと値を追加するのだ。(キー値が同じなら上書き)
    • ちなみに、coding()ハンドラは不要である。


on set_key(a_record, a_key, a_value)
(run script "{|" & a_key & "|:" & a_value & "}") & a_record
end set_key

不便を補う工夫:JSONに変換する

  • AppleScriptによる自動ログイン・自動入力を実現するauto_loginスクリプト。
  • iPhoneiPadでも自動ログインを実現するため、ブックマークレットに暗号化したログイン情報を埋め込む必要があった。
  • 但し、そのためにはレコードに保存しているログイン情報をJSONに変換してJavaScript側に渡さなくてはならない。
  • coding()ハンドラを知らなかった当初、悶々と1週間、寝ても覚めても悩んでいた。
coding()ハンドラ以降
  • coding()ハンドラを覚えた今、そんな悩みは皆無である!


--リスト・レコード複合体をJSONコードに変換する
--(リストの{}[]に変換する、リスト内の|を削除する)
on json_from(list_recode) coding(list_recode) _parse_list_record(result's items, 1) as text
replace(result, "|", "") end json_from
--json_fromから呼び出され、再帰的にリストの{}[]に変換する
on _parse_list_record(str_list, i) set output to {} repeat
set v to str_list's item i
if v = "{" then
set box to _parse_list_record(str_list, i + 1) set output's end to box
set i to i + (box's length) else if v = "}" then
set obj to run script "{" & output & "}"
if obj's class = list then
return "[" & output & "]"
else
return "{" & output & "}"
end if
else
set output's end to v
set i to i + 1
end if
if i > str_list's length then exit repeat
end repeat
output
end _parse_list_record

  • リスト・レコード複合データを、JavaScriptが認識するJSONに一発で変換できるのだ。
  • リストの括弧を[]に変換して、予約語とバッティングするキーに付加された|を削除している。
    • ちなみに、厳格なJSON形式ではない。
    • SafariJavaScriptはちゃんと認識した。

JSONについて

  • JSON(=JavaScript Object Notation)はその名のとおり、JavaScriptと非常に相性が良い。
  • do JavaScriptで実行すると、JSONで返したデータは、リスト・レコード複合データとして取得できる。


tell application "Safari"
"var json={a:1,b:2,c:3};json['d']=4;json;"
do JavaScript result in document 1
end tell

--結果:{d:4.0, b:2.0, c:3.0, a:1.0}

  • 数値に少数表示が付加されているのは気になるが、値は同じ。
  • また、返り値のレコードの要素の順序も保証されないようだ。
  • ちなみに、do JavaScriptを実行するためには、Safariで最低一つのタブ(あるいはウィンドウ、ドキュメント)が開いている必要がある。
  • Safariを常用している環境では問題ないかもしれないが、他のブラウザを常用していると、Safariを起動して、make new documentする必要があるかも。

不便を補う工夫:JavaScriptとの連携

  • 自在にJSONに変換できるようになった今、AppleScriptのレコードに不足する機能は、JavaScriptで補うことも簡単。
    • 但し、果たしてそれがベストなのかどうかは疑問...。*1
レコードのキーを取り除く


--レコードから指定したキーを取り除いたレコードを返す(元のレコードは変化しない)
on reject_key(a_record, a_key) if a_record's classrecord then return ""
set json to json_from(a_record) tell application "Safari"
if documents's number = 0 then make new document
"json=" & json & ";delete(json['" & a_key & "']);json;"
do JavaScript result in document 1
end tell
end reject_key

レコードのキーをリストで返す


on record_keys(a_record) if a_record's classrecord then return ""
set json to json_from(a_record) tell application "Safari"
if documents's number = 0 then make new document
"var a=" & json & ";var k=[];for(i in a){k.push(i)};k;"
do JavaScript result in document 1
end tell
end record_keys

ダウンロード

以上のハンドラは、依存するreplace()ハンドラも含めて、_json.scptにまとめた。


on split(sourceText, delimiter) --considering «constant conszkhk»--AppleScript2.0以降は無効
if sourceText = "" then return {} set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set theList to text items of sourceText
set AppleScript's text item delimiters to oldDelimiters
return theList
--end considering
end split

on join(sourceList, delimiter) --considering «constant conszkhk»--AppleScript2.0以降は無効
set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set theText to sourceList as text
set AppleScript's text item delimiters to oldDelimiters
return theText
--end considering
end join

on replace(sourceText, text1, text2) join(split(sourceText, text1), text2) end replace

所感

  • 以上のように、coding()ハンドラは様々な処理のベースとなる機能を提供する。
  • coding()ハンドラによって、今まで諦めていたことが可能になり、新たな世界が広がった。
  • オブジェクトをソースコードに変換する機能は非常に重要。
  • 本来は、AppleScriptが基本機能として備えていて欲しい。
  • 例外処理をうまく利用することで、想像以上の機能を追加できる。
  • そいえば、RailsRubyの例外処理を思いきり活用することで、便利な機能を追加していたのを思い出した。
  • 例外処理を利用することは、良くも、悪くも(セキュリティホールなど)何らかの突破口になることが多い。
  • AppleScriptの強みは、対応アプリケーションを手軽にコントロールできるところ。
  • また、do shell script、do JavaScriptを経由して、様々な言語環境を活用できるところ。
  • テキスト入力、アラート表示、リスト選択、ファイル選択など、貧弱だけど若干のGUIも提供する。
  • それらを活用して異なる環境を繋げて連携させるという役割がある。
  • coding()ハンドラによって、言語環境の連携がより強化されたのだ。
  • 非常に癖のある言語だが、OSX環境においては、その連携の魅力は他のどの言語も敵わない。

それがAppleScriptを使い続ける理由になっているのかも。

*1:それならJavascriptを使えばいい?しかし、JavaScriptのみでは、ブラウザ環境から外にアクセスできない(と思っている)という制約があるので...。