おばあちゃんの知恵袋

知られているようで、案外知られていないかもしれないかもしれない(?)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 で操作できるようになります。設定は至って簡単。セキュリティはどうなのか知りませんが。

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