Automator でサービスを作成する前に...

ようやくのことで Snow Leopard を入れてみました。環境を整備し、ソフトウェアを入れ、じっくりと触ってみました。なんだか、描画が変になることがありますし、微妙な違和感(バグかどうかよくわからない現象が時々起こる)があります。Leopard の方が使いやすかったなぁ...。Leopard は手に馴染んでいた。まぁ、使い勝手は慣れてくるのでしょうが。

さて。

今更の感もありますが、Snow Leopard の目玉の一つ、Automator。以前にも Automator の記事を書きましたが、Snow Leopard になっていろいろと便利になったようなので、Snow Leopard に対応したものを。

Automator は、基本的にはなんらかの「入力」を受け取り、それらを「加工」し、加工したものを「出力」します。この処理プロセスはプログラムの基本なのですが、Automator はプログラムらしさを排除し、わかりやすいインターフェースで処理プロセスを作成することができるアプリケーションです。

Automator が入力として受け取れるデータはいろいろとありますが、基本は「ファイル/フォルダ」か「テキスト」です。出力も同様です。

Automator は入力されたデータを「加工」するのですが、データを加工するのが「アクション」です。アクションは単体で動作するものもありますし、それだけでは動作しないものもあります。アクションを一つ、または複数つなげることで目的のデータに加工し、出力します。アクションは直前のアクションが加工したデータを入力として受け取り、次のアクションに加工したデータを渡します。最後のアクションが出力するデータが最終的な結果となります。

アクションを一つ、または複数並べたものを「ワークフロー」といいます。ワークフローを保存しておくことで様々な場面で繰り返し利用することができます。

Automator を起動すると、最初に「ワークフロー」のテンプレートを選択することになります。利用目的によってこれらのテンプレートから一つを選び、ワークフローの作成を開始します。

テンプレート「ワークフロー」は汎用的なワークフローです。目的に応じてワークフローを作成し、保存しておきます。保存したワークフローの利用方法は多岐に渡ります。

保存されたワークフローを ~/Library/Scripts に入れておくことで、スクリプトメニューから利用することができます。また Terminal からワークフローを指定して automator コマンドで処理をさせることができます。

テンプレート「ワークフロー」で作成したワークフローは「プリントプラグイン」、「イメージキャプチャ・プラグイン」としても利用できます。これらは特別に Automator のテンプレートを利用しなくても特定のフォルダの中に入れておくだけで利用できます。

また、ワークフローを「アプリケーション」として保存しておくとドラッグ & ドロップでデータを受け取ることができるようになります。アプリケーション形式だと「iCal アラーム」としても利用できます。

基本的にはテンプレート「ワークフロー」でワークフローを作成、保存しておくことで上記のような利用が可能になります。しかし、このテンプレートでは「サービス」と「フォルダアクション」として利用できるワークフローにはなりません。「サービス」と「フォルダアクション」だけは少し特殊で、これらはテンプレートを利用しないと作成することはできません。

「フォルダアクション」は最初に対象となるフォルダを設定しておく必要があります。フォルダを設定し、保存(~/Library/Workflows/Applications/Folder Actions に保存される)した時点でフォルダアクションは実行されるようになります。停止は ~/Library/Workflows/Applications/Folder Actions から対象のワークフローを取り除くことによって行います。

「サービス」は ~/Library/Services にワークフローとして保存されるのですが、普通のワークフローではなく、サービス経由でデータを受け取れるように設定されて保存されます。この設定があるため、通常のワークフローは「サービス」として利用することができません。

と、まぁ基本的なところは以上でしょうか。

なんといっても目玉は「サービス」なんでしょうね。ところで、Snow Leopard になってから /Library/Scripts にインストールされるスクリプトが減りましたね。Finder 関連のものは全てなくなってしまいました。また、ColorSync のスクリプトは ColorSyncScripting を利用していたのが sips で代用するようになっていますね。Finder 関連のものがなくなったのは Automator の「サービス」があるからいいでしょ、ということでしょうか。

なくなった Finder 関連のスクリプトの代替サービスは Mac OS X Automation: Services Downloads で入手することができます。ここからダウンロードできるサービスとアクションは全て入れておくといいと思います。便利なものが揃っています。中でも面白いのが「Website Popup」というアクション。URL を渡すと半透明の HUD ウィンドウにウェブサイトを表示してくれます。

今回、久しぶりに Automator を本格的に触ってみました(Mac OS X 10.5 の時は全く使っていませんでした)が...、なかなか楽しませて頂きました。Finder 関連のスクリプトがなくなるのもむべなるかな。

しかし、分かりにくいところもありますが。とてつもなく分かりにくいのが、変数。その次に分かりにくいのが「サービス」の「選択項目」と「検索対象」。両方とも理解できるとどうってことないのですが。

まず、「サービス」の「選択項目」。基本的には「選択されているテキスト」か「選択されているファイルまたはフォルダ」のいずれかになります。さらに「選択されているテキスト」は「URL」、「アドレス」、「電話番号」、「日付」、「メールアドレス」のいずれかから選択することができます。例えば、URL を選んでおくと URL 以外の文字列を選択してもサービスのメニューには表示されません。

サービスはこのように賢く振る舞ってくれるのですが、正確には URL を含んでいる文字列を認識し、それをワークフローの入力として渡します。ですから、入力された文字列は、必ずしも正確な URL だとは限りません。正確な URL を抜き出す作業はユーザーに一任です。この作業を行ってくれるアクションは Mac OS X Automation: Services Downloads からダウンロードできます。Get URLs from Text がそれですが、Get Dates from Text というアクションも一緒にぜひとも入れておきましょう。

「ファイルまたはフォルダ」も同様で、例えば「イメージファイル」を選択しておくと「イメージファイル」以外が選択されているときはメニューに表示されません。

「検索対象」は作成した「サービス」がどのアプリケーションで表示されるかを設定するためのものですが、「すべてのアプリケーション」を選択することでどのアプリケーションであってもメニューに表示されるようになります。問題は「選択項目」でこれを「入力なし」にするとアプリケーションのサービスメニューにしか表示されないようになります。コンテキストメニューの方では表示されません。

つまり「選択項目」を「入力なし」、「検索対象」を「すべてのアプリケーション」にすると、すべてのアプリケーションで利用できるけど、アプリケーションメニューのサービスメニューにしか表示されないサービスになるのです。このようなサービスはいつでもどのような状況からでも利用できるメニューになるのでキーボードショートカットを与えておくと便利に利用できます。

「選択項目」と「検索対象」でどこのメニューに表示されるか、どのようなときに表示されるかが決定されます。

Automator は Mac OS X 10.5 の時から「変数」が使えたのですが...正直、目的の変数をどのように作るのか理解するのには骨が折れました。

「変数」は便利なように最初からデフォルトの値を持ったものがあります。歯車のアイコンがついているものがそれです。他方、値をカスタマイズできるように設定を行える変数があります。V というアイコンがついているものがそれです。

これらの用意さている変数を組み合わせて目的の変数を作成します。では、例えば、ファイルを保存するのに一意の名前を付けたい、といったとき。スクリーンキャプチャしてファイルを保存するとき、「Capture-ユーザー名年月日時間」といった書式のファイル名を作りたいとします。

必要なのは「ユーザー名」と「年月日」と「時間」、それに保存する「場所」です。これらは用意されている変数から作成します。

まず、Automato の「変数」を表示し「ユーザー」を選択、その中にある「ユーザー名(ショートネーム)」をダブルクリックします。

Automator で変数を配置する

すると、ウィンドウの下部にある「変数」に「ユーザー名(ショートネーム)」が追加されます。

変数一覧に追加された変数

同様の手順で「場所」から「ピクチャ」を。そして、「日付と時刻」から「今日の日付」と「現在の時刻」を追加します。これらは値をカスタマイズできる変数です。現在、変数一覧は次のようになっています。

