アプリケーションによるファイル参照

ファイルを扱うアプリケーション...、Finder や System Events でのファイル参照について。

どうも AppleScript によるファイル参照とこれらのアプリケーションによるファイルの参照がごちゃごちゃになっていて変なエラーに悩まされている...という人を時折見かけます。

AppleScript の分かりにくい部分ですね。

この違いとアプリケーションとの連携をみていきましょう。


それはオブジェクトの参照です

まずは、Finder や System Events などでのファイル指定はファイルの参照ではなく、アプリケーションが扱うオブジェクトへの参照...ということを理解しましょう。

次のスクリプトを実行してみましょう。

tell application id "com.apple.finder"
    -- デスクトップにあるファイルを指定
    set theFile to file 1 of desktop
end tell
-- ファイルの内容を読み込む
read theFile

このスクリプトはエラーになります。もし、この参照が AppleScript でのファイルの参照ならエラーにならないはずです。

この参照は Finder のオブジェクトの参照にすぎず、ファイルの参照ではありません。まずはこのことを理解しましょう。

オブジェクトの参照

AppleScript は、アプリケーションやオブジェクトに対してなんらかの命令を送り、なんらかの処理を行う...と思っていると理解の妨げになるかもしれません。

AppleScript で行う操作の大部分はオブジェクトの属性の変更です。そのために頻繁に用いる命令は set です。AppleScript は tell 構文で処理するターゲットを指定し、set で処理を行うことがほとんどです。極端な話、tellset の使い方が分かれば、AppleScript の大半は理解できるのではないか?と思っているぐらいです。

AppleScript では処理するオブジェクトやオブジェクトの属性を指定するために頻繁に参照を利用します。もちろん、これはファイル参照とは別物です。オブジェクト参照とは、処理したいオブジェクトを特定するものです。

オブジェクト参照は、目的のオブジェクトからコンテナ(オブジェクトが入っている容れ物)へと of をつなげて作ります。

目的のオブジェクト of オブジェクトの容れ物 2 of オブジェクトの容れ物 1

デスクトップにあるフォルダの中の最初のファイルを Finder で指定するには次のようになります。

tell application id "com.apple.finder"
    file 1 of folder 2 of desktop
end tell

英語の文法的には問題ないんですけどね、これで。スクリプトの意味としては『アプケーション Finder のデスクトップにある 2 つめのフォルダの中にある最初のファイルオブジェクト』ぐらいの意味で、それ以上でも以下でもない。

通常はオブジェクトの参照を使ってファイルになんらかの処理を行います。

tell application id "com.apple.finder"
    -- 重要なファイルなのでロックしておいてね
    set locked of file 1 of folder 2 of desktop to true
end tell

このようにオブジェクトが持っている属性を変更(もしくは取得)します。

おそらく、この辺りの参照でつまづく人は Finder がファイルやフォルダの処理を行うアプリケーションだから、ファイルに対する入出力も Finder を通して行うだろう、と勝手に予測をしてしまうからではないでしょうか。

Finder を通して得た参照には Finder で行う処理以外はできません。ファイルの移動や複製、エイリアスの作成やロック、コメントの付加など Finder で行う操作はできます。逆に言うとこれらの操作は AppleScript 単体ではできません。あくまで Finder(もしくは、System Events)を通してのみできる操作です。

ただ、Finder と AppleScript の親和性は高いので、alias による参照は Finder で利用できますし、Finder のファイル・フォルダオブジェクトの参照を alias に変換することはできます。

この変換は他のアプリケーションでファイルを利用するために必要になってきます(大多数のアプリケーションでは alias 参照を通してファイルを処理します)。

tell application id "com.apple.finder"
    -- Finder の参照を alias クラスに変換
    set theFile to file 1 of folder 2 of desktop as alias
end tell
-- 他のアプリケーションでは alias による参照を使ってね
tell application "Safari"
    open theFile
end tell

alias を Finder に渡してもそのまま処理してくれます。

-- デスクトップにあるファイルの alias 参照
set theFile to alias ((path to desktop as text) & "test.scpt")
tell application "Finder"
    -- 複製を作る
    duplicate theFile
end tell

しかし、これは Finder が alias を利用できるというだけで、他のアプリケーションでもこのように利用できるというわけではありません。どこまで利用できるかはそのアプリケーションを調べてみないことにはなんとも言えません(ファイルやフォルダの処理は System Events でも同様なことができます。また、alias も扱えるので、Finder の代替としても利用できます)。

理解をさらに困難にさせるのが AppleScript の file クラスと Finder の file クラスというように同音異義語が多数登場することです。

このような同音異義語がどのような文脈で利用されているか...これを意識していないと変なエラーに悩まされることになります。

Finder でファイル参照

アプリケーションを通してなんらかの処理を行う...これが AppleScript の正しい使い方です。その処理に特化したアプリケーションを使えば効率がいいし、ぶっちゃけ楽だからです。

Finder で選択しているファイルを AppleScript の基本的な機能だけで取得するのは困難ですが、Finder を使えば一行です。

tell application id "com.apple.finder"
    -- 現在選択している項目を alias で取得
    set theseItems to selection as alias list
end tell
repeat with thisItem in theseItems
    (* 個々の項目に対しての処理を記述 *)
end repeat

また、Finder では alias list というクラスが定義されていて、ファイルやフォルダオブジェクトの参照を alias で一括して取得することができます。他のアプリケーションに渡す場合は、このようにして一括で処理できます。

最前面にあるウィンドウのファイル一覧を得るには次のようにします。

tell application id "com.apple.finder"
    -- 前面のウィンドウに表示されているファイル一覧を取得
    set theseFiles to files of Finder window 1 as alias list
end tell

Finder は、フィルタ参照(条件に合致したオブジェクトを特定するための参照方法)を使えるので、デスクトップ上にある拡張子が scpt でロックがかかっているファイルの一覧などという複雑なファイル指定も行えます。

tell application id "com.apple.finder"
    -- デスクトップにある拡張子 scpt でロックされているファイルの一覧
    files of desktop whose name extension is "scpt" and locked is true
end tell

ただ、POSIX path への変換は Finder のオブジェクトからはできないので、ちょっと手間がかかります。

tell application id "com.apple.finder"
    -- 選択項目を alias で取得
    set fileList to selection as alias list
end tell
-- POSIX パスに変換
repeat with thisItem in fileList
    set contents of thisItem to POSIX path of thisItem
end repeat

ひとてま必要なのがツラいところです。

System Events でファイル参照

System Events は Finder と同じようにファイルやフォルダを操作することができます。が、両者に互換性はなく、ちょっと癖があります。

tell application id "com.apple.systemevents"
    -- デスクトップにあるファイル一覧を取得
    set fileList to file of desktop folder
end tell

このスクリプトを実行してみると分かりますが、不可視のファイルも取得しています。Finder では不可視のファイルは無視します。

また、Finder のように一括で alias に変換するような方法もありません。その代わりに pathPOSIX path という属性があるので、この属性を使って一括で取得することができます。

tell application id "com.apple.systemevents"
    -- デスクトップにある全ファイルの HFS パスを取得
    set pathList to path of file of desktop folder
    -- デスクトップにある全ファイルの POSIX パスを取得
    set POSIXpathList to POSIX path of file of desktop folder
end tell

System Events でもフィルタ参照が使えるので Finder でやったような条件に合致したオブジェクトを取得することができます。

tell application id "com.apple.systemevents"
    -- デスクトップにある拡張子 scpt のファイルの HFS パスを取得
    set pathList to path of files of desktop folder whose name extension is "scpt"
end tell

まとめ

このようにファイルを処理する方法が複数用意されているということが AppleScript の理解の妨げになっているような気がします。

