その後の「文字列で指定したハンドラを実行する」

まずは、Excellent!! と言いたい。

文字列で指定したハンドラを実行する」を書いたところ、ppmweb さんと Script factry さんから有益な情報とスクリプトを送って頂きました。

正直なところ、私の方法は無意味に osacompile を使っていたり、その上 run script 命令まで使ってなんだかな、と言った趣きがありました。その後、Script factry さんがシンプルな方法を発表されていて、実用として使うならこちらかなと思っていたのですが、ppmweb さんにご教授いただいた方法もまた、シンプルなのでした。

この方法の出典は、AppleScript-Users ML です。

ppmweb さんは、この方法でレコードのラベル、スクリプトオブジェクト内のハンドラ、スクリプトオブジェクトのプロパティも文字列で取得可能な事を調べてくださり、また、load script 命令で読み込んだスクリプトのハンドラなども文字列で取得可能だという事を証明して下さいました。まさに、Great, Excellent, Beautiful and Lovely!!

今宵は気分良く酔えそうです。送って頂いたスクリプトは 2 つあって、一方がメインとなるスクリプトで、他方がメインから読み込まれるスクリプトになっています。まず、読み込まれる方のスクリプト。

Script Editor で開く

on externalShow(theText)
    display dialog theText
end externalShow

このスクリプトを externalTextShower.scpt として保存しておきます。メインとなる方のスクリプトは以下。

Script Editor で開く

(* ==== Demo ==== *)


-- Demo 1 

callHandler(me, "showText", {"I'm a top level handler."})

on showText(theText)
    display dialog theText
end showText


-- Demo 2

callHandler(TextShower, "show", {"I'm a handler of the script object."})

script TextShower

    on show(theText)
        display dialog theText
    end show

end script


-- Demo 3
-- 以下のコードの実行には externalTextShower.scpt が必要
(*set thePath to join(":", items 1 thru -2 of split(":", path to me as text)) & ":externalTextShower.scpt"
set externalTextShower to load script file thePath

callHandler(externalTextShower, "externalShow", {"I'm a handler of the loaded script file."})*)



(* ==== Call Handler Core ==== *)

on callHandler(targetScript, handlerName, handlerParams)


    set handlerParamLabels to {}
    set numberOfHandlerParams to count handlerParams

    if numberOfHandlerParams > 0 then
        repeat with i from 1 to numberOfHandlerParams
            set the end of handlerParamLabels to "handlerParam" & (i as text)
        end repeat
    end if


    set runCommandParamLabels to {"targetScript", "theHandler"} & handlerParamLabels
    set runCommandParams to {targetScript, extract_usrf(targetScript, handlerName)} & handlerParams


    run script "on run{" & join(",", runCommandParamLabels) & "}
    tell targetScript to " & handlerName & "(" & join(",", handlerParamLabels) & ")
end" with parameters runCommandParams


end callHandler


-- Taken from <http://lists.apple.com/archives/applescript-users/2006/Sep/msg00446.html>.
-- Usage Examples:
--     extract_usrf({label1:"value1", label2:"value2"}, "label1") -- "value1"
--     extract_usrf(ScriptObjectA, "propertyName") -- "value of ScriptObjectA's property propertyName"
--     extract_usrf(me, "handlerName") -- «handler handlerName»

to extract_usrf(theRecord, fieldName)
    run script "on run{r}
    return " & fieldName & " of r
end" with parameters {theRecord}
end extract_usrf



(* ==== Text Manupilation ==== *)

on split(separator, sourceText)

    tell AppleScript
        set oldDelimiters to text item delimiters
        set text item delimiters to {separator}
    end tell

    set listOfText to text items of sourceText

    tell AppleScript to set text item delimiters to oldDelimiters

    return listOfText

end split


on join(separator, listOfText)

    tell AppleScript
        set oldDelimiters to text item delimiters
        set text item delimiters to {separator}
    end tell

    set theText to listOfText as text

    tell AppleScript to set text item delimiters to oldDelimiters

    return theText

end join

こちらも名前を付け、先の読み込まれるスクリプトと同階層の場所に保存しておきます(スクリプト内のコメントにある Demo 3 でスクリプトを読み込んで実行するのですが、このとき同一階層からスクリプトファイルを指定しているので)。

では、Great で Excellent で Beautiful で Lovely なこのスクリプトがなぜ動くのかを推論とともにお送りしたいと思います(スクリプトは分かりやすいように単純化しています)。

Script Editor で開く

script MyObject
    on x()
        display dialog "MyObject"
    end x
end script

run script "on run {target}
tell target to x()
end run" with parameters {MyObject}

run script "on run {target}
tell target to x()
end run" with parameters {me}

on x()
    display dialog "me"
end x

with parameters とは気がつかなかった(馬鹿をさらけだすようですが、本当に気がつかなかった)。

文字列で指定したハンドラを実行する」で指摘したと思うのですが、run script 命令は新しいスクリプトオブジェクトを生成します(生成する...これは勝手な推論で根拠はありません)。現在実行されているスクリプトとは別の新しいスクリプトオブジェクトなので、以下のスクリプトは動かないのでした。

