Archive for 8月, 2013

2013/08/31 AppleScriptの「ジョーシキ」 2013年夏版

AppleScript世界の常識は世間の非常識でもあり、なかなか相互理解は難しいものだと感じます。

「AppleScriptで○○をやりたい」という話があったとしても、「それって、そもそもAppleScriptでできないしー」とか「その○○についてはオレ興味持ってないしー」と、ストレートに言えればよいのですが……なかなか、そうも行かないケースが多々あります(いろんな技術の合わせ技で乗り切れるケースも多し)。

そこで、まずはAppleScript世界のジョーシキなるものをリストアップして、いったん客観的なところから再確認してみようじゃないか、というのが本記事の主旨です。

常識0:GUIベースのアプリケーションの一括処理のための、生産性向上のための道具

AppleScriptは、複数のGUIベースのアプリケーションを組み合わせた一括処理(バッチ処理)を記述するための逐次実行型のインタプリタ式言語です。アプリケーションとしても保存でき、その場合にはランタイムアプリケーション内に暗号化された(編集不可な)AppleScriptが格納されます(編集可能な状態でもアプリケーション保存は可能です)。

ショートカットキーから1命令だけ呼び出すような操作ではなく、メニューから呼び出して複雑な処理を数百ファイルにもわたって処理したり、ドロップレットを記述してファイルのドラッグドロップ処理を行うようなものです。

当初は、「わかりやすい」ことに重点が置かれ、短いプログラムを書くことが奨励されてきましたが、やはり短いプログラムではできることもかぎられています。現場のニーズ「より高速に」「より高機能」を満たすべく、AppleScriptで書くプログラムも大規模化してきました。

見た目や規模がかわっても、ユーザーの生産性を向上させるための「道具」として存在・認知されてきました。プログラマーよりもより現場に愛される道具として広く海外や日本の一部業種を中心に利用されてきました。

近年のMacユーザー層の拡大にともない、「一部業種」から広い分野へと活躍の場を移しつつある、というのが昨今の動向です。

常識1:すべてのアプリケーションをコントロールできるものではない

まず、すべてのアプリケーションは、起動と終了ぐらいはAppleScriptからコントロールできます。AppleScript非対応の「プレビュー.app」については、起動や終了は可能ですが、その他のファイルを開くとかコマンドを呼び出すといったことはできません(いまのところは)。

AppleScriptからコントロールできるアプリケーションは、基本的にMac用の「AppleScriptに対応しているアプリケーション」だけです。これを、どのように確認するかといえば、アプリケーションのアイコンをAppleScriptエディタのアイコンにドラッグ&ドロップするだけです。

prev1.png
prev2.png

AppleScript対応しているアプリケーションであれば、AppleScriptから利用可能な命令やオブジェクトの一覧「AppleScript用語辞書」が表示されます。AppleScriptからコントロール可能なアプリケーションを「スクリプタブルな」アプリケーション、と呼びます。

もしくは、AppleScriptエディタの「ウィンドウ」メニューから「ライブラリ」を選択すると、対応アプリケーションの一覧を見られます。

aslib.png

ただし、アプリケーションによってはダミーのAppleScript用語辞書を入れた「えせスクリプタブル」(実際にはコントロールできない)なものも存在します。この「えせスクリプタブル」の代表的な例は、AppleのiBooks Authorです(Apple純正アプリはよくこの手を使って、初版では対応したように見せかけておいて、後でバージョンアップ時に対応します)。

ibks.png

常識2:対応アプリのすべての機能をコントロールできるものでもない

めでたく、AppleScript対応アプリケーションにたどり着いたとします。

そのアプリはさまざまな機能を持っていたとして、AppleScriptから利用できる(コントロールできる)のはAppleScript用語辞書に書かれたコマンド/属性だけです。全部ではありません。

たとえば、SafariはAppleScript対応アプリですが、「iCouldタブ」といった機能はAppleScriptからは利用できません。アプリケーションがコントロールできるのは、AppleScript用語辞書に書かれた内容だけだからです。

ictab.png

アプリケーションのAppleScript対応はアプリケーション開発者にその内容や方向性はゆだねられています。命令語についても、とくに「この単語を使え」というものがあるわけでもありません。

開発者がどうしてもこの機能はScriptから使えるようにしたいとか、エンドユーザーから機能追加を求められても面倒なので勝手にやってもらうためにScript対応しておくとか、そういう感じです。

常識3:非対応機能、非対応アプリのコントロールを可能にする「救済手段」としてGUI Scriptingが追加された

「できないことはできません」とも言っていられないので、通常のAppleScriptからのコントロール手段とは別の方法が提供されることになりました。

通常、AppleScriptはアプリケーションの内部からコントロールするのですが、それとは別に、「外側」からコントロールする手段「GUI Scripting」がOSの標準機能として(Mac OS X10.3あたりから)提供されるようになりました。

GUI Scriptingの登場当初、アプリケーションの種類によってコントロールしやすいものとしにくいものが見られました。Carbonベースのアプリケーションはコントロールしにくく、Cocoaベースのアプリケーションはコントロールできる場合が多かったのです。このあたりは、OSのバージョンアップ(とくに、Mac OSX 10.5以降)によってGUI Scriptingが強化されたことと、Mac OS X 10.4以降でMacのIntel化が進んだことに伴ってCocoaベースのアプリケーションが増えた(主にCocoaベース/Xcode上でしか開発不可能)ことでやりやすくなってきました。

とはいえ、GUI Scriptingからコントロールできるのは、その実体であるSystem Eventsの「Process Suite」に名前が定義されているオブジェクトだけです。

また、GUI Scriptingにおいて何らかのGUI部品に対して送信できるコマンドは、クリックするか選択するかぐらいのものです。たとえば、ドラッグ&ドロップという操作はできませんし、複数選択とかいう凝った操作もできません。

