seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

AppleScriptを使ってみるよ!(言語編1/2)

動機

AppleScriptMacOSXにおける、WindowsWSHのようなものだと思うよ。

iTunes for Windows」はCOMオートメーションをサポートしているから、VBS/JScriptで簡単に操作できるけれど、本家、iTunesでもAppleScriptを使えば同じように制御できるらしいよ。

とはいえ、Mac新参者(といっても、もう2年…)にとってはAppleScriptは、いままでの、どの言語とも違うように見えるよ。
なので言語基礎から勉強してみることにしたんだよ。

つまり、これはAppleScriptエバンジェリストが書いたものではなくて、まったくの新米が自習のために綴ったノートだよ。

多々間違いがあると思うので、AppleScriptの文法をまじめに勉強するなら
http://developer.apple.com/mac/library/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html
を見ると良いと思うよ。(そして、間違いがあったら教えてほしいよ。)

開発環境


スクリプトを記述するためのエディタはOSX付属の「AppleScriptエディタ」を使うと良いと思うよ。(慣れたらXCodeのAppleScriptStudioも使ってみるよ。)
どこにあるか分からなければControl+SPCのスポットライトで検索すればよいと思うよ。

Hello, Worldはやらなきゃだよね


まずは、Hello, Worldだよ。
起動したら、表示メニューから「イベントログを表示」を選んでおくよ。
以下のように入力するよ。

log "Hello, World"

これで、画面下のイベントログに

(*Hello, World*)

と表示されるよ。

ちなみに(*から*)の間はブロックコメントを意味しているよ。
(*ではじまり、*)で終わるまでの間はコメントとしてスクリプトとしては無視されるからプログラムの説明をかけるよ。
一行コメントは、--で始まり文末までがコメントになるよ。


ちなみに、display dialogでダイアログが出るみたいだよ。

(*ダイアログを出してみる*)
display dialog "Hello, World" -- ダイアログを出す

変数を使ってみるよ


変数を使ってみるよ。

変数の代入はset VARIABLE_NAME to VALUE の形式で記述するよ。
変数に型はないよ。どんな型でも入るみたいだよ。


スクリプトにコードの文字(リテラル)として指定できる値には「真偽値(boolean)」「数値(number/real)」「文字列(text)」と、「リスト(list)」「レコード(record)」があるよ。
文字列(text型)は当然ユニコード対応だから日本語も大丈夫だよ。

set A to true
set B to 123
set C to "文字列"
log A
log B
log C

実際に値が何の型であるのか確認したいなら、class of を使うと良いよ。

log class of 1 -- integer
log class of 1.1 -- real
log class of {1, 2, 3} -- list
log class of "abc" -- text


文字列はダブルクォートで囲むC言語系と同じようにバックスラッシュでエスケープするよ。
(ダブルクォートやバックスラッシュ自身はバックスラッシュでエスケープする必要があるよ。*1 )

  • \r (return)
  • \n (linefeed)
  • \t (tab)

括弧内の英語表記は文字列定数として定義されているよ。
この他

  • space

という半角空白の定数も定義されているよ。
AppleScriptではreturnで改行を意味するらしいよ。(LFではないみたい。)


ちなみに、変数単独でも評価できるから、スクリプトの最後に変数だけ置いておくと、それがスクリプトの結果になるよ。


変数名は英文字で始まる、英数字とアンダースコアの組み合わせで、大文字・小文字は識別されないよ。(変数名はエディタの整形時に勝手に変換されたりするよ。最初の一文字は大文字が推奨されてるっぽいよ。)


変数名の前後をバーチカルバーで囲んだ場合は何でもかけるよ。(バーチカルバー以外は。)

set |変数 1| to 123 (* 推奨されない *)
log |変数 1| -- やめたほうがいい。

でも、読みにくいから使わないほうがいいって言われてるよ。


リスト型を使ってみるよ


リストは中括弧(波括弧)である{}で囲んで、0個以上の値をカンマで羅列したものだよ。(最後の要素の後ろにカンマを許さない仕様だよ)