Script Editor で開く

set x to 10

run script "x"
--> x変数は定義されていません。

定義されていないなら定義されているオブジェクトを渡せばいいのです。もし、新しいスクリプトオブジェクトが生成されるのならば、そのスクリプトオブジェクトの run ハンドラの引数に渡せばいいのです(run script 命令は run ハンドラを実行するから)。このシンプルな発想。

  1. 文字列で run ハンドラとその引数を作る
  2. run script 命令に文字列を渡す
  3. run script 命令は文字列をコンパイルする
  4. コンパイルした時点では構文エラーにならない
  5. run script 命令がコンパイルされたスクリプト(スクリプトオブジェクト)の run ハンドラを実行
  6. run ハンドラの引数が実際のオブジェクトの参照に置き換わる
  7. MISSION COMPLETE

4 の時点で構文エラーにならないのは以下のスクリプトがエラーにならないのと同じ理由です。

Script Editor で開く

on run {target}
    tell target to x()
end run

ようするにこのスクリプトを保存し、run script 命令で保存したファイルを引数を指定して実行しているのと同じことなのです。

ppmweb さん、本当にありがとうございました。

そして、Script factory さんからはいくつかの間違いを指摘いただきました。

script Obje
end script

on x(num)
    return num
end x

--set x of Obje to x

tell Obje to x(10)

スクリプトオブジェクトにハンドラをインストールしている部分をコメントアウトしました。それでもこのスクリプトは動きます。つまり、同一スクリプト内でのハンドラの有効範囲(可視性、スコープ)です。以下のようにしても動く事からハンドラは同一スクリプト内にある限り、ほとんどの場所から参照できるのです。

ちゃらんぽらん : 文字列で指定したハンドラを実行する

まず、このスクリプトが動くのがなぜか?

引用にもあるようにスコープが原因だと書いていますが、スクリプトオブジェクト Obje が上位のスクリプトを継承しているから、という事のようでした。以下のスクリプトを動かしてみると分かります。

Script Editor で開く

script Obje
end script

parent of Obje
--> «script»
name of parent of Obje
--> "Script Editor"

ほんとだ。継承している...。だから動いたのか、と納得。って、AppleScript Language Guide に書かれていたような(うろ覚え)。そうか、継承か。

次の指摘。

読み込まれた方でハンドラが共有されます(されてしまいます)。この事を利用したのが最初に紹介したスクリプトです(蛇足ですが実際はハンドラだけでなく変数でもスクリプトオブジェクトでも文字列で指定する事ができます)。

ちゃらんぽらん : 文字列で指定したハンドラを実行する

ハンドラが共有されると書きましたが、これも間違い。

osacompile で変数名だけのファイルを書き出した場合、グローバル変数だけを持ったスクリプトを作っているのと同じになり、load script したとき、トップレベルのスクリプトのプロパティと値が共有されるから動くのだそうです。この辺りの事は、Script factory さんの侵略の global 変数 に詳しいです。参考までにいくつかのスクリプトを。

Script Editor で開く

set num to 10

x()

on x()
    set num to 20
end x

num
--> 10

次にハンドラ内でグローバル変数を定義。

Script Editor で開く

set num to 10

x()

on x()
    global num
    set num to 20
end x

num
--> 20

num 変数は共有されます。自分自身を表す my を使うと、global を利用しなくても変更できます。

Script Editor で開く

set num to 10

x()

on x()
    set my num to 20
end x

num
--> 20

しかし、num 変数をローカル変数にすると動きません。

Script Editor で開く

local num
set num to 10

x()

on x()
    set my num to 20
    --> num のタイプを reference に変換できません。
end x

num
--> 10

そして、最後の指摘。handler はローカル変数におさめる事はできなくて、property にならおさめる事ができる。だから、以下のスクリプトは動かないのでした。

Script Editor で開く

x()

on x()
    set theResult to handlerWithName("greeting")
    theResult("Mac")
    --> «script» は theResult メッセージを認識できません。
end x

on getNum()
    return 81
end getNum

on greeting(yourName)
    display dialog "Hello, " & yourName
end greeting

on handlerWithName(str)
    set dir to path to temporary items folder from user domain as Unicode text
    set fileName to "tmp.scpt"
    set scptFile to dir & fileName
    set posixFile to POSIX path of scptFile
    set str to quoted form of str

    do shell script "osacompile -e " & str & " -o " & (quoted form of posixFile)
    set theReult to run (load script file scptFile)
end handlerWithName

...まだまだ分からない事も多いけど、納得。

ppmweb さん。Script factory さん。本当にありがとうございました。「ちゃらんぽらん」を続けていてよかったよ。また、このサイトをご覧になっている皆様もありがとうございます。「ちゃらんぽらん」はあなたに支えられています。

えっと...また、引用。

素敵な回答をいただいた方には、感謝の気持ちを込めて個人的にほっぺにチュッてします。おでこでも可。

ちゃらんぽらん : 文字列で指定したハンドラを実行する

ほっぺにチュッ。

おでこでも可。

えっと...。

どういたしましょう?

0 件のコメント :

コメントを投稿