そのため、ファイル処理だけを統一的に扱うためのスクリプトを作っている、という人もいます。

個人的にはファイルやフォルダの処理には Finder を使っています。慣れているし、System Events を使うより圧倒的に楽だからです。

肝心なのはアプリケーションによるオブジェクトの参照と AppleScriptの aliasfile による参照を混同しないこと。複数のアプリケーションの連携を行うなら、オブジェクトの参照から alias の参照に変換する、もしくは POSIX パスに変換しておくといった処理をアプリケーションの境界で行っておくことです。

この辺りのことを意識しておけば、不用意なエラーに悩まされることは少なくなると思います。

AppleScript でファイルの参照

さて。AppleScript でファイルを開くいろいろな方法をみてきました。

Finder や System Events などのアプリケーションや StandardAdditions.osax のファイル選択ダイアログを使えば、処理したいファイルの指定はそれほど難しいものではありません。

しかし、スクリプトの中ではもっと直接的にファイルの指定を行いたいときが多々あります。

ということで、今回は AppleScript で利用するファイルの参照、パスの話です。


HFS パス

OS X 以前の話です。その頃 AppleScript はコロン(:)区切りのパスを使っていました。

ユーザー cherry の書類フォルダの中にある style.css というファイルを表すと、次のようになります。

-- Macintosh HD は起動ディスク
"Macintosh HD:Users:cherry:Documents:style.css"

このコロン区切りのパスを「HFS パス」と呼びます。HFS は OS X のファイルシステムですね。このパスはルート(この例だと起動ディスク)から書き始めます。だから、他の人の環境だと異なっているかもしれませんし、外部ディスクになるとルートも変わります。

現在の AppleScript だとルートの省略ができるようで、以下のような書き方でもありです

-- 起動ディスク以下の Applications フォルダ
"Applications:"

HFS パスではコロンまでが一つのディレクトリです。

"Macintosh HD:" -- ルートディレクトリ
"Macintosh HD:Users:cherry:Documents:" -- 書類フォルダ
"Macintosh HD:Users:cherry:Documents:style.css" -- style.css ファイル

最初のコロンまでが起動ディスク(もしくはルートディレクトリ)を表しています。最後にコロンがつくとディレクトリ(フォルダ)、そうでないならファイルとみなされます。

OS X 以前から利用されていたということもあり、AppleScript ではこの HFS パスを多用します。これは現在(OS X 10.11)でも変わっていません。

POSIX パス

POSIX パスは UNIX ベースの OS X ではなじみが深いものです。スラッシュ区切りのこのパスは、さまざまな場面で目にするものです。

先ほどのユーザー cherry の書類フォルダの中にある style.css を POSIX パスで表すと次のようになります。

"/Users/cherry/Documents/style.css"

簡潔ですね。この POSIX パスは特別なことをしなくても AppleScript では(文字列として)利用できます。

AppleScript というのはグルー言語です。いろんな機能を組み合わせて複雑なバッチ処理を行うことを目的としています。シェルスクリプトなんかと同じですね。

UNIX ベースの OS X なんだからバッチ処理はシェルスクリプトでいいじゃないか、と言えばその通りなのですが、AppleScript の優位性というのは各種アプリケーションを操作することができることです。

もともとはアプリケーションの自動化が主な目的だったのですが、現在では AppleScript を中心にシェル(を通しての外部プログラミング言語)、Objective-C、JavaScript...と、いろいろなプログラミング言語を結びつける役割も果たしています。

こういう特性のため AppleScript でも POSIX パスを利用することが多々あります。ファイルの選択は AppleScript で行って処理はシェルで行う...といった感じですね。

ファイル参照のためのクラス

POSIX パスも HFS パスもファイルやフォルダの場所を示すための記述方式にすぎません。ぶっちゃけただの文字列です。このままでは AppleScript ではファイルの参照として認識しません。

では AppleScript でファイルを参照するにはどうするのかというと、alias、もしくは file クラスを利用します。

ごくおおざっぱにいってしまえば、alias クラスは『既にあるファイルを指定するためのクラス』で file クラスは『存在していないファイルを指定するためのクラス』です。

既にファイルがある場合に使うのが alias クラスです。

alias "Macintosh HD:Users:cherry:Desktop:Work:main.html"

デスクトップの Work フォルダにある main.html というファイルを指定しています。このとき、この場所にファイルがないと実行時にエラーになります(古いバージョンの AppleScript ではコンパイル時にエラーになる)。

AppleScript は実行時に alias で指定されたファイルやフォルダを記憶します。記憶しておくことで、次回からはファイルやフォルダを追跡します。つまり、ファイルやフォルダを移動させたり、名前を変更しても大丈夫なんです。

次のスクリプトを試してみましょう。


初回実行時は変数 theFile が未定義なので必ずエラーになります。エラーが起きるとファイル選択を求められます。その後、現在の場所を表示し、適当な場所に移動させます。

もう一度、実行してみましょう。ファイルは元の場所にないにも関わらず、ファイル選択を求められませんし、ファイルは再度異なった場所に移動されます。

alias は指定されたファイルやフォルダの情報を記憶します。このため、ファイルの位置や名前が変更されても追跡することができます。そういう特殊なもののため、実行時に指定するファイルやフォルダが存在しないとエラーになります。

いまいちピンとこない...という方もいると思われます。もうちょっと直感的に理解できる仕組みが Finder にあります。それは、Finder で作るエイリアス(「ファイル」メニューの「エイリアスを作成」)です。

この Finder で作るエイリアスと AppleScript の alias クラスは同じ仕組みです。

では、file クラスをみてみましょう。

file クラスは alias とは異なり、常に一つの場所を参照します。指定された場所にファイルがなければファイルを作成し、あれば、そのファイルを使用します。alias のようにオリジナルのファイルを追跡しません。

例えば、以下のようにすればデスクトップに「new file.md」というファイルを作成します。

-- ファイルのパスを文字列から作る
set theFile to (path to desktop as text) & "new file.md"
-- パスを file クラスとして扱い新規ファイルを作成
set fh to open for access file theFile with write permission
close access fh

再度このスクリプトを実行すると、すでにある「new file.md」に上書きします(なにもデータを追加していませんが)。「new file.md」を他の場所に移動させ、再度実行すると同じ場所に同じ名前でファイルを作成します。

このように file クラスは常に指定された場所だけを参照します。

aliasfile は、それぞれ同じようにファイルを参照しますが、その意味するところは異なっています。目的に合わせて利用することが大事です。

パスの相互変換

現在では、POSIX パスでファイルを指定するのが、記述のしやすさや汎用性の点からいっても楽です。指定したいファイルやフォルダをそのまま Script Editor.app のドキュメントにドラッグ & ドロップして挿入もできますし。

しかし、AppleScript では POSIX パスをそのまま利用するということは稀です。もう少し厳密に書くと、AppleScript やアプリケーションで定義されている命令やクラスが POSIX パスを受け取るようになっていません。

パスの指定は POSIX パスが楽だけど、AppleScript の命令などで利用するにはどうすればいいのか?

そのためにパスの変換を行います。パスの変換方法を覚えれば、HFS パスと POSIX パスを相互に変換できます。

変換には as 演算子を使います。as 演算子は型の変換を行う演算子です。

set num to 29.87
class of num
--> real
-- 整数値に変換
set num to num as integer
class of num
--> integer
-- 文字列に変換
set num to num as text
class of num
--> text

変換できない値を変換しようとするとエラーになりますが、as による型変換は頻繁に利用するので覚えておきましょう。

AppleScript ではファイル参照に利用するためのクラスが 3 つあります。aliasfilePOSIX file です。aliasfile クラスは後で説明します。POSIX パスを変換するために利用するのが POSIX file です。