set D0 to {}
set D1 to {1}
set D2 to {1, 2, 3}
log D0
log D1
log D2

リストに入れる型は問わないので、いろいろ混ぜられたりするよ。

set DComp to {false, 0, "NO"}
log DComp

リストは「item 番号 of 変数名」の形式でアクセスできるよ。
番号は1始まりだよ。

set DComp to {false, 0, "NO"}
set item 1 of DComp to true
log item 1 of DComp
log DComp


リストは先頭または末尾へのセットで追加ができるよ。

set Lst to {}
set beginning of Lst to 1
set end of Lst to 9
log Lst -- {1, 9}になるはず
log length of Lst -- 2: リストの長さを返す (count Lstでも同じ)

リストに追加するにはリストの結合を使う手もあるよ。

set lst to {}
set lst to lst & {"def"} -- 後ろに結合
set lst to {"abc"} & lst -- 前に結合
log lst -- {"abc", "def"}になるはず

リストにそれ以外を結合するとリストに結合されるけれど、リスト以外とリストを結合するとリストになるとは限らないので注意が必要だよ。



リストの最初の要素を除く残りを返すLispのcdrみたいなのもあるよ。

log rest of {1, 2, 3, 4, 5} -- {2, 3, 4, 5}を返す

要素1個のリストだと結果は空になり、空のリストだと実行時エラーになるよ。

carに相当するのは無いから、こうするんだと思うよ。

log first item of {1, 2, 3, 4, 5} -- 1を返すよ

文字列はリストの親類(文字列は文字のリストみたい)だから、だいたい同じようなことができるよ。

log item 2 of "abc" -- b

ただし、文字列は不変クラス(immutable)なので追加や変更はできないよ。

set Str1 to "abc"
set item 2 of Str1 to "c" -- エラー

レコード型を使ってみるよ


レコードは要素に名前をつけたリストみたいだよ。

set R1 to {A:"item1", B:"item2", C:"item3"}
log R1

名前をつけると、

log A of R1

のように名前でアクセスできるようになるよ。
たぶん、戻り値で複数の値のセットを返すときに便利じゃないかなと思うよ。


参照とcopyをつかってみるよ


変数と値の関係は「参照」のセマンティクスだよ。

set R1 to {A:1, B:2, C:3}
set R2 to R1
set A of R2 to 0
log R1 -- (*A:0, B:2, C:3*) R1のAも0に!
log R2 -- (*A:0, B:2, C:3*) 

R2のAを0に変更したつもりなのに、R1のAも0なったよ。
つまり、{A: 1, B: 2, C:3}という実体は1個しかなくて、それにR1とR2という名前をつけただけなので両方の変数とも同じ実体を見ているということみたいだよ。


実体をコピーするには「copy」を使うといいよ。
copyは既存のオブジェクトから新しいオブジェクトを構築するもので、そのセマンティクスは「deep copy」だよ。
(copyが使えるのはAppleScriptオブジェクトだけで、アプリケーションオブジェクトはだめだよ。)

set R1 to {A:1, B:2, C:3}
copy R1 to R2
set A of R2 to 0
log R1 -- (*A:1, B:2, C:3*) 影響なし
log R2 -- (*A:0, B:2, C:3*)

真偽値や数値みたいな不変な値型であれば、そもそもcopyの必要はないよ。




逆に、明示的に参照もとれるよ。

set R1 to {A: 1, B: 2, C:3}
set R1_A_ref to a reference to A of R1 -- 参照で取る
set A of R1 to 1234 -- 実体を書き換え
log contents of R1_A_ref -- 1234 (明示的参照外し)
log R1_A_ref -- 1234  (自動参照外し)


あるいは、

set A to {1, 2, 3}
set r to a reference to item 1 of A -- 参照で取る
log r -- 1
set item 1 of A to "a"
log r -- a
log A -- a, 2, 3

「a reference to」がないケースと比較してみると良いと思うよ。


演算子を使ってみるよ


演算子はいわゆる手続き型の他言語と比較しても大差ない直感的なものだよ。

(* 論理積 *)
log true and false -- false

(* 論理和 *)
log true or false -- true

