store script 命令でスクリプトの動的保存

run script 命令と同じぐらい話題に上らない store script 命令。ということで、今回は store script 命令。store script 命令っていうのはスクリプトオブジェクトをファイルに保存する命令です。これも個人的には頻繁に使ってます。

基本的な使い方は簡単です。

Script Editor で開く

script StoredScript
    on run
        display dialog "Hello, World"
    end run
end script

store script StoredScript

このスクリプトを実行するとスクリプトを保存する場所を尋ねてくるので、適当な名前を付けて保存します。保存されるのはコンパイル済みスクリプトファイル(拡張子 scpt のファイル)です。

保存されたスクリプトを Script Editor で開いてみると以下のようになっていると思います。

on run
    display dialog "Hello, World"
end run

基本的には保存したスクリプトを load script 命令で読み込むことを意識しているのだと思います。store script 命令は。だからでしょうか。スクリプト定義(on script、end script)の部分は削除されています。

実行時にいちいち場所を尋ねられるのも面倒なので、多くの場合 in オプションを使って直接ファイルを指定します。

Script Editor で開く

script StoredScript
    on run
        display dialog "Hello, World"
    end run
end script

set my_path to path to me
tell application "Finder" to set working_folder to folder of my_path as text
set script_file to working_folder & "Stored Script.scpt"

store script StoredScript in file script_file replacing yes

set the_object to load script file script_file
run the_object

このとき、保存先のファイル名の拡張子を「scptd(スクリプトバンドル)」にしておくと、保存されるファイルはスクリプトバンドルになります。

それでは、store script 命令はどういうときに使うのか?

  1. 一時的なデータの保存
  2. スクリプトの動的生成

store script 命令はスクリプトオブジェクトを保存するので、一時的なデータやスクリプトの設定などをそのまま書き出すことができます。Property List を使うという方法もありますが、AppleScript のデータをそのまま保存できるので面倒がなくていいです(リストやレコードを保存するなら write 命令を使ってファイルに書き出すこともできますが)。これは、従来からよく利用されている使い方です。

そして、もう一方の「スクリプトの動的生成」。単純にコンパイル済みスクリプトを保存するだけなら関係がないのですが、スクリプトの実行時にスクリプトアプリケーションを動的に生成して処理を分散させたい、ということがあります。

Mac OS X には osacompile というコマンドがあります。store script 命令とこの osacompile を組み合わせるとスクリプトアプリケーションの動的生成が可能になります。例えば、以下のスクリプトはタイマーを動的に生成します。

Script Editor で開く

on Timer(sec)
    script Timer
        property sentence : "Are you ready? I'm ready."
        property period : sec
        property wakeup : missing value

        on alarm()
            say sentence
        end alarm

        on quit
            set wakeup to missing value
            tell me to continue quit
        end quit

        on idle
            if wakeup is missing value then
                set wakeup to (current date) + period
            end if

            if (current date) > wakeup then
                alarm()

                tell me to quit
            else
                return 1
            end if
        end idle

        on run
            tell me to idle
        end run
    end script
end Timer

property timeList : {"30 seconds", "1 minute", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "45 minutes", "1 Hour"}
property secondsList : {30, 60, 180, 300, 600, 900, 1800, 2700, 3600}

on run
    tell me
        activate

        set thisItem to choose from list timeList default items (item 1 of timeList) with prompt "タイマーを設定"
        if thisItem is false then return
        set thisItem to thisItem as text

        repeat with i from 1 to count timeList
            if item i of timeList is thisItem then exit repeat
        end repeat
        set sec to item i of secondsList
    end tell


    set tmpFolder to path to temporary items folder from user domain as text
    set desktopFolder to path to desktop as text
    set scptFile to tmpFolder & "tmp.scpt"
    set appFile to desktopFolder & "Timer-" & (sec as text) & " seconds.app"
    store script Timer(sec) in file scptFile replacing yes


    do shell script "osacompile -s -o " & quoted form of (POSIX path of appFile) & " " & quoted form of (POSIX path of scptFile)

    tell application appFile to run
end run

実行すると時間を尋ねます。時間を設定するとデスクトップにスクリプトアプリケーションを生成します。例えば、時間に 1 分を指定するとアプリケーション起動時から 1 分後にお報せを行い、終了します。生成されたスクリプトアプリケーションは繰り返し使えます。