POSIX file というのはちょっと特殊なクラスで POSIX パスを HFS パスに変換するためだけに利用します。基本的には後で説明する file クラスと同等のものです。

-- Applications フォルダ内の Mail.app
"/Applications/Mail.app" as POSIX file
--> file "Macintosh HD:Applications:Mail.app"

変換すると file クラスのパスが返ってきます。この変換は頻繁に利用するので、わざわざ as で変換しなくてもいいようになっています。

-- 次の書き方でも可
POSIX file "/Applications/Mail.app"
--> file "Macintosh HD:Applications:Mail.app"

ちなみに POSIX パスでは ~ が現在ログインしているユーザーのホームを表していますが、AppleScript では正しく解釈されません。POSIX パスを利用するときは相対パスではなく、絶対パスを使ってください。

fileas 演算子で alias に変換することもできます。

set theFolder to POSIX file "/usr/local/bin/"
tell application id "com.apple.finder" to open (theFolder as alias)

file クラスも alias クラスも as 演算子で HFS パス文字列に変換できます。

set theFolder to POSIX file "/usr/local/bin/"
theFolder as text
--> "Macintosh HD:usr:local:bin:"

次に HFS パスを POSIX パスに変換するにはどうするのか?

alias クラスと file クラスはそれぞれ、POSIX path という属性を持っているので、これを利用します。

-- POSIX パスを file に変換
set theFile to POSIX file "/Applications/Utilities/X11.app"
--> file "Macintosh HD:Applications:Utilities:X11.app"
-- file を POSIX パスに変換
POSIX path of theFile
--> "/Applications/Utilities/X11.app"

POSIX file はクラスだけど、POSIX path は属性です。このへんを勘違いしないようにしましょう。

いろいろなパスを取得する

スクリプトからファイルを参照するための記述方法をみてきました。今度はスクリプトの中からいろいろなパスを取得してみましょう。

StandardAddtions.osax に path to という命令があります。この命令は特定の場所を返してくれる便利な命令です。

例えば、実行しているスクリプトの場所を得るには次のようにします。

path to me

スクリプトが保存されているならファイルの場所が返ってきます。では、ログインしているユーザーのホームを取得してみます。

path to home folder

どんどんいきましょう。

-- 現在のユーザーの書類フォルダ
path to documents folder
-- 現在のユーザーのピクチャーフォルダ
path to pictures folder
-- 現在のユーザーのムービーフォルダ
path to movies folder
-- 現在のユーザーのデスクトップ
path to desktop
-- 現在のユーザーのダウンロードフォルダ
path to downloads folder

これらのパスを path to で取得するのはユーザーによってユーザー名や起動ディスク名などが異なるからですね。異なっていても同じようにスクリプトを動かすために path to を使います。

では、次のスクリプトを実行してみましょう。

path to library folder

今度はユーザーのライブラリフォルダの場所が返ってくるかと思いきや、違う場所が返ってきました。

OS X にはライブラリフォルダが複数箇所にあります。システムが利用するもの、ログインしている全ユーザーが利用するもの、そして、現在ログインしているユーザーが使用するもの。これらのうちどのライブラリフォルダかを指定していないため、予想と異なった場所が返ってきたのです。

path to 命令には from というオプションがあり、このオプションでどの場所かを指定します。

-- 現在のユーザーのライブラリフォルダ
path to library folder from user domain
-- 全ユーザーが利用するライブラリフォルダ
path to library folder from local domain
-- システムが利用するライブラリフォルダ
path to library folder from system domain

このように複数箇所にあるフォルダのパスを得るときは from による場所の指定を忘れないようにしましょう。

また、path to 命令には as オプションがあり、パスを alias で取得するか、文字列で取得するかを指定することができます。デフォルトでは alias でパスが返ってきます。

文字列で取得するのは、起点となるパスから特定のファイルを指定するためです。ユーザーの書類フォルダの中にある style.css ファイルを取得するなら次のようにします。

set documentsFolder to path to documents folder as text
set theFile to documentsFolder & "style.css"

これで各環境におけるユーザー名の違いや起動ディスク名の違いを考慮する必要がなくなります。

特定のフォルダのパスを文字列で取得し、目的のファイル名と文字列の結合を行い、filealias、もしくは POSIX パスに変換...と、これがファイル参照の一連の手続きですが、この操作は頻繁に行うのでいろいろと試して慣れておくといいでしょう。

ときどき目にする勘違いなのですが、これが AppleScript におけるファイル参照であって、Finder や System Events 等のファイルを扱うアプリケーションによるファイルの参照とは別のものです。これらのアプリケーションにおけるファイルの参照はファイルの参照ではなく、オブジェクトの参照だということを間違えないようにしましょう。

AppleScript でファイルを開いてみましょ

Finder でファイルをダブルクリックし、ファイルを開き、アプリケーションで処理をする。もしくは、Finder でファイルを選択し、それをアプリケーションアイコンにドラッグ & ドロップしてファイルを開く...。

たかだかファイルやフォルダを開くだけなのですが、日常的にやっている作業でもあります。それゆえに自動化の恩恵はそれなりにあるのではないでしょうか。

この作業を Applecrip で代替してみましょう。


例えば未対応なアプリケーションで

アプリケーションの起動と終了」でも書いたようにアプリケーションは AppleScript に対応しているものとそうでないものがあります。アプリケーションが AppleScript に対応していてもどのていど対応しているかが異なります。

しかし、ファイルを開くということだけならほとんどのアプリケーションで利用することができます。アーカイブユーティリティ.app は OS X に標準でついてくる圧縮されたファイルを解凍するアプリケーションですが、これは AppleScript に対応していません。しかし、基本的な命令をうけつけるアプリケーションでもあります。

では、AppleScript からファイルを開くとどうなるか?

-- デスクトップにある Archive.zip を指定
set theFile to (path to desktop folder as text) & "Archive.zip"
tell application "Archive Utility"
    -- アーカイブユーティリティで開く
    open file theFile
end tell

予想通り、圧縮されたファイルが解凍されます。このようなアプリケーションは(AppleScript で操作できているように見えますが)「AppleScript 未対応」です。本来の AppleScript に対応しているアプリケーションは「用語説明を持っている」アプリケーションです。分かりにくいのですがここでは未対応に分類します。

と、こういうことを踏まえて、AppleScript でどこまでできるのか?

ファイルを開く基本は Finder です

AppleScript に対応していなくてもファイルを開くだけであれば、Finder を通せばどんなファイルでも開けます(そのファイルを開けるアプリケーションがあれば)。

tell application id "com.apple.finder"
    -- ウィンドウがなければ終了
    if not (front Finder window exists) then return
    tell front Finder window
        -- 前面のウィンドウにある拡張子が mp4 のファイルを取得
        set fileList to document files whose name extension is "mp4"
    end tell
    -- ファイルを開く
    open fileList
end tell

これは、Finder でファイルをダブルクッリクする動作と同じで、デフォルトのアプリケーションでファイルを開きます。なにも変更していないなら、QuickTime Player が起動してファイルが表示されます。

また、Finder は開くアプリケーションを指定してファイルを開くことができます。これも AppleScript で制御できます。openusing オプションを利用します。

tell application id "com.apple.finder"
    -- ウィンドウがなければ終了
    if not (front Finder window exists) then return
    tell front Finder window
        -- 前面のウィンドウにある拡張子が mp4 のファイルを取得
        set fileList to document files whose name extension is "mp4"
    end tell
    -- ファイルを VLC.app を使って開く
    open fileList using (path to application "VLC")
end tell

この方法を使えば、テキストファイルなら CotEditor で、画像ファイルなら Photoshop でといったこともできますね。

open と Finder の open の違い

AppleScript を書き始めたころ理解しにくかったのが、同じ命令がいくつもあることでした。