gui.png

常識4:必要に応じてshell scriptなどの機能も積極的に活用

AppleScriptとアプリケーションだけでは実現できないものも、UNIXベースのOSであるMac OS Xだからこそ、shell scriptやshell commandを呼び出していろいろと解決できるケースが多々あります。

簡単で確実な方法が別に(shellコマンドなどで)あれば、利用できるものはなんでも利用するというのが正しい態度だと思います。

ただし、同じOSバージョンの環境であれば同様に動作が再現できることが条件です。別途ビルドする必要があるものであれば、AppleScriptのバンドル内に格納して一緒に配布できるとよいでしょう。

常識5:Cocoaの機能を直接呼び出すAppleScriptObjCも使える

アプリケーションではなく、APIそのものであるCocoaの機能をAppleScriptから利用できるようになってきました。

Cocoaの機能をAppleScriptから直接呼び出して、従来ではできなかった処理や、GUIベースのアプリケーションをXcode上で開発できるようにしたものです(Mac OS X10.6以前はAppleScript Studioという機能でGUIベースのアプリ開発を行っていました)。

ただし、これは「アプリの機能をコントロールするもの」ではなく、アプリをアテにせずに自前の機能を作るものなので、AppleScriptObjCが利用できるようになったことで、アプリケーションの操作範囲が増えたりするものではありません。

「できること」の組み合わせで処理を構築

できないことばかりをリストアップしてしまいましたが、それでもなお、GUIベースのアプリケーションの自動化において、AppleScriptは他の言語よりも圧倒的に有用なものであると感じています。

まず、複数のアプリケーションの連携を記述できること。さまざまなアプリケーションと連携して動作するような記述は、VBAやJavaScriptなどでは記述しづらいものです。

また、OSの機能を引きずり出してきたり、プロセス管理の機能を利用できることも大きいです。操作対象となるアプリケーションがクラッシュしてしまった場合でも、そのクラッシュそのものを検出して作業を継続することも可能です。実際に、クラッシュしやすいAdobe InDesignを相手に夜通し作業を行うようなバッチをよく書いていました。もちろん、クラッシュした時点のデータは「追試処理」で再実行するようにしていました。

AppleScriptだけで自力でさまざまな機能を実現するための、ライブラリ構築時代へ

AppleScriptだけである程度の機能を記述できるようになってきており、その経験の積み重ねによって「アプリケーションをあてにしない処理」というのも増えてきました。

たとえば、カレンダー.app(旧iCal)は予定を管理するアプリケーションですが、内部で有益な機能を持っています。でも、用語辞書に書かれていないかぎり、AppleScriptから利用することはできません。

そこで、AppleScriptだけでカレンダー計算を行うとか、祝日の計算を行うといった処理をAppleScriptで記述し、カレンダーのアプリケーションがなくてもとくに問題はなくなってきましたし、AppleScriptObjCでGUIを作成することも可能です。

方向で、AppleScriptで組んだ部品をAppleScriptのユーザー同士でWebに掲載したりして大量にやりとりするようになってきました。本Blogもそうした場のうちのひとつです。

2013/08/28 Recording(記録)機能は現在「なかったこと」に

AppleScriptに対する「誤解」の1つに、「ユーザーの操作を記録できないじゃないか」というものがあります。

もう何年もクリックしたことのないボタンが、AppleScript Editor上に、たしかにありますね。「記録」と書いてあるような気がしますが、気のせいでしょう。

rec2.png

一応、Finderはギリギリ「記録」(Recording)に対応しているようですが、Appleからガイドラインが開発者に向けて示されており、「Recordingに対応する必要はない」ということになっています。逆に、いまだに(OS X10.8で)FinderにRecordingが残っているのを知って腰を抜かしたぐらいです。

Finder以外でAppleScriptのRecordingに対応しているアプリケーションは…………ちょっと思い当たらないのですが(TextWranglerが対応しているそうです)、とにかくRecordingは「ないもの」「なかったもの」と見なすのが正しい認識です。

Recording機能の廃止(まだ廃止されてないけど)にあたって、Appleからの説明があり、

「(開発者が)対応するのが大変な割に、Recordingした内容を見ても(逆に難解な記述になるため)初心者の理解の助けに全然ならない。実行環境が異なると動作が再現しなかったりで、使い回せない。」

といった話をされたように覚えています(ものすごい昔の話なのでうるおぼえ)。

Automatorでも操作記録はできるようですが、アレはクリック座標やキーストロークを記録しているだけで……画面サイズや、ウィンドウの位置などもろもろの条件が合わなければ動作内容は再現できないもので、配布しても無意味です(そういう人を見かけたことがあります)。

では、AppleScriptはプログラマー用の言語かといえば……C++とかObjetive-Cとか、記号の塊でもっと読みにくいと思うのですが……逆に読める分だけ「こうなっているに違いない」という思い込みを誘発し、それが違ったときに逆ギレを生むようです。

「操作した内容をそのまま記録」することは、やってもあまり意味がないという結論が見えていますが、逆に「操作するようにプログラミングする」ことは不可能ではありません。

メニューの操作やボタンのクリック、キー入力などをAppleScriptで記述する「GUI Scripting」(UI Element Scripting)というのが、それに該当します。

 GUI Scriptingによる記述(1)
 GUI Scriptingによる記述(2)
 GUI Scriptingによる記述(3)
 GUI Scriptingによる記述(4)
 Preview.appでプリンタと用紙サイズを指定して印刷
 文字入力モードを制御

ただ……これが「やさしい(Easy)」かどうかは意見の分かれるところで……ボタンを押すためには、ボタンが存在するかどうか調べる必要があり、さらにボタンが存在していたとしても、押せる状態かどうかあらかじめ調べる必要があり……そういう意味では、「ちゃんと動くAppleScript」を書くのにGUI Scriptingを利用することは、「楽」だとは思いません。

