オブジェクト指向AppleScript言語
今までAppleScriptに備わっているオブジェクト指向的な仕組みを、あまり積極的に利用していなかった...。アプリケーションの補助的な操作に利用することが多く、シンプルなスクリプトを手順に従って並べるだけで結構満足できていた、ということもある。それに何より、オブジェクト指向的に書く方法、もっと言えばAppleScript自体をあまり良く理解できていなかったというのもある。
いつも、その場限りの必要な知識だけ調べて、動いたらそれまで。試行錯誤のやっつけスクリプトだった。いい加減、ちゃんと理解しておきたい...。
Hello World
- 「こんにちは」とダイアログで表示するだけの最もシンプルなコードだが、この裏には実に多くの仕組みが隠されていた。
display dialog "こんにちは"
- 実は、runハンドラ(メソッド)に定義されたコードと同じように解釈されている。(厳密には同じではなかった)
on run display dialog "こんにちは" end run
- ちなみに、以下のような書き方をしてしまうと、エラーが発生して実行できない。(コンパイルも、保存もできない。)
- 構文エラー
- ハンドラ run が 2 度以上も指定されています。または、ハ
ンドラ run と重複する命令がスクリプトの最上位にありま
す。
display dialog "こんにちは" on run display dialog "こんにちは" end run
- ここまでの理解:スクリプトエディタの実行ボタンを押した時は、そこで編集中のコードに対してrunハンドラ(メソッド)が呼び出される。
ハンドラ(メソッド)
ファイルのドロップで実行
- 以下のコードをフォーマット:アプリケーションとして保存すれば...
on open display dialog "ファイルがドロップされました。" end open
-
- アイコンには以下のように矢印が表示されている。(openハンドラがない時は表示されない。)
-
- そのアイコンにファイルをドロップしてみると、openハンドラ内のコードが実行された。
- アイコンをダブルクリックで起動しても何も起こらない。
- もし、runハンドラとして実行されるコードも用意されていれば...
display dialog "ダブルクリックされました。" on open (aFile) display dialog "ファイルがドロップされました。" & (aFile as text) end open
-
- アイコンをダブルクリックすると、「ダブルクリックされました。」とダイアログが表示された。
- ちなみに、ドロップされたファイルは引数として受け取ることができるのであった。
様々なハンドラ
- フォーマット:アプリケーションで保存した時に利用できるハンドラ
display dialog "ダブルクリックされました。" --ファイルがドロップされた時に実行される。 on open (aFile) display dialog "ファイルがドロップされました。" & (aFile as text) end open --アプリケーション実行中に、起動操作した時に実行される。 on reopen display dialog "Dockアイコンをクリック、またはアプリケーションアイコンがダブルクリックされました。" end reopen --アプリケーションが終了する直前に実行される。 on quit display dialog "終了します。" end quit --5秒毎に実行される。(オプション:実行後、自動的に終了しない チェックありの場合に有効) on idle beep return 5 --次に実行するまでの秒数を返す end idle
- フォルダアクション系のハンドラ
- adding folder items to
- closing folder window for
- moving folder window for
- opening folder
- removing folder items from
ユーザーが定義するハンドラ
- ハンドラはあらかじめ用意されているもの以外にも、ユーザーが自由に定義することができる。
- 定義したハンドラは、on以降のハンドラ名で呼び出すことができる。
- もちろん、今までのopenハンドラだって同様に呼び出すことができる。
say_hello() --ユーザー定義のハンドラは、引数が不要でも括弧は必要 open on say_hello() --ユーザー定義のハンドラは、引数が不要でも、括弧は必要 display dialog "こんにちは" end say_hello on open display dialog "ファイルがドロップされました。" end open
スクリプトオブジェクト
- scriptブロックで囲むことで、そのscriptブロックが所有するコードのように振る舞う。(まるでscriptブロック名のオブジェクトのようだ。)
say_hello() of Sazae say_hello() of Tara script Sazae on say_hello() display dialog "こんにちは、サザエでございます。" end say_hello end script script Tara on say_hello() display dialog "こんにちは、タラです。" end say_hello end script
- 誰の(どのスクリプトの)ハンドラかを明示することで、同じハンドラ名でも区別して呼び出すことができるのだ。
継承
- 継承を利用すると、タラちゃんにsay_hello()ハンドラは不要になる。(タラちゃんへのsay_hello()は、親であるサザエさんが肩代わりしてくれるので。)
say_hello() of Sazae say_hello() of Tara script Sazae on say_hello() display dialog "こんにちは、" & first_name() & "です。" end say_hello on first_name() "サザエ" end first_name end script script Tara property parent : Sazae --親は script Sazae on first_name() "タラ" end first_name end script
自動継承
say_hello() of Sazae say_hello() of Tara on say_hello() display dialog "こんにちは、" & first_name() & "です。" end say_hello script Sazae on first_name() "サザエ" end first_name end script script Tara on first_name() "タラ" end first_name end script
継承されるプロパティの共有
- 継承関係にあるオブジェクト間では、プロパティは共有される。
- どちらのfirstNameを変更しても、変更後の結果はどちらから確認しても同じ値になっている。
script Sazae property firstName : "サザエ" end script script Tara property parent : Sazae --親は script Sazae end script set firstName of Sazae to "タラ" firstName of Tara --結果: "タラ"
script Sazae property firstName : "サザエ" end script script Tara property parent : Sazae --親は script Sazae end script set firstName of Tara to "タラ" firstName of Sazae --結果: "タラ"
- ここまでの理解:スクリプトオブジェクトを直接指定した時、プロパティはクラス変数のように振る舞う。
スクリプトオブジェクトのsetによる代入
- スクリプトオブジェクトはsetによって、変数に代入することができる。
- setでscript Personが、変数sazaeに代入された場合...
- 変数sazaeは、script Personそのものを指し示している。(変数sazae = script Person)
script Person property firstName : "" on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script set sazae to Person --sazaeにPersonを代入する set_firstName("サザエ") of sazae say_hello() of sazae --ダイアログ: "こんにちは、サザエです。" say_hello() of Person --ダイアログ: "こんにちは、サザエです。" set_firstName("xxxx") of Person say_hello() of sazae --ダイアログ: "こんにちは、xxxxです。" say_hello() of Person --ダイアログ: "こんにちは、xxxxです。"
- ここまでの理解:上記のようにsetで代入した場合、Personクラスのように振る舞う。
スクリプトオブジェクトのcopyによる代入
- スクリプトオブジェクトはcopyによって、変数に代入することができる。
- copyでscript Personがコピーされて、変数sazaeに代入された場合...
- 変数sazaeは、script Personのコピーであって、オリジナルではない。(変数sazae ≠ script Person)
script Person property firstName : "" on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script copy Person to sazae --personをコピーして、sazaeに代入する set_firstName("サザエ") of sazae say_hello() of sazae --ダイアログ: "こんにちは、サザエです。" say_hello() of Person --ダイアログ: "こんにちは、です。" set_firstName("xxxx") of Person say_hello() of sazae --ダイアログ: "こんにちは、サザエです。" say_hello() of Person --ダイアログ: "こんにちは、xxxxです。"
- ここまでの理解:上記のようにcopyで代入した場合、Personインスタンスのように振る舞う。
スクリプトオブジェクトのコピー時の初期化
- コピー(インスタンス化)する時の初期化は、結構面倒くさい。だから、初期化ハンドラを用意しておくと幸せになるかもしれない。
スクリプトオブジェクト内に初期化ハンドラを定義する
- new(aName)of Personを実行すると、script Personのコピーが生成される。
- script Personのオリジナルにもアクセス可能。
- 自分でcopy Person to ...を利用してコピーすることも出来る。
script Person property firstName : "" on new(aName) copy me to aCopy --meは自分自身を表す(ここでは script Person になる) set_firstName(aName) of aCopy aCopy end new on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script set sazae to new("サザエ") of Person set tara to new("タラ") of Person say_hello() of sazae --ダイアログ: "こんにちは、サザエです。" say_hello() of tara --ダイアログ: "こんにちは、タラです。" say_hello() of Person --ダイアログ: "こんにちは、です。"
初期化ハンドラで囲む
- make_person(aName)を実行した時だけ、script Personのコピーが生成される。
- script Personのオリジナルにはアクセスできない?
- copy Person to ...は利用できない?
on make_person(aName) script Person property firstName : aName on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script end make_person set sazae to make_person("サザエ") set tara to make_person("タラ") say_hello() of sazae --ダイアログ: "こんにちは、サザエです。" say_hello() of tara --ダイアログ: "こんにちは、タラです。" say_hello() of Person --AppleScriptエラー: "Person変数は定義されていません。"
クラスメソッドとインスタンスメソッドを明確にする
- かといって、トップレベルの初期化ハンドラとして定義してしまっても...
- Personオブジェクトとしてのまとまりがなくなってしまう...。
- そこで、初期化ハンドラで囲んだ後、さらにscriptとして定義してしまう方法を思い付いた。
- 外側のscript名をPersonに、内側のscript名を_Personとでもしておけば、クラスとインスタンスの関係をかなり表現できそう。
script Person on new(aName) script _Person property firstName : aName on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script end new end script set sazae to new("サザエ") of Person set tara to new("タラ") of Person say_hello() of sazae --ダイアログ: "こんにちは、サザエです。" say_hello() of tara --ダイアログ: "こんにちは、タラです。" say_hello() of Person --AppleScriptエラー: "«script Person» は say_hello メッセージを認識できません。" --say_hello()はインスタンス的ハンドラなので、Personクラスに対しては当然のエラー
ほとんどのoooo of xxxxは、xxxx's ooooに置き換え可能
- これまでAppleScriptで象徴的な表現、oooo of xxxx of yyyyという書き方を続けてきた。
- しかしこの表現方法だと、最後まで読まないと根本は何かを理解できないところが煩わしい。
- 何より日本人の自分が自然と感じる感覚とは、言葉の入ってくる順番が反対なのだ...。
- しかし、AppleScriptは柔軟な言語で、実は以下のようにも表現できたのであった。
...(中略)... set sazae to Person's new("サザエ") set tara to Person's new("タラ") sazae's say_hello() --ダイアログ: "こんにちは、サザエです。" tara's say_hello() --ダイアログ: "こんにちは、タラです。" Person's say_hello() --AppleScriptエラー: "«script Person» は say_hello メッセージを認識できません。" --say_hello()はインスタンス的ハンドラなので、personクラスに対しては当然のエラー
- つまり oooo of xxxx of yyyy = yyyy's xxxx's oooo なのである。
- 「's」を「.」と置き換えて理解すれば、かなりRubyの表現に近づいた。
- ここまでの理解:AppleScriptもかなりオブジェクト指向なスクリプト言語であった。
ここはどこ、私はだれ?
- スクリプトオブジェクト内に初期化ハンドラnewを定義する時に、meというキーワードを利用した。
自分自身
- meは自分自身を返すのだが、自分自身とは何だろうか?
me --結果: «script»
tell application "Finder" me end tell --結果: «script»
script Sazae me end script Sazae run --結果: «script Sazae»
script Sazae who() end script on who() me end who Sazae run --結果: «script Sazae» who() --結果: «script»
- meは、«script» あるいは «script Sazae» を返している。
- つまり、meを実行する環境のスクリプトオブジェクトを指し示しているのだ。
スクリプトオブジェクトのルーツ
- では、«script»の親はいるのだろうか?そして、そのまた親は...。以下のように調べてみた。
me --1.結果: «script» parent of me --2.結果: «script AppleScript» parent of parent of me --3.結果: current application parent of parent of parent of me --4.AppleScript エラー: &1 を取り出すことはできません。
- 2.結果:«script AppleScript»
- «script AppleScript»は、スクリプトコードの中で AppleScript というキーワードで表現される。
- AppleScript Language Guideには、AppleScript itself (the AppleScript component) と書かれている。
- AppleScript自身(AppleScriptの構成要素)とは、何だろう?AppleScriptを解釈して実行するインタプリター(翻訳機)とか、言語環境そのものと考えれば良いのだろうか。
- 日本語とは何だろうと置き換えて考えた時、まず日本語の文法を説明して、その文法を理解できる人間の心であり、その心が集まってコミュニケーションする環境であると言える。
-
- AppleScript's text item delimiters
- 結果: ""
- AppleScript's text item delimiters
-
- AppleScript's version
- 結果: "2.0.1"
- AppleScript's version
- 3.結果:current application
- current applicationは、ユーザーの操作を受け取って、その操作をAppleScriptでオブジェクトに伝えるアプリケーションと考えれば良いだろうか。
- 以下のようなコードで試してみた。(フォーマット:スクリプト、ファイル名:me.scpt で保存した。)
display dialog (path to current application as text) --osascriptコマンドで実行する場合、上記display dialog...はコメントアウトして保存 path to current application as text
- スクリプトエディタから実行ボタンを押して実行した場合
- ダイアログ: "Leopard HD:Applications:AppleScript:Script Editor.app:"
- メニューバーのスクリプトメニューから実行した場合
- ダイアログ: "Leopard HD:System:Library:CoreServices:AppleScript Runner.app:"
- ターミナルのosascriptコマンドから実行した場合
- 返り値: "Leopard HD:usr:bin:osascript"
- フォーマット:アプリケーションとしてme.appという名前で保存して実行した場合
- ダイアログ: "Leopard HD:Usrs:zari:Library:Scripts:me.app"
- me.scpt、あるいはme.appを起動する環境によって、current applicationは違っている。
- つまり、current applicationは、ユーザーの操作によってAppleScriptを話してくれるオブジェクトと考えることが出来そう。
- 4.AppleScript エラー:&1 を取り出すことはできません。
- ここでエラーが発生するということは、これ以上親を遡ることが出来ないということ。
- me(自分自身)の元祖は、上記のcurrent applicationということになる。(OSレベルではさらに詳細な仕組みが隠されているかもしれないが。)
- ここまでの理解:AppleScriptの継承関係(1がルート)
- current application(スクリプトエディタ、AppleScript Runner.app、osascriptコマンド、アプリケーション形式のスクリプト)
- AppleScript(文法を解釈するインタプリター、言語環境)
- トップレベル スクリプト(scriptブロック外側のすべてのコード)
- スクリプト オブジェクト(scriptブロック内のコード)
Hello Worldが表示されるまで
- ここで最初に戻って、以下のコードがどのように実行されているか考え直してみる。
display dialog "こんにちは"
- 以上のような仕組みであると(自分では)考えることにした。
そのメッセージは誰に向けられているか?
- もし、以下のようにtellブロックで囲んだとすれば...
tell application "Finder" display dialog "こんにちは" with icon note end tell
-
- display dialog "こんにちは"...の送信先はFinder.appになる。
- with icon noteオプションで表示されるアイコンもFinderの絵になっている。
-
- ちなみに、tellブロックがない時はスクリプトエディタ(current application)のアイコンだった。
-
-
- もし、スクリプトメニューやme.appからの起動であれば、そのcurrent applicationアイコンになると思う。
-
- AppleScriptとは、オブジェクト(アプリケーションやスクリプトコード自身)間でメッセージをやり取りするための言語なのであった。
- そのメッセージ(スクリプトコード)はどのオブジェクトに向けて送信されているのか、常に意識しておく必要がある。
- 以前、以下のようなコードを書いて、frontmost_app()が認識されなくて困ったことがあったが、今ならその理由も簡単に理解できる。
--最前面のアプリケーションを取得する on frontmost_app() tell application "System Events" set pList to name of every process whose frontmost is true item 1 of pList end tell end frontmost_app tell application "System Events" tell process frontmost_app() ...(中略)... end tell end tell
- tell application "System Events"ブロック内のため、frontmost_app()が"System Events"に向けて送信されていたのだ...。
- frontmost_app()はトップレベル スクリプトなのだから、my frontmost_app()とすれば、ちゃんと認識されるはずだ。
AppleScriptを理解するオブジェクトの気持ちが、少しだけ分かったような気になった。
参考ページ
- AppleScript Language Guide
- 英語の情報なのだが、何はともあれAppleScriptを利用するのであれば読んでおくべき。
- 例え英語の解説が分からなくとも、豊富なサンプルコードの実行結果を確認しておくだけで、かなり理解が深まる。
- ちなみに、自分は今まで英語だからといって敬遠していた。熟読したのは、今回が初めて。もっと早くに読んでおくべきだった...。
そして、上記ページの理解を深めるために、以下の素晴らしいサイトがたいへん参考になりました。感謝です!
トップレベル スクリプトとrunハンドラ内のスクリプトの違い
- 素敵なブクマコメントを頂いた。
「初期化ハンドラで囲む」でscript PersonのオリジナルにアクセスできないのはPersonがmake_personハンドラのローカルオブジェクトだから、だっけか。make_personハンドラが呼ばれてる間しか存在しないものな。
- なるほど!新たな思考が巡り出す。(Gururiさん、ありがとう!)
- 以下のように、make_personハンドラ中でPersonオブジェクトを取得する場合、
- script Personブロックより手前で取得しようとするとエラーになってしまう...。
- script Personブロックの後ろでは問題なく取得できた。
- Personをresultに置き換えても問題なく取得できた。
property obj_list : {} on make_person(aName) --set temp to obj_list & Person --構文エラー:属性 Person が何度も指定されています。 script Person property firstName : aName on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script set obj_list to obj_list & Person --resultに置き換えても問題なく取得できた。 --set obj_list to obj_list & result end make_person set sazae to make_person("サザエ") set tara to make_person("タラ") say_hello() of item 1 of obj_list --ダイアログ: "こんにちは、サザエです。" say_hello() of item 2 of obj_list --ダイアログ: "こんにちは、タラです。"
property obj_list : {} set Person's firstName to "サザエ" set obj_list to obj_list & Person set Person's firstName to "タラ" set obj_list to obj_list & Person say_hello() of item 1 of obj_list --ダイアログ: "こんにちは、タラです。" say_hello() of item 2 of obj_list --ダイアログ: "こんにちは、タラです。" script Person property firstName : "" on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script
- さらに、上記スクリプトをそのままに、runハンドラで囲んでみた。
- AppleScript エラー:Person変数は定義されていません。と警告された。
property obj_list : {} on run set Person's firstName to "サザエ" --AppleScript エラー:Person変数は定義されていません。 set obj_list to obj_list & Person set Person's firstName to "タラ" set obj_list to obj_list & Person say_hello() of item 1 of obj_list say_hello() of item 2 of obj_list script Person property firstName : "" on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script end run
property obj_list : {} on run script Person property firstName : "" on say_hello() display dialog "こんにちは、" & firstName & "です。" end say_hello on set_firstName(aName) set firstName to aName end set_firstName end script set Person's firstName to "サザエ" set obj_list to obj_list & Person set Person's firstName to "タラ" set obj_list to obj_list & Person say_hello() of item 1 of obj_list --ダイアログ: "こんにちは、タラです。" say_hello() of item 2 of obj_list --ダイアログ: "こんにちは、タラです。" end run
- 以上の結果から、以下のように解釈することにした。
- ハンドラ内のコードは、ハンドラが呼び出されて初めて評価(実行)される。
- script Personブロックも、評価(実行)されて初めてオブジェクトを生成する。
- 評価されることでPersonオブジェクトが生成され、Person'sやof Person等でアクセスできるようになる。
-
- もしrunハンドラが見つかれば、runハンドラの内側を1行目から順に実行する。
- 実行中のコードより後ろにあるscriptブロックは未知なので、そのオブジェクトは生成されていない状態。
- その状態でオブジェクトにアクセスしようとするとエラーが発生する。
- 以上のような流れで、実行されていると(自分では)解釈してみた。
- 最初の疑問に戻って、
- 初期化ハンドラmake_personではオリジナルのPersonにアクセスできない?のではなく、
- オリジナルのオブジェクトの生成は、make_personでしか実行できない、ということなのであった。
また少し、AppleScriptの心に近づけた気がした。