Finder の open は Finder で定義されているもので using なんていう便利なオプションが追加されています。

もう一方の open は AppleScript で定義されているもので using なんてオプションはありません。でもね、見た目は一緒なんです。では、それらをどうやって見分けるのか?

これらの見分け方は命令を送っている対象が誰か?で分かります。

QuickTime Player は AppleScript に対応しており、open 命令を持っています。

tell application "QuickTime Player"
    open fileList
end tell

だけど、using というオプションはないのでただ、ファイルを開くだけです。

命令を送っている対象というのは tell 構文で指定されています。tell で指定されていないなら、それは AppleScript に最終的に送られます。

tell 構文というのは命令を送る対象を指定する構文なので、対象はアプリケーションでなくてもかまいません。

set theList to {1, 2, 4, 10.1, "文字", 0.1, "abc"}
tell theList
    length
    --> 7
    strings
    --> {"文字", "abc"}
    numbers
    --> {1, 2, 4, 10.1, 0.1}
    reverse
    --> {"abc", 0.1, "文字", 10.1, 4, 2, 1}
end tell

どのアプリケーションが持っている命令か?どこに命令を送っているか?というのは結構重要なことでこれを見極めていないとスクリプトがエラーになる、動かないといった原因になります。

再びの URL Scheme

以前にも URL Scheme のことを書いたことがありますが、あれは iPhone アプリのことでした。現在では OS X のアプリケーションでも AppleScript には対応していないけど、URL Scheme は設定されているというアプリケーションが増えてきました。それらを使ってファイルを開いてみましょう。

Apple 純正のものだと辞書.app や FaceTime.app、App Store.app、連絡先.app などがわりと有名ですね。

URL を AppleScript で開くには open location 命令を使います。この命令は URL をデフォルトのアプリケーションで開く命令です。http で始まる URL なら Safari が起動しますし、itms で始まるなら iTunes が起動します。

ちょっと余談なんですが、open location は StandardAdditions.osax という AppleScript 独自の機能拡張で定義されています。特別に変な使い方をしていない限り、StandardAdditions.osax は必ずインストールされています。

普段は意識せずに使っているので気がつきませんが、StandardAdditions.osax は AppleScript に足りない機能を追加するかなり重要なものです。AppleScript 自体に定義されている命令なんて activatecopycountgetlaunchrunset ぐらいです(いま delay の説明を見ていたら delay はビルトインコマンドだと書かれていた。なら、StandardAdditions.osax の用語説明にのせるなよ。っていうかいつのまに変わった?)。

たかだかこれだけの命令しかないので StandardAdditions.osax がないとなにもできなかったりします。なんでこのことを長々と書いているかというと、どこで定義されている命令かということが重要なのと、OS X 10.8 以降では use を使って StandardAddition.osax の命令を使うよって宣言が必要になるからです(まぁ、面倒)。

閑話休題。

URL Scheme は基本的にはそのまま開いてしまえば、アプリケーションが起動します。

-- 連絡先.app を起動
open location "addressbook://"
-- カレンダー.app を起動
open location "ical://"
-- 辞書.app を起動
open location "dict://"
-- FaceTime.app を起動
open location "facetime://"
-- App Store.app をセキュアに起動
open location "macappstores://"

しかし、URL Scheme には便利なオプションがあります。例えば、連絡先.app なら登録している人の ID を指定して編集を行う事ができます。

tell application id "com.apple.AddressBook"
    set theList to selection
    if theList is {} then return
    set personID to id of item 1 of theList
end tell
open location "addressbook://" & personID & "?edit"

ほとんど意味のないサンプルですが、URL Scheme を使うとこういうことが出来ます。が、問題なのは URL Scheme に渡すことができるオプションや値の説明がほとんどない、ということです。

マップ.app の URL Scheme は Map Links で説明がありますが、こういう説明はほとんどの場合ありません。なので自分で検索して使い方を見つけるしかないのが現状です。これが、URL Scheme の問題点。

Apple のアプリケーションでさえまともな説明がないのですから、その他のアプリケーションなんかほとんど使い方が分かりません。使えると便利そうなのだけど、使い勝手があまり考慮されていないという...。

自分の環境で使える URL Scheme を調べるには「自分のMacで使えるURLスキームをリスト表示するコマンド - Macの手書き説明書」を参考にしてみてください。

OS X 上での URL Scheme はアプリケーションで設定されているものを使うより、自分で好きな URL Scheme を作ってそれを使うというのが便利な気がします。

この方法を使えば、AppleScript でなんでもかんでも処理できてしまいますね。

open ハンドラを使ってみる

最後に話を戻して...open についてもう一度。

これまで書いてきたように open をそのまま使ってファイルをアプリケーションに渡す...というのは AppleScript ではどちらかというとあまり利用されていない使い方だと思います。

では、どういう使い方をするのかというと open ハンドラを使ってドロップレットを作る...というのがよく見かけるものです。

on open theseItems
    -- ドラッグ & ドロップされた項目を処理します
    repeat with thisItem in theseItems
        -- ここのファイル処理を記述します
    end repeat
end open

これを Script Editor.app でアプリケーションとして保存すると、ドラッグ & ドロップを受け入れるアプリケーション(ドロップレットと呼ばれたりします)になります。

そうなんです。スクリプトはアプリケーションとして保存することができるのです。

on run
    -- ここに起動したときの処理
    display dialog "run イベントが呼び出されました"
end run
on open theseItems
    -- ドラッグ & ドロップされた項目を処理します
    display dialog "open イベントが呼び出されました"
    repeat with thisItem in theseItems
        -- ここのファイル処理を記述します
    end repeat
end open
on reopen
    -- ここに reopen イベントを受け取ったときの処理
    display dialog "reopen イベントが呼び出されました"
end reopen
on quit
    -- ここに終了時の処理
    display dialog "quit イベントが呼び出されました"
    continue quit
end quit
on idle
    -- ここに一定時間ごとに行う処理
    display dialog "idle イベントの定期処理です"
    return 60
end idle

このスクリプトを「実行後、自動的に終了しない」アプリケーションとして保存します。起動させると最初に runidle が呼び出されます。idle はこの後、60 秒後に再度呼び出されます。

Dock にあるアイコンをクリックすると reopen が呼び出されます。Finder からなんらかの項目をドラッグ & ドロップすると open が呼び出されます。終了させると quit が呼び出されます。

完全にアプリケーションですね。で、このスクリプトをみてなにか思いつかないでしょうか?

そうです。先に書いた「基本的な命令を受けつけるアプリケーション」そのものです。実のところ、このサンプルを通しての方が「基本的な命令を受け付けるアプリケーション」っていうものが理解しやすいと思います。それらのアプリケーションはこういう中身をしているんだ、と(ごくごく乱暴にまとめてしまっていますが、動作原理を知る分にはいいでしょう...?)。

起動中のアプリケーションに run を送ったとき、なにをするかはアプリケーション次第、と書きました。run は基本的には起動時に一回処理されるだけなのですが、このサンプルをみるともう一回ダイアログが表示されますね。

open ハンドラがあれば外部からの AppleScript でファイルを開くことができます。reopen ハンドラがあれば、Dock のアプリケーションアイコンのクリックで動作します。で、どんな処理をするかはアプリケーション次第です。

こういう AppleScript アプリケーションを作って外部から操作したりするとより AppleScript の理解ははやまると思います。

では、実際に open ハンドラを使ったサンプルを作ってみます。

Finder でファイルをダブルクリックすると通常ならそのまま設定されているアプリケーションでファイルが開かれます。例えば、拡張子 md の Markdown 書類だと Xcode で開かれたりします(Xcode がインストールされていなるなら)。しかし、Atom で開きたい、もしくは CotEditor で開きたいなんて場合もあります。