自分としては、「すでに動作確認がとれているルーチンをコピペしてきて、呼び出すのが一番簡単」だと思っていますが……間もなく、いろいろと状況が変わってくることでしょう。使い回しが、もっと簡単になってくると思います(数ヶ月以内に)。

2013/08/27 アプリケーションフォルダ以下のアプリケーションのローカライズ数をカウントする

バンドルパッケージ内のフォルダ/ファイルを普通にAppleScriptのaliasで扱えることを確認するための実験です。

アプリケーションフォルダへのパスを求め、その下にある「アプリケーション」をspotlightですべてリストアップ。求めたアプリケーションの中の「Contents:Resources:」フォルダ内にある「なんちゃら.lproj」フォルダの数を数えて、さらにそれらのフォルダ名の一覧を作成。フォルダ数の多いものから降順ソートを行っています。

Mac OS X自体のローカライズ言語数が30(OS X 10.8.4上にて)なのに、なぜかローカライズ言語数が92もあるVLCとか、53もあるGoogle Chromeは何をやっているのか興味があるところです(そもそも、判別できるのやら)。

ちなみに、このScriptを作ること自体が目的ではなく、最終的に作りたい「別のモノ」の練習のために作ったものなので「mdlsコマンドでローカライズ言語数が分るよ」とかいう指摘はしないでください。

スクリプト名:アプリケーションフォルダ以下のアプリケーションのローカライズ数をカウントする
set outList to {}

set startDir to path to applications folder
set bList to getFileListWithSpotLight(“kMDItemKind”, “アプリケーション”, startDir) of me –日本語環境下でのみ有効

repeat with i in bList
  tell application “Finder”
    set appName to name of i
  end tell
  
  
set appPath to i as string
  
set resPath to appPath & “Contents:Resources:”
  
tell application “Finder”
    tell folder resPath
      try
        set fList to (every folder whose name ends with “.lproj”) as alias list
        
set fnList to (name of every folder whose name ends with “.lproj”) as alias list
        
set fn2List to shellSortAscending(fnList) of me
        
        
set the end of outList to {appName, length of fList, fn2List}
      on error
        set the end of outList to {appName, 1, {“English.lproj”}}
      end try
    end tell
  end tell
  
end repeat

set cList to shellSortListDescending(outList, 2) of me

