おばあちゃんの知恵袋

知られているようで、案外知られていないかもしれないかもしれない(?)AppleScript ネタの整理。

最初にお断り。以下の文章内で「«」と「»」がでてきますが、「«」 は、Option キー + バックスラッシュキー。「»」 は、Option キー + Shift キー + バックスラッシュキー。HTML では、そのまま表示できない文字なので代替です。特に細かい部分を説明せずにずらずらと並べていきます。

まず、ダイアログを表示せずにログアウトするスクリプト。

Script Editor で開く

tell application "loginwindow" to «event aevtrlgo»

次は、ファイル書き出し関連。まずは、以下のスクリプトを試してみます。

Script Editor で開く

the clipboard as record

このスクリプトを実行する前に TextEdit なんかで複数のフォントや色を使った文字列をコピーしておきます。コピーしてから上記のスクリプトを実行すると結果の部分にずらずらっと AppleScript が表示できないデータが表示されます。このデータの中に «class utf8» とか «class rtf » 等があります。次のようにすると clipboard から UTF-8 で文字列を取り出すことができます。

Script Editor で開く

«class utf8» of (the clipboard as record)

例えば、次のようにすることで UTF-8 でファイルに書き出すことができます。

Script Editor で開く

on writeFile(theFile, theData)
    set fh to open for access file theFile with write permission
    try
        set eof fh to 0
        write theData as «class utf8» to fh
        close access fh
        return true
    on error errMessage number errNumber
        try
            close access fh
        end try
        return false
    end try
end writeFile

読み込むときも同様で as «class utf8» で UTF-8 で読み込むことができます。また、Safari で表示している Web ページ等をコピーして次のようにして RTF ファイルとして書き出すこともできます(画像はついてきませんが)。

Script Editor で開く

set theData to «class RTF » of (the clipboard as record)

set theFile to (path to desktop folder as Unicode text) & "Write-RTF.rtf"
set fh to open for access file theFile with write permission
try
    set eof fh to 0
    write theData to fh
    close access fh
on error eMessage number eNumber
    try
        close access fh
    end try
    return {eNumber, eMessage}
end try

どんどん進みましょう。次は、a reference to について。

a reference to は、リストの処理を高速に行いたいときなどに使われることがよくありますが、ハンドラとのデータのやり取りに利用することができます。この辺りのことは AppleScript PARKTips に詳しいです。

通常、ハンドラは複数の引数と単一の戻り値で作られます。たとえば、先のファイル書き出しハンドラ writeFile(theFile, theData) は、二つの引数をとり、真偽値を返します。しかし、エラーが起きたときにはエラーの内容をダイアログで表示させたいときがあります。このようなとき、エラー番号とエラーメッセージを返すようにハンドラを変更するか、ハンドラ内でダイアログで表示する、という方法が考えられます。

上記のファイル書き出しハンドラなどでは、ダイアログを表示しても問題ないので困ることはないのですが、Image Events 等、一部のバックグラウンドアプリケーションは、tell ブロック内でダイアログを表示できないので困るときがあります。そこで、以下のようにして a reference to を用いてハンドラの返り値以外にエラー情報をやり取りします。

Script Editor で開く

on run
    set imageFile to choose file without invisibles
    set saveFile to choose file name
    set errorInfo to {|errorNumber|:missing value, |errorMessage|:missing value}
    set infoRef to a reference to errorInfo
    my resizeImage(imageFile, saveFile, 320, infoRef)
    errorInfo
end run

on resizeImage(theFile, saveFile, maxSize, theInfoRef)
    tell application "Image Events"
        try
            launch
            set imageRef to open file theFile
            scale imageRef to size maxSize
            save imageRef in file saveFile as JPEG without icon
            metadata tag "adfser" of imageRef -- わざとエラーを発生
            close imageRef
            return true
        on error eMessage number eNumber
            try
                close imageRef
            end try
            set contents of theInfoRef to {|errorNumber|:eNumber, |errorMessage|:eMessage}
            return false
        end try
    end tell
end resizeImage

このようにすると、ちゃんと処理が完了したかどうかは返り値で調べることができ、エラーが起きたときはエラーの内容を調べることができます。ハンドラには参照を渡し、ハンドラ内部では参照の内容を直接書き換える...と、まとめればこういうことです。a reference to の使い方としては、当たり前といえば当たり前なんですが、あまり話題にされることがないような利用方法だと思ったので...。あ、ちなみに個人的によく引っかかってしまうのですが、Image Events を使うときに最初に launch するのをよく忘れてしまいます。launch していないとエラーになるんですね。Image Events って。

毛色の変わったところで say コマンド。英語しかしゃべれない可愛いやつ(?)なんですが、say コマンドで発話のスピードを変更したり、数秒間止まらせたり...そんな方法の紹介。

say では、機能を制御する方法としていくつかのタグが用意されています。例えば、

Script Editor で開く

say "100"

とすると、『One Hundred』とそのまま英語で読み上げます。これをそのまま数値として読ませるには、[[nmbr LTRL]] というタグを使います。

Script Editor で開く

say "[[nmbr LTRL]]100"

こうすると、『One Zero Zero』と発話します。文字列をアルファベットとして読ませるには、[[char LTRL]] タグを使います。

Script Editor で開く

say "[[char LTRL]]AppleScript"

使いたいタグを文中にそのまま埋め込むといいので、利用は至って簡単です。音量を変えたいときは、[[volm real]] を使います(real は、0.1 から 1.0 の実数)。早さを変えたいときは、[[rate int]] を使います(int は、140 から 210 の整数値)。

Script Editor で開く

say "[[volm 0.1]]To be or not to be.[[volm 0.5]] That is the question." -- 音量
say "[[rate 140]]To be or not to be. That is the question." -- 遅く
say "[[rate 210]]To be or not to be. That is the question." -- 速く

読み上げている途中で一時停止させるには、[[slnc int]] を使います(int は、整数値。秒の指定。1000 で 1 秒)。

Script Editor で開く

say "To be or not to be. [[slnc 2000]]That is the question."

これで 2 秒間停止します。また、強調の [[emph -]] や [[emph +]] 等もあります。これらは、次のようにして組み合わせて指定することができます。

Script Editor で開く

say "[[volm 0.3 ; rate 165]]To be or not to be. [[volm 0.8 ; slnc 2000 ; rate 210]]That is the question."

他にも音程の調整等がありますが、これぐらいあれば英語の勉強(?)には十分でしょう。これらのタグは、Apple の資料に記載されているものですので、興味がある方は、参照してみるといいかもしれません。と、思って Apple Developer Connection で探してみたけど、見当たらない。探し方が悪いのかな?

最後に。意外に知られていないのが、これ。

Script Editor で開く

strings of {11, "A", "B", 112}

こうするとリストの中から文字列だけを取り出せます。他にも numbers、reals、integers、lists、records なんかも使えます。また、count 命令はクラスを指定することでそのクラスの数を数えることができます。

Script Editor で開く

count of numbers in {11, "A", "B", 112, {10, 20, 30}, 1.2, 5.6}

そんなこんなで、今年はこれで最後です。では、また。

レコードのふしぎ

ありゃー、Sony...。

いや、もうこれだけでなんとなく察するものがあるという人も多いのではないでしょうか。このサイトでは、あんまり企業や製品のことについて言及することは避けようと思っているのですが、あまりと言えばあまりな成り行きについ。

閑話休題。「AppleScript でアルゴリズム」でクイックソートを掲載したのですが、面白いことをデザイナーの池田さんが教えてくれました。

池田さんのサイトでは「AppleScript 実験室」と題していろいろな実験結果を掲載されています。Mac OS X になってから、AppleScript でいろいろ実験をしてその結果を掲載しているサイトというのは少ないので、こういうサイトはとても貴重です。

さて、その「AppleScript 実験室」には「レコード内のリストの参照」というコンテンツがあります。池田さんが行った実験というのは、「レコード内のリストの要素の参照と通常のリストの要素の参照、どれぐらいアクセス速度が異なるか」というものです(詳しくは、池田さんのサイトの方を参照してください)。おそらく、リストを参照するよりレコード内のリストを参照する方が遅くなるだろう...という予測をもとに実験を行ったのだと思いますが、予想外の結果になっています。レコード内のリストを参照した方が速いのです。

この書き方だと誤解がありますが、リストに a reference to を使うより、レコードに a reference to を使う方が速い、という方が正確ですね。以下の結果を見てもらうと分かりますが。

実験結果をクイックソートに適用したものを送ってきてくださいました。これを動かしてみると、確かに速いのです。なぜ?

クイックソートでは分かりにくいので、以下のようなスクリプトを作って試してみました。

set theList to {}
repeat with i from 1 to 10000
    set end of theList to i
end repeat

set cd to current date
repeat 100000 times
    item 10 of theList
end repeat

(current date) - cd

整数値をリストに 1 万項目入れて、10 番目の要素を 10 万回取り出すだけのスクリプトです。これは、環境によって結果が異なりますが、iMac G5 では約 160 秒。これではあまりにも遅いので、通常は a reference to を使ってリストの参照を用います。

set theList to {}
set theListRef to a reference to theList -- リストを参照に変換

repeat with i from 1 to 10000
    set end of theListRef to i
end repeat

set cd to current date
repeat 100000 times
    item 10 of theListRef -- リストの参照にアクセス
end repeat

