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 の一部の機能だけを使ってみました。まだ、他にも機能があるのですが、長くなってしまうので次回に...と。