–> {{”VLC.app”, 92, {”ach.lproj”, “af.lproj”, “am.lproj”, “an.lproj”, “ar.lproj”, “ast.lproj”, “az.lproj”, “be.lproj”, “bg.lproj”, “bn_IN.lproj”, “bn.lproj”, “br.lproj”, “ca.lproj”, “cgg.lproj”, “ckb.lproj”, “co.lproj”, “cs.lproj”, “cy.lproj”, “da.lproj”, “de.lproj”, “el.lproj”, “en_GB.lproj”, “English.lproj”, “es.lproj”, “et.lproj”, “eu.lproj”, “fa.lproj”, “ff.lproj”, “fi.lproj”, “fr.lproj”, “fur.lproj”, “ga.lproj”, “gd.lproj”, “gl.lproj”, “gu.lproj”, “he.lproj”, “hi.lproj”, “hr.lproj”, “hu.lproj”, “hy.lproj”, “ia.lproj”, “id.lproj”, “is.lproj”, “it.lproj”, “ja.lproj”, “ka.lproj”, “kk.lproj”, “km.lproj”, “kmr.lproj”, “ko.lproj”, “ky.lproj”, “lg.lproj”, “lt.lproj”, “lv.lproj”, “mk.lproj”, “ml.lproj”, “mn.lproj”, “mr.lproj”, “ms.lproj”, “my.lproj”, “nb.lproj”, “ne.lproj”, “nl.lproj”, “nn.lproj”, “oc.lproj”, “or_IN.lproj”, “pa.lproj”, “pl.lproj”, “ps.lproj”, “pt_BR.lproj”, “pt_PT.lproj”, “ro.lproj”, “ru.lproj”, “si.lproj”, “sk.lproj”, “sl.lproj”, “sq.lproj”, “sr.lproj”, “sv.lproj”, “ta.lproj”, “te.lproj”, “tet.lproj”, “th.lproj”, “tl.lproj”, “tr.lproj”, “uk.lproj”, “uz.lproj”, “vi.lproj”, “wa.lproj”, “zh_CN.lproj”, “zh_TW.lproj”, “zu.lproj”}}, {”Adobe Application Manager.app”, 61, {”ar_AE.lproj”, “ar.lproj”, “cs_CZ.lproj”, “cs.lproj”, “da_DK.lproj”, “da.lproj”, “de_DE.lproj”, “de.lproj”, “el_GR.lproj”, “el.lproj”, “en_GB.lproj”, “en_US.lproj”, “English.lproj”, “es_419.lproj”, “es_ES.lproj”, “es_LA.lproj”, “es_MX.lproj”, “es_NA.lproj”, “es.lproj”, “fi_FI.lproj”, “fi_FL.lproj”, “fi.lproj”, “fr_CA.lproj”, “fr_FR.lproj”, “fr_XM.lproj”, “fr.lproj”, “he_IL.lproj”, “he.lproj”, “hu_HU.lproj”, “hu.lproj”, “it_IT.lproj”, “it.lproj”, “ja_JP.lproj”, “ja.lproj”, “ko_KR.lproj”, “ko.lproj”, “nb_NO.lproj”, “nb.lproj”, “nl_NL.lproj”, “nl.lproj”, “no_NB.lproj”, “no.lproj”, “pl_PL.lproj”, “pl.lproj”, “pt_BR.lproj”, “pt.lproj”, “ro_RO.lproj”, “ro.lproj”, “ru_RU.lproj”, “ru.lproj”, “sv_SE.lproj”, “sv.lproj”, “tr_TR.lproj”, “tr.lproj”, “uk_UA.lproj”, “uk.lproj”, “zh_CN.lproj”, “zh_TW.lproj”, “zh-Hans.lproj”, “zh-Hant.lproj”, “zh.lproj”}},……

–指定階層下で、指定クリエータコードのファイルを取得(Mac OS X 10.4.x以上で動作)
on getFileListWithSpotLight(aMetaDataItem, aParam, startDir)
  
  
set sDirText to quoted form of POSIX path of startDir
  
set shellText to “mdfind ‘” & aMetaDataItem & ” == \”" & aParam & “\”‘ -onlyin “ & sDirText
  
try
    set aRes to do shell script shellText
  on error
    return {}
  end try
  
set pList to paragraphs of aRes
  
set aList to {}
  
repeat with i in pList
    set aPath to POSIX file i
    
set aPath to aPath as alias
    
set the end of aList to aPath
  end repeat
  
return aList
end getFileListWithSpotLight

–シェルソートで入れ子のリストを降順ソート
on shellSortListDescending(aSortList, aKeyItem)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (contents of item aKeyItem of (oBj’s list’s item (j - gap + 1)) < item aKeyItem of temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortListDescending

–入れ子ではないリストの昇順ソート
on shellSortAscending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) > temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortAscending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/26 パッケージ内容を表示させつつファイル選択

Mac OS X 10.6あたりから、choose fileやchoose folder、choose from listにいろいろオプション指定が行えるようになってきました。そのあたり、確認するのを怠っていたので再確認のために書いたものです。

choose fileやchoose folder、path toでバンドル内のファイルを選択したり取得できるようになってきたわけで……一昔前までは、バンドルはあくまでファイル(に見えるフォルダ)として扱われ、バンドル内のパスについてはaliasで指定できないことになっていたように記憶しています。

ところが、気がつくとバンドル内のパスも指定できるようになっているというわけで、試しに書いてみました。

おまけに、「info for」命令が「deprecated」とStandardAdditionsのAppleScript用語辞書に書かれていたので、用語辞書に書かれているようにSystem Eventsにファイルのプロパティを求めるようにしてみました。

着実に、「Finderが存在しない環境」でも困らないよう、それに備えたAppleScriptの機能改修が行われているように見えます。

choosefile1.png

▲アプリケーションバンドル内のフォルダから、アイコンファイルの選択が可能

スクリプト名:パッケージ内容を表示させつつファイル選択
set a to choose file with prompt “てすとファイル選択” of type {“com.apple.icns”} with showing package contents and invisibles

–info forだと「deprecated」だとAppleScript用語辞書に書かれているので、あえて変えてみた
tell application “System Events”
  set b to properties of a
end tell

–> {short version:”", container:folder “Macintosh HD:Applications:ebiBookReader.app:Contents:Resources:” of application “System Events”, path:”Macintosh HD:Applications:ebiBookReader.app:Contents:Resources:documenticon.icns”, file type:missing value, sound volume:”Cherry”, physical size:221184, URL:”file://localhost/Applications/ebiBookReader.app/Contents/Resources/documenticon.icns”, id:”documenticon.icns,-100,54651444″, displayed name:”documenticon.icns”, busy status:false, kind:”Apple アイコンイメージ”, creator type:missing value, version:”", name extension:”icns”, POSIX path:”/Applications/ebiBookReader.app/Contents/Resources/documenticon.icns”, name:”documenticon.icns”, modification date:date “2013年8月14日水曜日 9:54:14″, size:219211, class:alias, type identifier:”com.apple.icns”, package folder:false, stationery:false, creation date:date “2013年8月14日水曜日 9:54:14″, default application:alias “Cherry:Applications:Preview.app:” of application “System Events”, visible:true, product version:”"}

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/26 指定ファイルのクリエーターとファイルタイプを変更する

指定ファイルのクリエーターとファイルタイプを変更するAppleScriptです。

OS X 10.6以降では、Finder上でクリエーターとファイルタイプを無視する……ことになっています。しかし、ちょっと古めのプログラム(QuickTime Player 7とか)だと、これらをファイルに書き込んで……あろうことか、Finderがこれらを無視するはずが、考慮して「情報を見る」の内容を変更していたりします。

おかげで、MPEG-4できちんとファイル書き出しをしたはずなのに、Finder上の情報で「QuickTimeムービー」などと表示され、海外のお客から「もしかしてこれだと困るかも〜」と指摘される始末。

そこで、ファイルタイプとクリエーターを変更するというよりは「消去する」ことを目的として本ルーチンを作成しました。

スクリプト名:指定ファイルのクリエーターとファイルタイプを変更する
set a to choose file
set aRes to changeFileCreatorAndFileType(a, missing value, missing value) of me

–指定ファイルのクリエーターとファイルタイプを変更する
on changeFileCreatorAndFileType(aFile, aCreator, aFileType)
  
  
try
    tell application “Finder”
      set file type of file aFile to aFileType
      
set creator type of file aFile to aCreator
    end tell
  on error error_message
    return error_message
  end try
  
  
return true
  
end changeFileCreatorAndFileType

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/25 AppleScriptによるWeb CGIの歴史

いまでも、「StandardAdditions」のAppleScript用語辞書「Internet Suite」には、Web CGIのサポート命令「handle CGI request」が残っています。

web2.png

現在、Mac OS X単体ではAppleScriptのCGIは利用できないようになっています。そういう盲腸のような機能や、FinderからSystem Eventsに移管した命令などについては、AppleScript用語辞書に「Obsoleteである」とか「deprecated」などの記述が行われるのが普通です。

dep1.png

動作しないにもかかわらず、そうした表記が(handle CGI requestには)行われていない点には疑問を持っていました。

昔はけっこう使われていた? AppleScript CGI

Classic Mac OSの時代には、OS内の機能である「Web共有」がAppleScriptのCGIをサポートしていました。この時代に、Classic Mac OS+AppleScriptによるCGI作成が一部で盛り上がっていました(それ以外に方法がなかったから、というのがおそらく一番の理由)。

web1.jpeg

Mac OS Xの時代になると、下地がUNIXなので普通にApache Web Serverがインストールされており、(どのバージョンまでかは忘れましたが)Mac OS X ServerにAppleScript CGIをサポートするコンポーネントが含まれていました。

途中から、Mac OS X Serverにこのモジュールが含まれないようになり、MacでWebサーバーを構築し、AppleScriptでCGIを書くことが難しくなってきました。もちろん、手元に置いたマシンで何らかのCGIを動かすよりも、データセンターにレンタルサーバーを借りたほうが安上がりで手間もかからない、AppleScriptのCGIだとスケーラビリティーがないよね、という話も前提としてあります(自分もその立場です)。

ただ、大規模サーバーはともかくとして、リモートコントロール手段としてのWeb CGI、Webブラウザベースでインタフェースを作成してプログラムを動かす手段としてのCGIには、一部のデベロッパーやユーザーからのニーズがあったようです。

そのニーズを汲んで、AppleScript CGIを利用するためのソフトウェア「acgi dispatcher」が開発されます。

acgi dispatcher

disp1.png

James Sentman氏がREALbasicを用いて「acgi dispatcher」というアプリケーションを作成しました。これによって、AppleScript CGIを動かせるようになっていました。

実際に、このacgi dispatcherを利用してAdobe InDesign Serverを呼び出して動かしてみたりもしました(InDesign Server自体は評価に値しないレベルでしたが、、、)。

ただ、同氏がMacによるX-10ホームオートメーションの企業「Sand Hill Engineering」に就職したこと、さらに同社内でX-10ホームオートメーション用Webアクセス製品「X2Web」の開発、販売に着手したことにより、内容的にはX2Webに一本化されたようで、一応バージョン3のfc1まで出ていたacgi dispatcherは事実上の終焉を見ています(ダウンロードもできなくなっています)。

Apache-Apple Event Bridge(AAEB)

David M. Dantowitz氏によって2010年6月にリリースされた「Apache-Apple Event Bridge」はMac OS X 10.6までサポートされた(おそらく、それより新しいバージョンでも動く)ソフトウェアで、いまもダウンロードでき利用できるようです(10米ドル)。

ただし、2010年8月26日以降は情報が更新されておらず、やや放置状態との印象を受けます。

バージョンアップが行われなくなった理由には、Appleの戦略の変更が考えられます。2010年11月4日に行われた1U Server製品「Xserve」の廃止と、それにともなうMac OS X Serverの戦略変更です。

OS標準の「Web共有」機能の位置付けが変化

Web CGI以前に、OS自体の「Web共有」機能の位置付けにもOS X 10.8において変化が見られました。10.8からはWeb共有そのもののGUIベースのコントロールが廃止。ただし、Apache Web Serverの機能は存続しています。

webs1.png
▲システム環境設定「共有」で選択可能なサービス(OS X 10.7)
webs2.png
▲システム環境設定「共有」で選択可能なサービス(OS X 10.8)

GUIベースのUIがなければ操作できないようなエンドユーザー環境におけるセキュリティ向上(何も知識もなくWeb共有をオンにしてセキュリティホール化するリスクの低減)を目指しているのかどうかなど、確かなところは分りませんが、Appleとしては「Web共有」を見えないものにしておきたいようです。ただ、使っているアプリもあるでしょうから機能そのものは外さない、と。

otto’s remoteはどのような実装に?

最近、iPhoneやiPadなどのiOSデバイスからMacのAppleScript/Automatorを呼び出すアプリケーション「Otto’s Remote」のリリースが予告されました。

案外、このCGIの仕組みを利用したアプリケーションなのでは? などと見ています(iCloud経由で実行リクエストをファイルでiOS→Macに投げて、返り値もiCloud経由でiOSに送るような実装もアリです)。

2013/08/25 おもてなしのフォルダ/ファイル選択 v2

ユーザーが選択したフォルダ/ファイルを記憶して2回目移行の実行時にそれを利用する「おもてなしのフォルダ選択」「おもてなしのファイル選択」の、他の環境に渡したときのエラー対策版です。

スクリプト名:おもてなしのフォルダ選択 v2
property sDir1 : missing value
property sDir2 : missing value

if sDir1 = missing value then
  –初回実行時
  
set aFol to choose folder with prompt “処理対象のフォルダAを指定してください”
  
set sDir1 to aFol
else
  –2回目以降
  
try
    set aFol to choose folder with prompt “処理対象のフォルダAを指定してください” default location sDir1
  on error
    set aFol to choose folder with prompt “処理対象のフォルダAを指定してください”
    
set sDir1 to aFol
  end try
end if

if sDir2 = missing value then
  –初回実行時
  
set bFol to choose folder with prompt “出力対象のフォルダBを指定してください”
  
set sDir2 to bFol
else
  –2回目以降
  
try
    set bFol to choose folder with prompt “出力対象のフォルダBを指定してください” default location sDir2
  on error
    set bFol to choose folder with prompt “出力対象のフォルダBを指定してください”
    
set sDir2 to bFol
  end try
end if

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

スクリプト名:おもてなしのファイル選択 v2
property aFileF : missing value
property bFileF : missing value

if aFileF = missing value then
  –初回実行時
  
set aFile to choose file with prompt “ファイルAを選択してください。”
  
set aFileF to aFile
else
  –2回目以降
  
try
    set aFile to choose file with prompt “ファイルAを選択してください。” default location aFileF
  on error
    set aFile to choose file with prompt “ファイルAを選択してください。”
    
set aFileF to aFile
  end try
end if

if bFileF = missing value then
  –初回実行時
  
set bFile to choose file with prompt “ファイルBを選択してください。”
  
set bFileF to bFile
else
  –2回目以降
  
try
    set bFile to choose file with prompt “ファイルBを選択してください。” default location bFileF
  on error
    set bFile to choose file with prompt “ファイルBを選択してください。”
    
set bFileF to bFile
  end try
end if

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/23 おもてなしのファイル選択

choose fileでdefault locationが使えることに気付かず、いままでchoose fileだけは別扱いしていましたが……choose fileでdefault locationの指定ができるので、使うと便利という話です。

もともと、choose folderについては複数のフォルダ選択を行うような場合に、それぞれのフォルダ選択で同じものを指定することが圧倒的に多いので、前回の選択フォルダをAppleScriptのプロパティに記憶しておいて、2回目以降はそれを使うという処理をよくやっていました。

選択するフォルダ数が増えてくると、フォルダ選択作業は拷問に近いものがあるので、このように「前回のフォルダを覚えてデフォルト位置にする」という処理は、あるのとないのとでは(使わされる側にとって)雲泥の差です。

スクリプト名:おもてなしのフォルダ選択
property sDir1 : missing value
property sDir2 : missing value

if sDir1 = missing value then
  –初回実行時
  
set aFol to choose folder with prompt “処理対象のフォルダAを指定してください”
  
set sDir1 to aFol
else
  –2回目以降
  
set aFol to choose folder with prompt “処理対象のフォルダAを指定してください” default location sDir1
end if

if sDir2 = missing value then
  –初回実行時
  
set bFol to choose folder with prompt “出力対象のフォルダBを指定してください”
  
set sDir2 to bFol
else
  –2回目以降
  
set bFol to choose folder with prompt “出力対象のフォルダBを指定してください” default location sDir2
end if

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

そして、ファイル選択でもdefault locationを(一度選択したファイルを)記憶しておいて、2回目以降はそれを使い回すようにできるわけです。

これを、「おもてなしのファイル選択」と呼んでいます。これまた、何回も実行する開発時のテストプログラムを自分で使うような場合には……あるのとないのとでは雲泥の差です。

ただ……納品時に「自分の環境でテストして、自分のローカルフォルダの情報が入ったままのアプレット」をそのまま相手に送ってしまうと、「これ、動かないけどどーなってんの?」という指摘を受けるので要注意です(実際に海外のクライアント向けにやってしまいました)。

プロパティに保存してあるaliasの情報を一度検証して、エラーを起こすようであればデフォルト値は指定しないという処理が必要かもしれません。

スクリプト名:おもてなしのファイル選択
property aFileF : missing value
property bFileF : missing value

if aFileF = missing value then
  –初回実行時
  
set aFile to choose file with prompt “ファイルAを選択してください。”
  
set aFileF to aFile
else
  –2回目以降
  
set aFile to choose file with prompt “ファイルAを選択してください。” default location aFileF
end if

if bFileF = missing value then
  –初回実行時
  
set bFile to choose file with prompt “ファイルBを選択してください。”
  
set bFileF to bFile
else
  –2回目以降
  
set bFile to choose file with prompt “ファイルBを選択してください。” default location bFileF
end if

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/22 Calendar(iCal)からリマインダーのカレンダーが見える上に区別ができない

完全にバグだと思います。カレンダー(iCal)上のカレンダーやイベントを処理しようとしたときに、いろいろ問題が起こるという話です。カレンダー(iCal)のScriptingをそれほど本気で行うことはなかったのですが、仕事で依頼を受けて(本気で)調べてみたら、驚愕の事実が判明。

cal1.png

カレンダー(iCal)アプリケーションには、オブジェクトとして「カレンダー(calendar)」が定義されており、AppleScriptから各calendarにアクセスできます。

cal2.png

GUI上では、↑のように見えます。これらの名称をAppleScriptから取得すると……

cal3.png

カレンダー(iCal)上に存在していないカレンダーの名称まで取得できてしまいます。

致命的なのは、同じ「ホーム」といった名称のカレンダーが存在しているにもかかわらず、同じcalendarのプロパティを見てみても、識別するための属性値が何もないということです。

「ホーム」というありがちなcalendar名称はともかく、「マンガの発売予定日」という名称はどこかで見たことがありました。そういえば………

cal4.png

「リマインダー」アプリに、そういうリストを登録していました。

cal5.png

そこで、「リマインダー」上のリスト名称をちょこっと変更。「ホーム」を「ホーム(reminder)」に。他も同様。

cal6.png

この状態で、カレンダー(iCal)アプリにAppleScriptから名称の取得を実行すると……

cal7.png

うわ、カレンダー(iCal)に問い合わせたはずなのに、リマインダーのリストまで取得できてしまっていることが確認できました。

カレンダー(iCal)もリマインダーも同じくEvent Kitを利用しているのだと思いますが……違うアプリのデータ内容が見えてしまうのは、いかがなものでしょうか?

早くバグレポートを書かないと、10.9でも同じ症状が発生しているので困ります。

2013/08/21 Numbers 09で新規ドキュメントを作成

Numbers 09(バージョン2.3)で、新規ドキュメントを作成するAppleScriptです。

正直、NumbersのAppleScript対応度は「落第レベル」で、本来できなくてはいけない操作をAppleScriptからコントロールできません。その代表的なものが、AppleScriptから新規ドキュメントを作成できないというものです。

numb1.png

常識的な記述を行ってみても、エラーが出るばかりで……目的を果たすことができません。

そこで、海外のMLの過去ログを整理して、Numbers関連の投稿を掘り返してみると………………ありました。とても、上品だとかエレガントだとかいう言葉とはほど遠いやりかたですが、実際にやるとできます。

そのやり方とは、Numbersのアプリケーションバンドル内にある、テンプレートへのパスを求めて、

numb2.png

それをNumbersからオープンするというものです(うわ〜、いやすぎる)。

テンプレートをオープンすると、新規ドキュメントがオープンされたのと同じ状況を作り出せるわけです。

個人的には、やり方が存在するのであれば使ってみようというところで……しかし、本当にiWork系アプリのAppleScript対応度はよろしくないですね。GUIから操作すると使えるのに、大量のデータを変更しようとしても手作業というのは困ります。

numb3.png

スクリプト名:Numbersで新規ドキュメントをオープンする
set tempPath to path to resource “Blank.nmbtemplate” in bundle (path to application “Numbers”) in directory “Templates”
tell application “Numbers”
  open tempPath
end tell

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/06 delay命令の待ち時間をキャンセルする

すでに書いてあったり動いているAppleScriptのプログラムで、プログラム中に書かれた時間待ち命令「delay」の待ち時間をなくしたい場合にどうしたらよいか、という考察です。

(1)プログラムを書き換える
常識的に考えると、この対応が普通です。
delay命令のパラメータ(秒数)をグローバル変数なりプロパティ値にすることで、プログラムの一部を書き換えることでdelayの秒数を変化させられます。

(2)delay命令をのっとる
非常識な人が好んで使いそうな方法です。
delay命令をon delayのハンドラで乗っ取ってしまい、delay命令そのものをキャンセルする方法です。

スクリプト名:delay命令をキャンセル(書き換え前)
display dialog "a"
delay 5
display dialog "b"

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

スクリプト名:delay命令をキャンセル(書き換え後)
display dialog "a"
delay 5
display dialog "b"

on delay aParam
  –continue delay
end delay

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/03 ASOCでLocalizable.stringsの内容を取得

AppleScriptObjCのプロジェクト中で、Localizable.stringsから現在の言語環境に合った(ローカライズされた)文字列を取得するサンプルです。

海外のサイトを検索してみても、意外と見つからなかったのでまとめておきました。以下、操作はXcode 4.6.3で行っています。

当初は「NSLocalizedStringとかNSBundleでも使うのかな?」とメドをつけて探していたものの……一向にみつからず、少々肝を冷やしましたが(納品前ギリギリだったので)、そういうやり方ではなくAppleScript(StandardAdditions)の標準命令で持っている、「localized string」で書けばよいことが分りました。

loc1.png

とりあえず、素の状態のAppleScriptObjCプロジェクトに「Localizable.strings」ファイルを追加して、プロジェクトに日本語ローカライズを施す手順を説明しておきます(自分のためでもあります)。

loc11.png
▲AppleScriptObjCプロジェクトで、「File」>「「New」>「File…」を実行してファイルをプロジェクトに新規追加

loc2.png
▲ファイル種別で「Other」から「Empty」を選択

loc3.png
▲プロジェクト中に「Localizable.strings」の名前で作成

loc4.png
▲プロジェクト中のファイル一覧(Project Navigator)中で、追加したLocalizable.stringsファイルを選択

loc5.png
▲右側にある(と思われる)Inspectorの「File Inspector」タブ中にある、「Localization」項目中の、「Localize」ボタンをクリック

loc6.png
▲どこの言語(ロケール)として位置づけるのかを聞かれるので、迷わず「English」を選択

loc7.png
▲プロジェクトの設定を選択して、PROJECTの「Info」タブ中の「Localizations」を選択

loc8.png
▲Language一覧の下にある「+」をクリックすると、どのロケールを追加するか選択できるので、「Japanese(ja)」を選択(ほかのでもいいけど)

loc9.png
▲ローカライズ対象のファイルはこれこれで4つあって、Englishロケールのものをそのままコピーするけどいいよね? という画面。「Finish」ボタンをクリック

loc10.png
▲4つのファイルがローカライズされたよ、という表示になった。ただし、元にした「English」の内容をそのままコピーしただけの状態なので、具体的なローカライズ作業はこれから

loc111.png
▲Project Navigator上で、指定した4ファイルのファイル名先頭に「▶」がついて、ローカライズされた状態になった(それぞれのロケールのファイルがさらに1階層加わった)状態

loc12.png
▲Localizable.stringsのロケール一覧を表示。「Localizable.strings(English)」と「Localizable.strings(Japanese)」の2つのファイルが見える

この状態で、それぞれのLocalizable.stringsファイルに対して、

■Localizable.strings(English)
“test1″ = “This is test string”;

■Localizable.strings(Japanese)
“test1″ = “これはテスト用文字列です”;

と、書いておき、AppleScriptObjCコード中にて、

set a to localized string of “test1″

と、書くだけで実行中の言語環境設定にもとづいて、ローカライズ文字列が取得できます。

→ Xcodeプロジェクトのダウンロード

2013/08/03 Combo Boxの操作

AppleScriptObjCでNSComboBoxを操作したときのサンプルです。

ポップアップメニュー部分に値をセットするのと、追加を行うあたりの動作を書いてみました。

ポップアップメニュー側にデフォルトの一覧を設定しておいて、それ以外のユーザー入力値がテキストフィールド側に入力されてボタンをクリックしたら、入力値をポップアップメニュー側に追加します。

ソートされずに追加されてしまうので、ソートしてから追加したほうがよいのか……あるいは、DataSourceを指定すれば、自動的にソートされた状態が維持されるんじゃないかとか、もう少し突っ込んでおきたいところです。

combo1.png

→ Xcodeプロジェクトのダウンロード

AppleScriptObjCファイル名:AppDelegate.applescript

– AppDelegate.applescript
– comboBox

– Created by Takaaki Naganoya on 2013/07/24.
– Copyright (c) 2013年 Takaaki Naganoya. All rights reserved.


script AppDelegate
  property parent : class “NSObject”
  
  property aCombo : missing value
  
  property itemList : {“72″, “90″, “100″, “144″, “288″}
  
  on applicationWillFinishLaunching_(aNotification)
    
    repeat with i in my itemList
      aCombo’s addItemWithObjectValue_(i as string)
    end repeat
    
    aCombo’s selectItemAtIndex_(0) –デフォルト値の指定
    
  end applicationWillFinishLaunching_
  
  on applicationShouldTerminate_(sender)
    return current application’s NSTerminateNow
  end applicationShouldTerminate_
  
  
  on clicked_(sender)
    
    set aVal to aCombo’s stringValue(sender)
    
set aVal to aVal as string
    
display dialog aVal
    
    if aVal is not in my itemList then
      set the end of itemList to aVal
      
      aCombo’s addItemWithObjectValue_(aVal)
      
aCombo’s reloadData()
      
    end if
    
  end clicked_
  
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/01 指定アプリケーションがオープン可能なファイル種別をリストアップ v3

必要に迫られて、少々場当たり的に書いた、「指定アプリが扱えるファイルのタイプとファイル拡張子のリストをタブ区切りテキストで取得するAppleScript」のバージョンアップ版です。

v2を掲載して、「もうこのプログラムをアップデートすることはないだろー」と思っていたのですが、さらなる追求の手が……

「path to choose application」で選択したアプリのパス(alias)が取れる、とのご指摘。驚いてGoogleで検索してみたものの、その情報を掲載しているページは10件に満たず……まだそんな、隠し機能っぽい記述があったのかと、しみじみと味わわせていただきました。

ということで、滅多に使わない命令「choose application」(AppleScriptを覚えたてのころは、使って遊んでいたような)から「path to」で一気にaliasを取得できるところがみどころです。

ほかは…………まあ、いいじゃないですか。何にもないけど。

スクリプト名:指定アプリケーションがオープン可能なファイル種別をリストアップ v3
–指定アプリのInfo.plistからCFBundleTypeNameとCFBundleTypeExtensionsの情報をタブ区切りテキストで取得するツール v3

script spd
  property aList : {}
  
property aText : “”
end script

–初期化
set aList of spd to {}
set aText of spd to “”

–処理対象アプリケーションの選択
set apPath to path to (choose application with prompt “Doc Typesを取り出す対象のアプリケーションを選択”) –この記述で10行以上のコードが不要に

–指定アプリケーションのバンドル内のInfo.plistのパスを取得
set apPathStr to POSIX path of apPath
set aInfoPath to apPathStr & “Contents/Info.plist”

tell application “System Events”
  set vRec to value of property list file aInfoPath –★【重要】 ここで、”quoted form of”を付けるとエラーになるので注意!!!★
  
  
try
    set dtypeList to CFBundleDocumentTypes of vRec
  on error
    display dialog “とくに、特定のファイルタイプのオープンをサポートしているアプリケーションではないようです”
    
return
  end try
  
end tell

repeat with i in dtypeList
  
  
set j to contents of i
  
  
set tmpStr to recToString(j) of recToStrKit
  
set tmpStr to tmpStr as string
  
  
if tmpStr contains “CFBundleTypeName” then
    –常識的なものへの対処
    
set aName to CFBundleTypeName of j
    
  else if tmpStr contains “CFBundleTypeOSTypes” then
    –Illustrator対策
    
set aaNames to CFBundleTypeOSTypes of j
    
set aName to retArrowText(aaNames, “, “) of me
    
  else if tmpStr contains “LSItemContentTypes” then
    –Excel対策
    
set aaNames to LSItemContentTypes of j
    
set aName to retArrowText(aaNames, “, “) of me
    
  end if
  
  
  
try
    set extName to CFBundleTypeExtensions of j
  on error
    set extName to {“”}
  end try
  
  
if length of extName > 1 then
    set extStr to retArrowText(extName, “, “) of me
  else
    set extStr to contents of first item of extName
  end if
  
  
–拡張子の指定のあるものだけをレポート対象とする
  
if extStr is not equal to “” then
    set aText of spd to aText of spd & aName & tab & extStr & return
  end if
  
end repeat

–あとかたづけ
set aList of spd to {}

return contents of aText of spd

–リストを任意のデリミタ付きでテキストに
on retArrowText(aList, aDelim)
  set aText to “”
  
set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to aDelim
  
set aText to aList as text
  
set AppleScript’s text item delimiters to curDelim
  
return aText
end retArrowText

script recToStrKit
  
  
–エラートラップを使って、わざとエラーを発生させ、エラーメッセージからレコードをstringに変換する
  
on recToString(aRec)
    
    
–レコードを無理矢理stringにcastして、エラーメッセージを取得する
    
try
      set a to aRec as string –ここでエラー発生
    on error aMes
      set a to aMes
    end try
    
    
–エラーメッセージ文字列から、元のレコードの情報を組み立てる
    
set b to trimStrFromTo(a, “{”, “}”)
    
set b to “{” & b & “}”
    
    
return b
    
  end recToString
  
  
  
on trimStrFromTo(aStr, fromStr, toStr)
    –fromStrは前から探す
    
if fromStr is not equal to “” then
      set sPos to (offset of fromStr in aStr) + 1
    else
      set sPos to 1
    end if
    
    
–toStrは後ろから探す
    
if toStr is not equal to “” then
      set b to (reverse of characters of aStr) as string
      
set ePos to (offset of toStr in b)
      
set ePos to ((length of aStr) - ePos)
    else
      set ePos to length of aStr
    end if
    
set aRes to text sPos thru ePos of aStr
    
    
return aRes
    
  end trimStrFromTo
  
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に