(* 論理否定 *)
log not true -- false
log not false -- true


(* 等価演算子 *)
log 1 = "1" -- false
log "1" is 1 -- false
log "1" as number is 1 -- true
log "1" is "1" -- true
log "a" is "A" -- true : あれ、びっくりだね! 文字列の中身も大文字小文字関係ないよ
log 1 is 1.0 -- true
log 1 is equal to 0 -- false
log 1 is not 0 -- true
log 1 is not equal to 0 -- true

log {1, 2, 3} = {1, 2, 3} -- true
log {1, 2, 3} = {3, 2, 1} -- true : 並びも判定される

log {A:1, B:2} = {B:2, A:1} -- true : 並びは不問
log {A:1, B:2} = {A:1} -- false


(* 比較演算子 *)
log 1 > 0 -- true
log 1 < 0 -- false
log 1 >= 0 -- true -- >=と入力するとエディタで勝手に変換されるよ
log 1 <= 0 -- false -- <=と入力するとエディタで勝手に変換されるよ

log "abc" < "xyz" -- 文字列の並びで比較
log "ab" <= "AB" -- true
log "ab" < "AB" -- false : 注意。文字列の判定も大文字小文字関係ないからね。


(* 算術演算 *)
log 1 + 2 -- 3
log 2 * 3 -- 6
log 3 / 4 -- 0.75
log 3 div 4 -- 0 : 割り算だが結果は整数、端数切り捨て
log 5 mod 2 -- 1 : 剰余
log 2 ^ 4 -- 16 : 累乗

(* リストまたは文字列に対する演算 *)
log "abcdefg" starts with "ab" -- true : "ab"で始まっているか?
log "abcdefg" starts with "g" -- false

log "abcdefg" ends with "fg" -- true : "fg"で終わっているか?
log "abcdefg" ends with "ab"

log {5, 4, 3} starts with 5 -- true : リストが5で始まっているか?
log {5, 4, 3} starts with 3 -- false

log {5, 4, 3} ends with 3 -- true: リストが3で終わっているか?
log {5, 4, 3} ends with 5

(* リスト、文字列、レコードに対する演算 *)
log {3, 4, 5, 6} contains 4 -- true : 4を含むか?
log 4 is in {3, 4, 5, 6} -- 左辺と右辺が逆なだけ
log {3, 4, 5, 6} contains {4, 5} -- true : 4, 5の並びがあるか?
log {3, 4, 5, 6} contains {6, 7} -- false : 並びが一致しないので偽

log "abcdefg" contains "d" -- true : "d"があるか?
log "d" is in "abcdefg" -- 左辺と右辺が逆なだけ
log "abcdefg" contains "def" -- true : "def"があるか?
log "abcdefg" contains "fed" -- false : "fed"という並びがないので偽

log "abc" & "def" -- abcdef : 結合するよ
log {1,2,3} & {4,5,6} -- {1,2,3,4,5,6} : 結合するよ

(* ネストしたリストの場合 *)
log {{1}, {2}, {3}} contains {2} -- false
log {{1}, {2, 2}, {3, 3, 3}} contains {{2, 2}} -- true : 判定可能

(* レコードの場合 *)
log {A:1, B:2, C:3} contains {C:3} -- true : 有りと判定
log {A:1, B:2, C:3} contains {C:4} -- false : 一致しないので該当なしと判定
log {A:1, B:2, C:3} contains {C:3, B:2} -- true : 有りと判定(並びは不問)
log {A:1, B:2, C:3} contains {C:3, D:4} -- false : 足りないので該当なしと判定
log {A:1, B:2, C:3} contains {D:4} -- false

文字列の判定方法の変更ができるよ。

既定だと文字列の中身の大文字・小文字は無視されるけれど、これらはブロックで囲んで変更できるよ。

  • 大文字小文字も判定したい [case] 既定は無視
  • 空白を無視したい [white space] 既定は無視されない
  • その他いろいろあるよ[numeric strings, diacriticals, hyphens, punctuation] (でも出番は少ないかもだよ)