追加された変数一覧

「時刻」と「日付」の書式をカスタマイズします。変数一覧にある「今日の日付」をダブルクリックすると小さなウィンドウが表示されます。「名前」は変数名になります。the_date としておきます。「フォーマット」から「カスタムフォーマット...」を選択し、下図のようにします。

日付のフォーマットを変更

年月日の間にあるハイフンは自分で入力します。また、月日はそれぞれ 2 桁になるように変更しておきます。

月日を 2 桁表示に

同様の手順で「現在の時刻」も下図のように変更しておきます。

時刻のフォーマットを変更

以上で必要な変数は揃いました。後はこれらを組み合わせてファイル名を作成します。変数一覧の部分でコンテキストメニューを表示すると「新規変数...」というメニューが現れます。これが、欲しかった変数で見つけるのに時間がかかった変数です。

コンテキストメニューの「新規変数...」メニュー

この変数は「テキストとデータ」にある「テキスト」という変数なのですが、まさか、これを使って複数の変数を組み合わせるなんて思いもよりませんでした。ともかく、「新規変数...」を選択するとこの変数が追加されます。追加された変数をダブルクリックし、下図のようにします。

変数の変更

分かりにくいのですが「値」は変数一覧にある変数をドラッグ & ドロップして追加します。『ピクチャ』「Capture-」『ユーザー名(ショートネーム)』「_」『the_date』「T」『the_time』という並びになっています(『』で囲んでいるものが変数一覧からドラッグ & ドロップしたもの。「」で囲んでいるものは記述したもの)。

この file_name という変数をワークフローのところにドラッグ & ドロップし、「結果を表示」アクションを使うと変数がどのような結果を返すかが確認できます。

変数の結果を確認

こうやって作った変数はスクリーンキャプチャの保存先や、新規テキストファイルの保存先などとして利用することができます。拡張子は必要ありません。適宜、補ってくれます。

変数を保存先に適用

変数はこうやって使うのですね...。全く知りませんでした。

実際にいくつかサービスを作ってみようと思ったのですが...そこにたどり着くまでが長かった。ということで、次の機会にでも。

Safari のウィンドウとドキュメント

Safari がタブに対応したのはいつでしたでしょうか?タブに対応したのはともかく、AppleScript からタブを操作できるようになったのはいつからでしたでしょうか。こういう時に参考になるのが AS Hole(AppleScriptの穴)。ここを見ると AppleScript でタブがまともに利用できるのは Mac OS X 10.5 以降、Safari 3 以降のようです(Safariの最前面のウィンドウ内のタブを順々に表示)。

何で、今更タブのことなんか?

Safari の AppleScript 対応度というのは Finder などに比べると貧弱なものです。それでも Safari の自動化に何らししょうがないのは AppleScript 内から Safari に JavaScript が実行させることができるからです。

それは、アプリケーションに AppleScript をどのように対応させるか?といった問題へのシンプルな答えでもあります。「ウェブブラウザの自動化や処理がどういう時に必要なのか?」といったことを考えた時、JavaScript を実行できることが何よりも問題の解決に役立ってくれるでしょう。

AppleScript への対応というのは、このようにキモとなる部分さえ外していなければ、Safari の do JavaScript 命令の実装だけで解決してしまうものなのです。これは他のアプリケーションでも同様です。そのため用語辞書を見るとアプリケーション作成者の AppleScript の理解度が分かったりできて面白かったりします。

閑話休題。

ようするに do JavaScript 命令は古くは document オブジェクトを引数に指定していたのですが、タブを操作できるようになると、tab オブジェクトを引数に渡すように変更されたのです。そして、tab オブジェクトは window の要素なのです。

この変更により、古いスクリプトは修正が必要になりました。

また、少し話は変わりますが、Safariで開いているページの一時保存・リストアで紹介されているスクリプト。重宝しています。

重宝しているのだけど、時々エラーになってしまうことがあるようです。どういう時にエラーになるかというと、ダウンロードしている項目を表示する「ダウンロード」というウィンドウや環境設定のウィンドウなどが前面にある時です。要するに document を持たないウィンドウが前面にある時です。

現在の Mac のアプリケーションの多くは Cocoa/Objective-C で作成されています。これらのアプリケーションは多くの window を利用しますが、ドキュメントベースのアプリケーション(書類の編集などができるアプリケーション)は window の中に document というオブジェクトを持っています。細かいことは省きますが、document を持っている window と持っていない window(設定パネルやダイアログ等)の 2 種類があるのです。

AppleScript で document というと、多くの場合この document を持った window のことを指します。例えば、TextEdit などで Book Review.rtf という書類を開いており、最前面に環境設定を開いていたとします。

tell application "TextEdit"
    name of window 1
    --> 環境設定
    name of document 1
    --> "Book Review.rtf"
    name of windows
    --> {"Book Review.rtf", "環境設定"}
end tell

windows として複数の window オブジェクトを取得すると全てのウィンドウが取得できます(document も含む)。そして、上記のスクリプトでは Book Review.rtf を最前面にすると window 1 は Book Review.rtf になります。また、document を持たない環境設定などの window は閉じても AppleScript から取得できます(これらの window の属性 visible が false になるだけで、存在自体が消滅するわけではない)。

これらは AppleScript で window と document を操作する時の注意点ですが...何が問題かというと、Safari の場合、JavaScript を実行させるためにタブを持ったウィンドウを取得しなければならないのだけど、最前面の document が必ずしも最前面の window だとは限らない、ということです。

document を持った window で最前面のものを取得する必要があります。

こういうことを知ってかどうかは分かりませんが、window の属性に document というものがあります。少し前に追加されたものだと思うのですが。また、以前はきちんと動作しなかったような...。この属性を利用して document を持った最前面の window を取得することができます。

Script Editor で開く

tell application "Safari"
    set front_window to my get_front_window()
    if front_window is missing value then return

    tell current tab of front_window
        do JavaScript "new Date();" in it
    end tell
end tell

on get_front_window()
    tell application "Safari"
        if not (front document exists) then return missing value
        set the_document to front document
        set window_ref to a reference to (windows whose document of it is not missing value)
        repeat with this_window in window_ref
            if document of this_window is the_document then return contents of this_window
        end repeat
        return missing value
    end tell
end get_front_window

tab オブジェクトが document の要素だったら話は簡単なんだけど、そうはいかないんでしょうね。この window の document 属性を使って前面の document オブジェクトを正確に指定する...という方法は、他のドキュメントベースのアプリケーションでも利用できるものなのですが、Script Editor(現在では AppleScript Editor)では、用語辞書を表示する window も document を持っている window オブジェクトなので注意が必要だったりします(つまり、document 1 という参照で用語辞書を表示している window を指してしまうこともある)。

Script Editor で開く

tell application id "com.apple.ScriptEditor2"
    name of windows whose document of it is not missing value
    --> {"名称未設定", "StandardAdditions.sdef"}
end tell

document を持っている window はアプリケーションにより異なる...と。System Events を使って GUI Scripting を利用すれば、各アプリケーションで共通して利用できるハンドラも作成できると思うのですが、System Events はどうも処理が遅くて...乗り気になれなかったり(必要があればもちろん使います)。

「iTunes に自動的に追加」と AppleScript

全く知らなかったのですが、iTunes 9 になって iTunes Music フォルダ(現行バージョンでは iTunes Media に名称が変更されている)に「iTunes に自動的に追加(英語環境では Automatically Add to iTunes)」というフォルダが追加されているのですね。このフォルダに iTunes が取り扱えるファイルを放り込んでおくと、自動的に iTunes に取り込んでくれる...そういうフォルダなのだそうです。

どうも、このような機能は待ち望まれていたもののようです。検索を行うと評判はいいようで、関連する記事も散見されます。例えば、ライフハッカー[日本版]の「「iTunes 9」でウォッチフォルダから新曲自動追加ができるようになった : ライフハッカー[日本版]」という記事。

