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

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 やスクリプトオブジェクトがどのような性質を持っているか理解できた...でしょうか?

できねえよ。

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

0 件のコメント :

コメントを投稿