無視する場合はignoring ATTR … end ignoring の複文で指定するよ。
逆に有効(無視しない)場合は、considering ATTR … end considering の複文で指定するよ。

ATTRには上記のcase, white spaceとかを指定するよ。

囲まれた中では、指定された属性が有効または無効になるよ。

log "a" = "A" -- true
considering case
  log "a" = "A" -- false : 大文字小文字も判別しているよ
end considering

log " a b " = "ab"
ignoring white space
	log " a b " = "ab" -- true : 空白は無視されているよ
end ignoring

エラーハンドリング


エラーハンドリングしてみるよ

エラーが発生するとスクリプトの実行は中断されるよ。

log 1 / 0 -- 0除算で実行時エラー発生

エラーはtry … on error … end tryで囲むことでハンドリングできるよ

try
	log 1 / 0
on error msg
	log "エラーが発生したよ。:" & msg
end try -- end errorでも可、同じ意味だよ

on errorの後ろにはメッセージ他、エラー番号等々取れるよ。
一括して取得するから、Java/C#/C++みたいに例外ごとにハンドルするタイプじゃないみたいだよ。


「on error」を指定しないと単にエラーを無視するよ。

try
	log 1 / 0 -- 0除算エラー
end try
log "done."


自分でエラーを挙げることもできる。

try
	error "予期せぬ例外が発生しました。" number 500 -- ユーザ定義エラーは+500から+10000まで使える

on error msg number errno
	display alert msg & return & "errno=" & errno as warning
end try

制御文もいろいろあるみたいだよ

制御文を使ってみるよ。

条件分岐


基本のifはプリミティブな機能なので多言語と比較しても大差ないよ。
単文では、「if 条件式 then 文」の形式、
複文では、
「if 条件式 then」 から始まり、0個以上の「else if 条件式 then」と、0または1個の「else」を持つことができ、最後に「end if」で終わるよ。

set val to text returned of ↓
	(display dialog "数値を入れよ" default answer "" buttons {"確定"})
try
	if val as number >=100 then
		log "100以上なのかもしれない"
	else
		log "100未満なのかもしれない"
	end if
on error
	log "入力が不適切であったかもしれない"
end try


ちなみに一行が長くなりすぎた場合はOPTION + RETで継続行の印をつけて改行できるよ。この場合、改行されていなかったことになるよ。
(はてなダイアリでは表示できないけどね。)

繰り返し構文


「repeat 〜 end repeat」で囲んだものを無限に繰り返すよ。
途中で抜けるには「exit repeat」文で抜けるよ。

local i
set i to 10
repeat
	if i <= 0 then exit repeat
	log i
	set i to i - 1
end repeat


繰り返す回数が分かっていれば、times句をつけた方が簡単だよ。

local i
set i to 0
repeat 10 times -- 10回繰り返し
	set i to i + 1
	log i
end repeat


条件が成立するまで繰り返すならばuntil句+条件式をつけるよ。

local i
set i to 10
repeat until i <= 0
	log i
	set i to i - 1
end repeat


条件が成立している間繰り返すならばwhile句+条件式をつけるよ。

local i
set i to 1
repeat while i <= 10
	log i
	set i to i + 1
end repeat
カウンタつき繰り返し構文


ループする回数が事前に分かっていて、且つ、ループカウンタを使うのであればwith句+from/to/byをつけるよ。

withの後ろには変数名を指定するよ。変数は定義済みであっても、なくても良いよ。
fromは開始、toは終了(含む)の数値(整数)を指定するよ。
byは増分値(整数)を指定するよ。省略した場合は1になるよ。
カウントダウンしたい場合はby -1のように指定するよ。

byが1以上の正の値である場合、終了値が開始値よりも小さいと何も実行されず、その場合変数も定義されないよ。(負の値の場合は逆)

repeat with i from 1 to 10
	log i
end repeat
log i -- 10になるよ。条件がいきなり不成立であれば変数未定義でエラーになるよ
列挙型繰り返し構文


リストをイテレートするのであればwith句+in句をつけるよ。
シンプルだけど、これがもっとも利用頻度が高いかもしれないよ。

repeat with i in {1, 2, 3, 4, 5}
	log i