このような「特定のフォルダを監視し、特定のファイルがフォルダに加えられると何らかの処理を行う...」ということをしたい時に、すぐに思いつくのはフォルダアクションです。あまり、利用されていない...のかな、もしかすると。

ライフハッカー[日本版]と同じことをするのは面白くないので、フォルダアクションで同じような機能を実装してみました。

以下のスクリプトを保存しておき、監視しておきたいフォルダにスクリプトを関連づけておくと、iTunes にファイルを自動的に追加することはできます。

Script Editor で開く

property file_extensions : {".mp3"}

on run -- debug
    set file_list to choose file with multiple selections allowed without invisibles
    set the_folder to path to home folder -- dummy
    adding folder items to the_folder after receiving file_list
end run

on adding folder items to this_folder after receiving these_items
    set music_files to {}

    repeat with this_item in these_items
        set {fine_name, ext} to my splitext(this_item as text)
        if ext is in file_extensions then
            set end of music_files to contents of this_item
        end if
    end repeat

    if music_files is not {} then
        set playlist_name to "Added: " & (short date string of (current date))
        tell application "iTunes"
            activate
            set added_tracks to add music_files

            if class of added_tracks is file track then
                set added_tracks to {added_tracks}
            end if

            if not (playlist playlist_name exists) then
                set the_playlist to make new playlist with properties {name:playlist_name}
            else
                set the_playlist to playlist playlist_name
            end if

            repeat with this_track in added_tracks
                duplicate this_track to the_playlist
            end repeat
        end tell
    end if
end adding folder items to

on splitext(file_name)
    set reversed_name to (reverse of (characters of file_name)) as text
    set num to offset of "." in reversed_name
    if num is 0 then
        set ext to ""
    else
        set reversed_ext to text 1 thru num of reversed_name
        set ext to (reverse of (characters of reversed_ext)) as text
        set ext_num to count ext
        set name_length to count file_name
        set file_name to text 1 thru (name_length - ext_num) of file_name
    end if
    return {file_name, ext}
end splitext

フォルダアクションの設定の仕方は...Mac OS X 10.5 と Mac OS X 10.6 では若干異なっているのでしたね。Mac OS X 10.5 の場合は「シゴタノ! —    フォルダアクションと Automator で時間をセーブする」を()、Mac OS X 10.6 の場合は「わかばマークのMacの備忘録 : setWeblocThumb」を参考にしてみてください。

これで監視しているフォルダに MP3 ファイルが追加されたら iTunes に自動的に取り込んでくれます。

しかし、試してみると分かると思うのですが、iTunes の add 命令はファイルの複製なのです。オリジナルのファイルはそのまま残ります。また、フォルダアクションは反応がよくて(よすぎて)、例えば、Safari でダウンロードしているファイルなんかだったりした場合、ダウンロード完了まで待ってくれません。だから、上記のスクリプトでは目的が達成できません(Mac OS X 10.6 のフォルダアクションは項目がコピーされている時は処理を待機するようですが...ダウンロードしている項目にも適用されるのかは分かりません)。

上記のフォルダアクションでもいいのですが、何となく釈然としない部分があります。フォルダの更新を監視するなら、launchd があるな...。launchd と AppleScript の組み合わせでいいじゃないか、と。しかし、この思いつきがハマる原因になるとは。

ハマるなどとはつゆとも思わず、とりあえず作り始めました。最初に書いたように「iTunes に自動的に追加」フォルダの名称は言語環境で異なるようで、英語環境では「Automatically Add to iTunes」になります。この辺りの問題に対応するため、バンドル形式のアプリケーションかスクリプトバンドルで保存し、localized string 命令を利用します。

そして、作成したスクリプトを launchd で実行するプログラムに指定します。プログラムの実行は osascript を利用するのですが...ここでハマる。

osascript でスクリプトファイルを指定して実行すると、どうも言語環境を考慮しないようで localized string 命令が英語環境の文字列を返します。まさか、このような部分に問題があるとも思わず、他の部分をいろいろ調べていました。

もちろん、launchd でアプリケーションを直接起動させてもいいのですが...この場合、アプリケーション内のプログラムの実態そのものを指定する必要があります。AppleScript のバンドル形式のアプリケーションの場合は、パッケージ内の /Contents/MacOS/applet がそれにあたります。が、これを起動させても目的の動作は行われません。他の Cocoa アプリケーションのようにこの方法も利用できない。バンドル形式ではないアプレットで保存すると問題はないのですが、そうなると localized string が利用できません。

osascript で実行すると localized string で問題があり、アプリケーションの指定にも問題がある...。どうしたものかと思いましたが、バンドル形式のアプリケーションで保存し、osascript でアプリケーションを起動させてしまえばいいじゃないかと思いつきました。

具体的な launchd の記述は以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.charan.AutomaticallyAddiTunes</string>
    <key>Program</key>
    <string>/usr/bin/osascript</string>
    <key>ProgramArguments</key>
    <array>
        <string>osascript</string>
        <string>-e</string>
        <string>tell application "Move to iTunes" to activate</string>
    </array>
    <key>WatchPaths</key>
    <array>
        <string>/Users/your_name/Downloads</string>
    </array>
</dict>
</plist>

これで、かなり満足したものができました。必要なファイルは以下からダウンロードできます。利用方法は同梱の書類を参照してみてください。まぁ、たいていの場合と同じように「ご利用はご自身の責任で」行ってください。

iTunes Music Library.xml を見つけ出す

ちょっと必要があって iTunes Music Library.xml の場所がどこにあるかを AppleScript で探し出したいんだけど...。目的はそれだけなんです。

iTunes Music Library.xml は iMovie や iPhoto、iDVD、Keynote など他のアプリケーションが利用するファイルです。iTunes が管理しているファイルやプレイリストなどの情報が記述されています。拡張子は XML ですが、中身は Apple の Property List 形式で記述されています。

なんで iTunes Music Library.xml を探し出したいかというと、iTunes が利用している iTunes Music フォルダの場所を参照したいからです。その情報が iTunes Music Library.xml に記述されているのです。

通常、iTunes はユーザーの Music フォルダ(~/Music)に iTunes というフォルダを作成し、そこで各ファイルの管理をするのですが、このフォルダの場所はユーザーが変更可能です。ですから、iTunes Music Library.xml が ~/Music/iTunes 以下に存在すると決めつけることはできません。

ところで、iTunes 9 になってファイルの整理方式が変更になったんですね。知りませんでした。うちの iTunes フォルダは古くからのもので、OS のバージョンアップの度に外付け HDD に退避し、バージョンアップ後に戻す...ということを繰り返しているので、現在では利用されていないファイルやフォルダが複数散見されます。このことが余計に事態を複雑にしてくれました。

他のアプリケーションが参照するファイルなんだから、iTunes Music Library.xml の場所を調べる方法が何かあるはず...という想定のもと調べてみると、com.apple.iApps.plist というファイルが見つかりました。ここに iTunes Music Library.xml の場所が記述されているのでした。

ユーザーの Preferences フォルダにある com.apple.iApps.plist は iTunes、もしくは iPhoto の起動時に作成されます。とりあえずこのファイルを見てみれば iTunes Music Library.xml の場所が分かります(まぁ、問題点がないわけでもないようですが)。

以下、iTunes Music Library.xml の場所を調べ、iTunes Music フォルダの場所を調べるスクリプト。エラー処理はしていないので、その辺りは大人の対応で。

Script Editor で開く

set music_folder to find_iTunes_folder()

on find_iTunes_database()
    set prefs_folder to path to preferences folder from user domain as text
    set prefs_file to POSIX path of (prefs_folder & "com.apple.iApps.plist")

    tell application "System Events"
        tell property list file prefs_file
            set file_list to value of property list item "iTunesRecentDatabasePaths"
            return item 1 of file_list
        end tell
    end tell