Finder の「情報をみる」でファイルを開くアプリケーションを変更することはできますが、それも面倒だったりします。

特定のファイルだったら、全部 CotEditor で開いてしまえ、というものを作りましょう。が、なるべく手間をかけずにやってしまいたい。


これをアプリケーションとして保存します。拡張子 md、html、json、py のファイルをドラッグ & ドロップすることで CotEditor でファイルを開くことができます。これら以外のファイルだった場合は Finder に処理を任せてしまいます。拡張子を追加すれば、他のファイルにも対応できます。

これでいちいちデフォルトのアプリケーションを変更する必要もないですし、Finder のウィンドウのツールバーにでも置いておけば、どこからでも利用できます。また、デフォルトのアプリケーションで開きたいのなら Finder でダブルクリックすれば事足ります。

ファイルを開くってことだけで結構いろいろできるでしょう?

...と、これで話が終わればいいんですが、初心者向きのスクリプトばかりで面白みがないなぁという人(誰だ、それは)向けに。

以下は完全に蛇足です。


本当は最初に書いたスクリプトはこれなんですけどね。分かりにくいだろうし、説明も面倒なことになりそうだし...ということで採用を見送りました。

こっちの方がカスタマイズしやすいし、シンプルなんだけどね(自画自賛)。

あと、通常なら Finder でファイルをダブルクリックして開くとデフォルトのアプリケーションが起動します。

が、これを Finder のダブルクリックで AppleScript で作ったアプリケーションを起動させてしまえば、いいんじゃねと思ったのでそんなこともやってみました。例えば、JPEG ファイルをダブルクリック、AppleScript でリサイズ、Mail.app で開くみたいな感じです。

まぁ、作ってからドロップレットでいいじゃんと反省したのですが、単純に Finder のダブルクリックに反応させるにはどうしたらいいのだろう、と思ったのです。

作り方は Info.plist を編集するだけでできます。

まぁ、本当の蛇足ですね。これは。

AppleScript でアプリケーションの起動と終了

Pseudo TotalTerminal(偽の TotalTerminal)を書いていて思ったのですが、アプリケーションを起動させるってことだけでも
AppleScript ではいろいろ書き方があります。

ちょっと思いついただけでも activatelaunchopenreopenrun...と、さまざま。これらすべて挙動が異なっているのですが、どうも違いが分かりにくい。だからかどうかは分からないのですが、System Events を使ったプロセスの制御を行うスクリプトをよく見かけます。

どうもその手のスクリプトをみていると...修正したくなります。いやいやそんな面倒なことをしなくても...なんて。

たぶん日本語で読める AppleScript の説明が少ないのが一番の問題なのでしょうが。そういうことを踏まえてちょっと AppleScript の基本的なことをまとめてみます。

まずはアプリケーションの制御...起動と終了についてです。


アプリケーションの起動

activate はアプリケーションが起動していないなら起動させ、最前面に表示します。Finder でアプリケーションアイコンをダブルクリックしたときの挙動ですね。

アプリケーション起動の最も一般的な命令です。

-- アプリケーションを必要なら起動し、最前面に表示
tell application id "com.apple.Terminal" to activate
activate application id "com.apple.Terminal"

並べて書いていますが、どちらも効果は同じです(意味はちょっと違うけど、そこを深く追求しだすと分かりにくくなるので)。

activate は表示されているアプリケーションに対して使う命令です。つまり、バックグラウンドアプリケーションに対しては本来使いません(そもそもバックグラウンドアプリケーションは最前面に表示されない)。

launch もアプリケーションを起動させる命令ですが、すでにアプリケーションが起動しているならなにも効果はありません。activate のように最前面にしたりはしません。

また、アプリケーションが起動していないなら起動を行いますが、初期化処理は行いません。Terminal は通常なら、起動時にウィンドウを表示させますが、launch で起動させた場合はウィンドウ表示が行われません。

-- アプリケーションを起動させるが、初期化処理は行わない
tell application id "com.apple.Terminal" to launch
launch application id "com.apple.Terminal"

run 命令でもアプリケーションを起動させます。しかし、アプリケーションは非表示で起動されます。

-- アプリケーションを起動させるが、非表示
tell application id "com.apple.Terminal" to run
run application id "com.apple.Terminal"

すでにアプリケーションが起動している場合、run ではなにが実行されるかアプリケーションの設定により異なります。この辺りが activatelaunch と異なるところです。アプリケーション側が run 命令を受け取った時にこうしなさい、というプログラムが組まれているならそれを行います。

多くの場合はなにも起こりませんが、再度、初期化処理が行われる場合もあります。

open 命令は直接アプリケーションを起動する命令ではありません。

分かりやすくいうと「ファイルをアプリケーションアイコンにドラッグ & ドロップする動作」と同じです。結果としてアプリケーションを起動させてファイルを開かせることになります。

今までの命令と異なるのは引数として開かせる対象のファイルやフォルダが必要になることです。

-- 画像ファイル
set imageFile to (path to pictures folder as text) & "2016.jpg"
-- テキストファイル
set textFile to (path to desktop as text) & "目次.md"
-- フォルダ
set theFolder to path to documents folder
-- 全てをリストでまとめる
set fileList to {imageFile, textFile, theFolder}
-- Finder の open は Finder でファイルをダブルクリックした時と同じ効果
-- 結果的に目的のアプリケーションを起動しファイル表示を行う
tell application id "com.apple.finder" to open fileList
-- 画像ファイルを他のアプリケーションで開きたいならそのアプリケーションで open
-- Safari に先のリストを渡すと...
-- 画像とテキストファイルは開く
tell application "Safari" to open fileList
-- AppleScript に対応していない Day One なら?
-- 起動はするけどなにも行われない
-- ただ、Day One が起動している時に実行すると画像ファイルを添付した新規エントリ作成
tell application "Day One" to open fileList

open 命令は AppleScript で定義されている命令ですが、多くのアプリケーションで利用できます。

もう少し詳しく説明すると、AppleScript からみてアプリケーションは以下の 3 つのタイプに分類できます。

  1. AppleScript に対応している
  2. AppleScript に対応していないが、基本的な命令を受け付ける
  3. AppleScript に全く対応していない

Day One は 2 番の「AppleScript に対応していないが、基本的な命令を受け付ける」アプリケーションです。この手のアプリケーションは多く、AppleScript でちょっとした処理が出来たりします。

最後の reopen もアプリケーションの起動として利用できます。

すでに起動しているアプリケーションの Dock にあるアイコンをクリックした時と同じ挙動を行わせる命令...といえば分かりやすいでしょうか。

run 命令に似ているのですが、reopen の場合必ずなんらかの処理が行われます。多くの場合は起動時と同じ処理...例えば、新規ウィンドウを作る等。Terminal の場合、ウィンドウがない場合は新しいウィンドウを表示します。すでにウィンドウがあるならなにもしません。

まとめましょう。

単純にアプリケーションを最前面に持ってきたいなら activate を使います。

静的にアプリケーションを起動させたいだけなら launch を使います。アプリケーションを静的に起動させ、かつ、起動時の処理も行わせたいなら run と組み合わせます。

起動しているアプリケーションに起動時の処理を再度行わせたいなら reopen を使います。

アプリケーションを起動し、ついでになんらかのファイルを開きたいなら open を使うと効率的です。

注意しなければいけないのは runlaunch を起動しているアプリケーションに使ったときにどういう処理をするかはアプリケーションに依存する...というところでしょうか。実際のところこれら以外の命令であってもどのように処理するかはアプリケーション依存なのだけど。だから AppleScript でアプリケーションを操作する場合はテストが必須です。

配布されているようなスクリプトならなおさら。長いこと AppleScript を使っているけど、いまだに人が書いたスクリプトが読めないことがありますし...(これはこれで問題だ)。