まぁ、動的生成なんてあまり需要がないかもしれないですが。

run script 命令の利用法

AppleScript Programming Tips...と書いたけど、内容はたいしたことはない。[Cocoa Programming Tips](http://hmdt.jp/cocoaProg/index.html "Cocoa Programming Tips | HMDT Programming Tips") の真似がしたかっただけです。知っているとちょっと役に立つかもしれないことをまとめられるといいかな、と。

run script 命令について。load script 命令はわりと話題にでるけど、run script 命令のことってあまり見ない。これは、おそらく run script 命令を使うと処理が遅くなる、ということに起因しているのではないでしょうか。でも、たいして遅くならないよ。今の Mac なら。頻繁に使うとなんだけど、ちょっと使うぐらいなら気にはならない。AppleScript は Mac OS X(特に 10.4 以降)になってからできることが飛躍的に増えて、run script を使うことが個人的には増えました。

run script 命令は文字列(もしくはスクリプトファイルやスクリプトが記述されたテキストファイル)を、実行時にコンパイルし AppleScript として評価する命令です。実際は scripting component(利用するスクリプト言語のこと)を指定することができるのですが、AppleScript 以外を使ったことがないので他の scripting component については割愛します。

run script 命令に渡す文字列は実行時にコンパイルされるので、文法的に正しいなら省略して書いても構いません。

Script Editor で開く

run script "tell app \"finder\" to activate"

文字列をその場でスクリプトとしてコンパイルするので、実行しているスクリプトとは別物のスクリプトが生成されます。ですから、run script で生成したスクリプトは変数やプロパティは共有されません。

Script Editor で開く

set x to 10

run script "set x to 20\r display dialog x as text"

display dialog x as text

このようにそれぞれ個別の変数 x を持っています。ただ、run script 命令はスクリプトの run ハンドラを実行するスクリプトですので、with parameters オプションを使って実行するスクリプトから値を渡すことはできます。

Script Editor で開く

set x to 10

set x to run script "on run argv\nset x to argv\ndisplay dialog x as text\nreturn 20\nend run" with parameters x

display dialog x as text

実際のところ run ハンドラが引数を受け取れるようにしておく状況というのは、

  1. run script から利用するためのスクリプト
  2. シェルスクリプトとして利用するためのスクリプト

のどちらかだと思います。しかし、以下のように常に run ハンドラが値を受け取ることができるようにしておいても何も問題はありません。

Script Editor で開く

on run (argv)
    -- argv は、ハンドラらしく on run (argv) と書いても OK
    -- argv はリストとして解釈される

    if argv is me then
        -- なんらかの処理
        display dialog "me"
    else
        -- run script で値を渡された時の処理
        set argv_class to class of argv
        display dialog argv_class as text
    end if
end run

このスクリプトを Script Editor 等で実行すると display dialog "me" が実行されます。run ハンドラ実行時に渡す引数がないとき、argv は me(そのスクリプト自身)になります。だから、上記のように引数があるときとない時で処理を分岐することができます。

Script Editor で開く

on run (argv)
    -- argv は、ハンドラらしく on run (argv) と書いても OK
    -- argv はリストとして解釈される

    if argv is me then
        -- 通常の処理
        -- このスクリプトを編集、実行しているときはこの if 文が実行される
        display dialog "me"
        -- 自分を呼び出してみる
        -- with parameters に渡すのはリストでもただの文字列でも OK
        run script (path to me) with parameters {10, "100", (current date) as text}
        run script (path to me) with parameters "Hello, World"
        -- with parameters なしで実行すると...予想通り、無限ループ
        -- run script (path to me)
    else
        -- run script で値を渡された時の処理
        set argv_class to class of argv
        display dialog argv_class as text
        repeat with this_item in argv
            display dialog this_item
        end repeat
    end if
end run

このスクリプトを保存してから実行してみてください。run script の動きと渡される引数が何かが分かります。

以前にハンドラを文字列で指定するという話題(文字列で指定したハンドラを実行するその後の「文字列で指定したハンドラを実行する」)を書きましたが、あの時も run script を使いました。スクリプトとして解釈できる文字列を利用する run script 命令は使い方次第でいろいろなことができます。

例えば、System Events の keystroke 命令。この命令は便利なものだけど、ショートカットキーを押すのに修飾キーを指定しないといけない。この修飾キーがあるために汎用的なハンドラにするのは if 文だらけになりそうだ...というとき。以下のように run script を使うときれいにまとまる。

Script Editor で開く

do_keystroke("Finder", "n", "command down") -- 新規 Finder window
do_keystroke("Finder", "n", "{command down, shift down}") -- 新規フォルダ

on do_keystroke(process_name, key_string, modifier_keys)
    -- Send key stroke specify application.
    -- modifier_keys argument is string. "command down" or "{command down, shift down}" ...
    tell application "System Events"
        tell process process_name
            if not frontmost then set frontmost to true
            keystroke key_string using (run script modifier_keys)
        end tell
    end tell
end do_keystroke

例えば、Spaces。Mac OS X 10.5 からは System Events で「システム環境設定」の設定を調べたり、変更したりできるようになりました。Spaces もそのひとつ。以下のスクリプトで Spaces でどのアプリケーションがどの操作スペースに割り当てられているかを調べることができます。

Script Editor で開く

tell application "System Events"
    tell expose preferences
        tell spaces preferences
            application bindings
        end tell
    end tell
end tell

実行してみると分かるのですが、返ってくるのは以下のようなレコード。

{|com.apple.safari|:9, |com.apple.terminal|:65544}

アプリケーションの id がラベルになっていて、操作スペースの数字が値になっている。65544 というのは全ての操作スペースに割り当てた時のものだな。では、上記のようなレコードを作って追加すれば新しいアプリケーションも追加できるのだな。

...と、全くその通りに追加できるのですが、ふと思う。このようなレコードをどうやって作ればいいのだろうか?と。ご存知の通り、AppleScript のレコードは他のプログラム言語にあるようなハッシュや辞書のような使い勝手のいいものではなくて、レコードに新しくラベル(キー)を追加することができない。こういうときに run script。以下のスクリプトでは前面のアプリケーションに操作スペースを割り当てます(スクリプトメニューなどから利用します)。

Script Editor で開く

property upper : "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
property lower : "abcdefghijklmnopqrstuvwxyz"
property all_spaces : 65544

tell application "System Events"
    set front_app to my front_application()
    set identifier to my to_lower(bundle identifier of front_app)
    set app_name to name of front_app

    tell expose preferences
        tell spaces preferences
            set props to properties
            set num to (spaces columns of props) * (spaces rows of props)
            set bindings to application bindings of props
        end tell
    end tell

    set {spaces_list, menu_list} to my make_menu(num)
end tell

tell application (path to frontmost application as text)

    set the_result to choose from list menu_list default items (item 1 of menu_list) with prompt (app_name & " をどこに割り当てますか?")
    if the_result is false then return
    set the_result to the_result as text

    repeat with i from 1 to count menu_list
        if item i of menu_list is the_result then exit repeat
    end repeat
    set target_spaces to item i of spaces_list
    set the_record to my make_record(identifier, target_spaces)
    if bindings contains the_record then
        display dialog app_name & " は " & item i of menu_list & " に既に割り当てられています" with icon 1 buttons {"OK"} default button 1
        return
    else
        display dialog app_name & " を " & item i of menu_list & " に割り当てます" with icon 1
    end if
end tell

tell application "System Events"
    tell expose preferences
        tell spaces preferences
            set application bindings to the_record & bindings
            application bindings
        end tell
    end tell
end tell

on make_record(identifier, spaces_number)
    set r to ("{|" & identifier & "|:" & spaces_number as text) & "}"
    return run script r
end make_record

on front_application()
    tell application "System Events" to return first item of (application processes whose frontmost is true)
end front_application

on make_menu(num)
    set spaces_list to {}
    set menu_list to {}

    repeat with i from 1 to num
        set end of spaces_list to i
        set end of menu_list to "Spaces " & (i as text)
    end repeat

    set end of spaces_list to all_spaces
    set end of menu_list to "All Spaces"
    return {spaces_list, menu_list}
end make_menu

on to_lower(the_text)
    set char_list to characters of the_text

    considering case
        repeat with char in char_list
            set num to offset of char in upper
            if num is not 0 then set contents of char to character num of lower
        end repeat
    end considering

    return char_list as text
end to_lower

これは単純なサンプルですが、Spaces のように簡単に追加や削除がやりにくいレコードで結果を返すアプリケーションって、増えているのです。格好のいいものではないのですが、レコードの動的生成はよく使います。あと run script でよく利用するのは、先ほども書いたハンドラを文字列で指定するとき。単純にハンドラを文字列で指定するだけなら以下のような方法で十分でしょう。

Script Editor で開く

on run
    set my_path to path to me as text
    set the_method to run script ("say_hello of (load script file \"" & my_path & "\")")

    the_method("Bob")
end run

on say_hello(your_name)
    display dialog ("Hello, " & your_name)
end say_hello

保存されていることが条件ですが、これもよく利用します。

Mac OS X にインストールされている声の一覧を取得する

Mac OS には昔から音声認識とテキスト読み上げを行う機能が備わっていました。Mac OS X の現在、英語だけですが...。Mac OS X 10.5 からは新しい Alex というキャラクターが加わりました。そして、この Alex が非常に滑らかに英文を発話する。英語のサイトを見ているときに発話させてみて、その流暢さを堪能することもしばしば...。

以前にも書いたように整理中のスクリプトファイルを実行しつつ取捨選択をしているのですが、このなかにテキスト読み上げで利用できる Mac OS X にインストールされている声の一覧を取得するスクリプトがありました。問題は、このルーティンが使えなくなっていること。使えないとなると、(困りはしないのですが)困る。一念発起(というものでもないですが)して原因と対策を探ってみました。

このルーティンは単純に /System/Library/Speech/Voices にあるファイル一覧を取得して拡張子を削除しているものですが...いつの頃からか、読み上げに利用できる声のファイル形式が変わっていたのですね。それぞれの声がプラグインになっている...。

声の一覧が欲しいという要望は少ないながらもあるようで、Web を探してみるとありました。それ専用のアプリケーション(AppleScript で操作できる)があったりと。中でも気を引いたのが PyObjC を使ったもの。なるほどね、と思いました。

Mac OS X 10.5 からは RubyCocoa や PyObjC が標準で入っています。これを使って Cocoa の機能を利用すればいいのか。NSSpeechSynthesizer の availableVoices を使えば、声の一覧が取得できます。

#!/usr/bin/python
# vim: fileencoding=utf-8

from AppKit import NSSpeechSynthesizer

voices = NSSpeechSynthesizer.availableVoices()
voices_attrs = [ NSSpeechSynthesizer.attributesForVoice_(v) for v in voices ]
for v in voices_attrs: print v['VoiceName'].encode('utf-8')

こんな Python スクリプトファイルを用意して、Terminal 等で実行すれば、声の一覧が取得できます。

$ python voices.py 
Agnes
Albert
Alex
Bad News
Bahh
Bells
Boing
Bruce
Bubbles
Cellos
Deranged
Fred
Good News
Hysterical
Junior
Kathy
Pipe Organ
Princess
Ralph
Trinoids
Vicki
Victoria
Whisper
Zarvox

AppleScript の do shell script を使って上記をのスクリプトを実行し、改行で分割すれば OK。...。...。...。...。...いや、待て。これが答えか?AppleScript 使いを自認し、AppleScript の使い方や Tips を紹介するサイトが PyObjC に頼っていいのか?いや、駄目だ。それは私の矜持が許さない。

Script Editor で開く

tell application "Automator" -- Xcode でも可
    set voices_list to call method "availableVoices" of class "NSSpeechSynthesizer"
    set voices_names to {}
    repeat with this_voice in voices_list
        set voice_attrs to call method "attributesForVoice:" of class "NSSpeechSynthesizer" with parameters {this_voice}
        set end of voices_names to |VoiceName| of voice_attrs
    end repeat
    voices_names
end tell

これでこそ AppleScript。Automator を使えば、Cocoa の機能を利用できる(Mac OS X 10.6 ではおそらく動かないのでは?)。まぁ...PyObjC や RubyCocoa を使うのが最も現実解ですね。で、それを使ったサンプルスクリプトを。実行するとそれぞれの声がデモテキストを使って自己紹介をします。

Bushism

ブッシュ?

ハードディスク内の AppleScript ファイルを整理していると過日、書きました。まぁ、これがなかなか面白い。時間の経つのも忘れて実行したり、使えるものは修正したり...。久しぶりに AppleScript の面白さを確認しました。そんな中で在りし日のブッシュさんの発言を表示するスクリプトがありました。

中身は見てもらうと分かるのですが、SOAP を使っているだけです。ただ、妙に凝っています。少しおふざけが混じっています。人によっては下品と受け止めるかもしれません。まぁ、ジョークと受け取ってもらえれば...。もちろん、なんらかの抗議や文句があった場合は、速やかに削除します。抗議のある時は右側のメールアドレスから抗議文を送ってください。ちょっとだけ細工をして Amazon のアフィリエイトを使って小金を儲けようと企んでいます。気に入らなかったらその部分を削除してください。

作りためていた多くの SOAP や XML-RPC を使った AppleScript は動かなくなっているのにこれだけは動いている...。SOAP や XML-RPC を使った AppleScript は、依存しているサーバーアプリケーションによるので、サーバーアプリケーションがサービスを停止した場合、使えなくなるんですね。だいたい Apple の XML-RPC や SOAP の解説資料で使っているサービスが使えないなんて...。

AppleScript で diff

書いてはほったらかし、書いてはほったらかし...そんな使い勝手のいい AppleScript ですが、ハードディスクを整理していたら、でるわでるわコンパイル済みスクリプトファイル...。要不要をチェックしながら整理しているのですが、同じようなスクリプトのどこが違うのかを確認するのはいちいち面倒だな...と思っていたのです。ファイルを開いてみないと分からないし、Script Editor では行番号すら表示してくれない。で、思いつきました。FileMerge があるじゃないかと。

そのままでは AppleScript のスクリプトファイルを FileMerge で比較することはできませんが、FileMerge は Filter の設定でファイルにフィルターをかけることができるのでした。そして、Mac OS X 10.5 以降ならスクリプトファイルのコードを表示してくれる osadecompile があるのでした。

osadecompile はその名の通り、AppleScript(等の)コンパイルされたスクリプトファイルから中の文字列を表示してくれるツール。スクリプトファイルが「実行専用」で保存されていない限り「コンパイル済みスクリプト(拡張子: scpt)」、「アプリケーション(拡張子: app)」、「スクリプトバンドル(拡張子: scptd)」、「アプリケーションバンドル(拡張子: app)」、「テキスト(拡張子: applescirpt)」のそれぞれのコード文字列を表示してくれる。

使い方は至って簡単で、ターミナルなどで

$ osadecompile ~/Desktop/Classic.app

とするだけ。スクリプトファイルを引数にとり、オプションなどは一切なし。これを FileMerge の「環境設定(英語しかリソースがないので Preferences... となっている)」にある「Filters」で以下のように設定。

/usr/bin/osadecompile $(FILE)

「Extension」は「scpt」、または「applescript」、「Display」は「Filtered」、「Apply」は「No」で。

FileMearge 環境設定

これでスクリプトファイルの比較ができるや...と思いきや、このままでは日本語の混じったスクリプトファイルだと表示されなかったりする...(まぁ、プログラムファイルに日本語などを混ぜてはいけないってことなんだろうけど、日本語もごちゃ混ぜで記述できる気楽さが AppleScript のいいところなんだよな)。また、スクリプトアプリケーションやスクリプトバンドルなどもそのままではうまく表示できない。フィルタースクリプトを書いて処理すればいいんだろうけど...挫折してしまいました。

誰か、書いてくれないかな。

Twitter と AppleScript

Twitter を始めた...のだけど、気楽につぶやく方法ないかなと「Twitter AppleScript」で検索をしてみた。すると、iTunes で聴いている曲をつぶやくという AppleScript が結構あったりする。少し前に話題になったことみたいだけど。長いことほったらかしだったので、なんだか時代に取り残されている感がひしひしと感じられるなぁ...。

ああ。時代に取り残されているといえば、ウチはまだ Leopard です。Snow Leopard ではありません。だから、検証は Mac OS X 10.5 でってことになってます。

で、Twitter API を眺めたり、いろいろ調べているうちに Basic 認証を 2010 年 6 月以降は非推奨にするという情報を発見(Twitter APIのBASIC認証は2010年6月に「廃止予定」 - 頭ん中)。

OAuth を使わないといけないようになってきたんだな...。いや、さすがに OAuth を AppleScript でサポートするのは面倒そうだ。先のリンクにもあるようにブラウザを使用しないアプリケーション向けに新しい API を提供するようなので、それを見てからの方がいいかな。スクリプトを作るの...と、気分が萎えてしまった。