(current date) - cd

こうするだけで約 3 秒になります。次に池田さんが行った実験を適用します。

set theList to {}
set theListRef to a reference to theList

repeat with i from 1 to 10000
    set end of theListRef to i
end repeat

set theRecord to {numList:{}}
set theRecordRef to a reference to theRecord -- レコードの参照に変換
set numList of theRecordRef to theList

set cd to current date
repeat 100000 times
    item 10 of numList of theRecordRef -- レコードの参照にアクセス
end repeat

(current date) - cd

こうすると、さらに速くなって約 1 秒。レコードにアクセスする方が速いのかと思い、a reference to を使わずに比較してみるけど、リストのときと変わりません。

set theList to {}

repeat with i from 1 to 10000
    set end of theList to i
end repeat

set theRecord to {numList:{}}
set numList of theRecord to theList

set cd to current date
repeat 5000 times
    item 10 of numList of theRecord -- ここ
end repeat

(current date) - cd

このスクリプトの numList of theRecord を theList に変えても速度的に差はないんですね。a reference to で参照に変換したときだけ速いんです。原因不明。この結果を理解するには、AppleScript の内部に踏み込まないといけないような。どなたか、なぜ速くなるかご存知でしょうか?

誤解のないように、追記。a reference to を使うと速度的には速くなりますが、乱用すると逆に遅くなります。なんでもかんでも速くなるわけではないので。

AppleScript でアルゴリズム

掲示板の方でソートについての話題が出ていたので、取り上げてみる。

AppleScript でアルゴリズム?ソート?と思う方もいるかもしれません。AppleScript は速度的に遅いからソートなんてできない、大量のデータを使う必要があるなら、AppleScript 以外の解決方法を見つけた方がいい。というような印象があるような気がします。

こういったことってケースバイケースなので一概にどんな方法がいいとは言えません。タブ区切りのテキストデータで 2 列目でソートしたいというならシェルスクリプトの sort を使えばいいですし、どうしても動作速度が必要なら、AppleScript Studio の Table View(data source)に表示させてしまえばいいし。

ただ、知的好奇心、あるいは興味のために AppleScript でバブルソートや単純選択ソート、ハッシュマップや二分木といった有名なアルゴリズムを書いてみるのも面白いかもしれません。圧縮や暗号化や検索なんかも。例えば、次のスクリプトは単純なクイックソート。C 言語で紹介されていたものをそのまま AppleScript に置き換えたものです。

Script Editor で開く

set tmp to {}
repeat with i from 1 to 1000
    set end of tmp to i
end repeat

set theList to {}
repeat with i from 1 to 1000
    set end of theList to some item of tmp
end repeat

set cd to current date
qSort(1, count theList, a reference to theList)
display dialog (current date) - cd

(*
bottom = 最小値、top = 最大値、theList = ソートを行うリスト
*)
on qSort(bottom, top, theList)
    if bottom is greater than or equal to top then return

    set base to item bottom of theList
    set lower to bottom
    set upper to top

    repeat while (lower is less than upper)
        repeat while (lower is less than or equal to upper and item lower of theList is less than or equal to base)
            set lower to lower + 1
        end repeat

        repeat while (lower is less than or equal to upper and item upper of theList is greater than base)
            set upper to upper - 1
        end repeat

        if (lower is less than upper) then
            set tmp to item lower of theList
            set item lower of theList to item upper of theList
            set item upper of theList to tmp
        end if
    end repeat

    set tmp to item bottom of theList
    set item bottom of theList to item upper of theList
    set item upper of theList to tmp
    qSort(bottom, upper - 1, theList)
    qSort(upper + 1, top, theList)
end qSort

これでも 1000 項目で 0 〜 1 秒。10000 項目で 8 〜 9 秒。ベースの取り方と再帰の部分、ほとんどソートされているときの最適化を施せば、もう少し速くなるのでは?と思う。

まぁ、AppleScript はアプリケーションを操作できるのでソートも検索もアプリケーションに任せてしまうのが一番手っ取り早いのですが。

消えた NSMovieView

リモート AppleEvent の続きが気になるところですが(気にならない?)、ちょっと中断。

iTunes のポッドッキャスト。音声/映像の配信に興味があって試してみたりしています。ポッドキャストは、RSS なこともあって RSS 同様(わけの分からない説明です)ほっておくとどんどん記事(音声)が溜まっていきます。

テキスト情報ならまとめて読むことも可能ですが...音声をまとめて聞くのは少し気合いを入れないと消化できません。iTunes は、管理するのには最適なんですが。

どうにかならないものか...と思案して、AppleScript Studio でポッドキャストの再生、通知アプリケーションを作ろうと思い立ちました。

現在、こんな画面になっています。プログラム的になんら難しいことをしていないのに、ここまでくるのに 3 日間かかっています。問題は、NSMovieView。これ、Interface Builder からなくなっていませんか?探したが見当たらず。仕方がないので Developer Tools 付属のサンプル Talking Head の NSMovieView をコピーしていました。しかし、低機能すぎます。

もしかして、QTMovieView を使えってことなのでしょうか?といっても、AppleScript Studio には QTMovieView を使うための Class は用意されていません。QTMovieView に tell movie view 〜 としてもエラーになるばかり。

最終的に NSMovieView はあきらめて、おとなしく QTMovieView を使うことに決定。もちろん、call method で操作します。って、なんで音声や映像を再生するだけのために call method を使わにゃならんねん。

リモート AppleEvent (3)

有線 LAN で接続した Mac を遠隔操作。一回目は、前準備。二回目は、スクリプトを書いて Mac を動かしました。今回は、前回提起された問題。認証ダイアログの抑制について。

そもそも、パスワードやユーザ名などは、Mac の場合「キーチェーンアクセス」というアプリケーションが一括管理してくれます。もちろん、リモート AppleEvent でのホスト側のこういった情報も管理してくれています。

キーチェーンアクセスは、/Application/Utilities/ にあります。起動します。デフォルトの設定を変えていないなら、ログインというキーチェーンがあると思います。前回のスクリプトを実際に試してみたなら、ホスト側の名前がついたインターネットパスワードという種類のキーがあると思います(この場合、iBook800.local)。ない場合は、作成してください(作成方法は割愛)。