end repeat

このループ変数にはリストの各要素が参照として渡されるよ。
したがって、リストに対する変更も可能だよ。

set lst to {1, 2, 3, 4, 5}
repeat with i in lst
	if i mod 2 = 0 then -- 暗黙の参照はずし
		set contents of i to 100 + i -- 参照先へのセット
	end if
end repeat
log lst -- 1, 102, 3, 104, 5

ハンドラを定義してみるよ


AppleScriptオブジェクト指向言語だよ。もしかしたらJava/C++系よりもSmalltalkに近いのかもしれないよ。メッセージパッシングのメタファが用いられているっぽいよ。これを内部的にはイベントと呼んでいるみたいだよ。
(ちなみに、Raw Codesというのは内部のイベントのやりとりを表現したものらしいよ。二重カギ括弧で囲まれた部分がそうらしいけど、自分で書く必要はないと思うよ。)


イベントなのでメソッドと違うのはon/toで始まるイベントハンドラの定義ってところかなと思うよ。たぶん、イベントが発生したら実行するという意味でonなんだろうね。やってることは同じだよね。

on myEvent(msg) -- myEventが発生したら実行する
	log "message: " & msg
end myEvent

myEvent("hello, world") -- myEventを発生させる

ハンドラはラベル付きパラメータ形式で宣言できるよ。(ラベルがないのは位置パラメータ形式。)

on myEvent given message:msg, value:val
	local ret
	set ret to display dialog msg default answer val buttons {"OK"}
	return text returned of ret
end myEvent

log (myEvent given message:"hello, world", value:"123")


あるいは、AppleScript定義済みのラベルを使うほうが可読性が良いかもしれないよ。

on myEvent from msg to val -- 定義済みラベル from/toで引数を定義しているよ
	set end of val to msg -- 引数のオブジェクトを変更するよ
	return val
end myEvent

set lst to {}
myEvent from "hello, world" to lst -- 定義済みラベルfrom/toで引数を指定しているよ。また、lstに結果を受けるよ (参照渡し)
log lst

このハンドラの定義で使っている「from」「to」が定義済みのラベルだよ。
この他にも「out, above, against, apart from, around, aside from, at, below, beneath, beside, between, by, for, from, instead of, into, on, onto, out of, over, since, thru (or through), under」が自由に使えるので、引数の意味に似合ったラベルであれば、それを使うといいよ。

これらの組み合わせも可能なので、詳しくは
http://developer.apple.com/mac/library/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_handlers.html#//apple_ref/doc/uid/TP40000983-CH7g-SW2
を見ると良いと思うよ。


また、上記例みたいに、ハンドラに渡す引数はdate, list, record, script型は参照渡し(by reference)で、それ以外は値渡し(by value)だよ。なので、上記のように引数valのリストに対してハンドラ内で与えた変更は呼び出しもとにも影響を与えるよ。引数がtextやnumberである場合は、そもそも不変(immuetable)であるので変更されようがないよ。


このようにして定義したハンドラは、暗黙でスクリプト自身がトップレベルのオブジェクトであるので、このイベントハンドラスクリプト自身のメンバという位置づけになるらしいよ。


トップレベスクリプト用の特別なハンドラ(run)を使うよ


厳密には、トップレベスクリプトスクリプトのエントリポイントである「runハンドラ」をもてるよ。
明示的にrunハンドラがない場合は暗黙にスクリプトそのものがrunハンドラであるかのように上から順に実行されるよ。
たぶん、言語仕様的にはrunハンドラを明示的に書くほうがすっきりすると思うよ。

on myEvent(msg) -- myEventが発生したら実行する
	log "message: " & msg
end myEvent

on run -- runイベント(スクリプトの実行イベント)が発生したら実行する
	myEvent("hello, world") -- myEventを発生させる
end run

なお、runハンドラを記述した場合はダブリになるので暗黙のrunハンドラは使えないよ。(つまり、ハンドラの外で実行文は記述できないよ。)


スクリプトのプロパティを定義するよ