end find_iTunes_database

on find_iTunes_folder()
    set xml_file to find_iTunes_database()
    tell application "System Events"
        tell property list file xml_file
            set music_folder to value of property list item "Music Folder"
        end tell
    end tell

    return do shell script "python -c \"import sys, urlparse, urllib; print urllib.unquote(urlparse.urlparse(sys.argv[1]).path)\" " & quoted form of music_folder
end find_iTunes_folder

ModuleLoader.osax の真価

load script を置き換える」に続き、ModuleLoader.osax のことなどを。ModuleLoader.osax の機能をOSAX としてではなく、Script Editor のプラグインとして利用できないのかなぁ...などと妄想する日々。この記事に関して Script factory さんが補足記事を書いてくれています。ModuleLoader.osax の理解が深まると思いますので参照してみてください。

ところで、Windows で自動化を行うソフトとして WinMacro なるものが紹介されていますね。この記事を読んで...QuicKeys

よくよく考えてみたらこういう OS 全体の操作を記録、操作するようなソフトウェアって Mac に古くからありますね。いまなら Automator でしょうか。AppleScript は記録がろくに動かなくなってしまいましたが...。

まぁ、Windows のことはよくわからないので OS の自動化ソリューションにどのようなものがあり、どれくらいの需要があるのかは知らないのですが。

さて。

まずは、お詫びです。「load script を置き換える」で次のスクリプトが動かなかった件ですが...ModuleLoader.osax のバージョンが古い為に起こったエラーでした。

on run
    -- /Library/Scripts 以下から AppleScript Help.scpt を検索
    -- additional paths オプションで検索パスを指定
    -- 検索パスを other paths オプションで additional paths で指定したパスだけに制限
    set modules_folder to path to scripts folder from local domain
    find module "AppleScript Help.scpt" additional paths ¬
        {modules_folder} without other paths
end run

最新版(2.2.1)にしたら動きました。...すいませんでした。お手数をおかけいたしました。

load script を置き換える」では ModuleLoader.osax の基本的な機能だけを使ってみました。しかし、ModuleLoader.osax が真価を発揮するのはここからです。

次のスクリプトを Value.scpt として ModuleLoader.osax の検索パスの通ったフォルダに保存しておきます。単純な値を保持しておくだけのスクリプトオブジェクトです。

Script Editor で開く

property _value : missing value

on set_value(val)
    if val is _value of me then return
    set _value of me to val
end set_value

on get_value()
    return _value of me
end get_value

そして、次のスクリプトを Value Wrapper.scpt として保存します。

Script Editor で開く

property value_object : module "Value"

on get_value()
    return value_object of me
end get_value

on set_value(val)
    if value_object's get_value() is val then return
    value_object's set_value(val)
end set_value