アプリケーションの終了

起動したアプリケーションは終了しなければいけません(そんなことはない)。

終了は起動に比べてシンプルで quit 命令しかありません。この命令は通常の終了であり、強制終了ではありません。

quit application id "com.apple.Terminal"

ただ、アプリケーションによってはファイルの保存が必要になることがあります。ファイルが未保存の場合、多くの場合アプリケーションが保存されていないが本当に終了してもいいか?と問いかけてくることでしょう。

ときどき保存が必要なのにその確認を行わないアプリケーションもあります。こういった命令を利用するときは事前に動作確認をしておく必要があります。

また、保存は行わずに直ちに終了させたい場合があります。そういうときは quit のオプションを使います。

-- 保存されていなくても直ちに終了する
quit application id "com.apple.TextEdit" saving no
-- 保存をしてから終了する
quit application id "com.apple.TextEdit" saving yes
-- 保存するかどうか尋ねる
quit application id "com.apple.TextEdit" saving ask

状況によってこれらのオプションを使い分けます。

この命令は通常なら終了できないようなアプリケーションでも終了させることができます。分かりやすいのが Finder でしょうか。Finder は通常なら終了メニューがないので終了できませんが、quit で終了させることができます。

-- Finder の再起動
quit application id "com.apple.finder"
-- 終了処理が終わらないうちに起動命令が伝わるのでちょっと待つ
delay 1
activate application id "com.apple.finder"

Finder なら別にいいのですが、logwinwindow や SystemUIServer などという背後で動いている重要なアプリケーションでも quit を送ることができます。おそらくそれらが終了することはないのですが、なにが起きるかは分からないのでやめておきましょう。

quit 命令でよくあるのがアプリケーションの一括終了。


コメントはいつもより多めにしています。

AppleScript に対応していないアプリケーションについて

アプリケーションの起動と終了に関して見てきましたが、以上の命令は特別に AppleScript に対応していなくても使えるものです。

先にも書きましたが「AppleScript に対応していないが、基本的な命令を受け付けるアプリケーション」は AppleScript から操作できます。

では、基本的な命令とはなんでしょうか?

  1. アプリケーションを開く
  2. 書類を開く
  3. 書類を印刷する
  4. アプリケーションを終了する

これらの処理です。では、これらの処理ができるのかどうかをどうやって調べるのかというと System Events を使います。

System Events というのは OS やシステムに関する設定などを行ってくれるバックグラウンドアプリケーションです。このアプリケーションは process というクラスを持っていて、これを使うとプロセスに関する情報を調べることができます。

tell application id "com.apple.SystemEvents"
    -- Terminal が起動していないなら終了
    if not (process "Terminal" exists) then return
    tell process "Terminal"
        -- バンドル ID
        bundle identifier
        --> "com.apple.Terminal"
        -- 表示されているか隠されているか?
        visible
        --> true
        -- 最前面かどうか?
        frontmost
        --> false
        -- Unix でのプロセス番号
        unix id
        --> 16448
        -- kill してみる
        do shell script "kill 16448"
    end tell
end tell

AppleScript に関連する情報は process クラスの has scripting terminologyaccepts high level events 属性を調べます。AppleScript に対応しているなら has scripting terminology が真になり、対応していなくても accepts high level events が真なら基本的な命令を受け付けることができます。

tell application id "com.apple.SystemEvents"
    -- Terminal が起動していないなら終了
    if not (process "Terminal" exists) then return
    tell process "Terminal"
        -- 基本的な命令を受け付けるか?
        accepts high level events
        --> true
        -- AppleScript 対応か?
        has scripting terminology
        --> true
    end tell
end tell

accepts high level events が真なら activatequit といった命令は受け取れます(...と教科書通りにいけばいいのですが、実際はそうじゃないこともあります。結局、最終的には手作業で調べなきゃいけないはめになる。それが AppleScript)。

System Events にはこの他に UI Element というクラスが定義されていて、(個人的には)最終手段ともいえるプロセス自身の泥臭い操作もできます。

activateopenquit が使えればなんとかなる...と個人的には思っているのですが、世の中の大半の人はそうではないようで、この UI Element を使ってナニか面倒なことをよくやっている人が多いです。もっと楽をしようよ。

AppleScript を使う場合、そのアプリケーションに精通している必要があります(そもそもそのアプリケーションでなにができるが分からないのに操作したいなんてナンセンス)。AppleScript を使うまでもなく、アプリケーション自体に自動化できる装置がついていたりすることもあります。

Day One のようなアプリケーションは AppleScript に対応していませんが、シェルで操作できるツールが提供されていたりします(ところで、Day One 2 が出るんですね。AppleScript 対応...とかないかな)。このツールでできるのは新規作成だけですが、Day One はユーザーの Library フォルダに記事をファイルで保存しています。これは SQLite で SQLite ならシェルで操作できます。

また、有名どころの Web サービスなんかだと API が提供されていたりします。do shell script で curl 使えばなんとかなるでしょう。

Mac 版の Kindle だと AppleScript に対応していないし、そもそも API もない。それでも URL Scheme があったりするので特定のページや書籍を開くことはできます。

こういうふうに方法はいろいろあります。もちろん状況次第ですが、なるべくなら UI Elements の利用は控えておく方がいいです。すぐに使えなくなるから。

tell application id "com.apple.SystemEvents"
    tell process "Terminal"
        -- 「ターミナル」メニュー
        tell menu bar item 2 of menu bar 1
            -- 「環境設定」を表示
            click menu item 3 of menu 1
        end tell
    end tell
end tell

メンテナンスが大変なスクリプトの一例。こういうのやウィンドウのボタンをクリックするのとか。こういうのはメニューやボタンがその場にないと動かなくなるから。で、メニューやボタンなんてバージョンアップでわりと簡単に移動するから。そもそもコメントがないとなにをやっているか分からない。

最後の方はアプリケーションの起動や終了と関係ない話でしたね。といってもこのメニューを操作してアプリケーションを終了させるなんてスクリプトをわりと見かけたりします。もう、みんな大丈夫だよね。そう、こういうときは

quit を使え

最低でも kill にしておきましょう。

Pseudo TotalTerminal(偽の TotalTerminal)

今年に入ってやっと El Capitan にアップグレードしました。今までは新しい OS は必ずクリーンインストールしていたんですが、今回は Yosemite から直接 El Capitan に。不具合が出るかどうか分かりませんが、今のところ順調。

El Capitan といえば、ちょっと前に TotalTerminal が動かない、という話題を拝見しました。

最終的には Alfred のワークフローで問題は解決したようなのですが、問題としてオモシロイと思ったので作ってみました。


TotalTerminal の挙動をすべて再現できるわけではないのですが、以下の部分を作ってみます。

ひとまずこのスクリプトを実行することで、

  • Terminalが動いていなければ起動して最前面に
  • Terminalが裏にいるなら最前面に
  • Terminalが最前面にいるなら裏に回す

ことはできるようになった。


El CapitanでTotalTerminalが動かないならAppleScriptで代用すればいいじゃない? - TOKOROM BLOG

アプリケーションの状態を調べる方法はいろいろありますが、起動しているかどうかを確認したいなら、AppleScript が定義している application クラスの running 属性を調べてみればいい。

running of application id "com.apple.Terminal"
--> false

結果は真偽値で返ってきます。true なら起動中。これで最初の「Terminal が動いていなければ起動して最前面に」は以下のようにかけます。

if not (running of application id "com.apple.Terminal") then
    -- 起動していないなら起動して最前面に
    activate application id "com.apple.Terminal"
end if

次の「Terminal が裏にいるなら最前面に」も application クラスの frontmost 属性を使って調べることができます。