オブジェクト指向としては属性と振る舞いがあるわけで、スクリプト自身がオブジェクトということは、もちろん、スクリプト自身にも属性(プロパティ)がもてるよ。

property MyValue : "Hello, World"

on run
	set result to display dialog "スクリプトの値" default answer MyValue
	set MyValue to text returned of result
end run

プロパティはオブジェクトに対して割り当てるラベル付きの値だよ。
定義するときに「初期値」を指定するよ。

このコードはトップレベスクリプトにプロパティを割り当てて、それをダイアログで表示し、ダイアログで入力した値で更新して終了するだけのものだよ。

トップレベスクリプトに対してプロパティを定義すると、そのプロパティはスクリプトオブジェクトが終了しても維持されるよ。ただし、再コンパイルすると初期化されるよ。(たぶん、トップレベスクリプトオブジェクトはコンパイルした時点でコンパイラによって唯一のインスタンスができたという意味なんだと思うよ。)
だから、二度目に実行すると前回入力した値が残っているはずだよ。


ドロップレット用ハンドラ(idle, quit, open)


トップレベスクリプトオブジェクトにはrunハンドラの他に、openとidle, quitの3つのハンドラを定義することでスクリプトの応用性を高めることができるらしいよ。idleとquitは常駐スクリプト用だよ。


常駐スクリプトにするには、スクリプトの保存時に種類をアプリケーションにして実行後、自動的に終了しないに設定するよ。
そうすると暗黙もしくは明示的なrunイベントハンドラ終了後もスクリプトは常駐しつづけるよ。(普通の実行可能なスクリプトアプレットと呼ぶのに対して、常駐してドロップを受け入れるものをドロップレットと呼ぶらしいよ。)


すでに起動しているときに実行しているスクリプトのドック上のアイコンに、ファイルがドロップされたりするとopenイベントが発生するよ。複数個のアイテムがドロップされえるので受け取るドロップ情報はリストだよ。


idleイベントスクリプトが暇になったときに初回はすぐに呼び出され、その後、デフォルトでは30秒ごとに定期的に呼び出されるよ。idleハンドラの戻り値として整数で且つ1以上の値を返した場合、それが次回呼び出しまでの秒数になるよ。
0または数値以外を返すとデフォルト値が使われるよ。


quitイベントは終了時に呼び出されるよ。
終了したくないときは、ここでイベントを無視すればいいよ。終了したい場合は、「continue quit」でイベントを継続してやる必要があるよ。

on open names
	local msg
	set msg to ""
	repeat with i in names
		set msg to (msg & i as text) & return
	end repeat
	display dialog msg buttons {"ok"}
end open

on idle
	beep
	return 3 -- 3秒ごとにアイドルイベントを起こすように
end idle

on quit
	display dialog "bye" buttons {"ok"}
	continue quit -- quitのイベントチェインを継続する (しないと終了できなくなるよ)
end quit

独自スクリプトオブジェクトの定義


独自オブジェクトの構築をするよ

AppleScriptではユーザ定義のプロパティをもつことができるのはrecordとscriptオブジェクトの2つだけだよ。
scriptは「script」ではじまって「end script」で終わるブロックで定義するよ。
トップレベスクリプトと同様に

  • property
  • ハンドラ

を定義することができるよ。
スクリプトはトップレベスクリプトで定義できるだけでなく、ハンドラの中や他のスクリプトの中でも定義できるよ。

script MyObj
	property callCount : 0
	to myEvent()
		set callCount to callCount + 1
		return callCount
	end myEvent
end script

tell MyObj -- 複文形式での呼び出し
	set x to myEvent()
	log x
end tell

tell MyObj to myEvent() -- 単文形式での呼び出し

他のオブジェクト指向言語と異なり、どうやらインスタンス化するという発想ではなくて、定義を実行した時点でオブジェクトは生成されるっぽいよ。(ここはJavaScript/ECMAScriptっぽいかも。)

tellで送信先オブジェクトを暗黙指定できるよ


tellからend tellまでのブロックでは、その中で記述したイベント送信は、tellで指定したオブジェクトへのイベント送信となるよ。
tellはネストすることができ、その場合、送信先オブジェクトが切り替わるよ。