これを選択するとキーチェーンアクセスのウィンドウ上部に情報が表示されると思います。アカウント(ユーザー名)と場所(eppc://iBook800.local:3031)が書かれています。前回、ホストを指定するのに eppc://iBook800.local しか指定していませんでした。最後のコロン以下の 3031 は、リモート AppleEvent が利用するポート番号です。

キーチェーンアクセスにこうやって保存されているなら、AppleScript では「Keychain Scripting」というバックグラウンドアプリケーションを使ってキーチェーンや登録されているキーの情報を取得することができます。

ここで問題が出てきます。Mac OS X 10.3 と 10.4 の Keychain Scripting は、バグがあってそのままではすんなりと利用できません。10.4.2 のアップデータでなおったとありますが、本当はなおっていません。この問題を回避するためにアップルは情報を公開しています。しかし、ここで紹介されているスクリプトを試してもエラーになります。というのも、このスクリプト自体が間違っているから。

Keychain Scripting は、パスワードを要求されると「Keychain Scripting でエラーが起きました:アプリケーションは実行されていません。」とエラーが表示されます。これが、Keychain Scripting のバグです。

tell application "Keychain Scripting"
    password of key 1 of keychain 1
end tell

-- Keychain Scripting でエラーが起きました:アプリケーションは実行されていません。

なぜ、こんなエラーが起きるかの原因は分からないですが、次のような手順で回避できます。(1) Keychain Scripting が起動しているなら、一度終了させる。(2) Finder で開く。アップルも同じような解決方法を掲載しているわけですが、(1) の部分に問題がありそのままでは利用できないものになっています。

掲載されているものでは System Events で Keychain Scripting のプロセスを調べて System Events の中で Keychain Scripting を終了させています。

tell application "System Events"
    quit targetApp
end tell

これが間違い。System Events ではアプリケーションを指定して終了させることはできません。結果、プロセスはそのまま残るので再度エラーになります。まずは、このバグを回避するために次のようなスクリプトを作成します。

on run
    startKeychainScripting()

    tell application "Keychain Scripting"
        password of key 1 of keychain 1
    end tell
end run

on startKeychainScripting()
    -- Keychain Scripting の起動確認
    tell application "System Events" to set bool to exists process "Keychain Scripting"

    -- 起動しているなら終了
    if bool then
        keychainScriptingKiller()
        -- System Events からプロセスが見えなくなるまで
        --これがないと Finder でエラーが発生する
        repeat
            tell application "System Events" to set bool to exists process "Keychain Scripting"
            if not bool then exit repeat
        end repeat
    end if

    -- Keychain Scripting の再起動
    restartKeychainScripting()
end startKeychainScripting

on restartKeychainScripting()
    -- kscr は、Keychain Scripting のクリエータータイプ
    -- com.apple.KeychainScripting でも可
    -- Finder で Keychain Scripting を起動
    tell application "Finder" to open application file id "kscr"

    -- System Events からプロセスが見えるようになるまで
    --これがないと次の処理でエラーになる
    repeat
        tell application "System Events" to set bool to exists process "Keychain Scripting"
        if bool then exit repeat
    end repeat
end restartKeychainScripting

on keychainScriptingKiller()
    tell application "Keychain Scripting" to quit
end keychainScriptingKiller

いつもよりコメントを多めにしています。安全のために Keychain Scripting を利用する度に「終了/Finder で開く」の処理を行っておきます。

これが他の環境でも動くかどうか分からないですが。シェルを使っての起動、終了でもいいと思います。もし、動かない場合はシェルでやってみるといいかもしれません。

しかし、キーチェーンを利用するまでに時間がかかりますね...。

リモート AppleEvent (2)

さて、前回の準備はできたでしょうか? Ethernet で接続した iBook と iMac。iMac の方から iBook を AppleScript で操作しましょうというこの試み。この手の情報って探してみてもなかなか見つからなかったりします。

「システム環境設定」の「共有」の「サービス」タブにある「リモート AppleEvent」にチェックを入れたらこれだけで Mac を遠隔操縦することができます(要注意。セキュリティについてはここでは触れません)。では、スクリプトを書いて試してみます。

通常、Finder のスクリプトを書くときは、次のようにして Finder を指定します。

tell application "Finder"
    (* ここに処理を記述 *)
end tell

リモート AppleEvent で接続された Mac(ホスト)の Finder を操作するには、次のように Finder を指定します。

application "Finder" of machine "eppc://Mac の名前"

ホスト側の Mac の URL 指定が追加されます。「eppc://」は、リモート AppleEvent が利用するプロトコルです。「Mac の名前」は、「システム環境設定」の「共有」で設定したコンピューターの名前になります。iBook には、iBook800 という名前を付けました。これは、「共有」の「コンピューター名」の下の「ローカルサブネット上のほかのコンピュータから、iBook800.local でこのコンピュータにアクセスできます」というように書かれている「iBook800.local」になります。Bonjour で使われるコンピューター名になるのですが、同じものをリモート AppleEvent でも利用します。

先ほどの Finder の指定は、次のようになります。.

tell application "Finder" of machine "eppc://iBook800.local"
    (* ここに処理を記述 *)
end tell

なんらかの処理を記述して構文確認を行うと、ホスト(iBook)の認証ダイアログが表示されます。構文確認を行うだけでも認証が必要なのですね。それではいささかめんどうなので、通常は using terms from を使ってクライアント側(iMac)のアプリケーションの用語辞書を使って構文確認を行います。

using terms from application "Finder"
    tell application "Finder" of machine "eppc://iBook800.local"
        version
    end tell
end using terms from

これで構文確認はできます...?できないですね。思いっきり認証ダイアログが表示されます。これを回避するためにホスト側を変数に入れてしまいます。

set remoteFinder to application "Finder" of machine "eppc://iBook800.local"

using terms from application "Finder"
    tell remoteFinder
        name of startup disk
    end tell
end using terms from

これでクライアント側(iMac)の Finder を使って構文確認ができます。では、実行してみましょう。

...また、認証ダイアログが表示されましたね。このダイアログに「キーチェーンに追加しますか?」というチェックボックスがあるのでホスト(iBook)のユーザー名とパスワードを入力してキーチェーンに追加しておきます。

認証は、キーチェーンに追加して全て解決、というわけには(なぜか)いきません。たとえば、AppleScript を終了して再度スクリプトを実行するときに先ほどの認証ダイアログが表示されます。どうしてでしょう?安全のため?ともかく、これでは認証ばかりで面倒です。次は、これを解決してみましょう。

リモート AppleEvent (1)

複数台の Mac を持っているとやってみたくなるじゃないですか。AppleScript で他の Macintosh を操縦って。そんなことないか?

AppleScript を使い始めたときからやってみたかったのです。が、複数台の Macintosh を持っていないし、セキュリティ的にどうなのかもよく分からない。今でも分かっていません。その辺りは調べていないので、これを読んでも不安な方は利用しない方がいいと思います。

ともかく、複数台の Macintosh を持っているのだから、試してみようと調べてみました。これが大変だったりします。で、防備録として。まだ、よく分かっていないのでこうできるとか、無線 LAN でとかインターネットを利用してこうできるとかの突っ込みもあるかと思いますが、掲示板にでも書いておいてもらえると嬉しいです。

まず、使う Mac は、有線 LAN でローカル接続です。インターネットに接続していません。単純に Ethernet でつないでいます。Mac は、iBook G3 800 GHz(Mac OS X 10.3.9) と iMac G5 2 GHz(Mac OS X 10.4.2)です。iMac から iBook を操作します。Ethernet をつないだらほとんど勝手に両者はつながります。これでファイル共有などができるようになります。どちらの Mac も管理者として起動しているとします。

つながったら操作される Mac(iBook)のシステム環境設定の「共有」の「サービス」タブを開き、「リモート AppleEvent」にチェックを入れます。ついでにコンピュータの名前も設定しておきます。ここでは、iBook800 にしておきました。これで iMac 側から iBook を AppleScript で操作できるようになります。設定は至って簡単。セキュリティはどうなのか知りませんが。

ここまでが準備段階です。

load nib

なんで、いまさら Mac OS X 10.3.9 と Mac OS X 10.4 をいったりきたりしているか?

Xcode 2.1...というより、AppleScript Studio が問題なんですね。新しいことはいいことだ、とばかりにインストールしたのはいいものの使いにくくない(=バグが多くない)でしょうか?。

AppleScript Studio を使っていると、挙動がおかしいところがよくある。スクリプティング自体が悪いのか、Xcode の問題なのか、AppleScript Studio か...どれかよく分からない。で、10.3.9 上の Xcode 1.5 を使って確認のためにいったりきたり。

Xcode ではなく、AppleScript Studio 1.4 の問題な気もします。具体的には、次のようなコード。

property notifWindow : missing value

on clicked theObject
    if name of theObject is "debug" then
        if notifWindow is missing value then
            load nib "Notification"
            set notifWindow to window "notification"
            show notifWindow
        else
            show notifWindow
        end if
    end if
end clicked

ウィンドウにボタンがあってクリックすると nib ファイルをロードしてその nib のウィンドウを表示する...ロードしたウィンドウを閉じて、再度ボタンをクリックしてウィンドウを表示させる。こういったことをしたいわけです。上記を実行すると、ウィンドウを閉じて再度表示するときにエラーになります。

このコード自体は珍しいものでもなく Dev Tools のサンプルの中にもありますし、今まで正常に動いていました。上記のコードを以下のようにすると動きます。

property notifWindow : missing value

on clicked theObject
    if name of theObject is "debug" then
        if not (exists notifWindow) then
            load nib "Notification"
            set notifWindow to window "notification"
            log ("Nib loaded")
            show notifWindow
        else
            log ("Nib unloaded")
            show notifWindow
        end if
    end if
end clicked

if の条件文を変更しただけです。実行すると分かるのですが、毎回ロードしています。プロパティの値を調べても、最初に nib をロードしたときのウィンドウの参照がちゃんと入っている。にもかかわらず、二度目の呼び出しでウィンドウがないと判断されてしまいます。もちろん、ウィンドウを閉じたときに解放しているのではありません。

結論。悪いのは、AppleScript Studio 1.4。また、Xcode にも問題はいっぱいあります。AppleScript からの操作がおかしい...ということがよくあります。特に、Xcode 2.1。

Xcode 1.5 は、まだまだ手放せそうにないです。

ハンドラを引数で渡す?

Perl や JavaScript や C 言語や...その他いろいろなプログラム、スクリプト言語では関数ポインタを使って汎用的な関数を作ることがあります。AppleScript で同じようなことができないか...と常々考えていました。

AppleScript は、ハンドラを変数に入れることができるのでこれが使えるかと思いましたが、上手くいきません。しかし、スクリプトオブジェクトを使えば関数ポインタのようなことができます。

Script Editor で開く

script MaxObject
    on comp(x, y)
        return x is less than y
    end comp
end script

script MinObject
    on comp(x, y)
        return x is greater than y
    end comp
end script

on run
    minOrMax({19, 16, 13, 10, 15, 20, 8}, MinObject)
end run

on minOrMax(theList, theObject)
    set firstItem to item 1 of theList
    set restList to rest of theList
    repeat with i from 1 to count of restList
        if theObject's comp(firstItem, item i of restList) then set firstItem to item i of restList
    end repeat
    return firstItem
end minOrMax

minOrMax() ハンドラは、リストの中の最小値(もしくは、最大値)を返すハンドラです。このハンドラにスクリプトオブジェクトを渡し、スクリプトオブジェクトで定義しているハンドラを呼び出しています。関数ポインタという感じではないですが...他に適切な呼び方を知りません。こういう方法をなんというのでしょう?

肝は、スクリプトオブジェクト内で同じ名前の比較ハンドラ(comp() ハンドラ)を定義していることです。こうすることで同一名のハンドラを複数作成することができ、かつ、渡すスクリプトオブジェクトによりハンドラの挙動を変えることができます。これって、よく知られているスクリプトオブジェクトの使い方なんでしょうか?

スクリプトオブジェクトは、まだまだ便利に使えそうです。もっと、研究しなくては...。

スリープの解除を検出

AppleScript でスリープから復帰したことを調べられないかと思い、探してみる。

なんのことはない。system.log にスリープとスリープ解除は書き出されているのですね。で、次のようなスクリプト。

Script Editor で開く

property theRes : ""

on run
    set theRes to do shell script "grep -i 'system wake' /var/log/system.log"
end run

on idle
    set tmp to do shell script "grep -i 'system wake' /var/log/system.log"
    if tmp is not theRes then
        set theRes to tmp
        display dialog "Wake up!" buttons {"OK"} default button 1 giving up after 15 with icon 1
    end if
    return 1
end idle

これだけでスリープ解除を調べることができました。詳しく動作の検証を行っていないのでもしかしたら動かなくなったりするかもしれません。

ちなみに、

Script Editor で開く

do shell script "grep -i 'system sleep' /var/log/system.log"

とすることでスリープを検出することができますが、何か動作を行う前にスリープしてしまい、スリープが解除されたときにスクリプトが実行されるのでこの方法は使えません。

AppleScript Studio 1.4 の Data View

AppleScript Studio 1.4 になって Data View(Table View と Outline View)の機能が強化されました。AppleScript Studio の機能強化は、Data View がほとんどだ...という素朴な感想はおいておくとして。

従来は、table view に新しい項目を追加するときは、新しい data row を作成し、その data cell のそれぞれの内容を設定していきました。

tell table view 1 of  scroll view 1 of window 1 
    set theRow to make new data row at end of data rows of data source 1 
    set content of data cell "name" of theRow to "新しいデータ"
end tell

こんな感じですね。新しく作り、内容を設定...ということを行うためどうしても時間がかかっていました。

AppleScript Studio 1.3 で table view にデータを追加するのに append という命令が追加されました。この命令を使うと data source にデータを追加するのに AppleScript のレコードを利用できます。

set theRecord to {{name:"新しいデータ"}}
tell table view 1 of scroll view 1 of window 1
    append data source 1 to theRecord
end tell

データの追加が簡単になり、より早くなったわけです。ただ、このようにデータの追加は早くなったのですが、問題がないわけではありませんでした。このようにレコードでデータを追加したなら、レコードでデータを取得したい...そう思ってもできなかったのです。

そして、AppleScript Studio 1.4 では、ようやくこれができるようになりました。table view に追加した項目を content(contents)属性で一括して設定/取り出しができるようになったのです。取り出しの際に data source の属性 returns records の真偽値を変更することでリストで取り出したり、レコードで取り出したりができます。

1.3 までは、このデータの設定が append 命令でレコードを利用できても同じ形式(リスト、レコード)でのデータの取り出しができなかったため、データの保存や復元、検索が面倒だったのです。が、1.4 でようやく AppleScript になじんだ方法でデータを扱えるようになり、速度的にも申し分ありません。

-- テーブル内のデータを保持するリスト
-- たとえば、以下のような感じ。
-- {{"myName","myAddress","myAge"},{"myName","myAddress","myAge"},...}
-- このリスト内にデータが5000項目あったとして...
property : theList

tell table view 1 of scroll view 1 of window 1
    set update views of data source 1 to false
    set contents to theList
    set update views of data source 1 to true
end tell

データにもよるのでしょうが 1 〜 2 秒で追加完了します。

1.4 の Data View は、いちいち data row を作成して data cell の内容を設定して...ということをしないでもいいですし、アプリケーションの起動時に data source を作成するということをしなくてもいいようになっています。おこなうことは、Interface Builder で Table View(Outline View)を配置してそれぞれ列の identifier を設定するだけです。そして、Table View(Outline View)のイベントハンドラ awake from nib を利用して次のように初期化します。

on awake from nib theObject
    set content of theObject to {}
end awake from nib

これだけで data source が作られて初期化完了なのですから、なんとも便利になったものです。新規に data row を追加するときは、もちろん append 命令を利用します。

iTunes のイコライザ

JAM LOG さんのところで、「iTunesのイコライザのパーフェクト設定」なる記事を見て衝動的に作ってしまいました。

Script Editor で開く

tell application "iTunes"
    activate
    set newPreset to make new EQ preset
    set name of newPreset to "Perfect"
    set band 1 of newPreset to 3
    set band 2 of newPreset to 6
    set band 3 of newPreset to 9
    set band 4 of newPreset to 7
    set band 5 of newPreset to 6
    set band 6 of newPreset to 5
    set band 7 of newPreset to 7
    set band 8 of newPreset to 9
    set band 9 of newPreset to 11
    set band 10 of newPreset to 8
    --preamp is -6.0?
    set preamp of newPreset to -6
    set current EQ preset to newPreset
end tell

JAM LOG さんのところから MacBSの日常生活的日記さんをのぞいて、結果、プリアンプも設定しています。どちらがいいかは聞く人次第。

確かに音がくっきりとしますね。

print ハンドラ

どれだけの人が知っているのでしょうか?AppleScript には、run ハンドラや idle ハンドラ、reopen ハンドラ、quit ハンドラといった標準で実装しているいくつかのハンドラがあります。

これらのハンドラの中に print というハンドラがあります。print という名前ですので印刷を行うのかと思いますが、違います。print ハンドラはスクリプトアプリケーション同士で通信を行うハンドラです。たとえば、以下のようなスクリプトを「実行後、自動的に終了しない」にチェックを入れてアプリケーションとして保存します。

Script Editor で開く

property counter : 0

on idle
    set counter to counter + 1

    return 10
end idle

on print
    return counter as Unicode text
end print

このアプリケーションを「Counter」という名前で保存したとします。このアプリケーションを起動させておきます。そして、以下のようなスクリプトを実行します。

Script Editor で開く

tell application "Counter"
    print
end tell

Counter アプリケーションは、idle ハンドラで 10 秒ごとに counter の値を増やしていきます。ですので、10 秒ごとに増えた値が結果として返ってきます。また、print ハンドラには引数を渡すことができるので次のようなこともできます。先ほどの Counter アプリケーションを次のように修正します。

Script Editor で開く

property counter : 0
property myName : "Counter"

on idle
    set counter to counter + 1

    return 10
end idle

on print {nameOption, greetingOption}
    if nameOption and greetingOption then
        return "Hello, my name is " & myName & ". " & counter & " counted."
    else if nameOption and not greetingOption then
        return "My name is " & myName & ". " & counter & " counted."
    else if not nameOption and greetingOption then
        return "Hello, " & counter & " counted."
    else
        return counter as Unicode text
    end if
end print

次のように呼び出します。

Script Editor で開く

tell application "Counter"
    print {false, true}
end tell

だからどうだというサンプルなのですが。print ハンドラは、idle ハンドラのようにアプリケーションとして保存し、かつ、実行後終了しない常駐アプリケーション形式でないと使えないハンドラです。スクリプト間通信を行いたいときに手軽に使えるので使い道があると思うのですが、いまだかって print ハンドラが入ったスクリプトを見たことがなかったり...。

Safari で文字列を URL エンコード

JavaScript の話題ばかりだと愛想つかされてしまいそうです。そもそも、その成果がこのサイトに現れていないのはなぜ?とも思います。

実際には、サイトに JavaScript 関連の HTML をあげています。リンクをしていないだけで。Ajax は、ローカルだとテストできないんですね。そんなわけで色々とアップロードしてテストしているのですが、まぁ、人様に見せるほどのものではないし、JavaScript のサンプルだってあちこちに同じようなものがありますし。そのうち、Google 検索でひっかかるようになるかもしれません。

これだけだとなんですので、Safari で JavaScript を使ってエンコードをするスクリプトなんかを。

Script Editor で開く

set weekdayList to {"日", "月", "火", "水", "木", "金", "土"}
set theList to {}

repeat with i in weekdayList
    set end of theList to encodeWithJS(i)
end repeat

theList

on encodeWithJS(str)
    tell application "Safari"
        if exists front document then
            return do JavaScript "encodeURI('" & str & "')" in front document
        end if
        return str
    end tell
end encodeWithJS

on decodeWithJS(str)
    tell application "Safari"
        if exists front document then
            return do JavaScript "decodeURI('" & str & "')" in front document
        end if
        return str
    end tell
end decodeWithJS

このスクリプトを利用した結果がなにを意味しているか分からない、という人にはほとんど価値のないスクリプト。Ajax なんかで文字化けを回避するために文字列をエンコードしておきたいときがあるので、そんなときに使います。それ以外でも使えますが。Safari は、AppleScript から利用できる JavaScript 実行エンジンですね。

なんとなく、カレンダー

あいかわらず、AppleScript をほったらかしで JavaScript や XHTML、CSS、PHP に熱中しています。というか、AppleScript Studio でビューツールバーアイテムがやっぱり使えなくて意気消沈してしまいました。

Script factory さんは、時々チェックしているサイトさんなのですが、ついに、FilterScripts for mi が Objective-C で書き直されたようです。このアプリケーションが公開されたとき、刺激されて同じようなコンセプトのアプリケーションを AppleScript Studio で作成したのです。まぁ、その昔あった Script Runner のようなものですが。

Objective-C を導入する気持ち、非常によく分かります...。個人的には、Cocoa/Objective-C でアプリケーションを作成して Objective-C からAppleScript を呼び出すという方向で開発を行っています。なので、AppleScript Studio を使うのはある意味苦痛だったりします。あれもできない、これもできない、という感じで。

AppleScript Studio には、それなりにいいところがあるんですけどね。あるのかな?そう信じたい...。そんな AppleScript の最近の成果は、カレンダーです。それほど珍しいものでもないのですが、ちょっと、月ごとのカレンダーを簡単に参照したくて作ってみました。

Script Editor で開く

on run
    set calendarList to getCalendar(current date)
    set beginning of calendarList to {"日", "月", "火", "水", "木", "金", "土"}

    set calendarText to ""
    repeat with thisItem in calendarList
        set calendarText to calendarText & listToText(thisItem, tab) & return
    end repeat
    set calendarText to (date string of (current date)) & return & return & calendarText

    display dialog calendarText with icon 1 buttons {"OK"} default button 1 giving up after 15
end run

on getCalendar(selectedDate)
    set theList to {{}, {}, {}, {}, {}, {}}
    set numDaysInMonth to {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

    set currentYear to year of selectedDate
    set currentMonth to month of selectedDate as number

    set day of selectedDate to 1
    set time of selectedDate to 0

    copy selectedDate to firstOfMonth
    set startOffset to weekday of firstOfMonth as number

    set daysInMonth to item currentMonth of numDaysInMonth
    if ((currentMonth is 2) and (isLeap(currentYear))) then set daysInMonth to daysInMonth + 1

    set dayLabel to 1
    set num to 0
    repeat with i from 1 to 6
        set currentWeek to item i of theList
        repeat with j from 1 to 7
            set num to num + 1
            if (num is less than startOffset or num is greater than or equal to (daysInMonth + startOffset)) then
                set end of currentWeek to ""
            else
                set end of currentWeek to dayLabel
                set dayLabel to dayLabel + 1
            end if
        end repeat
    end repeat
    return theList
end getCalendar

on isLeap(theYear)
    return (((theYear mod 4) is 0 and ((theYear mod 100) is not 0)) or (theYear mod 400) is 0)
end isLeap

on listToText(theList, delimStr)
    set the AppleScript's text item delimiters to delimStr
    set theText to theList as text
    set the AppleScript's text item delimiters to ""
    return theText
end listToText

細かい部分を気にせずに作っているので実際に使うには不都合があると思いますが。getCalendar に date 値を渡せばその月のカレンダーをリストで返します。月によっては利用しないリストも含まれているのですが、それは気にしない。ダイアログで見る限りは、特に困らない。

AppleScript Studio でツールバーを使ってみる (2)

さて、何はともあれツールバーを使ってみましょう。AppleScript Studio では、Objective-C でデリゲートメソッドとして用意されているメソッドが toolbar クラスの属性として用意されています。AppleScript Studio ではこれらの属性を使ってツールバーを作成します。

ツールバーを利用するには、

  1. ウィンドウにつけるツールバーを作成
  2. ツールバーで利用出来るツールバーアイテムを登録
  3. デフォルトのツールバーアイテムを設定する
  4. ウィンドウに接続する

というステップを踏まなくてはいけません。これらのステップは、ツールバーを利用する上での最低限必要なものです。

それぞれのステップを具体的に見ていきます。

ツールバーは、ウィンドウに付加するものなのでウィンドウの awake from nib イベントハンドラを利用するのが手軽だと思います。まずは、プロジェクトを作成しウィンドウに awake from nib イベントハンドラを接続します。awake from nib ハンドラ内で上記のステップを行います。最初にツールバーを作成します。

set theToolbar to make new toolbar at end with properties {name:"new toolbar", identifier:"new toolbar", allows customization:true, display mode:default display mode, size mode:default size mode, auto sizes cells:true}

ツールバーの作成は make 命令で行います。ここで属性をまとめて設定しています。

name -- AppleScript がオブジェクトを判別するための名前
identifier -- ツールバーの一意の識別子
allows customization -- ツールバーのカスタマイズが行えるかどうか
display mode -- ツールバーアイテムの表示形式。アイコンのみか、アイコンとラベルか等
size mode -- ツールバーアイテムの表示サイズ
auto sizes cells -- ツールバーアイテムを自動で保存するか

これら以外にも属性はありますが、それらは後ほど。ところで、これらの属性の中の auto sizes cells ですが、これは auto saves configuration 属性のことです。この属性は、ユーザーがカスタマイズしたツールバーアイテム(ツールバーに表示される個々のオブジェクト)を自動で保存するかどうかの属性ですが、構文確認を行うとなぜか auto sizes cells になってしまいます。しかし、動作自体は正しく行われているようです。auto sizes cells は、matrix クラスの属性です。

ツールバーの作成が出来たら次にツールバーで利用出来るツールバーアイテムの登録を行います。Objective-C では、toolbarAllowedItemIdentifiers: というデリゲートメソッドで行いますが、AppleScript Studio では、toolbar クラスの allowed identifiers 属性を利用します。ツールバーは、ここに登録したツールバーアイテムのみが利用出来るようになります。ツールバーは、ツールバーアイテムの識別子を利用して個々のオブジェクトを判別します。AppleScript Studio は多くの場合、name 属性を利用して個々のオブジェクトを判別しますが、ツールバーでは name 属性ではなく identifier(識別子)を多用します。

ツールバーアイテムには、フレームワーク側が標準で用意しているツールバーアイテムがあります。

print item identifier -- 印刷
show colors item identifier -- カラーパネル表示
show fonts item identifier -- フォントパネル表示
customize toolbar item identifer -- ツールバーカスタマイズ
space item identifier -- スペース
flexible space item identifier -- 伸張可能なスペース
separator item identifier -- 区切り

いろんなアプリケーションがツールバーを利用していますが、どのアプリケーションでも共通しているツールバーアイテムがあります。それらはこういった標準で用意されているツールバーアイテムを利用しているのです。これら標準で用意されているものを使ってみます。

set allowed identifiers of theToolbar to {"print item identifier", "show colors item identifier", "show fonts item identifier", "customize toolbar item identifer", "space item identifier", "flexible space item identifier", "separator item identifier"}

ツールバーで利用出来るツールバーアイテムをここで設定します。繰り返しますが、ここでは name 属性ではなく identifier 属性を利用します。

次にデフォルトのツールバーアイテムを設定します。これまた、Objective-C では toolbarDefaultItemIdentifiers: として用意されているデリゲートメソッドが toolbar クラスの属性として用意されています。default identifiers 属性は、最初にツールバーに表示されるツールバーアイテムを設定する属性です。

set default identifiers of theToolbar to {"print item identifier", "customize toolbar item identifer"}

ここでもツールバーアイテムの identifier 属性をまとめたリストを指定します。最後にウィンドウにツールバーを設定します。

set toolbar of theObject to theToolbar

スクリプトの全体は以下のようになります。

on awake from nib theObject
    if name of theObject is "main" then
        set theToolbar to make new toolbar at end with properties {name:"new toolbar", identifier:"new toolbar", allows customization:true, display mode:default display mode, size mode:default size mode, auto sizes cells:true}

        set allowed identifiers of theToolbar to {"print item identifier", "show colors item identifier", "show fonts item identifier", "customize toolbar item identifer", "space item identifier", "flexible space item identifier", "separator item identifier"}
        set default identifiers of theToolbar to {"print item identifier", "customize toolbar item identifer"}

        set toolbar of theObject to theToolbar
    end if
end awake from nib

これでビルドして実行するとツールバーがついたウィンドウが表示されます。ツールバーをカスタマイズしてからアプリケーションを終了し、再度起動させるとカスタマイズされたツールバーが表示されます。この自動保存が先ほどの auto saves configuration 属性になります。

ざっと駆け足でしたが、ツールバーの詳しいことは次回。

AppleScript Studio でツールバーを使ってみる (1)

やっとと言うかなんと言うか...、AppleScript Studio 1.4 においてようやくツールバーがサポートされました(それを言うならやっとこのサイトで取り上げたですね)。AppleScript Studio 1.4 のリリースノートやサンプルプロジェクトを眺めてみて...これは、Objective-C で行う場合をそのまま AppleScript 的に書いているだけですね。

もっと、楽に実装できるのかと思いました。Objective-C の方も相変わらずコードの方からでないとツールバーを制御することができないわけですが。しかし、用意されているイベントハンドラが代表的なものだけしかないので細かい制御となると Objective-C で行わないといけません。この辺りがいかんともしがたいのですね。AppleScript Studio...。

サンプルとクラスの属性を見ただけですが、画像でのツールバーしか使えない?ポップアップメニューやテキストフィールドとかの UI を利用したツールバーは作れないのでしょうか?そういえば、AppleScript Studio で NSView を操作するのって出来なかったような。アプリケーション実行中にウィンドウの NSView を張り替えようと思って試してみて出来なかった記憶があります。試してみる前から暗雲が。。。

もし、UI を使ったツールバーが利用できないなら、いまいちですね。検索用のテキストフィールドをつけることすら出来ないのですから。

ともあれ、今月は AppleScript Studio を久しぶりにいじってみます。

Database Events (5)

処理速度はともかく、Database Events は、field の value 属性に実際のデータを保存します。用語辞書を見ると value 属性は値に anything と記述されています。SQLite は、バージョン 3 になって一応型が定義されています。本当に何でも value に設定できるのか試してみます。

ここまでで文字列と数値(実数、整数)がそのまま保存できるのは分かっています。前回、特に断っていなかったのですが、AppleScript の date も保存できています。value 属性を読み込むと date 型の値が返ってきます。

では、他のものではどうなるでしょうか。ということで以下のスクリプト。

Script Editor で開く

read file ((path to pictures folder as Unicode text) & "Safari.tiff") as data

AppleScript のいろんなデータを保存してみました。結論から言うと、上記スクリプトの中で保存、再読み込みが可能なのは date クラスと真偽値だけです(エラーにならないのですが、返ってくる値が一律 false になります)。alias 参照は、保存できますが読み込んだときにスラッシュ区切りの POSIX path に変換されてしまいます。

AppleScript の参照も保存できません。ということは、ある field の value に他のデータベースの field の参照をいれておくこともできません。その field を参照すると他のデータベースの field にアクセスするというようなことは無理ですね(というか、それは無茶)。

文字列で保存できるならほとんどの状況でどんなデータも保存できるので困ることもないと思います。それでも、リストやレコードなんかは内部で変換でもして保存して欲しい気もします。まぁ、多くの場合、画像を as data で保存することなんてないでしょうし、画像ファイルのパス文字列が保存できればそれでいいでしょう。リストもレコードも保存方法を考えると解決できるものです。ハンドラやスクリプトオブジェクトをそのまま保存できるなら、ハンドラデータベースが構築できると思ったのですが、これも文字列にしておけばいいですね。

そんなわけでデータベースに保存しておくのは文字列と数値にしておくのが無難です。

データベースの名前ですが、これは日本語も使えます。

Script Editor で開く

set manList to {{"山本", "浩二"}, {"金本", "雅次"}, {"園山", "香織"}, {"井上", "雅子"}}

tell application "Database Events"
    make new database with properties {name:"交友録"}
    tell database "交友録"
        repeat with i from 1 to count manList
            set recordValue to item i of manList as Unicode text
            set theRecord to make new record with properties {name:recordValue}
            tell theRecord
                make new field with properties {name:"姓", value:item 1 of (item i of manList)}
                make new field with properties {name:"名", value:item 2 of (item i of manList)}
            end tell
        end repeat
        save
    end tell
end tell

このようにして field にも日本語を利用することができます。もちろん以下のように取得も可能です。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:交友録")

tell application "Database Events"

    tell database databaseFile
        name of records
        -- {"山本浩二", "金本雅次", "園山香織", "井上雅子"}
        value of field "姓" of records
        -- {"山本", "金本", "園山", "井上"}
        value of fields of (records whose value of field "名" of it contains "子")
        -- {"井上雅子", "井上", "雅子"}
    end tell
end tell

Database Events は、できたてのアプリケーションでまだまだ不安定なのか、よく反応が返ってこないことがあります。そんなときは、一度終了するともとに戻ります(System Events もですが...)。

Script Editor で開く

tell application "Database Events" to quit

また、単に処理に手間取ってなかなか結果が返ってこないこともあるので with timeout でタイムアウトも設定しておく方がいいかもしれません。

これまで長々と見てきましたが、Database Events は AppleScript で SQLite の全機能が利用できるということではなく、単純な行と列だけで表現する簡易データベースという感じです。機能的には新しい行の追加と削除ぐらいしかできないようで、行の並べ替えなんかは無理です(以前、例として載せたようなソートではなく、record をそのまま並べ替えることです)。並べ替えは実際にデータを表示するときに行えばいいのでそれほど問題はないですが、それだけコードが長くなるので、できれば実装して欲しいところです。

また、Database Events は、その用意されている用語からも分かるようにデータベースの中に複数の table を作ることもできません。プライマリーキーを設定してリレーショナルデータベースというのも(確証はないのですが、おそらく)できないでしょう。もちろん、SQLite でなら可能です。あれ? テーブルを指定できないということは、CoreData アプリケーションが保存した SQL フォーマットのデータベースも読むことができない、ということですね? ...できませんでした。。。てっきり、連携できると思っていたのですが。この辺りのことに期待していた人には悪い知らせです。すいません。

Terminal から sqlite3 コマンドを使うと分かるのですが、Database Events で作成したデータベースは、ZFIELD、ZRECORD、Z_METADATA、Z_PRIMARYKEY と 4 つのテーブルを持っています(midore さんに情報をいただきました。ありがとうございました)。ZFIELD で field が、ZRECORD で record が管理されているようです。Database Events は、これらのテーブルの内容を解析してデータを返しているようです。一方、CoreData アプリケーションが保存した SQL フォーマットのファイルもいくつかテーブルが作成されています。Developer Tools の CoreData のサンプルアプリケーション、OutlineEdit は次のようなテーブルを持っていました。ZNOTE、ZPRIORITY、Z_METADATA、Z_PRIMARYKEY。メタデータとプライマリーキーのテーブルは共通しています。こういうデータベースの作られ方というのは、CoreData を利用するときに特有のものなのでしょね(CoreData の資料にいい加減目を通さないと...)。

実際のところ、Database Events でデータベースを利用するためには、Database Events で利用できるデータ構成でないと駄目なのでしょう。sqlite3 を使えばどのアプリケーションが保存したデータベースも操作できるのですが、CoreData アプリケーションのデータベースを Database Events で利用することはできないということですね。

最後に。掲示板の方で Perl のハッシュのような使い方ができれば...とありました(TUNA さん。ありがとうございます)。ハッシュって連想配列ですよね? Perl 特有のデータ型の。Objective-C でいうところの辞書(NSDictionary)ですね。AppleScript で言えばレコード。あれは、Perl らしい使い方がいろいろと用意されていて便利ですね。Database Events で代用しようと思えば...、単純に record の name 属性をキーにして field をひとつ追加してその中にデータを入れておけばいいのでは? たとえば、次のような感じです。

Script Editor で開く

--すでに Hash.dbev というデータベースがある
set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:Hash.dbev")

--データの追加
my addRecord(databaseFile, "整数", 10)
my addRecord(databaseFile, "実数", pi)
my addRecord(databaseFile, "文字列", "我が輩は猫である")
my addRecord(databaseFile, "日付", current date)
--データの取得
getData(databaseFile, "整数")
--データの変更
setData(databaseFile, "文字列", "気は優しくて力持ち")
--キーの一覧を取得
keys(databaseFile)
--キーとデータをリストで取得
each(databaseFile)

on addRecord(databaseFile, keyStr, theData)
    tell application "Database Events"
        tell database databaseFile
            set theRecord to make new record with properties {name:keyStr}
            tell theRecord
                make new field with properties {name:"value", value:theData}
            end tell
            save
        end tell
    end tell
end addRecord

on setData(databaseFile, keyStr, newData)
    tell application "Database Events"
        tell database databaseFile
            set value of field "value" of record keyStr to newData
            save
        end tell
    end tell
end setData

on getData(databaseFile, keyStr)
    tell application "Database Events"
        tell database databaseFile
            set theResult to value of field "value" of record keyStr
        end tell
    end tell

    return theResult
end getData

on keys(databaseFile)
    tell application "Database Events"
        tell database databaseFile
            set theResult to name of records
        end tell
    end tell

    return theResult
end keys

on values(databaseFile)
    tell application "Database Events"
        tell database databaseFile
            set theResult to value of field "value" of records
        end tell
    end tell

    return theResult
end values

on each(databaseFile)
    tell application "Database Events"
        set theResult to {}
        tell database databaseFile
            set end of theResult to name of records
            set end of theResult to value of field "value" of records
        end tell
    end tell

    return theResult
end each

これでは使えるデータが文字列と数値のみですが。リストやレコードに対応すればもう少し便利に使えるかも。

Database Events (4)

気にはなっていたのですが、このサイトの RDF を Safari で表示するとスクリプトが見にくいですね。スクリプトのインデントを CSS で整形しているのでこうなってしまうのですが...。どうやって回避しようか。と、悩んでみる振りをしていますが、このままでいく可能性大ですが...。

Database Events (1)Database Events (2)Database Events (3) と続いてきましたが、基本的なことはいったん終了して他のことを調べてみます。まずは、誰もが気になる(と思う)処理速度。SQLite をコマンドラインで使用すると何万件とあるデータをコンマ xxx 秒といった感じで即座に取り込んでくれます。

AppleScript から Database Events を利用するのであれば、そこまでの速度は、期待できません。ともあれ、やってみます。

次のようなスクリプトを用意して current date で時間を計ってみました。

--テスト用データを作成
tell application "Mail"
    set dataList to {}
    set theList to messages of mailbox 2
    repeat with thisItem in (a reference to theList)
        set end of dataList to {subject of thisItem, sender of thisItem, date sent of thisItem}
    end repeat
end tell

tell application "Database Events"
    --データベースを作成
    make new database with properties {name:"Sample"}
    save database "Sample"
    tell database "Sample"
        set cd to current date -- 計測開始
        repeat with thisData in dataList
            set theRecord to make new record with properties {name:item 1 of thisData}
            tell theRecord
                make new field with properties {name:"sender", value:item 2 of thisData}
                make new field with properties {name:"date sent", value:item 3 of thisData}
            end tell
        end repeat
        set n to (current date) - cd --計測終了
        save
        return n
    end tell
end tell

Mail のメッセージでテストデータを作成し、それを新しく作成したデータベースに追加するスクリプトです。mailbox 2 の中には、1275 件のメールがありました。これらのデータを新規作成し、追加するのに 393 秒かかりました。約 6 分 30 秒です。ちょっと...遅くない?

いろいろ試してみたのですが、record の追加を一気に行うと速度的にはかなり遅くなります。どれぐらいのデータを一気に追加すると遅くなるか...。例えば、以下のようなスクリプトでデータ 1000 件を追加すると約 38 秒です。

Script Editor で開く

tell application "Database Events"
    make new database with properties {name:"Test"}
    save database "Test"
    tell database "Test"
        repeat with i from 1 to 1000
            make new record with properties {name:i as Unicode text}
        end repeat
        save
    end tell
end tell

このスクリプトで数を変えていろいろ試してみましたが、せいぜい我慢できるのは 100 件ぐらいまでです。ただ、すでに record が 1000 個ある状態で新しく record を 1 件追加しても速度が遅いというわけではないです。このように繰り返しで一気に追加するのが遅いのです。

速度のことでいえば、record の削除でも同じことがいえます。削除は、delete 命令で行えるのですが、record が 5000 件あるときに

delete records

とすると、タイムアウトになります。いつまでたっても削除処理が完了しないのでどれぐらい時間がかかるのか分かりませんが、削除のときも一気に削除を行おうとすると、いつまでたっても反応が返ってこない、という状態に陥ります。ただ、これも単純に一件を削除するだけなら処理速度的に問題はありません。

record の数が多くなったときに処理が遅くなるのは新規作成、削除のときだけではありません。フィルタ参照も遅くなります。これも、番号参照などで record を決め打ちするなら最初の record でも最後の record でも真ん中の record でも速度的には変わりません。

tell application "Database Events"
    tell database databaseFile
        record 1 -- 変わらない
        last record -- 変わらない
        middle record -- 変わらない
        some record -- 変わらない
        records whose name of it contains "10" --データ数が多いと遅くなる
        records 100 thru 200 --データの範囲が多いと遅くなる
    end tell
end tell

上記のように番号参照、相対参照、中央参照、任意参照といった取り出すデータ数が 1 つのときは、速度低下がありません。が、フィルタ参照は、データ(record)の数が増えるとそれだけ速度が低下します。範囲参照も一気に取り出すデータ数が多くなると速度が低下します。

この処理速度の遅さは、SQLite に問題があるのではありません。Database Events の問題だと思います。今後、改良されてもう少しましになるといいのですが、今のところ CSV を一気に取り込むなんて処理は期待できそうにないです。データを追加、削除するのは sqlite3 で行うとしても、フィルタ参照が遅くなるなら Database Events の使い道って著しく限定されてしまうような...。

Database Events (2)

前回、データベースを作成し、保存しました。今度は、データを追加していきます。データは、record と field で表現します。record が 行になり、field が列になります。最初に record を追加し、その record に field を作成し、データを追加していくという形になります。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:MyDatabase.dbev")

tell application "Database Events"
    tell database databaseFile
        set newRecord to make new record with properties {name:"1"}
        save
    end tell
end tell

make で新しくオブジェクトを作成するとき、Database Events では at オプションが(なぜか)利用できません。ですので、データベースに tell して、その中で新しい record を作成します。record には、name 属性と id 属性があります。id 属性は、一意のもので Database Events が自動的に付加します。変更はできません。name 属性は、行の名前に当たるものですが、これは field "name" と同じものです。

分かりにくいですね。record を新しく作成したとき、既にひとつだけ field が追加された状態で作成されます。それが field "name" です。name という列がひとつだけある表と考えるといいでしょうか。record を作成し、name 属性を設定しているのですが、実際は field "name" に値を設定しているのです。この辺りが少し分かりにくいかもしれません。

field を作成するのも database や record を作成するときと同じ感じです。では、次のようなデータ(都道府県の県庁所在地と人口)をデータベースに追加してみます。

Script Editor で開く

set theData to {{|name|:"北海道", capital:"札幌", population:5691737}, {|name|:"青森県", capital:"青森", population:1504358}, {|name|:"岩手県", capital:"盛岡", population:1427987}, {|name|:"宮城県", capital:"仙台", population:2340145}, {|name|:"秋田県", capital:"秋田", population:1209196}, {|name|:"山形県", capital:"山形", population:1249165}, {|name|:"福島県", capital:"福島", population:2138605}, {|name|:"茨城県", capital:"水戸", population:2990472}, {|name|:"栃木県", capital:"宇都宮", population:1998186}, {|name|:"群馬県", capital:"前橋", population:2013753}, {|name|:"埼玉県", capital:"浦和", population:6838164}, {|name|:"千葉県", capital:"千葉", population:5863182}, {|name|:"東京都", capital:"東京", population:11680490}, {|name|:"神奈川県", capital:"横浜", population:8324355}, {|name|:"新潟県", capital:"新潟", population:2487980}, {|name|:"富山県", capital:"富山", population:1126782}, {|name|:"石川県", capital:"金沢", population:1175511}, {|name|:"福井県", capital:"福井", population:828087}, {|name|:"山梨県", capital:"甲府", population:883847}, {|name|:"長野県", capital:"長野", population:2299468}, {|name|:"岐阜県", capital:"岐阜", population:2108530}, {|name|:"静岡県", capital:"静岡", population:3754758}, {|name|:"愛知県", capital:"名古屋", population:6875723}, {|name|:"三重県", capital:"津", population:1855860}, {|name|:"滋賀県", capital:"大津", population:1316331}, {|name|:"奈良県", capital:"奈良", population:1447496}, {|name|:"和歌山県", capital:"和歌山", population:1094120}, {|name|:"京都府", capital:"京都", population:2561860}, {|name|:"大阪府", capital:"大阪", population:8624045}, {|name|:"兵庫県", capital:"神戸", population:5500842}, {|name|:"岡山県", capital:"岡山", population:1958385}, {|name|:"鳥取県", capital:"鳥取", population:618868}, {|name|:"島根県", capital:"松江", population:765980}, {|name|:"広島県", capital:"広島", population:2876405}, {|name|:"山口県", capital:"山口", population:1540354}, {|name|:"徳島県", capital:"徳島", population:835781}, {|name|:"香川県", capital:"高松", population:1035579}, {|name|:"愛媛県", capital:"松山", population:1517190}, {|name|:"高知県", capital:"高知", population:821199}, {|name|:"福岡県", capital:"福岡", population:4955439}, {|name|:"佐賀県", capital:"佐賀", population:883960}, {|name|:"長崎県", capital:"長崎", population:1537280}, {|name|:"大分県", capital:"大分", population:1238496}, {|name|:"熊本県", capital:"熊本", population:1870473}, {|name|:"宮崎県", capital:"宮崎", population:1188341}, {|name|:"鹿児島県", capital:"鹿児島", population:1790437}, {|name|:"沖縄県", capital:"那覇", population:1313804}}

tell application "Database Events"
    make new database with properties {name:"MyDatabase"}
    tell database "MyDatabase"
        repeat with thisItem in theData
            set thisItem to contents of thisItem
            set theRecord to make new record with properties {name:|name| of thisItem}
            tell theRecord
                make new field with properties {name:"capital", value:capital of thisItem}
                make new field with properties {name:"population", value:population of thisItem}
            end tell
        end repeat
        save
    end tell
end tell

やはり、保存はきちんと行っておきます。ここでは record の name 属性に県名を指定しています。

Database Events (3)

前々回は、データベースを作成しました。前回は、データベースにデータを追加してみました。今度は、データを利用してみます。前々回、前回で作成した都道府県データベースを使っていますので、まずは、そちらを参照してみてください。

Database Events には、データの操作に関する命令なんかは用意されていません。ですので、ソートや検索などは AppleScript の機能を利用して行うことになります。record の name 属性に県名を指定しておいたので名前参照でレコードを指定することができます。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:MyDatabase.dbev")

tell application "Database Events"
    tell database databaseFile
        record "愛知県"
    end tell
end tell

範囲参照や全要素参照、任意参照、中央参照、番号参照も使うことができます。おおむねほとんどの参照方法を利用できますが、やはり、一番強力なのはフィルタ参照でしょう。以下のようにすることで人口 400 万人以上の県名だけを取得することができます。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:MyDatabase.dbev")

tell application "Database Events"
    tell database databaseFile
        name of records whose value of field "population" of it is greater than 4000000
    end tell
end tell

--> {"北海道", "埼玉県", "千葉県", "東京都", "神奈川県", "愛知県", "大阪府", "兵庫県", "福岡県"}

何人以上、何人以下ということも可能です。指定する field を変更することで文字列でフィルタ参照を行うことももちろん可能です。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:MyDatabase.dbev")

tell application "Database Events"
    tell database databaseFile
        name of records whose name of it contains "川"
    end tell
end tell

川の字の入っている県名だけを抜き出しています。フィルタ参照を利用することでデータの取得はかなり柔軟に行えると思います。問題は、ソートですね。AppleScript にはソートの命令がないので自前でソートのハンドラを作るか、シェルの sort コマンドを利用するのが最も手早い解決になります。例えば、人口 400 万人以上の県を人口でソートして結果を返す、といったことを行ってみましょう。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:MyDatabase.dbev")

tell application "Database Events"
    tell database databaseFile
        -- 人口 400 万人以上を抽出
        set theResult to value of field "population" of (records whose value of field "population" of it is greater than 4000000)
        -- 結果の比較用のリストを作成
        set beforeSortList to name of records whose value of field "population" of it is greater than 4000000
        --ソートし、人口が多い順に結果を反転
        my simpleSort(theResult)
        set theResult to reverse of theResult

        -- ソートした結果を元にレコードを取得
        set sortedList to {}
        repeat with thisValue in theResult
            set thisRecord to (records whose value of field "population" of it is (thisValue as integer))
            set end of sortedList to name of (item 1 of thisRecord)
        end repeat

        --結果を比較(分かりやすいように県名で)
        {beforeSortList, sortedList}
    end tell
end tell

on simpleSort(theList)
    -- 昇順
    set scoreCount to count theList -- リスト内の数   
    set sortedCount to 1 -- ソート済みの最初の範囲

    repeat while sortedCount < scoreCount
        set minimumIndex to sortedCount
        repeat with i from sortedCount to scoreCount
            if item minimumIndex of theList > item i of theList then
                set minimumIndex to i
            end if
        end repeat
        set temp to item minimumIndex of theList
        set item minimumIndex of theList to item sortedCount of theList
        set item sortedCount of theList to temp
        set sortedCount to sortedCount + 1
    end repeat
end simpleSort

ソートする項目が少ないので AppleScript だけで行っています。最初に人口のリストを取得しています。このリストをソートし、ソートした結果を元に繰り返しでフィルタ参照で目的の record を探しています。これで人口 400 万人以上の県を人口の多い順にソートした結果になります。ちなみにシェルの sort を使って同じ結果を取得するには、次のような方法があります。

Script Editor で開く

set theFolder to path to documents folder as Unicode text
set databaseFile to POSIX path of (theFolder & "データベース:MyDatabase.dbev")

tell application "Database Events"
    tell database databaseFile
        -- 人口400 万人以上を抽出
        set theResult to value of fields of (records whose value of field "population" of it is greater than 4000000)

        -- シェルの sort で使うスペース区切りのデータを作成
        set tmpText to ""
        set lastNum to count theResult
        repeat with i from 1 to count theResult
            set theList to item i of theResult

            if i is lastNum then
                set tmpText to tmpText & my listToText(reverse of theList, space)
            else
                set tmpText to tmpText & my listToText(reverse of theList, space) & (ASCII character 10)
            end if
        end repeat
        --return tmpText
        my shellSort(tmpText)
    end tell
end tell

on listToText(theList, delimStr)
    set the AppleScript's text item delimiters to delimStr
    set theText to theList as text
    set the AppleScript's text item delimiters to ""
    return theText
end listToText

on shellSort(theData)
    do shell script "echo " & quoted form of theData & " | sort -nr"
end shellSort

この場合は、最初のフィルタで全ての field の値を取り出しています。結果は、

11680490 東京 東京都
8624045 大阪 大阪府
8324355 横浜 神奈川県
6875723 名古屋 愛知県
6838164 浦和 埼玉県
5863182 千葉 千葉県
5691737 札幌 北海道
5500842 神戸 兵庫県
4955439 福岡 福岡県

このようなテキストになります。あるいは、このソートの方が使い勝手がいいかもしれません。いずれにしても AppleScript にソートがないために少しばかりスクリプトの手数が必要になってきます。

Database Events (1)

最近、Database Events を利用する状況が出てきたので、少し調べてみました。Mac OS X 10.4 を導入したときにも少し触れたことがあるのですが、今度は利用を前提にどれぐらい使えるものかを。

まず、基本的なことから。Database Events は、SQLite を AppleScript から操作できるバックグラウンドアプリケーションです(SQLite だと思う。どうも、情報が少なくて)。SQLite 自体は、C 言語のライブラリです。C 言語から利用できますし、Terminal からコマンドを使うことでデータベースを操作することもできます。SQLite は、SQL をベースに開発されています。SQL よりも処理が高速だったりします。SQL との違いは、ファイル単位でデータベースを構築することです。

ファイル単位というのがよく分からないと思いますが、MySQL のようにサーバーで利用する(のでも可能ですが)、巨大なデータベースを構築するというより、アプリケーションが利用するデータを保持しておく簡易データベースとして利用することができます。

Database Events で作成したファイルは、他のアプリケーションでも利用可能な汎用的なものになります。誤解を招く表現ですが、こういうことです。Mac OS X 10.4 では、CoreData が導入されています。CoreData を利用するアプリケーションは、SQLite のファイル形式でファイルを保存することができます。こういったファイルは Database Events で利用可能ですし、逆も同様です。

Database Events は、このようなことから最初にデータベースファイルを作成する必要があります。基本的な流れとしては、

  1. データベースファイルを作成
  2. レコードを追加する
  3. フィールドを作りデータを放り込んでいく

という形になります。

では、実際に使ってみます。Database Events は、/System/Library/CoreServices にあります。System Events や Image Events と同じですね。用語辞書を開いてみると...、データベースを利用するための最低限の属性しか定義されていません。database と record と field...。ソートや範囲を決めてのデータの取り出や検索や、そういったデータベースを操作するための命令のようなものはありません。これは、すべて AppleScript で処理をしろということなのでしょうか?

もう少し、使い勝手がよくてもいいような。まず、最初にデータベースを作成します。

Script Editor で開く

tell application "Database Events"
    make new database
end tell

database クラスの属性には name と location があります。name は、データベースの名前(通常データベースファイルの名前になります)、location は、データベースファイルの保管場所です。これらは、データベースを作成するときに指定しておくと効率的です。

Script Editor で開く

tell application "Database Events"
    make new database with properties {name:"MyDatabase"}
end tell

location を指定していませんが、Database Events はデータベースの保存場所をデフォルトで持っています。Documents フォルダ内に「データベース」(日本語環境です)というフォルダを作成し、その中に自動的に保存していきます。しかし、ここで注意しないといけないのは save 命令を使ってきちんと保存しないといくらファイルを作成しても利用できないようになることです。

Script Editor で開く

tell application "Database Events"
    make new database with properties {name:"MyDatabase"}
    save database "MyDatabase"
end tell

このように save を行うと ~/Documents/データベース/MyDatabase.dbev というファイルが作成されます。location を指定しない場合は、全てこのフォルダに保存されます。保存を行っておくと Database Events を終了した後でも次のようにすることでデータベースを再度利用することができます。

Script Editor で開く

set databaseFile to (path to documents folder as Unicode text) & "データベース:MyDatabase.dbev"

tell application "Database Events"
    tell database (POSIX path of databaseFile)
        properties
    end tell
end tell

save を行わなかった場合、いくらファイルが作成されていたとしてもそのファイルにアクセスすることはできません。この保存は、record を追加しても field を追加しても、その都度保存しないといけません。

機能アップ

機能がアップしているからかどうか分からないのだけど、次のような参照の書き方が利用できます。これって、今までも出来ていました?

Script Editor で開く

on run
    tell application "iCal"
        activate
        set theDate to current date

        set todoList to {}
        repeat with thisCalendarTodos in todos of calendars -- 1
            set propList to properties of thisCalendarTodos -- 2
            if propList is not {} then
                repeat with thisItem in propList
                    set dueDate to due date of thisItem
                    if dueDate is greater than theDate then
                        set num to dueDate - theDate
                        set finalDays to num div days
                        set theText to "残りの日数:" & finalDays & " 日" & return
                        display dialog theText buttons {"OK"} default button 1 giving up after 15 with title (summary of thisItem as string) with icon 1
                    end if
                end repeat
            end if
        end repeat
    end tell
end run

このスクリプトは、iCal の全 ToDo の期限を調べるスクリプトです。これ自体は、何の変哲もないのですが...、スクリプトで、2 とコメントしている部分が以前から出来ていたのかどうかが気になるのです。

repeat - in - の部分で全カレンダーの全 ToDo を取得しています。これは、全カレンダーの全 ToDo の参照になります。この参照から中身を取り出すには、contents of - とします。ここまでは、いいのです。その次の参照に対して properties としている部分です。これで、カレンダーの全 ToDo の全ての属性が取得できます

理屈としては理解できます。ただ、こういった記述が以前は出来なかったような気がするのです(出来たのかもしれませんが...)。こういう記述が使えるなら、繰り返しが極力短く記述できます。また、参照に対して処理を行うので処理速度的にも問題なしです。

AppleScript には、まだまだ知られざるアプローチが多々ありそうです。

WWDC のサンプル

過ぎ去られた話題の WWDC。Intel のことはともかく...前回、リファレンスに色々追加されています...といったことを書いたまま、尻切れとんぼで話を打ち切ってしまいました。

なぜかというと、サンプルが非常に濃い内容だったのです。そっちに夢中になっていて書くのを終えてしまったのでした。どんなものが追加されたかというと、前回も書いた Automator Programming Guide。次に、Xcode 2.0 のプロジェクトを 2.1 にアップデートする UpdateXcodeSubprojects。AppleScript で作成する Automator アクションをステップバイステップで解説する Duplicate Finder Items。そして、ScriptView

ScriptView は、AppleScript Studio で作る Script Editor です。これもステップバイステップの解説が付属しています。実際に中身を見るまでは半信半疑だったのですが、本当に AppleScript Studio で Script Editor が作れてしまいます。というのも、Developer Tools には Script Editor のインターフェースが付属しているからなのです。これは知りませんでした。このインターフェースを使えば、Script Editor の作成ができます。

/Developer/Extras/Palettes/OSAPalette.palette がそのインターフェースです。これを Interface Builder に追加すれば、Script Editor のインターフェースが作成できます。スクリプティングのほとんどの作業はこのインターフェースが受け持っており、AppleScript Studio で行うのは欲しい機能の追加です。

ということで、自分好みの Script Editor を作るのが流行る....かも。そんなことないか。