on _log()
    log (value_object's get_value())
end _log

たいして意味のないスクリプトです。Value.scpt をラップしただけで、メソッドは Value.scpt のメソッドを呼び出すだけのものです。最後にクライアント。次のスクリプトを Client.scpt としてデスクトップにでも保存しておきます。

Script Editor で開く

property value_object : module "Value"
property wrapper : module "Value Wrapper"
property loader : boot (module loader) for me

on run
    tell value_object
        log get_value()
        --> missing value
        set_value("Hello, world")
    end tell

    tell wrapper
        _log()
        --> "Hello,world"
        set_value("Hello, ModuleLoader")
    end tell

    tell value_object to log (get_value())
    --> "Hello, ModuleLoader"

    log (value_object is wrapper's get_value())
    --> true
end run

Value.scpt と Value Wrapper.scpt を読み込み、それぞれのメソッドを呼び出しています。このスクリプトのポイントとして次の点が上げられます。

  • 依存しているモジュールが常に最新の状態に保たれる
  • 同一のスクリプトから読み込まれた複数のスクリプトオブジェクトが同一のオブジェクトを参照している

これらは前回の load module では行えなかったことです。細かく見てみましょう。

Value.scpt は単純なスクリプトです。Value Wrapper.scpt は Value.scpt を読み込み、それを操作するスクリプトです。Client.scpt は Value.scpt を読み込み、直接それを操作し、同時に ValueWrapper.scpt も読み込み、オブジェクトを介して Value.scpt から読み込んだオブジェクトを操作します。

スクリプトの関係

load script でそれぞれのスクリプトをこのように読み込んだ場合、Value Wrapper.scpt で参照している value_object と Client.scpt で参照している value_object は異なったオブジェクトになります。しかし、ModuleLoader.osax を使って読み込んだ場合、両者の value_object は同じオブジェクトを参照します。

また、Value.scpt を再編集した場合、従来なら Value Wrapper.scpt、Client.scpt 共に再コンパイルしないと Value.scpt の変更は反映(再読み込み)されませんでしたが、ModuleLoader.osax は再コンパイルをせずとも最新の状態を反映してくれます。

これは、property の仕様(オブジェクトの属性、状態を記憶しておく)を覆すような動作ですので、問題といえば問題なのですが...。

AppleScript は基本的にイントロスペクションが弱い。スクリプトの内部からスクリプトについて調べることができません。例えば、グローバル変数の一覧を取得したり、スクリプトオブジェクト、ハンドラについて調べることもできません。この辺りのことができれば少しは違うのですが、現状ではスクリプトから AppleScript の内部にまで手を入れることができません。property の仕様を変更することでしか他の言語にあるようなモジュールの読み込みを実現できない...というのが AppleScript の問題点なのかもしれません。

まぁ...そういうことは置いておき、複数のスクリプトオブジェクトが同じオブジェクトを参照する...ということなら ModuleLoader.osax を使わずに行うことは可能です。方法は 2 通りあります。

  1. オブジェクトの参照をオブジェクトに渡す(オブジェクトコンポジション)
  2. オブジェクトをシングルトンとして設計する

オブジェクトコンポジションは問題なく行えますし、シングルトンは...厳密にはシングルトンにはできませんが、似たようなことはできます。どうするかは...またの機会にでも。

いずれにしても、どの方法を利用するかは作るスクリプト次第です。load script を用いるか、スクリプトバンドルと load script を組み合わせるか、ModuleLoader.osax を利用するか...。選択肢が増えることはいいことです。

スクリプトを見ていきます。

property value_object : module "Value"

この部分は property で指定した変数に(ここでは value_object)モジュールを読み込む指定を行っています。つまり、まだモジュールは読み込まれていないのです。モジュールが実際に読み込まれるのは boot 命令が評価されたときです。

property loader : boot (module loader) for me

まず、module loader 命令がスクリプトオブジェクトを読み込むスクリプトオブジェクトを生成します。次に boot 命令で module loader が返すオブジェクトを使ってスクリプトを読み込み、かつ読み込んだスクリプトオブジェクトが依存しているスクリプトを読み込みます。

つまり、boot 命令が評価されたときに Client.scpt は自身の属性 value_object と wrapper にスクリプトを読み込み、同時に Value Wrapper.scpt の属性 value_object にもスクリプトを読み込んでいるのです。

では、Value Wrapper.scpt は単体では動かせないのかというと...その通りです。

property value_object : module "Value"

この時点ではまだスクリプトは読み込まれていないため、Value.scpt のメソッドを実行することはできません。もちろん、単体でテストを行いたいときはあるでしょう。そういうときは run ハンドラなどで boot 命令を使用します。

Script Editor で開く

property value_object : module "Value"

on get_value()
    return value_object of me
end get_value

on set_value(val)
    if value_object's get_value() is val then return
    value_object's set_value(val)
end set_value

on _log()
    log (value_object's get_value())
end _log

on run
    -- Value Wrapper のテストを行うため、モジュールを読み込む
    boot (module loader) for me
    _log()
    --> missing value
    set_value("Hello, world")
    _log()
    --> Hello, world
end run

ところで... module loader 命令が返すスクリプトオブジェクトとはいったいなんなのでしょうか?

結論から書くと、ModuleLoader.osax が入っていたディスクイメージ内にある loader.applescript なのでした。だから loader.applescript のメソッドを呼び出そうと思えば呼び出せます。それがいいことかどうかはともかく。

ModuleLoader.osax をいろいろと触っていてふと、思ったのですが...同一のスクリプトファイルから個別のスクリプトオブジェクトを読み込むことはできないのでしょうか?

先にも見たように ModuleLoader.osax の boot 命令を使って同一のスクリプトファイルから読み込むと、依存しているスクリプト間では同一のスクリプトオブジェクトを参照するのでした。では、そうしたくないときは?

Script Editor で開く

property object_A : module "Value"
property object_B : module "Value"
property loader : boot (module loader) for me

on run
    object_A is object_B
--> true
end run

これだと同じスクリプトオブジェクトを参照してしまいます。load module 命令を使うのかな。

Script Editor で開く

property object_A : load module "Value"
property object_B : load module "Value"

on run
    object_A is object_B
    --> false
end run

しかし、これだと ModuleLoader.osax のモジュールが依存しているモジュールの自動アップデート機能が利用できません。boot 命令で個別のオブジェクトを作成するのはどうすればいいのでしょうか...?ちょっと、方法を思いつきません。

ModuleLoader.osax はここまで見てきた以外の機能も提供しています。プロジェクトごとの個別のライブラリの作成やモジュールを読み込んだときにフックをかけることもできます。また、モジュール間の依存関係を調べたり、検索パスにあるモジュールの検索を行うこともできます。それらは詳解しませんが、マニュアルと用語説明を参照すると理解できると思います。

最後に...。

AppleScript には他の言語のように共有されている、よく利用されている共通したライブラリというものがありません。例えば、PerlCPAN のように。Python なんかは電池内蔵といわれるぐらいライブラリが充実しています。

AppleScript には足りない命令、メソッド、関数が多くあります。AppleScript の歴史は長いです。が、これが標準といわれるほどのライブラリは存在しません。歴史が長く、足りないものが多いにも関わらず、それを補うライブラリがない。

多くの人が同じようなハンドラを毎回、毎回作っているのが現状です。どうしてなのでしょうか?

パソコンなんて人が楽をするためのただの道具です。AppleScript のようなプログラム言語はそのパソコンを使うことさえも省力化してしまおうとする言語です。なぜ、同じようなハンドラを何人も何人も毎回、毎回作っているのでしょうか?

ModuleLoader.osax は共有のライブラリを作るための一つの手段です。

確かに ModuleLoader.osax は、個性的な OSAX です。作者の AppleScript の使い方や考え方を反映しています。そのため使う人を選ぶと思います。

例えば、次のような記述。

property object_A : module "Value"
property loader : boot (module loader) for me

なんだかお呪いのようで、従来からのユーザーには取っ付きにくいものがあります。また、コンパイル時に property にスクリプトオブジェクトを代入するのを好まない人もいると思いますが、ModuleLoader.osax は property と組み合わせたときに真価を発揮します。

module loader 命令がスクリプトオブジェクトを返す...という考えようによっては強引な面もあります。設定ファイルも保存しますし。しかし、このような形で AppleScript をハックしないことには property にスクリプトオブジェクトを読み込めないのでしょう。

こういった仕様のためでしょうか。ModuleLoader.osax はマニア向けの OSAX だな、という感じがします。ただ、AppleScript に知悉している Script factory さんが出した答えがこの OSAX なのだとしたら、AppleScript でモジュールを気軽に扱う方法は他にないのだろうとも思います。実際、思いついたことのほとんどを既に Script factory さんが試しているのです。Google で検索して Script factory さんのサイトにたどり着くことのなんと多いことか...。

ModuleLoader.osax は AppleScript の初心者には取っ付きにくい OSAX だと思います。しかし、load script を置き換える load module を使うだけでも価値はあると思います。AppleScript に慣れてくれば、より高度な機能を使ってみるといいのです。

ModuleLoader.osax を利用するようになると AppleScript やスクリプトオブジェクト、ライブラリ/モジュールに対する認識が変わると思います。もしかすると、それこそが ModuleLoader.osax の真価なのでは...。

これからスクリプトライブラリを作ってみようと思っているなら、ModuleLoader.osax を試してみてはいかがでしょうか。なにか、いろいろと考えてしまうこと請け合いですから。

追記(10/03/28 19:42:11)- Script factory さんがこの記事についての補足(Script factory : ちゃらんぽらんさんの「ModuleLoader.osax の真価」へのコメント)を書いてくださっています。勘違いしていた部分もあるので、ぜひご一読を。

load script を置き換える

お手軽にコメントをしてもらえるように Google ドキュメントのフォーム機能を使ってみたのですが、よくよく考えたら、一方通行なのでした。コメントを頂くことはできるのですが、ブログのように一覧として表示はされません。コメントでの会話というのができない。メールアドレスを書いていただければ、返信も(メールで)できるのですが...(もちろん、アドレスはなくても構いません)。そんなコメント機能なのですが、ご意見等々を頂ければ幸いです。

あ。きちんとご意見は届いています。ありがとうございます。励みになっています。

さて...。

load script やスクリプトオブジェクトを使って既存のスクリプトを再利用するようになると、不満なところがいくつか出てきます。いくつかの不満点は工夫すればなんとか解決できるものですが...もっと気楽に利用したいと思うのは人間の性。

そんな状況を改善しようと、Script factory さんが ModuleLoader.osax という OSAX を作成されました。

OSAX は、AppleScript の機能を拡張するためのもので、display dialog や choose file、load script 命令も OSAX が提供している命令です。OSAX を利用すると AppleScript で正規表現も扱えるようになりますし、ミリ秒単位で時間の計測も行えるようになります。AppleScript に足りない機能を追加するものとして Mac OS X 以前は重宝していたものです。

OSAX はローカルの /Library/ScriptingAdditions、もしくは ~/Library/ScriptingAdditions に置いておきます。前者に置いておくと全てのユーザーが利用できますし、後者ならインストールしたユーザーだけが利用できます。

...ところで、OSAX ってどうなのでしょうか?

Mac OS X 以降、OSAX はあまり普及していません。もしかすると OSAX で AppleScript の機能を拡張できるということを知らない人も多いかもしれません。

Mac OS X の AppleScript 固有の問題、多くの OSAX が Mac OS X に対応しなかったこと等の事情が重なり Mac OS X 以降、個人的にも OSAX は利用しなくなりました。OSAX を利用するなら、代替方法を模索します。さいわいなことに Mac OS X 以降なら代替方法が複数あるのでそれで困らなかったのです。

この姿勢は今後も変わらないと思います。正直なところ「このスクリプトには OSAX が必要です。こちらからダウンロードしてください」とユーザー(もちろん、ユーザーには未来の自分も含まれます)の手を煩わせるのがいやなのですが...。

OSAX を配布するスクリプトに同梱することも可能ですし、スクリプトバンドルの中に入れておくという手もあるのですが、そのための条件が面倒なものが多いのも事実。そのあたりがどうも...ねえ...。

と、いろんな理由で OSAX を使っている人って案外少ないのではないでしょうか?と、思うのです。特に開発者は OSAX に依存することを嫌います。だから、OSAX の話題はここでは取り上げていませんでしたし、そういう要望も皆無でした。

OSAX を紹介するのは複雑な胸中なのですが...ModuleLoader.osax はここ(ModuleLoader)からダウンロードできます。まずは、ModuleLoader.osax をインストールしておいてください。インストールしていないと以降に掲載されているスクリプトは利用できません。

試してみた環境は、以下の通り。

  • Mac OS X 10.5.8
  • Intel Core 2 Duo
  • AppleScript 2.0.1
  • Script Editor 2.2.1

ModuleLoader.osax はディスクイメージで配布されています。このディスクイメージを開くと、中に「ModuleLoader をインストール.app」という AppleScript アプリケーションがあるので、起動します。

起動するとローカル以下にインストールするか、ホーム以下にインストールするかを尋ねられるので、自分がインストールしたい場所を指定します。インストールが完了後、もし、Script Editor が起動しているなら、いったん終了させて、再起動します。これで OSAX が提供する命令を利用できるようになります(ここではユーザーのホーム以下にインストールしました)。

ModuleLoader.osax は再利用可能なスクリプトを手軽に扱えるようにすることを目的にしています。そのため、再利用可能なスクリプト(作者は「モジュール」と表現しているので、以降、それに倣います)を一定の場所に保管しておく必要があります。

モジュールの保管場所(検索パス)は /Library/Scripts/Modules か、~/Library/Scripts/Modules 以下になります。ModuleLoader.osax はこれらの場所からモジュールの検索を開始します。検索パスの追加は可能になっています。

ModuleLoader.osax の使い方は同梱のマニュアル(ReadMe.html というエイリアスファイル)を見てもらうのが一番手っ取り早いです。まず、目を通しておくのがいいでしょう。

ModuleLoader.osax の特徴として以下の点があります。

  1. スクリプトファイルをファイル名で指定して読み込める
  2. scpt、scptd、app の拡張子を持ったスクリプトファイル、もしくはこれらのエイリアスから読み込める
  3. スクリプトを常に最新の状態で読み込む
  4. 複数のスクリプトファイル間の依存を解決してから読み込む
  5. スクリプトオブジェクトの共有が行える

load script 命令を置き換える load module 命令を使ってみます。最初に次のスクリプトを ~/Library/Scripts/Modules に Value.scpt という名前で保存しておいて下さい。このスクリプトをモジュールとして読み込みます。

Script Editor で開く

property _value : missing value

on set_value(val)
    if _value of me is val then return
    set _value of me to val
end set_value

on get_value()
    return _value of me
end get_value

では、読み込んでみます。

Script Editor で開く

on run
    set value_object to load module "Value"
    tell value_object
        set_value("Hello, world")
        get_value()
        --> "Hello, world"
    end tell
end run

動きますね(当たり前です)。実際に使ってみると分かるのですが、スクリプトファイルをファイル名だけで読み込めるのはとても気持ちのいいものです。

ModuleLoader.osax は拡張子 app, scpt, scptd のスクリプトファイル(もしくはこれらのエイリアスファイル)をモジュールとして認識します。上記のスクリプトを見てもらうと分かるように拡張子の指定は必ずしも必要ではありません。

また、検索パスにあるサブフォルダ以下も再帰的に検索します。検索パスのサブフォルダ以下にあるモジュールを指定する場合は検索パスからの相対パスを命令の引数に与えます。

Script Editor で開く

on run
    -- ~/Library/Scripts/Modules/Utilities にある File Utility.scpt を読み込む
    set value_object to load module "Utilities:File Utility.scpt"
end run

拡張子や相対パスを指定しても同一名のスクリプトファイルがあった場合はどうなるのでしょうか?結論からいうと、先に見つかった方を読み込みます。ファイル名の競合を避けたり、特定の検索パスから特定のモジュールを指定したい場合は load module 命令のオプションで対処します。

Script Editor で開く

on run
    -- ~/Library/Scripts/Modules/Value.scpt を読み込む
    -- additional paths オプションで検索パスを指定
    -- 検索パスを other paths オプションで additional paths で指定したパスだけに制限
    set modules_folder to (path to scripts folder from user domain as text) & "Modules:"
    set value_object to load module "Value.scpt" additional paths ¬
        {file modules_folder} without other paths
end run

逆に additional paths で指定したパス(と ModuleLoader.osax のデフォルトの検索パス)を全て含めたいなら other paths オプションに true を指定します。

上記のスクリプトはこれで動くのですが...しかし、以下のスクリプトは動きません。

Script Editor で開く

on run
    -- /Library/Scripts 以下から AppleScript Help.scpt を検索
    -- additional paths オプションで検索パスを指定
    -- 検索パスを other paths オプションで additional paths で指定したパスだけに制限
    set modules_folder to path to scripts folder from local domain
    find module "AppleScript Help.scpt" additional paths ¬
        {modules_folder} without other paths
end run

指定しているのは Mac OS X 10.5 インストール時に最初から入っているスクリプトファイルです。自分で作成したスクリプトなら階層の深いところにあっても探し出してくれるのですが...。古い環境で作られたスクリプトファイルだからでしょうか。

上記のスクリプトが動かなかったのは ModuleLoader.osax のバージョンが古かったためでした。バージョン 2.2.1 ならこのエラーは発生しません。確認不足でした。申し訳ございませんでした。

モジュールの検索パスをオプションで毎回指定してもいいのですが、すでにスクリプトライブラリを作成していたりするなら set additional module paths to 命令で既存のスクリプトライブラリの場所を ModuleLoader.osax に記憶させておくといいと思います。

個人的には既に ~/Library/Application Support に ASKit という名前でスクリプトライブラリを作っているのでそれを追加してみます。

Script Editor で開く

on run
    -- ModuleLoader.osax の検索パスに既存のライブラリを加える
    set support_folder to path to application support from user domain as text
    set modules_folder to support_folder & "ASKit:"
    set additional module paths to file modules_folder

    -- ModuleLoader.osax の検索パス一覧を取得
    module paths
end run

set additional module paths to 命令は実行すると ModuleLoader.osax が記憶してます(Preferences に Scriptfactory.ModuleLoader.plist という名前のファイルを作って、そこにパスを保存しています)。

だから set additional module paths to 命令は一回だけ実行すればよく、ライブラリを場所を変更したときなどに利用します。この命令で追加したパスは上書きはできますが、削除はできません。削除するには Scriptfactory.ModuleLoader.plist を削除するしかないようです。

ModuleLoader.osax は ~/Library/Scripts/Modules(もしくは、/Library/Scripts/Modules)を検索パスとして利用していますが、Modules フォルダが存在しない場合は検索パス自体が設定されません。

Script Editor で開く

on run
    -- Modules フォルダを作成していない状態で検索パス一覧を取得
    module paths
    --> {}
end run

だから、~/Library/Scripts 以下に Modules フォルダを作成せず、set additional module paths 命令で既存のスクリプトライブラリを追加しておけば、そこだけを検索するようになります(もしかすると、推奨されないのかもしれませんが...)。

ところで...ModuleLoader.osax の特徴としてモジュールの自動的なアップデートとあるのですが...。次のようなスクリプトで Value.scpt をいくら変更しても自動的にアップデートしないのですが、load module 命令ではアップデートしないのでしょうか?

Script Editor で開く

property value_object : load module "Value"

on run
    tell value_object to get_value()
end run

再コンパイルするとアップデートされるのですが...、これは当たり前ですね。

とりあえず、ModuleLoader.osax の一部の機能だけを使ってみました。まだ、他にも機能があるのですが、長くなってしまうので次回に...と。

スクリプトオブジェクトを理解してみる

load script 命令はもしかすると難しいものなのかもしれません。なぜかというと、スクリプトオブジェクトの存在が理解を阻むからです。しかし、load script 命令はスクリプトオブジェクトを返します。「load script 命令におけるスクリプトの再利用」では触れませんでしたが、今回はその辺りのことを。

AppleScript ではオブジェクトが全てです。値という言葉を使うことはありますが、「値 = オブジェクト」とほぼ同意です。AppleScript はオブジェクト単位で考えると理解しやすいかもしれません。

もちろん、スクリプトファイルに保存されたスクリプトもオブジェクトです。

Script Editor で開く

set documents_folder to path to documents folder
tell application "Finder"
    activate
    set the_window to make new Finder window
    set target of the_window to documents_folder
end tell

このようなスクリプトでもオブジェクトです。AppleScript では Java や Objective-C、Python、PHP などの他のプログラム言語と異なり「クラス」を作りません。AppleScript でクラスを定義できるのは、操作対象となるアプリケーションや AppleScript だけです。AppleScript でユーザーが作ることができるのは、「オブジェクト」だけです。

ユーザーが定義したオブジェクトのことを「スクリプトオブジェクト(Script Object)」といいます。スクリプトオブジェクトはデータ(属性)とアクション(メソッド。AppleScript ではハンドラ)を持つことができます。

通常、スクリプトオブジェクトは予約語 script を用いて定義します。

script valiable_name
    [ property parent: reference_of_parent ]
    [ property property_label: initial_value ]...
    [ handler_definition ]...
    [ statement ]...
end script

script から end script の間までがスクリプトオブジェクトの定義になります。valiablename はスクリプトオブジェクトの名前になります。parent 属性で指定する referenceof_parent は、スクリプトオブジェクトの親の参照です。他のスクリプトオブジェクトから属性やハンドラを継承したいときに利用します。

propertylabel は 属性の識別子で、initialvalue に初期値を記述します。handlerdefinition はハンドラ(アクション)の定義で、statement は run ハンドラに含まれるものになります。valiablename 以外は全て任意で、あってもなくてもかまいません。

だから、一番シンプルなスクリプトオブジェクトは、valiable_name だけを指定したものになります。

script ScriptObject
end script

しかし、このままでは何の役にも立ちません。他の言語のように後から属性を追加してデータを加えるということはできません。通常は属性とハンドラを定義し、それなりの体裁を整えておきます。

Script Editor で開く

script FirstScriptObject -- スクリプトオブジェクトの名前
    -- スクリプトオブジェクトの属性。初期値は空のリスト
    property the_list : {}

    on add_data(x) -- スクリプトオブジェクトのハンドラ
        set end of the_list to x
    end add_data

    -- スクリプトオブジェクトの任意の文(暗黙の run ハンドラに含まれる)
    add_data(10)
    add_data(20)
    add_data(30)
    the_list
end script

run FirstScriptObject
--> {10, 20, 30}

AppleScript では、トップレベルのスクリプトオブジェクト(Top-Level Script Object)から処理が開始されます。どのスクリプトにもトップレベルのスクリプトオブジェクトというものが存在していますが、それはユーザーの目には触れません。

トップレベルのスクリプトオブジェクトは、ユーザーが定義するものではありません。AppleScript のスクリプトに既に存在しているものです。トップレベルのスクリプトオブジェクトは決して明示されませんが、全てのスクリプトはトップレベルのスクリプトオブジェクトに含まれたものになります。

set documents_folder to path to documents folder
tell application "Finder"
    activate
    set the_window to make new Finder window
    set target of the_window to documents_folder
end tell

このスクリプトならトップレベルのスクリプトオブジェクトに属する run ハンドラの中のにあるスクリプトになります。言い換えるなら、ユーザーは意識していなくてもスクリプトオブジェクトを作成しているのです。上記のスクリプトは分かりやすく書くと次のようになっています。

(* script *) -- トップレベルのスクリプトオブジェクト
    (* on run *) -- トップレベルのスクリプトオブジェクトの run ハンドラ
        set documents_folder to path to documents folder
        tell application "Finder"
            activate
            set the_window to make new Finder window
            set target of the_window to documents_folder
        end tell
    (* end run *)
(* end script *)

擬似的なスクリプトですが、先のスクリプトはコメントアウトしている部分が暗黙のうちに追加されているのです。

トップレベルのスクリプトオブジェクトは普段は意識しないので感覚がつかみにくいかもしれませんが、トップレベルのスクリプトオブジェクトは全てのスクリプトオブジェクトの親になります(AppleScript では継承の関係を「親」と「子」で表現します)。また、トップレベルのスクリプトオブジェクトも親を持っています。

Script Editor で開く

script FirstObject
end script

me -- トップレベルのスクリプトオブジェクト
--> «script» 

-- FirstObject の親は?
parent of FirstObject
--> «script», トップレベルのスクリプトオブジェクト

-- トップレベルのスクリプトオブジェクトの親は?
set parent_object to parent of me
--> «script AppleScript», AppleScript 自身(AppleScript Component )

--> AppleScript の親は?
parent of parent_object
--> current application, スクリプトを実行しているアプリケーション

current application(スクリプトを実行しているアプリケーション)が最上位の親になります。AppleScript では下図のような継承関係が定義されています。

AppleScript Inheritance Chain

AppleScript でなんらかの命令をオブジェクトに送るとき、通常は「ターゲット(命令を処理するオブジェクト)」に対して命令が送られます。ターゲットで命令を処理できない(命令が定義されていない)場合、この継承関係をたどりながら命令が定義されているオブジェクトを探索します。

次のスクリプトがエラーにならないのは、最終的に Script Editor に問い合わせを行っているからです。

Script Editor で開く

-- トップレベルのスクリプトオブジェクトで定義された x ハンドラ
on x()
    -- 2. document という用語はスクリプトオブジェクトで定義されていない
    -- 3. AppleScript Component を探すが、見つからない
    -- 4. current application(Script Editor)で探し、見つかる
    get name of front document
end x

script FirstObject
    on run
        -- ハンドラ x は、ThirdObject では定義されていない
        -- 1. トップレベルのスクリプトオブジェクトを探しにいく
        x()
    end run
end script

run FirstObject
-- 5. "名称未設定" という結果が得られる

Script Editor 上でスクリプトを実行しているとき、current application(スクリプトを実行しているアプリケーション)は Script Editor になります。Script Editor では document という用語が定義されています。結果、スクリプトが記述されているドキュメントの名称が返されるのです。ちなみにこのスクリプトをアプリケーションとして保存し、実行するとエラーになります。

ユーザーが定義したスクリプトオブジェクトは、他のスクリプトオブジェクトを親に指定し、親の持っている属性やハンドラを継承することができます。親が指定されていない場合、先にも書いたようにトップレベルのスクリプトオブジェクトが自動的に親になります。

Script Editor で開く

script FirstObject
    on greeting(your_name)
        log "FirstObject's greeting"
        return "Hello, " & your_name
    end greeting
end script

script SecondObject
    property parent : FirstObject -- 親を指定
end script

greeting("Mac OS X") of FirstObject
--> "Hello, Mac OS X"
greeting("Mac OS 9") of SecondObject -- 親のハンドラを利用
--> "Hello, Mac OS 9"

FirstObject で greeting を定義せず、トップレベルのスクリプトオブジェクトで定義してみましょう。

Script Editor で開く

on greeting(your_name)
    log "Top-level Script Object's greeting"
    display dialog "Hello, " & your_name
end greeting

script FirstObject
    -- greeting は定義されていないのでトップレベルのスクリプトオブジェクトを探索
end script

script SecondObject
    property parent : FirstObject -- 親を指定
end script

greeting("Mac OS X") of FirstObject
--> "Hello, Mac OS X"
greeting("Mac OS 9") of SecondObject -- 親のハンドラを利用
--> "Hello, Mac OS 9"

parent で親を指定しないスクリプトオブジェクトでは自身にハンドラが定義されていない時、トップレベルのスクリプトオブジェクトで定義されているかどうかを調べます。トップレベルのスクリプトオブジェクトで定義されていない場合、AppleScript Component で定義されていないかどうか調べます。AppleScript Component でも定義されていない場合、current application で定義されているかどうか調べます。current application でも定義されていない場合、エラーになります。

スクリプトオブジェクトで親を指定している場合、親のスクリプトオブジェクトを経由しながらハンドラを探索しますが、最終的には current application まで辿っていきます。

このように全てのスクリプトオブジェクトがトップレベルのスクリプトオブジェクトを経由するということが分かっているなら、エラーなどをトップレベルのスクリプトオブジェクトに集約することができます。

Script Editor で開く

on _error(m, n)
    display dialog {n, return, return, m} as text
end _error

on run
    try
        -- 何らかの処理
        error number 0 -- わざとエラーを起こしてみる
    on error m number n
        _error("トップレベルのスクリプトオブジェクトでエラー発生", n)
        run FirstObject
        x()
    end try
end run

script FirstObject
    script SecondObject
        on run
            try
                -- 何らかの処理
                error number 2 -- わざとエラーを起こしてみる
            on error m number n
                _error("SecondObject でエラー発生", n)
            end try
        end run
    end script

    on run
        try
            -- 何らかの処理
            error number 1 -- わざとエラーを起こしてみる
        on error m number n
            _error("FirstObject でエラー発生", n)
            run SecondObject
        end try
    end run
end script

on x()
    try
        -- 何らかの処理
        error number 3 -- わざとエラーを起こしてみる
    on error m number n
        _error("x ハンドラでエラー発生", n)
    end try
end x

まぁ、あまり利用しないテクニックですが。

スクリプトオブジェクトで親を指定するときに大事なのは、それが誰のものかを明示することです。

Script Editor で開く

script FileFilter
    property file_extensions : {}

    on filter(this_item)
        tell application "Finder"
            if (class of this_item) is not document file then return false
            -- of me(もしくは my)で誰の file_extensions か明示している
            -- of me(もしくは my)がないとこの判定は失敗する
            return name extension of this_item is in file_extensions of me
        end tell
    end filter
end script

script ImageFileFilter
    property parent : FileFilter
    property file_extensions : {"jpg", "jpeg", "png", "gif", "pict", "tiff", "tif"}
end script

tell application "Finder"
    set selected_items to selection
    if selected_items is {} then return
    set image_files to {}
    repeat with this_item in selected_items
        if ImageFileFilter's filter(this_item) then
            set end of image_files to this_item as alias
        end if
    end repeat
    image_files
end tell

AppleScript には me(もしくは my)や it といったオブジェクトを指し示す予約語があります。これらの違いが分からない、と時々耳にします。me は「現在実行されているスクリプトオブジェクト」を指し、it は「現在のターゲット」を指します。

Script Editor で開く

set the_list to {10, 20, 30}

tell the_list
    it -- {10, 20, 30}
    me -- «script»

    class of it -- list
    class of me -- script

    -- 命令は現在のターゲットに送られる
    count -- 3
end tell

it -- «script»
me -- «script»

tell application "Finder"
    it -- application "Finder"
    me -- «script»
end tell

script FirstObject
    me
end script

script SecondObject
    property parent : FirstObject
end script

run FirstObject
--> «script FirstObject»
run SecondObject
--> «script SecondObject»

それぞれ、文脈によって何を指し示しているかが変わってきます。

親を指定したスクリプトオブジェクトでは親と同じハンドラを持つことができます。

Script Editor で開く

script DebugLog
    on debug_message(msg)
        tell me
            activate
            display dialog msg buttons {"OK"} default button 1 with icon 1
        end tell
    end debug_message
end script

script CustomDebugLog
    property parent : DebugLog

    on debug_message(msg)
        set msg to ((current date) as text) & ": " & msg
        log (msg)
    end debug_message
end script

DebugLog's debug_message("DebugLog's logging")
CustomDebugLog's debug_message("CustomDebugLog's logging")

DebugLog ではダイアログでメッセージを表示しますが、子(CustomDebugLog)の方は現在の日時を追加し、Script Editor のイベントログに書き出すようにしています。親の(DebugLog)が持っているハンドラを上書き(オーバーライド)して、機能を修正/拡張したのです。

子は親の機能を拡張することができますが、continue を使って処理をそのまま親に任せてしまうこともできます(委譲といいます)。

Script Editor で開く

script DebugLog
    on debug_message(msg)
        tell me
            activate
            display dialog msg buttons {"OK"} default button 1 with icon 1
        end tell
    end debug_message
end script

script CustomDebugLog
    property parent : DebugLog

    on debug_message(msg)
        set msg to ((current date) as text) & return & return & msg
        continue debug_message(msg) -- 親に処理を丸投げ
    end debug_message
end script

DebugLog's debug_message("DebugLog's logging")
CustomDebugLog's debug_message("CustomDebugLog's logging")

AppleScript ではハンドラの上書きというより、横取りといった方があっていると思うのですが...。例えば、警告音を鳴らす beep 命令をカスタマイズ(横取り)することができます。

Script Editor で開く

on beep
    log "beep"
end beep

beep

このスクリプトを実行しても警告音は鳴りません。警告音を鳴らすには continue で処理を委譲する必要があります。横取りしたものを元に返すのです。

Script Editor で開く

on beep
    log "beep"
    continue beep
end beep

beep

quit ハンドラで continue quit とするのは終了命令を横取りしたままになり、終了しないアプリケーションになるのでアプリケーションに終了命令を返しているのです。

Script Editor で開く

on quit -- 終了命令を横取り
    -- アプリケーションに終了命令を渡さないと終了しないアプリケーションになる
end quit

このスクリプトを「実行後、自動的に終了しない」アプリケーションで保存して、実行すると終了させても終了できないアプリケーションになります。quit ハンドラがあるときは次のように記述しておきます。

Script Editor で開く

on quit -- 終了命令を横取り
    -- アプリケーションに終了命令を渡さないと終了しないアプリケーションになる
    (* 終了前の処理が入る *)
    continue quit
end quit

スクリプトオブジェクトはスクリプト実行時に初期化され、作成されます。トップレベルのスクリプトオブジェクトは run ハンドラ実行時に初期化されます。スクリプトオブジェクトの初期化のタイミングは命令を受け取る直前です。この性質を利用すると異なる初期値を持つスクリプトオブジェクトを作成することができます。

Script Editor で開く

on FileFilter(extension_list)
    script FileFilter
        property file_extensions : extension_list

        on filter(this_item)
            return name extension of this_item is in file_extensions of me
        end filter
    end script
end FileFilter

set ImageFileFilter to FileFilter({"jpg", "jpeg", "png", "gif", "pict", "tiff", "tif"})

tell application "Finder"
    set selected_items to selection
    if selected_items is {} then return
    set image_files to {}
    repeat with this_item in selected_items
        if (class of this_item) is document file then
            if ImageFileFilter's filter(this_item) then
                set end of image_files to this_item as alias
            end if
        end if
    end repeat
    image_files
end tell

このようにして作成されたスクリプトオブジェクトは、たとえ初期値が同じものであっても異なるスクリプトオブジェクトになります。

Script Editor で開く

on objectMaker()
    script
    end script
end objectMaker

set obje_A to objectMaker()
set obje_B to objectMaker()
obje_A is obje_B
--> false

ちなみに set 命令ではオブジェクトは共有されます。

Script Editor で開く

set the_list to {1, 2, 3}
set new_list to the_list
set end of new_list to 4
the_list
--> {1, 2, 3, 4}

これは対象がスクリプトオブジェクトでも同じで、オブジェクトのコピーが欲しい場合は copy 命令を使います。

Script Editor で開く

on objectMaker()
    script
    end script
end objectMaker

set obje_A to objectMaker()

-- set ではオブジェクトは共有される
set obje_B to obje_A
obje_A is obje_B
--> true

-- 複製が欲しいときは copy 命令を使う
copy obje_A to obje_B
obje_A is obje_B
--> false

駆け足ですが、AppleScript やスクリプトオブジェクトがどのような性質を持っているか理解できた...でしょうか?

できねえよ。

...分かっています。全ての責は私の理解不足/文章力足らずに帰します。分からない、理解できない、もっとここを説明してほしい...という部分があればコメント(設置してみました)からお願いします。