もし、tellの中から自スクリプトのハンドラを呼び出したい場合は明示的に「of me」とすることで呼び出せるよ。

on myEvent(msg)
	log "Root#MyEvent: " & (msg as text)
end myEvent

script MyObj
	to myEvent(msg)
		MyEvent2(msg) of me -- 自身を示す「of me」は、ここでは省略可
	end myEvent
	
	to MyEvent2(msg)
		log "MyObj#myEvent2: " & (msg as text)
	end MyEvent2
end script

tell MyObj
	myEvent("#1")
	myEvent("#1B") of MyObj -- tellで指定しているので冗長
	myEvent("#2") of me -- 自身を示す
end tell
myEvent("#2B") of me -- 自身を示す「of me」は、ここでは省略可
トップレベスクリプトでなくてもrunハンドラを持てて実行できるよ。


Scriptオブジェクトなので、暗黙もしくは明示的なrunハンドラをもつことができるし、それを呼び出すこともできるよ。

script MyObj
	log "暗黙のrunハンドラ"
end script

tell MyObj to run

スクリプトオブジェクトをコピーしてみるよ


copyコマンドでオブジェクトをコピーした場合、その内包するプロパティがシャローコピーであるかディープコピーであるか確認してみるよ。

script A
	property lst : {}
	to print
		log lst
	end print
end script

set end of lst of A to "abc"
tell A to print -- {abc}

copy A to B -- copyコマンドは"deep copy"だよ

tell B to print -- Aと同じ結果を返す {abc}

set end of lst of A to "def"
set end of lst of B to "xyz"

tell A to print -- {abc, def}
tell B to print -- {abc, xyz} -- 保持しているプロパティも独立していることがわかるよ

copyコマンドでプロパティも含めてディープコピーされていることが確認できたよ。
(マニュアルにもdeep copyだと書いてあるよ。)


ただ、この例だと、スクリプトオブジェクトAは、実行するたびに「前回実行後」の値を引き継いでしまうので、どんどん追記されている形になるよ。
これはAppleScriptのグローバルスコープの変数およびスクリプトは再コンパイルされるか明示的に初期化されないかぎり永続化されるためだよ。
http://developer.apple.com/mac/library/documentation/AppleScript/Conceptual/AppleScriptLangGuide/conceptual/ASLR_variables.html#//apple_ref/doc/uid/TP40000983-CH223-SW3

たぶん、これは望まない動作だから、状態をもつスクリプトを確実に初期化するようなケースではファクトリパターンを使うのが良いのだと想うよ。


スクリプトオブジェクト同士の等値判定は望めないようだよ


copyコマンドで内容をそっくりコピーできることは分かったが、それが等しいかどうか判定するための方法は標準では存在しないようだよ。

script A
end script

copy A to B

log A = A -- true
log A = B -- 内容が同じでもインスタンスが異なればfalseになるらしいよ


等値判定をどうしてもやりたいのであれば、自分で、そうゆう比較ハンドラを書くのだろうね。


コンストラクタはファクトリパターンで代用するよ


単純にscriptブロックで定義してしまうと、オブジェクトは1個しか持てないし、コンストラクタのような初期化の方法もないので、もし複数個のオブジェクトもしくは初期化が必要な場合は(ハンドラでスコープを切る)ファクトリパターンを使うらしいよ。

on MyFactory(v_msg)
	script MyObj
		property msg : v_msg -- レキシカルスコープっぽいよ
		to myEvent()
			log "MyObj#myEvent: " & (v_msg as text)
		end myEvent
	end script
	return MyObj
end MyFactory

set Rec to {A:MyFactory("#1"), B:MyFactory("#2")}
log Rec
log msg of A of Rec -- #1と表示
log msg of B of Rec -- #2と表示
myEvent() of A of Rec -- MyObj#myEvent: #1と表示
myEvent() of B of Rec -- MyObj#myEvent: #2と表示

(* ちなみに *)
log class of A of Rec -- scriptを返す。定義しても「型」にはならないようだよ。

継承してみるよ