if not (frontmost of application id "com.apple.Terminal") then
    -- 最前面にいないなら前面に持ってくる
    activate application id "com.apple.Terminal"
end if

frontmosttrue だと最前面にいるということなので最後の「Terminal が最前面にいるなら裏に回す」は次のようになります。

tell application "Finder" to set visible of process "com.apple.Terminal" to false

ここだけ application クラスではどうにもできないので Finder で処理しています。全部を組み合わせると次のようになります。


以上をスクリプトファイルとして保存すれば完成。これでも一応動作するのですが、課題も書かれています。

現在の課題としましては、

  • Terminalが起動しているがウィンドウがないときはウィンドウがないままアクティブになる(困ってはないけど勝手にウィンドウ作ってくれればスマート)
  • かっこよくない(冗長かもだが、TotalTerminalみたいにかっこよく動くとより良い)
  • プロセスの名前が常に Terminal でよいのかがよくわからん
  • Terminalが既にカレントのときにCommand+Tabキーを送って裏に回してるけど、もっとスマートな方法あるんじゃない?

などがありそうです。


El CapitanでTotalTerminalが動かないならAppleScriptで代用すればいいじゃない? - TOKOROM BLOG

いくつかは解決できるけど、面倒なものもありますね。

Terminal が最前面に来た時にウィンドウがなければ新しく作るというのは reopen 命令を使えば解決します。

-- 最前面にいないなら前面に持ってくる
activate application id appID
-- ウィンドウがないなら新しく作る
reopen application id appID

次の「かっこよくない」というのは...ウィンドウを上からスライドして表示、非表示のことでしょうね。できなくもないのですが...ちょっと手間が必要ですね。また、いつかということで(結局、やらない)。

「プロセスの名前が常に Terminal でよいのかがよくわからん」というのは、確かに AppleScript ではいつでも問題です。

ちょっと前まではアプリケーション名がバージョンアップにより変わりましたなんてことはよくあり、その度にスクリプトの修正を繰り返していました。

それはさすがに面倒なのでアプリケーションのバンドル ID(bundle identifier)から名前を取得するようにしています。この ID が変更になることはそうそうないので...。ちなみにこの ID を書き換えるだけで他のアプリケーションにも対応できます(あら、不思議)。

最後の Command+Tab キーでの Terminal の非表示はプロセスの属性 visible で変更できるのでそれを用いています。

最後にショートカット割り当てですが ApptivateBetterTouchTool(有料になりましたね)、Karabiner などがあります。

もちろん AppleScript を実行できるランチャーならなんでもいいのですが、個人的には割当が簡単な BetterTouchTool を使っています。


初期設定ファイルが初期化されて困る

ちょっと前からおかしかったんですよね。

Script Editor の環境設定でフォーマットを設定しても、いつの間にか初期設定に戻ってしまう。Xcode と Script Editor を使っているとそういうことがちょくちょく起きることがあった。

Script_Editor_Format_Prefs

なので、自分好みの設定をした初期設定ファイルを保管しておき、不具合があればファイルを差し替えるというスクリプトを使っていました。不便は不便だけど、すぐに自分の設定を再現できるので重宝していました。

ところが、初期設定を置き換えても設定が反映されないという不具合が起きるようになりました。

いつの頃からか初期設定の扱いが変わっていたのですね。知りませんでした。もう、てっきり Script Editor のバグだと思っていました。

OS X 10.8?それとも 10.9 辺りから初期設定がキャッシュされるようになり、設定ファイルの変更や削除を行ってもキャッシュされている設定で上書きしてしまうのですね。

セキュリティ?安全性のためなのでしょうが、アプリケーションを作成している時に初期設定をクリアしても新しい設定が反映されないのでちょっと迷惑。

設定のキャッシュは cfprefsd というプロセスが行っているようで、バックグラウンドでずっと起動しています。こいつが動いている限り、設定ファイルを置き換えても元に戻ってしまうようです。

好みの初期設定ファイルで置き換えるには次のような手順でできます。

  1. アプリケーションを一旦終了する
  2. 現在の初期設定ファイルを削除
  3. 保管している初期設定ファイルを ~/Library/Preferences に
  4. cfprefsd を kill する

cfprefsd は kill しても勝手に再起動します。これで自分好みの設定をアプリケーションに反映できます。これらの手順を AppleScript でまとめます。

このような感じですね。仕組みが分かれば、あとは応用ですね。defaults コマンドの初期設定一括削除は覚えておくと便利です。

初期設定を削除したり管理者権限を要求したりしているので、ご利用はご自身の責任で。

結局、Script Editor の初期設定が初期化されてしまうのはナゼだかわからないので根本的な解決になっていません。なにが原因か分からないんですよね...。

Xcode 6 でファイルテンプレートを自作する

Xcode 3 までは、AppleScriptObjC にもファイルテンプレートがあったんですよね...。

Xcode 3 ぐらいまでなら Xcode のカスタマイズの知識もあったのに、今ではその知識も通用しない。Xcode 6 の使い勝手を良くするために日夜努力をしています(Xcode 3 を久しぶりに起動したら日本語化されていてちょっと感動しました)。

では、 Xcode 6 にファイルテンプレートを追加するための方法を。

追加するのは AppleScriptObjC のテンプレートですが、方法や考え方は他の言語のテンプレートを作るときでも同じです。プログラミング言語用と限定すると使い勝手が悪いですが、書式の決まったテキストファイルの生成などにも応用できます。

Swift でも Objective-C でも ReadMe でも BSD ライセンスでも請求書でもなんでもござれ。

では、本題。テンプレートの作り方自体は、Xcode 4 の頃から変わりはありません。

Xcode に最初から入っているテンプレートは Xcode 内にあります。

/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/

一方、自分で追加するテンプレートは次の場所になります。

~/Library/Developer/Xcode/Templates/

テンプレートは 2 種類あります。プロジェクトテンプレートとファイルテンプレートです。取り上げるのはファイルテンプレートです。ファイルテンプレートは Templates に File Templates をいうフォルダを作り、そこにテンプレートを配置します。

~/Library/Developer/Xcode/Templates/File Templates/

さらに File Templates/ 以下にフォルダを作ると、Xcode の新規テンプレート作成ウィンドウでカテゴリとして表示されます。例えば、ASOC というフォルダを作ると Xcode でも ASOC というカテゴリが追加されます。

Xcode_File_Template_Categories

以上、テンプレートの保存場所と分類方法です。次にテンプレートの構成要素について。

Xcode のテンプレートは必要なファイルをまとめたバンドル形式になっています。最低でも必要なファイルは以下の通りです。

  • ___FILEBASENAME___.applescript
    • テンプレートの雛形。拡張子が applescript になっているけど、swift や m など、目的に応じたもので可
  • TemplateIcon.png
    • Xcode 上で表示されるテンプレートのアイコン(48 x 48)
  • TemplateInfo.plist
    • テンプレートの詳細を記した plist ファイル

これらのファイルを拡張子 xctemplate を持ったバンドル(実際はただのフォルダ)にまとめ、Templates/ に置くと Xcode でテンプレートとして認識されるようになります(テンプレートの雛形も plist ファイルも中身が空でもいい)。

ここまでで Templates/ は次のようになっています。

Templates_Directory

実際に Xcode で新規ファイルを作成すると、テンプレート選択画面に表示されます。テンプレートの構成要素は以上です。

最後に一番大事な TemplateInfo.plist について。このファイルに記述されている内容に従って Xcode はテンプレート選択画面にオプションを追加したり、テンプレートに記述されている変数を展開していきます。