scriptオブジェクトは一般的なオブジェクト指向言語同様に継承できるよ。
スクリプトブロック内で「parent」という名前のプロパティの親にしたいオブジェクトを指定するだけで良いよ。
ちなみに、parentにはscriptオブジェクトを設定するのが普通らしいよ。(当然だよね。)
でも、他のオブジェクト、たとえばrecordやlistも指定できるらしいよ。
といっても、listで複数のオブジェクトを指定しても多重継承にはならないよ。
さしあたり継承は単一継承だけできると思っていて良いと思うよ。

script MyObj1
	property MyValue1 : "value1"
	
	to getMessage()
		return "MyObj1#getMessage"
	end getMessage
	
	to ShowMessage()
		display dialog getMessage()
	end ShowMessage
	
end script

script MyObj2
	property parent : MyObj1 -- 親オブジェクトの設定(継承)
	
	property MyValue2 : "value2"
	
	to getMessage() -- オーバーライド
		return "MyObj2#getMessage"
	end getMessage
	
end script

log MyValue1 of MyObj2 -- 親であるMyObj11のMyValue1が見られるよ
log MyValue2 of MyObj2 -- これは自分のプロパティ

ShowMessage() of MyObj1 -- MyObj1として呼び出し "MyObj1#getMessage"と表示するよ
ShowMessage() of MyObj2 -- MyObj2として呼び出し "MyObj2#getMessage"と表示するよ

parentプロパティによって、まず自分のオブジェクトを捜して、なければparentに指定されたオブジェクトを捜すチェインを構成するよ。
スクリプトにparentがあれば、同様に遡るよ。


オーバーライドしたハンドラから親ハンドラを呼び足したい場合はcontinueを使うよ。

script MyObj1
	to myEvent to msg
		set ret to "message to: " & (msg as text)
		log ret
		return ret
	end myEvent
end script

script MyObj2
	property parent : MyObj1
	to myEvent to msg
		log "before"
		set ret to continue myEvent to msg -- 親ハンドラの呼び出しを継続
		log "after"
		return "@" & (ret as text) & "@" -- 戻り値を変更したりして
	end myEvent
end script

log (myEvent of MyObj2 to "test123")

結果はこんな感じになるよ。

(*before*)
(*message to: test123*)
(*after*)
(*@message to: test123@*)

再利用可能 or モジュール化にしてみるよ


ハンドラまたはスクリプトをライブラリ化してみるよ。
単にスクリプトファイルにして読み込ませればいいだけだよ。

script MyScript
	property cnt : 0
	to MyEvent2()
		set cnt to cnt + 1
		display dialog "MyEvent2 : " & cnt
	end MyEvent2
end script

これを「script1.scpt」として一時保存しておくよ。

これを呼び出す側のスクリプトを作成するよ

set lib to (load script ((path to me) & "Contents:Resources:Scripts:script1.scpt" as text) as alias)

repeat
	MyEvent2 of (MyScript of lib) to "test123"
end repeat

これをアプリケーション「script2.app」として保存するよ。

アプリケーションにすると画面右上の「バンドルの内容」というボックスが開くようになるので、そこに先ほど保存したscript1.scptをScriptsの下にコピーするよ。

load scriptでスクリプトを読み込ませるために保存したスクリプトの位置が必要だよ。

(path to me)で自分自身の位置が分かるので、あとはバンドルしているスクリプトの場所を付け足せばいいよ。


ちなみに、スクリプト自身のプロパティはスクリプトを再コンパイルするまで前回値を維持するけれど、読み込まれるスクリプトは読み込まれるたびに初期化されるので、この場合、トップレベスクリプトが終了するとリセットされるよ。


終わり


とりあえず言語一般に共通する基礎部分は、だいたい、こんなものだと思ってみたよ。
次はAppleScript特有の(ように見える)「オブジェクトの選択」まわりの構文を調べてみるよ。

*1:なお、OSX一般的に言って、バックスラッシュはWindowsみたいに「\」ではなくて、OPTION + \ で本当のバックスラッシュを指定する必要があるよ。Emacsperlスクリプト書いてたときにはまったよ。