テンプレートに記述されている変数というのは、アンダースコア 3 つで囲まれたアルファベット大文字の文字列のことです。主な変数は次の通り。

  • ___FILENAME___
    • ファイル名(拡張子を含む)
  • ___FILEBASENAMEASIDENTIFIER___
    • C 言語のスタイルに変換したファイル名(拡張子を含まない)
  • ___PROJECTNAME___
    • プロジェクト名
  • ___PROJECTNAMEASIDENTIFIER___
    • C 言語のスタイルに変換したプロジェクト名
  • ___USERNAME___
    • ユーザーアカウントの名前(短縮)
  • ___FULLUSERNAME___
    • ユーザーアカウントのフルネーム
  • ___ORGANIZATIONNAME___
    • プロジェクトで設定した組織名
  • ___DATE___
    • 今日の日付
  • ___TIME___
    • 現在時間
  • ___YEAR___
    • 年を表す 4 桁の数字
  • ___COPYRIGHT___
    • コピーライト文字列
  • ___FILEEXTENSION___
    • ファイル拡張子

では、実際にテンプレートファイルに上記の変数を追加してみましょう。___FILEBASENAME___.applescript というファイルをテキストエディタで作成し以下のように記述。

--
--  ___FILENAME___
--  ___PROJECTNAME___
--
--  Created by ___FULLUSERNAME___ on ___DATE___.
--___COPYRIGHT___
--

script ___FILEBASENAMEASIDENTIFIER___
    property parent : class "NSObject"
end script

ここ、重要なのですが、このテンプレートファイルはテキストエディタで作成してください。決して Script Editor で作ってはいけません。Script Editor で作成し、保存するとファイルのエンコーディングが us-ascii になります。このエンコーディングだと Xcode で利用できなくなります。

テンプレートファイルは UTF-8、改行コード LF で保存してください。

次に TemplateInfo.plist を編集します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>DefaultCompletionName</key>
    <string>Empty AppleScript File</string>
    <key>Description</key>
    <string>An empty AppleScript File</string>
    <key>Kind</key>
    <string>Xcode.IDEKit.TextSubstitutionFileTemplateKind</string>
    <key>MainTemplateFile</key>
    <string>___FILEBASENAME___.applescript</string>
    <key>Name</key>
    <string>Empty AppleScript File</string>
    <key>SortOrder</key>
    <integer>1</integer>
    <key>Summary</key>
    <string>An empty AppleScript File</string>
    <key>Platforms</key>
    <array>
        <string>com.apple.platform.macosx</string>
    </array>
</dict>
</plist>

それぞれのキーは適切な値で設定され、意味は以下の通りです。

  • DefaultCompletionName(文字列)
    • ファイル保存時のデフォルトのファイル名
  • Description(文字列)
    • テンプレートの説明
  • Kind(文字列)
    • テンプレートの種類。現在、Xcode.IDEKit.TextSubstitutionFileTemplateKind しかない
  • MainTemplateFile(文字列)
    • テンプレートとして利用されるファイル名(拡張子含む)
  • Name(文字列)
    • テンプレート選択画面に表示される名前
  • SortOrder(整数値)
    • テンプレート選択画面に表示される順番
  • Summary(文字列)
    • テンプレート選択画面に表示されるテンプレートの説明
  • Platforms(配列)
    • 利用される OS。com.apple.platform.macosx なら、OS X。com.apple.platform.iphoneos なら iOS。指定がない場合はテンプレート選択画面の iOS、OS X の両方に表示される

これらを記述し、Xcode で新規ファイルを選択すると次のように表示されます。

Template_Dialog_Keys

テンプレートを選択し、進んでいくと保存できます。

Save_Dialog

以上でシンプルなテンプレートなら作れると思います。次に Xcode でオプションを入力するファイルテンプレートの作り方。

Xcode で Cocoa Class といったファイルテンプレートを選択すると、次のような画面が表示されます。

Cocoa_Source_Template_Options

新規ファイルを作成するときのオプションですね。これらのオプションは TemplateInfo.plist に Options キーとして記述することで追加できます。

Options で利用される主なキーは次の通りです。

  • Default(文字列)
    • UI のデフォルト値
  • Description(文字列)
    • UI の説明(ツールチップとして表示される)
  • Identifier(文字列)
    • UI の識別子。この識別子を使ってテンプレート内の変数を展開する
  • Name(文字列)
    • UI のラベル
  • Required(真偽値)
    • UI が必要かどうか
  • Type(文字列)
    • UI のタイプ。statictextcombopopupclassbuildSetting 等がある
  • NotPersisted(真偽値)
    • 入力値が今後も利用されるかどうか
  • Values(配列)
    • Type で combopopupclass を指定した時に表示される値
  • RequiredOptions(配列)
    • 他の入力値に対応して使用するか否かを選択する

これら以外にもキーはありますが、割愛。

例えば、Options の Type に text と記述しておくとテキスト入力欄が追加されます。TemplateInfo.plist に以下を追加します。

<key>Options</key>
<array>
    <dict>
        <key>Description</key>
        <string>Description of input value</string>
        <key>Default</key>
        <string>MyClass</string>
        <key>Identifier</key>
        <string>productName</string>
        <key>Name</key>
        <string>Class:</string>
        <key>NotPersisted</key>
        <true/>
        <key>Required</key>
        <true/>
        <key>Type</key>
        <string>text</string>
    </dict>
</array>

保存し、Xcode で新規ファイルを選択し、進んでいくと次のような画面が表示されます。

ASOC_Custom_Template_Options

今までは最後のファイル保存でファイル名をつける必要がありましたが、今後はここで入力した値がファイル名として利用されるようになります。

もう少し複雑なオプションをつけてみます。現在は NSObject を継承した単純なスクリプトを作るだけですが、今度は NSView を継承したスクリプトも選べるようにします。この方法を理解するために Xcode で新規の Cocoa クラスを作ってみます。

Xcode で新規ファイルを選択し、Source の Cocoa Class を選び、次に進みます。これらのオプションにはそれぞれ Identifier が TemplateInfo.plist で設定されています。

Cocoa_Source_Template_Options_Flow

Xcode はここで入力された値をつなぎ、その文字列をもとにテンプレートを選びます。この例なら NSViewControllerXIBSwift にある ___FILEBASENAME___.swift と ___FILEBASENAME___.xib が選ばれます。

Cocoa_Template_Directory

オプションから利用されるテンプレートが入っているフォルダを探し、そのフォルダ内のテンプレートファイルを開く...このような感じですね。

では、実際にやってみましょう。

TemplateInfo.plist の Options を編集します。

<dict>
    <key>Default</key>
    <string>NSObject</string>
    <key>Description</key>
    <string>What class to subclass in the new file</string>
    <key>Identifier</key>
    <string>cocoaSubClass</string>
    <key>Name</key>
    <string>Subclass of:</string>
    <key>Required</key>
    <true/>
    <key>Type</key>
    <string>popup</string>
    <key>Values</key>
    <array>
        <string>NSObject</string>
        <string>NSView</string>
    </array>
</dict>

これでポップアップメニューから NSObject か NSView を選択できるようになります。NSObject を選択すると NSObject というフォルダ内にある ___FILEBASENAME___.applescript を使ってファイルを作成します。

ASOC_Custom_Templates

以上をまとめたものを GitHub で公開しています。

触れていない部分も多々あったりします。Options の Identifier を使ったテンプレートの変数展開とか Options の RequiredOptions キーのこととか...。

Options で Identifier に fileName などと設定します。テンプレートファイル内に ___VARIABLE_fileName___ と記述しておけば、入力された値に置き換えられます。

RequiredOptions キーは、あるオプションが選択されたら利用できるようになるオプションです。NSWindowController が選択されたら、XIB も同時に作るチェックボックスが利用可能になる...といった感じです。

これらのオプションを利用すれば、そこそこ凝ったテンプレートも作れるのではないでしょうか。駆け足でしたが、Xcode 6 でカスタムテンプレートを作る方法でした。