Archive for the 'How To' Category

04/13 AppleScriptObjCアプリをAppleScript対応に(3)

AppleScriptObjCのアプリをスクリプタブルにする話の続きです。

とりあえず、30分もかからずにASOCアプリをスクリプタブルにできました。超簡単です。

そこで、以前から疑問に思っていたことをテストしてみました。

Xcode上では、プロジェクトを構成するさまざまなファイルをローカライズすることが可能です。つまり、各国語環境用に個別にファイルを用意しておいて、対応する言語環境で別々の内容を表示させることができるようになっています。

そこで、sdefファイルをローカライズして、日本語環境下では日本語の解説文が入ったAppleScript用語辞書を表示させられるかを試してみました。

asocs8.png

▲ローカライズされたAppleScript用語辞書。日本のユーザーしか使わないようなツールに英語だけの用語辞書を付けておくことはナンセンス。このようにして分かりやすくできる

結果はばっちり大成功。日本語環境では、日本語で説明の入ったAppleScript用語辞書がオープンされることが確認できました。こうして英語の用語辞書のほかに日本語の用語辞書を用意しておけばよいのではないか? と思われました。

→ プロジェクトのダウンロード(90Kバイト)

※記事掲載当初はアーカイブのダウンロードリンクが切れていました。2012/4/15現在は修正してあります

■総評

正直、AppleScriptで書かれたプログラムをAppleScriptから呼び出すのだから、処理内容自体を呼び出し側に書けばよいようにも思えますし、速度の面でもあまりメリットが感じられません。

リスト要素のソートなど、Cocoaの機能を用いると高速化できるものもありますが、Mac OS X 10.7以降であればAppleScriptエディタ上で直接AppleScriptObjCのプログラムが記述でき、Cocoaの機能も呼び出すことができます。わざわざ、操作が繁雑なXcode上でそれを行うメリットが大きいとも思えません。

AppleScriptでOSAX(のようなもの……つまり、Invisible Processでウィンドウとかメニューなどを持たないアプリ)に近いものが作れるわけで、それについてはなかなか便利でしょう(ライブラリを整備するのと自前OSAX作成とどちらが労力が少なくて済むかは、判断つきかねます)。

ですが……単純にやってみて「おもしろい」と感じられました。もっと高度な命令も実装できることが確認できれば、応用範囲がいろいろと広がるのではないかと思われました。

AppleScript用語辞書の(言語環境に応じた)ローカライズや、一部のAppleのアプリケーションで試行されているサンプルスクリプトの用語辞書への同梱など、「こうできた方が便利では?」というアイデアを試す場として利用できる、とは思っています。

スクリプタブルなアプリケーションを作るのがここまで簡単だとは思わなかったので、そのことが分かったことが最大の成果だと感じました。他人のプログラムを見ながら試して、動くようになるのに30分もかかりませんでした。

04/13 AppleScriptObjCアプリをAppleScript対応に(2)

AppleScriptObjCアプリをスクリプタブルにした話の続きです。

r/oの属性ばかりではなく、書き換えできる属性値を用意し、これをGUIにつないで書き換えが目で見て分かるようにしてみました。

さきほどの用語辞書の属性値「message」はsdefファイルの定義によりAppleScriptObjCプログラム中の「theMessage」プロパティに対応。さらに、Xcode上でtheMessageプロパティをNSTextFieldのvalueにバインドしてみました。

AppleScriptObjCファイル名:asoc1AppDelegate.applescript

– asoc1AppDelegate.applescript
– asoc1

– Created by 長野谷 隆昌 on 12/04/12.
– Copyright 2012 Piyomaru Software. All rights reserved.


– http://macscripter.net/viewtopic.php?id=36000

– MacScripters Meetingの投稿をもとに、いらない部分をそぎ落として分かりやすいように整理したもの
– Original post by akader

script asoc1AppDelegate
  
  
property parent : class “NSObject”
  
  
property tF : missing value –bind to NSTextField
  
  
property theMessage : missing value –bind to tF’s value
  
  
  
  
  
on applicationWillFinishLaunching_(aNotification)
    
– Insert code here to initialize your application before any files are opened
  
end applicationWillFinishLaunching_
  
  
  
on applicationShouldTerminate_(sender)
    
– Insert code here to do any housekeeping before your application quits
    
return current application’s NSTerminateNow
  
end applicationShouldTerminate_
  
  
  
  
on application_delegateHandlesKey_(sender, theKey)
    
    
return theKey is in {“theMessage”}
    
  
end application_delegateHandlesKey_
  
  
end script

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

アプリケーション「asoc1」を実行すると、こんな感じです。

asocs5.png

スクリプト名:asoc1のメッセージを書き換える
tell application “asoc1″
  set message to “ぴよまるソフトウェア”
end tell

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

上記のAppleScriptを実行すると、

asocs6.png

のようにテキストフィールドの内容が変化します。逆に、テキストフィールドの内容を「message」属性を介して取得することもできます。

ただし、テキストフィールドに文字入力中の内容を取得しようとした場合、すぐにテキストフィールドの内容に対する変更が値に反映されないなどの現象が見られました。そこで、Xcode上でテキストフィールドのvalueをバインドしているところで、

asocs7.png

「Continuously Updates Value」のチェック項目があるので、チェックを入れると……入力した内容がすぐに属性「message」に反映されるようにはなるのですが、今度はアプリケーションの動作が若干もたつく感じがしました。外すとそのようなことはなくなったので、少しひかえめな連動を行うべきなのかもしれません。

「ひかえめな連動」というのは、GUI上で入力中のフィールド内容を即座に求めるのではなく、内容が確定して環境設定に書き込んだ内容に対してアクセスするような連動、ということです。キーボード入力された内容をすぐ取得するのは避けたほうがよいでしょう。

04/12 AppleScriptObjCアプリをAppleScript対応に(1)

MacScripter.netで探してAppleScriptObjCアプリをAppleScript対応(スクリプタブル)にする方法を確認してみました。貴重な情報を提供してくれている投稿者の方々には深く感謝しています。

実際の投稿記事はこちら。この一連の記事は同投稿を精査して、より単純化して資料を加え解説するものです。

■Info.plistを編集

まずはXcode上でAppleScriptObjCのプロジェクトを1つ作成してみましょう。サンプルでは、「asoc1」という名前のプロジェクトを作成しました。

asocs2.png

最初に、Info.plist(各Xcodeプロジェクト内でのファイル名は異なります。上の画面では「asoc1-info.plist」が該当)を編集し、キーが「Scriptable」値が「Yes」(Boolean)、キーが「Scripting Definition file name」値が「myApp.sdef」のエントリ(合計2つ)を作成します。

asocs1.png

■sdefファイルをプロジェクトに追加する

sdef(Scripting DEFinition)ファイルをプロジェクトに追加します。Xcodeで「New File」を実行し、「empty file」をプロジェクトに追加。追加ファイルのファイル名を「myApp.sdef」とします。

内容はこんな感じ。画像では内容が見えない場合には、あとでアーカイブ中の実物を見てください。

asocs3.png

なお、sdefファイルの記述がもっと簡単にできる、Shadow Labの「Sdef Editor」というフリー・ソフトウェアが存在します。もっと込み入ったAppleScript用語辞書を作成する場合には、同ソフトウェアを併用するとよいでしょう。

asocs9.png
▲Shadow LabのSdef Editor

■AppleScriptObjCプログラム側にハンドラ追加

on application_delegateHandlesKey_(sender, theKey) ハンドラを追加。予約語messageに対応する「theMessage」を受信したときにtrueを返します。ただ、それだけ。

■AppleScriptObjCプログラム側にプロパティ追加

property theMessage : missing value

これだけ追加しておきましょう。

■ためしに、ビルド

これでXcode上でCommand-Rでビルド&実行するだけで、スクリプタブルなアプリケーション(AppleScript用語辞書つき)になります。アプリケーションに対してプロパティを取得すると、これだけでアプリ名やバージョン番号などの最低限の情報を取得できます。

AppleScript用語辞書をAppleScriptエディタでオープンして内容を確認することも可能です。

スクリプト名:asoc1でアプリのプロパティを取得する
tell application “asoc1″
  properties
end tell

–> {message:missing value, frontmost:false, class:application, name:”asoc1″, version:”1.0″}

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

02/18 AppleScriptObjC Explorer 2 v2.2

AppleScriptObjCの世界をほぼ1人で引っ張っている感のある、Shane StanleyによるAppleScriptObjCに特化したスクリプトエディタが「AppleScriptObjC Explorer 2」です。

exp1.jpg

▲構文色分けがまったくできていないXcode 4(左)と構文色分けに対応しているAppleScriptObjC Explorer(右)

exp2.jpg
▲インデントも正確なAppleScript Explorer 2

まだ使い始めて日が浅いのですが、その特徴を紹介すると……

・Xcode 4ではまったく効かない「AppleScriptの構文に応じた色分け」ができる(これはでかい! というか、Xcode 4がダメすぎ)
・Xcode 4ではまったくダメな「インデント」が正確(Xcode 4、本当にダメすぎ)
・Xcode 4では絶望的な、ログ表示機能を備える
・単体で動作可能なAppleScriptObjCベースのアプリケーションを作成可能
・Xcode 4の外部エディタとして動作可能(XcodeでScriptのファイルを選択して、コンテクストメニューから「Open With External Editor」を実行。あらかじめFinder上で拡張子とアプリケーションの関連を変更しておく必要がある)

exp3.jpg

・定番の処理(Arrayを作るとか、ソートするとか)はメニューからScript本文中にInsert可能

exp4.jpg

exp5.jpg

・Script文中で入力補完機能を利用可能(ここだけ、まだ試していない)

といったところでしょうか。AppleScriptObjCが使えるMac OS X 10.6/10.7で動作します。

構文色分けができないとプログラミング効率が悪すぎるのと、インデントが正確でないXcode 4にシビレを切らしてAppleScriptObjC Explorerを購入(5,000円……為替レートの変動で変わる??)。

ただ、デバッグ機能についてはアプリケーション起動時に呼ばれる「applicationWillFinishLaunching_」イベントハンドラの中で実行されるコードしかデバッグできなさそうなので、デバッグ機能に期待して購入すると、期待外れになってしまうかもしれません。

総合的には……残念ながら、値段の割には……と、感じる点が多々あります。

AppleのCocoaについてのヘルプからAppleScriptObjC用のヘルプとして変換して表示するとか、既存のObjective-Cのコードをプロジェクトにインポートすると、その呼び出し用のAppleScriptObjCコードがScript本文中に展開されるとか……そういう機能を期待したいところです。

いまの段階では、ちょっと気の利いた構文色分けエディタ、というレベルです。それ以上でもそれ以下でもありません(Shane Stanleyへのお布施、という意味ではとても意義があるかもしれない)。

とりあえず30日間は試用ができるため、Xcode 4にブチ切れまくっている方は試してみるとよいでしょう。

01/30 AppleScriptObjCのメモリー浪費現象を解決?!

AppleScriptObjCで作成したプログラムが、実際に作業で使ってみたらメモリを馬鹿喰いして困る、という話を海外でよく見かけます。

自分も、1,000色ほどの色セットと任意の色の近似色を求めるAppleScriptObjCのプログラムを作成し、任意の1色の近似色検索を行う分には問題なかったものの……まとめて数十〜数百色を処理しようとしたところ、MacBook Pro搭載の8GBのメモリーをアレヨアレヨという間に喰いつぶされてしまいました。

その時には急いでいたので、根本的な解決をあきらめ……近似色検索処理だけを通常のAppleScriptのアプレットに追い出し、AppleScriptObjCのプログラムからAppleScriptアプレットをtell文で呼び出すように処理変更。

余計なプロセス間通信を行うので、余計に時間がかかるようになりましたが、メモリを喰いつぶしてスワップしまくるよりはマシという判断でした。事実、プロセス分離を行う前よりもずいぶん処理速度が落ちたのですが……背に腹はかえられません。

後日、時間に余裕があるときに調査を行ったところ……以前に掲載したCocoaベースのソートルーチンが問題の発生源であることが判明(わかってたけど)。

その場でプログラムから作成したオブジェクトについては、ループ処理などで後続の処理がすぐに発生した場合……自動的にはメモリー上から解放されないということが分りました。

ソートルーチンはサブルーチン化しておいたので、AppleScript的にはローカル変数などで処理される安全な「離れ小島」ですが、Cocoaアプリケーション的には「離れ小島ではない」という状態だと理解しました。

解決策は、作成したオブジェクトを明示的にリリース(release)すること。ほんの短い追加ではあるものの、数百件のデータを処理してもまったくメモリーを馬鹿喰いすることなく、処理を継続できました。

このあたり、Info.plistの記述を書き換えると回避できる問題なのかどうか、もうちょっと調べる必要がありますが……

スクリプト名:Cocoaで入れ子のリストを昇順ソート v2(AppleScriptObjC)
set aRecList to {{aName:“ひよこさん”, favoriteFood:“やきそば”, moneyInWallet:30}, {aName:“ぴよこ”, favoriteFood:“やきうどん”, moneyInWallet:20000}}
set sortedList to cocoaSortListAscending(aRecList, “moneyInWallet”)

–Cocoaで入れ子のリストを昇順ソート v2(AppleScriptObjC)
–連続して呼び出した場合にメモリを馬鹿喰いする現象に対処
on cocoaSortListAscending(theList, keyItem)
  
  
– make unique set
  
tell current application’s NSSet to set theSet to setWithArray_(theList)
  
  
– define sorting
  
tell current application’s NSSortDescriptor
    set theDescriptor to sortDescriptorWithKey_ascending_(keyItem, true)
  end tell
  
  
–sort
  
set sortedList to theSet’s sortedArrayUsingDescriptors_({theDescriptor})
  
  
—————————————————————————————
  
theSet’s release –** Important!! ** 重要!!
  
—————————————————————————————
  
  
return sortedList
  
end cocoaSortListAscending

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

12/24 AppleScriptObjCでの高速化手法〜script文によるpropertyアクセス

AppleScriptObjCでさまざまな小物ツールを作っては日常的に使っています。

ただ、困ったことに……大量のリスト型変数を扱うのに必須な、「a reference to」によるリスト型変数の間接アクセスによる高速化が、AppleScriptObjCの環境では使えません(プログラム中に記述できるものの、実行時にエラーになってしまいます)。

そこで、いろいろ高速化の手段はないものかと考えることに。

実際に、カラーピッカーで選んだ任意の色の類似色を1,000色ぐらいの色見本データからピックアップして提示する、というツールを作ってみたときに、a reference toによるアクセスができず、AppleScriptエディタ上で動作させるよりも遅くなってしまいました。これは由々しき事態です。

そこで、同じ問題に直面している人がいるのではないかと考え、海外のサイトを探し回り……MacScripterでそのものズバリの内容を見つけました。

「Objective-Cに書き換えれば高速化が……」という身もふたもない内容の投稿もありましたが、a reference toと同程度の高速化が実現できる手法を見つけ、実際に自分のコードに入れてみて効果を確認。

それが、script文によって(論理的に)別Scriptにpropertyを追い出して、別Script上のpropertyにアクセスするというものです。

script speedUp
  property aList:{}
end script

script mainScriptDelegate

  set the end of speedUp’s aList to 1
  
end script

のように間接アクセス。これで10倍以上の(こまかく計測していないのですが)スピードアップが図れました。たかだか1,000項目ぐらいではテストデータとしては小さすぎるので、数万アイテムぐらいのテストデータを作って、別途試してみたいところです。

このほか、ソートルーチンにいつものshell sortではなくCocoaベースのソートルーチンを投入してみました。ちょっとこちらの効果は定かではないのですが、前述の近似色サーチのAppleScriptObjCプログラムでは、

 スピードアップいっさいなし:8〜10秒ぐらい
 +Cocoaベースのソートを投入:3〜4秒ぐらい
 +Script文による間接アクセス適用:1秒以下

ぐらいの差が出たので、Cocoaベースのソートルーチンもそれなりに効いているのでしょう。このCocoaベースのソートルーチンは、{{age:20, aName:”ひよこさん”}, {age:43, aName:”ぴよぴよ”}}のような構造のリストをソートするものです。入れ子のリスト……というと厳密には正しくはないですね。レコードのリストというべきか。KeyItemには”age”とか”aName”といったラベルを指定します。

スクリプト名:Cocoaで入れ子のリストを昇順ソート
–Cocoaで入れ子のリストを昇順ソート(AppleScriptObjC)
on cocoaSortListAscending(theList, keyItem)
  
  
– make unique set
  
tell current application’s NSSet to set theSet to setWithArray_(theList)
  
  
– define sorting
  
tell current application’s NSSortDescriptor
    set theDescriptor to sortDescriptorWithKey_ascending_(keyItem, true)
  end tell
  
  
–sort
  
set sortedList to theSet’s sortedArrayUsingDescriptors_({theDescriptor})
  
  
return sortedList
  
end cocoaSortListAscending

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

08/13 OS X 10.7, Lionで複数GUIログインしてユーザー間アプリ制御

OS X 10.7, Lionからはネットワーク経由での複数GUIログインができるようになりました。つまり、1台のLionのMacに対して、LAN経由でWindowsとかiPadなどの上で動くVNCソフトウェアから同時にログインしてMac上のGUIアプリを使えるようになっています。

lion_share.jpg

GUIログインしたユーザーアカウントで、それぞれアプリケーションを起動して、1台のMacの中で複数のユーザー環境間でAppleEventを投げ合ってアプリケーションをコントロールすることができるようになりました。

じゃあ、現在のユーザーアカウントから、「画面共有」アプリをコントロールして、複数GUIログインを実行できないのか? 複数GUIログインを行うプロセスそのものもAppleScriptで自動化してしまえば、実行するのに時間がかかって一度に1つしか起動できないアプリケーション(InDesignとか、Illustratorとか、SAPとか、、)を1台のマシン上で同時に複数起動。1つのAppleScriptから複数のアプリケーションを同時にコントロールするようなことができるんじゃないか? と、考えていろいろ試してきました。

結論からいえば、「すべての手順を自動化できてはいない。けれど、複数のGUIログインしたユーザー環境の間でアプリケーション制御はできている」というのが現状です。

share_local.jpg

まず、普通に「画面共有」アプリにlocalhost接続を命令すると「自分の画面は共有できません」といってハネられます。しかし、ポート番号をつけてやるとその制限を無視されるようになります。

AppleScriptエディタ上で「open location “vnc:localhost:5901″」などと書いて実行すると、現在使用中のマシンに対して「画面共有」アプリ経由で多重ログインが行えます。

# ポート番号は5900〜6000の間で使用可能(vnc用にリザーブされているポート番号)
# 「vnc://localhost」ではなく「vnc:localhost」でした(転記ミス)

vn1.jpg

ユーザー認証が通ると、画面を切り替えるか、仮想画面を使用するか聞いてくる。仮想画面を選択すると……

vn2.jpg

現在のMacの画面内に、同じMacに別アカウントからアクセスするウィンドウがオープンする。

ここまできちんと機能が実装されていながら、「画面共有」でlocalhost(自分)を指定するとハネられる仕様になっているのは、動作に一部問題があるから? 別アカウントのユーザー環境を操作しようとすると、レインボーカーソルが回りっぱなしになってリモート側の操作が行えなくなることが多々ありました(電源ボタンを押してダイアログを出すと解除された)。

ユーザー名「user1」、パスワード「user1」というアカウント(同一マシン上)に対してリモート制御。すでに起動してあるiCalに対してコントロール。これを行う前に、かならず「システム環境設定」の「共有」で「リモートアップルイベント」をオンにしておく必要がある。

別ユーザー上のiCalに「hiyoko」という新しいカレンダーを作成してみたところ。

sh10.jpg

別ユーザーのiCal上に「hiyoko」カレンダーが作成された。LANごしにコントロールしているわけではないので、スピードも速い。

sh1.jpg

sh2.jpg

「画面共有」アプリのウィンドウを縮小したところ。別アカウントのウィンドウが左側に小さく表示されている。

sh3.jpg

「画面共有」アプリのウィンドウはクローズしてしまっても、ログイン状態は維持される。ためしにクローズしてみても……

sh4.jpg

別ユーザーアカウント「user1」「user2」はログイン状態が維持されていることが分かります。

1台のMac上で複数ユーザーアカウントのログインを行い、それぞれの環境でGUIアプリケーションを起動してアプリケーション制御を行う……といっても、扱いはLANごしに別マシン上の別アカウント上のアプリケーションをコントロールしているのと同じです。

そのため、リモートのAppleEventを受け付けるアプリケーションでないと直接は制御できない。最近は、セキュリティ維持のためにリモート制御を受け付けないアプリケーションが増えた。

しかし、制御先のユーザー環境にAppleScriptで作ったアプレットを起動しておいて、そのアプレット経由でコントロールするようにすれば、リモート制御非対応のアプリもコントロールできます。

こんな感じで、あり余るCPUパワーを生かすようなシステムを作りやすくなってきた……のかもしれません。

04/25 AppleScriptによる並列処理v3a

AppleScriptによる並列処理サンプルのバージョンv3aです。v3を外部公開できるよう、実際のメイン処理部分をすべて削除して、並列処理の評価用に最小限のダミー処理を付加したものです。

本サンプルは、

(1)並列処理呼び出し部分(呼び出す側)「parallelMain_v3a」
(2)並列処理実行テンプレートAppleScript(呼び出される側。スレッド)「mainScript_v2a」

から構成されます。

(1)をAppleScriptエディタで「parallelMain_v3a.app」の名前で、アプリケーションとして保存します。「実行後、自動的に終了しない」にチェックを入れておいてください。

(2)については、通常のAppleScript書類として保存しておいてください(ここでは、「mainScript_v2a」の名前で保存されているものと仮定して説明します)。

parallelMain_v3a.appをFinder上でダブルクリックするなどして起動すると、並列実行するAppleScriptファイルを聞いてきます。「mainScript_v2a」を指定してください。

次に、処理対象フォルダを聞いてきますので、適当にファイルが入っているフォルダを指定してください。本サンプルプログラムではファイルに対して処理は行いません。ただし、処理終了マーク(ラベル)を振るため、ラベルが変更されては困るファイルが入っていないことが前提です。

最後に、結果の書き出し先ファイルを聞かれるので、適当な名前を指定しておいてください。ダミーデータ(乱数データ)がリスト形式で書かれます。

あとは、メイン側がThreadのアプレットを自動生成して起動し、処理ファイルを1つずつThread側に渡して、処理結果を受け取ります。

今後の改良の方向性は、

(a)Threadがコケたりエラー終了した場合のリカバリ
(b)テンプレートAppleScriptがいまひとつ簡潔ではないため、テンプレートのうちThreadとして振る舞うために必要な部分と本当の処理部分を分割して、並列処理を行いやすくする
(c)開発効率を優先して雑に書いた部分をもう少しなんとかする
(d)できれば、CPU負荷を計測してスレッド数を自動で増減させたい(CPUコアの個数分だけThreadを生成し、まだCPUの処理能力にアキがあれば追加するような方向で)

といったところでしょう。

スクリプト名:parallelMain_v3a
–AppleScriptによる並列実行テスト v3a(外部公開用)

–データの受け渡しを一括で行わずに、ファイルを処理するたびに受け渡すようにした

property threadMax : 4 –threadの最大数(ご自由に変更してください)

property threadNameList : {}
property threadFileList : {}
property doneList : {}
property asAppName : “thread” –thread側のアプリケーション名 (thread_1〜thread_N)  変更可
property myName : “” –メイン側の自分の名前。thread側からトークバックしてもらうのに必要

property aResList : {}

property resFileA : “”

property procFileList : {} –処理対象のパス一覧

on run
  –変数の初期化
  
set aResList to {}
  
  
set doneList to {} –これを初期化し忘れてドえらい目に逢った
  
set threadNameList to {} –これを初期化し忘れてドえらい目に逢った
  
  
–メイン側プログラムの名前を取得(コールバックのために必須)
  
set mePath to path to me
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
  
  
–読み込むAppleScriptを選択
  
set aFile to choose file with prompt “並列実行するAppleScriptを選択してください”
  
try
    set aSubObj to load script aFile
  on error
    display dialog “AppleScript以外のファイルが指定されました。” buttons {“OK”} default button 1 with icon 2 with title “ERROR”
    
return
  end try
  
  
–指定フォルダ内のファイルを、Thread数で分割する
  
set aFolder to choose folder with prompt “処理対象フォルダを選択してください”
  
set procFileList to getFilePathList(aFolder) of me –数百〜数千ぐらいのファイル数を考慮。数万ファイル以上になる場合には修正を要する(本当)
  
  
–結果を書き出すファイルを指定
  
set resFileA to choose file name with prompt “処理結果を書き出すファイル(1)を指定してください”
  
  
  
–thread(アプレット)の書き出し先
  
set dFol to (path to desktop from user domain) as string
  
set threadNameList to {}
  
  
  
–threadsを書き出す
  
repeat with i from 1 to threadMax
    
    
set threadName to asAppName & “_” & i as string
    
    
set asPath to dFol & threadName
    
set asRes to makeASexecutable(aSubObj, asPath) of me
    
    
if asRes is not equal to true then –何らかの理由でApplet書き出しに失敗した場合
      display dialog asRes
      
return
    else
      set aThreadFilePath to (asPath & “.app”)
      
tell application “Finder”
        open file aThreadFilePath –threadを起動
      end tell
      
set the end of threadFileList to (aThreadFilePath as alias)
    end if
    
    
–threadに処理依頼を行う
    
set fItem to {contents of first item of procFileList} –要素は1つだけ。ただし、複数要素を渡す可能性もあるのでリスト形式で渡しておく
    
set procFileList to rest of procFileList
    
set aRec to {mainName:myName, paramList:fItem} –threadに渡すパラメータを組み立てる。myNameを渡しているのは、名前を指定してメイン側に結果を返してもらうため
    
–1ファイルのみ渡すようにした
    
    
    
ignoring application responses –ものすごく大事
      tell application threadName
        open aRec –これでthread側は処理開始
      end tell
    end ignoring
    
    
set the end of threadNameList to threadName
    
  end repeat
  
end run

–thread側からメッセージを受信する
on open aData
  set threadName to subName of aData
  
set aRes to resultList of aData
  
  
set aResList to aResList & aRes
  
  
if procFileList is not equal to {} then
    
    
–threadに処理依頼を行う
    
set fItem to {contents of first item of procFileList} –要素は1つだけ。ただし、複数要素を渡す可能性もあるのでリスト形式で渡しておく
    
set procFileList to rest of procFileList
    
set aRec to {mainName:myName, paramList:fItem} –threadに渡すパラメータを組み立てる。myNameを渡しているのは、メイン側へのトークバックのため
    
    
ignoring application responses –ものすごく大事
      tell application threadName
        open aRec –これでthread側は処理開始
      end tell
    end ignoring
    
    
  else
    set the end of doneList to threadName
    
    
tell application threadName
      quit
    end tell
  end if
  
  
  
–本来ならthreadの終了確認を個数を数えるという野蛮な方式で行うべきではない
  
if length of doneList = length of threadNameList then
    –threadから受け取ったデータのファイル書き出しなどを行う(1)
    
write_to_file_AsList(aResList, resFileA) of me
    
    
    
–thread実行ファイルの削除
    
try
      repeat with i in threadFileList
        set j to quoted form of POSIX path of i
        
do shell script “/bin/rm -rf “ & j
      end repeat
    end try
    
    
tell me to quit
  end if
end open

–ファイルの追記ルーチン「write_to_file_AsList」  (リストとして書き込み)
–データ、対象ファイル
on write_to_file_AsList(this_data, target_file)
  try
    set the target_file to the target_file as text
    
set the open_target_file to open for access file target_file with write permission
    
set eof of the open_target_file to 0
    
write this_data to the open_target_file starting at eof as list
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file_AsList

–AppleScriptのソースを取得して返す
on getASSource(macPath)
  set aName to (info for macPath size 0)’s name
  
set tmpPath to ((path to temporary items from system domain) as text) & (do shell script “/usr/bin/uuidgen”) & “.txt”
  
  
set scptText to do shell script “/usr/bin/osadecompile “ & quoted form of POSIX path of macPath
  
  
return scptText
end getASSource

–指定のテキストをAppleScriptとしてコンパイルし、実行可能なアプレットとして出力する
on makeASexecutable(aText, outPathAlias)
  
  
–出力先のaliasからファイル名のみ撮り出す
  
set aStr to outPathAlias as string
  
set fnRes to getLastLayerOfDirData(aStr) of me
  
  
–指定のファル名に拡張子が適切に付いていない場合には補う
  
if fnRes does not end with “.app” then
    set fnRes to fnRes & “.app”
  end if
  
  
–テンポラリフォルダ内のテキストファイルに書き出す
  
set aFileName to (do shell script “uuidgen”) & “.applescript”
  
set tmpAppName to (do shell script “uuidgen”) & “.app”
  
  
set aTempFol to path to temporary items folder from system domain
  
set aTempFolPosix to (POSIX path of aTempFol) & aFileName
  
  
set aTempPathText to (aTempFol as string) & aFileName
  
–write_to_file(aText, aTempPathText, false) of me
  
  
store script aText in file aTempPathText replacing yes
  
  
–ファイルキャッシュをHDDにシンクロさせる
  
–do shell script “sync”–> 実行しなくても大丈夫だった
  
  
–osacompileコマンドでアプレットを生成
  
set sText to “cd “ & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -x -o “ & fnRes & ” “ & aFileName
  
–set sText to “cd ” & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -o ” & fnRes & ” ” & aFileName
  
try
    –コンパイル時にアプリケーションの起動を待ったりする可能性があるので、待ち時間を1時間に設定
    
with timeout of 3600 seconds
      do shell script sText
    end timeout
  on error erMes
    return erMes
  end try
  
  
  
–テンポラリに書き込んだTextのAppleScriptを消す
  
try
    do shell script “rm -f “ & quoted form of aTempFolPosix
  end try
  
  
  
–生成したアプレットを指定のフォルダに移動する
  
set origAppPath to POSIX path of ((aTempFol as string) & fnRes)
  
set destPath to POSIX path of getOnlyFolder(aStr) of me & fnRes
  
  
try
    do shell script “mv -f “ & quoted form of origAppPath & ” “ & quoted form of destPath
  on error erMes
    return erMes
  end try
  
  
return true
  
end makeASexecutable

–テキストで与えられたパスデータから、フォルダのみを返す
on getOnlyFolder(a)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to {“:”}
  
set aList to text items of a
  
set bList to items 1 thru -2 of aList
  
set aText to bList as string
  
set AppleScript’s text item delimiters to curDelim
  
set aText to aText & “:”
  
return aText
end getOnlyFolder

–テキストで与えられたパスデータから、最終階層のテキストのみを返す
on getLastLayerOfDirData(aData)
  set aColon to “:” as Unicode text
  
copy aData to bData
  
if bData ends with “:” then
    set bData to text 1 thru -2 of bData
  end if
  
  
set aLen to length of bData
  
set rData to reverse of (characters of bData)
  
set rData to rData as string
  
set dPos to offset of aColon in rData
  
set resData to text (aLen - dPos + 2) thru -1 of bData
  
return resData
  
end getLastLayerOfDirData

–ファイルの追記ルーチン「write_to_file」
–追記データ、追記対象ファイル、boolean(trueで追記)
on write_to_file(this_data, target_file, append_data)
  try
    set the target_file to the target_file as text
    
set the open_target_file to open for access file target_file with write permission
    
if append_data is false then set eof of the open_target_file to 0
    
write this_data to the open_target_file starting at eof
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file

–文字置換ルーチン
on repChar(origText, targStr, repStr)
  set {txdl, AppleScript’s text item delimiters} to {AppleScript’s text item delimiters, targStr}
  
set temp to text items of origText
  
set AppleScript’s text item delimiters to repStr
  
set res to temp as text
  
set AppleScript’s text item delimiters to txdl
  
return res
end repChar

–指定フォルダ中のファイルのパスを返す
on getFilePathList(aFolder)
  
  
set fileList to {}
  
  
tell application “Finder”
    tell folder aFolder
      try
        –フォルダ中のファイルをalias listで取得
        
–あまりファイル数が多すぎると、Finderが音を上げる可能性があるので注意
        
with timeout of 3600 seconds
          set fileList to every file as alias list –whose name ends with “.dcm.txt”
        end timeout
        
      on error
        –ファイルが指定フォルダ中に1個だけだった場合
        
set fList to every file
        
set fileList to {}
        
repeat with i in fList
          set the end of fileList to i as alias
        end repeat
      end try
    end tell
  end tell
  
  
return fileList
  
end getFilePathList

–値のインクリメント
on incVal(aVar, aMax)
  if aVar < aMax then
    set aVar to aVar + 1
  else
    set aVar to 1
  end if
  
return aVar
end incVal

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

スクリプト名:mainScript_v2a
property mePath : “”
property myName : “”

on run
  set mePath to path to me
  
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
end run

–メイン側からのパラメータ受信用
on open aParamRec
  
  
set mainProgram to mainName of aParamRec
  
set pList to (paramList of aParamRec)
  
  
ignoring application responses
    tell application “Finder”
      set label index of mePath to 2 –視覚的に処理がはじまったことを表現(赤)
    end tell
  end ignoring
  
  
set aRes to mainProgram(pList) of mainScript of me
  
  
–メイン側に処理結果を通知
  
ignoring application responses –ここが超重要
    tell application mainProgram
      open {subName:myName, resultList:aRes}
    end tell
  end ignoring
  
  
ignoring application responses
    tell application “Finder”
      set label index of mePath to 7 –視覚的に処理が終わったことを表現(グレー)
    end tell
  end ignoring
end open

–ここに処理本体を記述
script mainScript
  
  
on mainProgram(fList)
    –ここに並列処理するプログラムを記述する
    
    
set fileSpecList to {} –IDリスト(IDのみ)
    
set aSpecList to {} –ID詳細情報のリスト
    
    
–指定フォルダ内をループ処理
    
repeat with i in fList
      
      
delay (random number from 1 to 10) –ダミー処理
      
      
ignoring application responses
        tell application “Finder”
          set label index of i to 4 –青
        end tell
      end ignoring
    end repeat
    
    
return (random number from 1 to 10) –ダミー
    
  end mainProgram
  
end script

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

04/22 AppleScriptによる並列処理 v3〜実行ムービー

AppleScriptを並列処理するAppleScriptの改良型、v3の実行中のムービーです。

先週末には出来上がっていたのですが、挙動がおかしい箇所があったので地道にデバッグしていました。

並列処理デモv3(5.7MB)

04/16 AppleScriptによる並列処理 v2〜実際に使ってみての評価

AppleScriptによる並列処理 v2を実際に使ってみての評価です。

■前提となる話

たまたま、大量かつそこそこの容量のテキストファイルの処理をする必要があったので、マシンのCPU性能を生かすために並列処理を行わせてみた。

通常のデスクトップ用アプリケーションを複数同時起動できるわけではないし、複数のAppleScriptアプレットから同時に1つのアプリケーションにアクセスした場合にきちんと排他処理してくれたりするかどうかは不明(昔は、FileMaker Proがbegin transaction〜end transactionの構文をサポートしていたが、他で見かけたことがない)。そんなに高度なことを期待してもダメ。

InDesignとかIllustratorの処理を1台で並列実行とかいう話は無理。ただし、もしもMac OS X10.7になって複数ユーザーが同時ログインして別々のGUI環境を同時に実行できるとかいう環境になってくると、1つのユーザー環境から他のユーザー環境上のアプリケーションをコントロールできる可能性は……ないこともない。

AppleScriptのプログラムを動的に実行バイナリに変換して、同じ仕事を手分けして処理するという程度の話がこの話のゴール地点。テキストファイルの大量処理という話ならけっこう使える。

ひととおり使えるレベルのプログラムに育ったら、1台で複数の処理をやるのと同様に、複数のマシンで分散処理を行うプログラムを作ってみるとよさそう。安くてそこそこの性能のマシンを複数台集めるとか、オフィスや学校にあるMacを夜間に動かして仕事を分散処理させるのはけっこうよさそう。

■評価

処理対象のファイル一覧をスレッド数で分割して、まとめて渡すというやり方はよくない。処理対象のファイルにはサイズにバラツキがあるので、それぞれのスレッドの処理時間がバラバラ。「数」で割って仕事を割り振るのは、無駄が多いことが分かった。

p1.jpg

たとえば、4スレッドで並列処理を行っても、割り当て分を終わらせたスレッドは早々にプログラムを終了してしまう。処理が早く終わったスレッドは、処理が終わっていないスレッドの未処理分を処理すべきなのに、それができていない。最後のほうでCPUの利用効率が落ちてしまう。

また、途中でトラブルに遭遇して停止した場合、1スレッド文の処理がいきなりすべて無駄になってしまうのは時間の無駄すぎます。

そのため、1ファイルずつスレッド側に渡して処理するように変更すべきだと思われました。……というか、もう出来上がってテスト中です。

p2.jpg

04/13 AppleScriptによる並列処理 v2

以前に掲載したAppleScriptで並列処理を行うプログラムの改良版です。

1ファイルあたり数千〜数十万行のテキストファイルを数百個処理する必要に迫られ、さまざまな高速化手法を投入してみたものの、通常のAppleScriptでは与えられたファイルを順次実行するだけ。昨今の複数CPUコアが常識化したMacでは、1つのCPUコアが忙しいだけで、他のコアはほとんど遊んでいる状態です。

メニーコアの、無駄に遊んでいるCPUのパワーを絞り出すため、ぴよまるソフトウェアではAppleScriptの並列実行に取り組んできました。

para2.jpg

以前に掲載したもの(v1)は、並列処理を行ううえで必要な技術のテストを行うことが目的だったので、実用性はほとんどありませんでした。

今回のv2では、並列処理実行数を柔軟に指定でき、実際に実戦でテストを行って実用性を確認できています。純粋に4スレッド指定した状態で、1スレッド状態の4分の1の時間で処理を終了しています。1時間以上かかる処理が15分程度で終了する、という感じです。

para1.jpg

v1では、メイン側とサブ側の間でファイルI/Oを用いてイベントのやりとりを行っていましたが、その後に助言をいただいたりテストを行う中で、「openハンドラ経由でデータをやりとりする場合に、ファイルでやりとりする必要はない」ことが分かってきました(これは知らなかった!)。

コマンド内容を一度ファイルに書き出さず、相手側のアプレットのopenハンドラにlistやrecordを直接渡せるため、信頼性や処理速度の向上が期待されます。

また、v1では(ファイルI/Oの信頼性計測のために)イベントを頻繁にやり取りしていましたが、実際にはそんなに頻繁にイベントのやりとりを行うわけではありません。「一度パラメータを与えれば終了時までイベントのやりとりは行わない」ケースが多いことが予想されます。v2では最初と最後だけイベントをやり取りするようにしてテストを実施しました。

p3.jpg

まずは、こちらのAppleScriptを実行形式(実行後終了しない)で保存してください。AppleScriptを呼び出すためのアプレットです。

スクリプト名:parallelMain_v2_exe
–ワンショットタイプのAppleScriptによる並列実行テスト v2

property threadMax : 4 –threadの最大数(環境ごとに静的に変えるか、何らかの動的な判断を行うか?)
property threadNameList : {}
property threadFileList : {}
property doneList : {}
property asAppName : “thread” –thread側のアプリケーション名 (thread_1〜thread_N)  変更可

property aResList : {}
property resFile : “”

on run
  
  
–メイン側プログラムの名前を取得(コールバックのために必須)
  
set mePath to path to me
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
  
  
–読み込むAppleScriptを選択
  
set aFile to choose file with prompt “並列実行するAppleScriptを選択してください”
  
set aSubObj to load script aFile
  
  
–指定フォルダ内のファイルを、Thread数で分割する
  
set aFolder to choose folder with prompt “処理対象フォルダを選択してください”
  
set splitList to splitFilePathToThreadNum(aFolder, threadMax) of me
  
  
–結果を書き出すファイルを指定
  
set resFile to choose file name with prompt “処理結果を書き出すファイルを指定してください”
  
  
  
–thread(アプレット)の書き出し先
  
set dFol to (path to desktop from user domain) as string
  
set threadNameList to {}
  
  
  
–Sub Threadsを書き出す
  
repeat with i from 1 to threadMax
    
    
set threadName to asAppName & “_” & i as string
    
    
set asPath to dFol & threadName
    
set asRes to makeASexecutable(aSubObj, asPath) of me
    
    
if asRes is not equal to true then –何らかの理由でApplet書き出しに失敗した場合
      display dialog asRes
      
return
    else
      set aThreadFilePath to (asPath & “.app”)
      
tell application “Finder”
        open file aThreadFilePath –Sub Threadを起動
      end tell
      
set the end of threadFileList to (aThreadFilePath as alias)
    end if
    
    
–Sub Threadに処理依頼を行う
    
set aRec to {mainName:myName, paramList:contents of item i of splitList} –Threadに渡すパラメータを組み立てる。myNameを渡しているのは、メイン側へのトークバックのため
    
    
    
ignoring application responses –ものすごく大事
      tell application threadName
        open aRec –これでThread側は処理開始
      end tell
    end ignoring
    
    
    
set the end of threadNameList to threadName
    
    
(*
    –処理のピークをずらすために、乱数秒だけウェイトを入れる
    set randNum to (random number from 0 to 5)
    delay randNum
    *)

  end repeat
  
end run

–thread側からメッセージを受信する
on open aData
  set threadName to subName of aData
  
set aRes to resultList of aData
  
  
set the end of doneList to threadName
  
  
set aResList to aResList & aRes
  
  
–本来ならthreadの終了確認を個数を数えるという野蛮な方式で行うべきではない
  
if length of doneList = length of threadNameList then
    –threadから受け取ったデータのファイル書き出しなどを行う
    
write_to_file_AsList(aResList, resFile) of me
    

    
    
–thread実行ファイルの削除
    
tell application “Finder”
      delete threadFileList
    end tell
    
    
tell me to quit
  end if
end open

–ファイルの追記ルーチン「write_to_file_AsList」  (リストとして書き込み)
–データ、対象ファイル
on write_to_file_AsList(this_data, target_file)
  try
    set the target_file to the target_file as text
    
set the open_target_file to open for access file target_file with write permission
    
set eof of the open_target_file to 0
    
write this_data to the open_target_file starting at eof as list
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file_AsList

–AppleScriptのソースを取得して返す
on getASSource(macPath)
  set aName to (info for macPath size 0)’s name
  
set tmpPath to ((path to temporary items from system domain) as text) & (do shell script “/usr/bin/uuidgen”) & “.txt”
  
  
set scptText to do shell script “/usr/bin/osadecompile “ & quoted form of POSIX path of macPath
  
  
return scptText
end getASSource

–指定のテキストをAppleScriptとしてコンパイルし、実行可能なアプレットとして出力する
on makeASexecutable(aText, outPathAlias)
  
  
–出力先のaliasからファイル名のみ撮り出す
  
set aStr to outPathAlias as string
  
set fnRes to getLastLayerOfDirData(aStr) of me
  
  
–指定のファル名に拡張子が適切に付いていない場合には補う
  
if fnRes does not end with “.app” then
    set fnRes to fnRes & “.app”
  end if
  
  
–テンポラリフォルダ内のテキストファイルに書き出す
  
set aFileName to (do shell script “uuidgen”) & “.applescript”
  
set tmpAppName to (do shell script “uuidgen”) & “.app”
  
  
set aTempFol to path to temporary items folder from system domain
  
set aTempFolPosix to (POSIX path of aTempFol) & aFileName
  
  
set aTempPathText to (aTempFol as string) & aFileName
  
–write_to_file(aText, aTempPathText, false) of me
  
  
store script aText in file aTempPathText replacing yes
  
  
–ファイルキャッシュをHDDにシンクロさせる
  
–do shell script “sync”–> 実行しなくても大丈夫だった
  
  
–osacompileコマンドでアプレットを生成
  
–set sText to “cd ” & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -x -o ” & fnRes & ” ” & aFileName
  
set sText to “cd “ & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -o “ & fnRes & ” “ & aFileName
  
try
    –コンパイル時にアプリケーションの起動を待ったりする可能性があるので、待ち時間を1時間に設定
    
with timeout of 3600 seconds
      do shell script sText
    end timeout
  on error erMes
    return erMes
  end try
  
  
  
–テンポラリに書き込んだTextのAppleScriptを消す
  
try
    do shell script “rm -f “ & quoted form of aTempFolPosix
  end try
  
  
  
–生成したアプレットを指定のフォルダに移動する
  
set origAppPath to POSIX path of ((aTempFol as string) & fnRes)
  
set destPath to POSIX path of getOnlyFolder(aStr) of me & fnRes
  
  
try
    do shell script “mv -f “ & quoted form of origAppPath & ” “ & quoted form of destPath
  on error erMes
    return erMes
  end try
  
  
return true
  
end makeASexecutable

–テキストで与えられたパスデータから、フォルダのみを返す
on getOnlyFolder(a)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to {“:”}
  
set aList to text items of a
  
set bList to items 1 thru -2 of aList
  
set aText to bList as string
  
set AppleScript’s text item delimiters to curDelim
  
set aText to aText & “:”
  
return aText
end getOnlyFolder

–テキストで与えられたパスデータから、最終階層のテキストのみを返す
on getLastLayerOfDirData(aData)
  set aColon to “:” as Unicode text
  
copy aData to bData
  
if bData ends with “:” then
    set bData to text 1 thru -2 of bData
  end if
  
  
set aLen to length of bData
  
set rData to reverse of (characters of bData)
  
set rData to rData as string
  
set dPos to offset of aColon in rData
  
set resData to text (aLen - dPos + 2) thru -1 of bData
  
return resData
  
end getLastLayerOfDirData

–ファイルの追記ルーチン「write_to_file」
–追記データ、追記対象ファイル、boolean(trueで追記)
on write_to_file(this_data, target_file, append_data)
  try
    set the target_file to the target_file as text
    
set the open_target_file to open for access file target_file with write permission
    
if append_data is false then set eof of the open_target_file to 0
    
write this_data to the open_target_file starting at eof
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file

–文字置換ルーチン
on repChar(origText, targStr, repStr)
  set {txdl, AppleScript’s text item delimiters} to {AppleScript’s text item delimiters, targStr}
  
set temp to text items of origText
  
set AppleScript’s text item delimiters to repStr
  
set res to temp as text
  
set AppleScript’s text item delimiters to txdl
  
return res
end repChar

–指定フォルダ中のファイルのパスをthread数に分ける
on splitFilePathToThreadNum(aFolder, threadMax)
  
  
set fileList to {}
  
  
repeat threadMax times
    set the end of fileList to {}
  end repeat
  
  
set aFolderStr to aFolder as string
  
  
tell application “Finder”
    tell folder aFolder
      set nameList to name of every file –whose name ends with “.dcm.txt”
    end tell
  end tell
  
  
set subCount to 1
  
set aLen to length of nameList
  
  
repeat with i in nameList
    set j to contents of i
    
set the end of item subCount of fileList to (aFolderStr & j)
    
set subCount to incVal(subCount, threadMax) of me
  end repeat
  
  
return fileList
  
end splitFilePathToThreadNum

–値のインクリメント
on incVal(aVar, aMax)
  if aVar < aMax then
    set aVar to aVar + 1
  else
    set aVar to 1
  end if
  
return aVar
end incVal

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

次に、並列実行されるAppleScriptです。こちらは、普通のAppleScriptとして保存し、アプレット実行時に「並列実行するAppleScriptを選択してください」とダイアログが出るので、その際に指定してください。

この試作品v2では、多数のファイルの処理を行うケースを前提にしており、処理対象ファイルをスレッド数で分割してそれぞれのスレッドに渡すようになっています。

4コアのCore i7の環境で、通常タイプのAppleScriptにくらべて処理時間は大幅に(CPUパワーの許す範囲でスレッド数を増やして)短縮できました。

並列処理で大量のデータを処理する際には、メモリー使用量にも気をつけ、アクティビティ・モニタで監視しながら実行するのがよいでしょう。

para3.jpg

▲AppleScript実行中に大量のデータを抱えすぎてえらい状態になっている例

今回は、何度もテスト実行を行いながらスレッド数の最適化を行いましたが、CPUのコア数や世代(Core Duo、Core 2 Duo、Core iなどなど)を取得してスレッド数の自動判定や増減をコントロールできるとよさそうだと思われました。

また、並列処理を行う対象のAppleScriptは、若干書き換える必要がありますが……書き換えを最低限で済ませられるような仕組みも作るとよさそうです(choose fileとかchoose folderのイベントハンドラをAppleScript自身で横取りするとできそう)。

スクリプト名:testScript
property mePath : “”
property myName : “”

on run
  set mePath to path to me
  
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
end run

–メイン側からのパラメータ受信用
on open aParamRec
  set mainProgram to mainName of aParamRec
  
set pList to paramList of aParamRec
  
  
set aRes to mainProgram(pList) of mainScript of me
  
  
ignoring application responses –ここが超重要
    tell application mainProgram
      open {subName:myName, resultList:aRes}
    end tell
  end ignoring
  
  
  
tell application “Finder”
    set label index of mePath to 6 –なんとなく、視覚的に処理が終わったことを表現してみた
  end tell
  
  
tell me to quit
  
end open

script mainScript
  on mainProgram(pList)
    –ここに並列処理するプログラムを記述する
    
    
–ここからダミー処理(無意味)
    
set randNum to (random number from 5 to 10)
    
delay randNum
    
–ダミー処理ここまで
    
    
return pList
    
  end mainProgram
end script

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

11/18 AppleScriptObjCでハマったこと

Appleからはほとんどドキュメントもサンプルも出てきていないので、実際にどう書いたらよいのかさっぱり分らなかったAppleScriptObjCですが、海外で電子媒体(PDF)の参考書が出てきたため、ようやく書き方や使い方が分ってきた今日このごろ。

book2.jpg
・Hamish Sanderson「Learn AppleScript」/Chapter 30「Creating Cocoa Applications with AppleScriptObjC」(50ページ、25ユーロ(だったか))
サンプルを多数掲載するのではなく、動作原理についてやさしく詳しく解説している入門書的な内容。図版がグレースケールで掲載されていることぐらいしか本書の入門書としての欠点が見つからない。かなり分りやすい。

book1.jpg
・Shane Stanley「AppleScriptObjC Explored」(132ページ、$US29.95)
サンプルプロジェクトが豊富なので、さわって動かして学習できる。ただし、細かい動作原理についてはあまり詳しく解説していないので、プログラムを組めるようにはなるものの疑問は残る。

……これらを参考に、AppleScriptObjCのプログラムを組めるようになってきたものの、やはり実戦で使ってみると分らないこともたくさんあります(MacScripterのフォーラムが情報源)。

これまでにハマったポイントをいくつかご紹介しましょう。AppleScript Studioのつもりで扱うと、いろいろとハマるポイントが……

■ウィンドウをクローズして再オープンしようとしたら固まる

アプリケーションのメインウィンドウ上のボタンをクリックして環境設定用のサブウインドウを表示。
環境設定ウィンドウを一度クローズして、再度オープンすると…………クローズボタンが押されたままの状態で固まっており、それ以降の操作を受け付けない。終了させるしかない。
いろいろ検索しまくってみたら、Interface Builder上で「Release When Closed」のチェックを「外す」とうまく動くことが分った。これは、一体どういうことなんだろう?

■eppcでLAN上のマシンをコントロールしようとしたらエラー

LAN上の他のMacをeppc://で指定してメッセージを送ってコントロールしようとしたら、アドレスが分らないと言われた。
AppleScriptエディタ上で実行する分には問題ない。

エラー回避のため、リモートマシンに命令する部分のAppleScriptだけテキストファイルに書き出して、osascriptコマンドでシェル経由で間接実行するようにしてみた。

この回避策はうまく行ったが、これがAppleScriptObjCの処理系のバグなのか、それともプロジェクト中のInfo.plistでeppcのURLプロトコルを宣言してあげないと通信がブロックされるのか、実際に試してみないとなんともいえない。ためしに、Info.plistにeppcのURLプロトコルを書いてみたが、状況に変化はなかった。

■「eppc://」のURLをshared user defautsに保存しようとして拒否された

LAN上のMacをコントロールするアプリケーションを作っていて、LAN上からマシンを選択するようにしてみた。

 choose URL showing {Remote applications}

でLAN上のプログラムリンクがオンになっているマシンを検索して、それをNSTextFieldにstringに変換しつつストア。
NSTextFieldの内容をShared User Defaultsにバインドして、そのまま初期設定ファイルに保存させようとしたら……ダメ。

  eppc://192.168.0.80:3031

のような(ただの)URLの文字列なのに、どうやっても保存できない。

さんざん試してみたところ、プロトコルを示す「eppc://」と、ポート番号を示す「:3031」を削除しないと保存できなかった。
AppleScript Studioではやりたい放題だったが、AppleScriptObjCではMac OS Xの機能の新たな一面を知ることになった。
っていうか、なんか微妙にめんどくさい。

ただ、出来上がったアプリケーションは「これ、本当にAppleScriptで作ってあるの?」という凝った動きができるし、GUIを操作する部分がやけに高速。そのうち、Mac OS X上でAppleScript Studioのアプリケーションが動かなくなるであろうことを考えると(Snow Leopard上では動く)、AppleScriptObjCへの乗り換えを次期OS「Lion」までに行っておくのがよいのではないでしょうか。

10/07 GUI ScriptingでWebコンテンツを操作

GUI Scriptingでできることは、Mac OS Xのバージョンアップとともに、徐々に広がりを見せつつありますが……実際に試してみて「ついにここまで来たか」と驚かされました。

思えば、Safari自体もバージョンアップにともなって、アクセシビリティ系の機能が向上。このあたりは、読み上げなどの機能がアップしたぐらいにしか考えていなかったのですが……よくよく考えると、アクセシビリティ系の機能がSafariを通じてWebコンテンツにアクセスできるのであれば、AppleScriptもGUI Scriptingを使ってWebコンテンツにアクセスできるのではないか?

やってみたら、大成功。「HTMLコンテンツ」の中にあるフォーム部品(radio button)や画像をクリックしたり、テキストで書かれていることを取得したり……自分には、DOM経由でアクセスするよりも簡単に操作できました(このへんは個人差が……)。

web1.jpg

UI BrowserでSafariのUIを調査……

web2.jpg

▲Safariの「HTMLコンテンツ」から、さらに下位階層を掘り下げていく……

web3.jpg

▲リンクのテキストにアクセス……

このサンプルは、本BlogをSafariでオープンして、ページ上部にあるメニュー「ABOUT」のリンクをクリックします。

この仕組みを利用して、Webブラウザに表示されているフォームにテスト用のデータをAppleScriptから投入し、連続テストを行うことも可能です。

ちなみに、このリストは「ブックマックバー」が非表示の状態で実行したものであり、「ブックマークバー」を表示させた場合には、リンクのクリック部分が、

click static text 1 of UI element 2 of list 1 of UI element 1 of scroll area 1 of group 2 of window 1

となります。

こういうことを考えると、(外部からコントロールできないことが確実な)Flashのコンテンツは勘弁してほしいという気に……。

スクリプト名:GUI ScriptingでWebコンテンツを操作するテスト
–AS HoleのURLをSafariでオープンする
set aURL to “http://piyocast.com/as/”
set webRes to openPageBySafari(aURL) of me

delay 5 –念のため、ページローディング&レンダリングを待つ

activate application “Safari”
tell application “System Events”
  tell process “Safari”
    –ページ上部にあるメニューから、「ABOUT」のテキストをクリックする
    
click static text 1 of UI element 2 of list 1 of UI element 1 of scroll area 1 of group 1 of window 1
  end tell
end tell

–指定のURLをSafariでオープンする
on openPageBySafari(aURL)
  tell application “Safari”
    set d to count every window
    
if d = 0 then
      make new window
      
tell document 1
        set URL to aURL
      end tell
    else
      if aURL is not equal to “” then
        tell document 1
          set URL to aURL
        end tell
      end if
    end if
    
set aRes to page_loaded(10) of me
  end tell
end openPageBySafari

–Safariでオープン中のURLのローディング完了を待つ
on page_loaded(timeout_value)
  delay 2
  
repeat with i from 1 to the timeout_value
    tell application “Safari”
      if (do JavaScript “document.readyState” in document 1) is “complete” then
        return true
      else if i is the timeout_value then
        return false
      else
        delay 1
      end if
    end tell
  end repeat
  
return false
end page_loaded

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

09/20 アプリケーションの状態を調べる

AppleScriptからアプリケーションの状態を調べたい、というニーズは多いようで……検索エンジンのキーワードで割と頻繁に調べられているようです。

個別のノウハウについては本Blog上で紹介していますが、体系的にノウハウを整理したことはないので、ここでまとめて紹介しておきます。

(1)アプリケーションがインストールされているかを調べる

アプリケーションの状態を調べるといっても、それがインストールされていなければ調べようがありません。最初にやるべきなのは、AppleScriptを実行するマシンにコントロール対象のアプリケーションがインストールされているかどうかを確認することです。

アプリケーションの存在確認は、Info.plistに書かれているCFBundleIdentifierを確認する必要があります。

appinfo1.jpg

Info.plistの内容確認は、Xcode Toolsをインストールすると一緒にインストールされる、「Property List Editor」で閲覧/編集が可能です。

appinfo2.jpg

Property List Editor上でキー値がそのまま表示されていない場合には、メニューの「View」から「Show Raw Keys/Values」を実行するとCFBundleIdentifierを確認できます。

appinfo4.jpg

appinfo3.jpg

このid値をもとに、Finderに対してapplicaion fileを求めると、インストールされている場合にはそのうち一番新しいバージョンのアプリケーションファイルへのaliasが得られます

(2)起動中のアプリケーションから情報を取得する

インストールされていることが確認できたら、launchなりactivateのコマンドを送って起動。そののち、起動中のアプリケーションから各種情報を取得する、というのが定石です。

たいていは、アプリケーションのプロパティを取得すれば、アプリケーションが持っている情報が得られます。ただし、ここで得られる情報はアプリケーション側のAppleScript対応度によってまちまちで、バージョン情報やアプリケーション名が分るだけ、というケースもあれば……現在選択中のオブジェクトの情報が分ったり、各種環境設定情報が分る場合もあります。

スクリプト名:アプリケーションのプロパティを取得する
tell application “iPhoto”
  properties
end tell

–> {last months album:album id 4.295966298E+9 of application “iPhoto”, selection:{album id 4.295966334E+9 of application “iPhoto”}, photo library album:album id 4.295966296E+9 of application “iPhoto”, name:”iPhoto”, version:”8.1.2″, frontmost:false, class:application, last import album:album id 4.295966297E+9 of application “iPhoto”,……(以下省略)

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

さらに、アプリケーションのプロパティを複数まとめて「properties」で取得できないアプリケーション(iTunesなど)もあるため、そういう場合にはAppleScript用語辞書をしらべて、ひとつひとつプロパティ値を取得する必要があります。

(3)System Events経由でプロセス情報を調べる

アプリケーションに対して直接問い合わせを行った場合に、環境設定情報やドキュメントの情報は取得できますが、アプリケーションプロセス自体の情報はあまり調べられません。

そこで、アプリケーション本体ではなく、System Eventsにプロセス情報を確認することになります。

スクリプト名:System Evewntsで指定IDのプロセスの状態を調べる
tell application “System Events”
  set pList to properties of first item of (every process whose bundle identifier = “com.apple.iPhoto”)
end tell
–>{enabled:missing value, unix id:46789, file:alias “Cherry:Applications:iPhoto.app:”, creator type:”iPho”, subrole:missing value, entire contents:{}, selected:missing value, application file:alias “Cherry:Applications:iPhoto.app:”, orientation:missing value, role:”AXApplication”, accepts high level events:true, file type:”APPL”, value:missing value, position:missing value, id:20128561, displayed name:”iPhoto”, name:”iPhoto”, class:application process, background only:false, frontmost:false, size:missing value, visible:true, Classic:false, role description:”アプリケーション”, maximum value:missing value, architecture:”i386″, partition space used:0, short name:”iPhoto”, focused:missing value, minimum value:missing value, help:missing value, title:”iPhoto”, accepts remote events:false, description:”アプリケーション”, total partition size:0, accessibility description:missing value, has scripting terminology:true, bundle identifier:”com.apple.iPhoto”}

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

ここでは、CFBundleIdentifierによってプロセスの特定を行っていますが、Mac OS X上で気をつけるべき点があります。たとえば、Adobe Illustratorであれば……複数の異なるバージョンをインストールして、同時に複数バージョンを起動しているケースがあるということです。

そのため、厳密にアプリケーションプロセスを指定する場合には、バージョン情報なども指定する必要も出てくることでしょう。

ただし、System Eventsから取得する意義があるのは、アプリケーションが最前面にあるかどうかを示す「frontmost」、可視状態になっているかどうかを示す「visible」、そしてUNIXから見た場合のプロセスIDである「unix id」ぐらいです。

unix idを取得できれば、あとはshellのpsコマンドをdo shell scriptコマンド経由で実行し(ps -lp [process id])アプリケーションプロセスの状態を調べられます。

ここまでくれば、PhotoshopだろうがiPhotoだろうがPreviewだろうが、単なるUNIXのプロセスとして扱えます。killするなりゾンビプロセスになっていないか調べるなり、CPUへの負荷状態を調べたりできることになります。

だいたい、AppleScriptでアプリケーションプロセスの「状態」を調べるノウハウというのは、こんな感じです。

09/19 openハンドラでファイルパス以外を渡す

読者の方からメールでお寄せいただいたプログラムの中に入っていて、「え? なにこれ?!」と驚いてしまったのが、このopenハンドラでファイルパス以外のものを渡すというやり方。

AppleScript Studioのアプリケーションに外部のAppleScriptから命令を渡すような場合に、コマンド入りのファイルを作成して、openハンドラ経由でそのファイルをAppleScript Studioアプリ側にオープンさせ、ファイルを経由してコントロール。

AppleScript Studioアプリケーションの内部ハンドラを外部からコントロールすることは事実上無理で、さまざまな迂回のための手段が考え出されてきましたが、ファイル経由でコマンド送信するのはなかなかいいやり方でした。……そう、処理スピードを考えなければ。

ファイルI/Oが毎回発生するので、スピードはいまひとつです。ただ、信頼性という意味では並列処理で連続して発生させても問題がなかったので、大丈夫だと考えています。

そのうえ、openハンドラをファイルI/Oを経由せずに呼び出せるのであれば、それに越したことはありません。

そんなところに飛び込んできた、openハンドラに渡せるのがファイルパスだけではないという情報。

さっそく、ためしてみました。

スクリプト名:openハンドラでファイルパス以外を渡す
set aRes to open {1, 2, 3}

on open {aFlag, bFlag, cFlag}
  display dialog aFlag as string
  
display dialog bFlag as string
  
display dialog cFlag as string
  
return true
end open

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

たしかに、openハンドラにファイルパス以外のものを渡しても大丈夫でした。ただ、openハンドラの受信パラメータに複数指定してしまうと、肝腎のファイルを(ドラッグ&ドロップで)渡したときにパラメータエラーになってしまいます。

drop1.jpg

そんなわけで、受信パラメータは1つに変更。その内容を判断して、処理するようにいろいろ試してみました。

スクリプト名:openハンドラでファイルパス以外を渡す 2
–recordをopenハンドラに渡すテスト
set aRec to {piyoID:10, piyoName:“piyopiyo”, piyoData:“ひよこさんだよ”}
set aRes to open aRec

–file aliasを単体でopenハンドラに渡すテスト
set aFile to choose file
set aRes to (open aFile)

–alias listをopenハンドラに渡すテスト
set aFol to choose folder
tell application “Finder”
  tell folder aFol
    set filesList to every file as alias list
  end tell
end tell
set aRes to open filesList

–レコード、リストに入ったaliasなどを受け付けるためのopenハンドラ
on open anObject
  set aClass to class of anObject as string
  
  
if aClass = “record” then
    set aID to piyoID of anObject
    
set aName to piyoName of anObject
    
set aData to piyoData of anObject
    
set aRes to ((aID as string) & return & aName & return & aData)
    
  else if aClass = “list” then
    –listが渡された場合
    
set bClass to (class of (first item of anObject)) as string
    
if bClass = “alias” then
      –alias listが渡されたものとして判定
      
set aRes to length of anObject –テストで長さを数えて返す。実際にはループでファイルを処理するような処理を行う
    end if
    
  else if aClass = “alias” then
    –エイリアス(ファイルパス)が単体で渡された場合。実際には、渡されたaliasのファイルをオープンしたりチェックしたりする
    
set aRes to anObject as string
    
  end if
  
  
return aRes
end open

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

04/27 AppleScriptによる並列処理テスト

AppleScriptで並列処理を行うサンプルです。メイン(GUI)側はAppleScript Studio(Xcode 3.1.4 on Mac OS X 10.5)、サブ側は通常のAppleScriptで記述しています。

子プログラムからメインプログラム(画面つきのプログラム)に向けて、ファイルシステム経由でイベントを6プロセス並列で送信。子プログラム側では1〜1000まで順次カウントアップし、すべての子プログラムがカウントアップを終了したら、その時間を表示するようになっています。

yyyeyoyycyayee2010-04-27-10458e.jpeg

→ テストプログラムをダウンロードする

使用方法は……アーカイブを展開したら、「docOpen1」アプリケーションを起動して、「START」ボタンを押すだけです。

newimage.jpg

結果は、手元のマシンでは……

  CoreDuo 2.0GHzのMacBook Pro:95〜100秒
  Core2Duo 2.4GHzのMacBook Pro:90〜95秒
  Core i7 2.66GHzのMacBook Pro:55〜60秒

といったところです(Mac OS X 10.6.3上で計測)。Core i7のMacBook Proでは6プロセス並行して走らせてもまだCPUに余力があり、HyperThreadingで仮想的に4コアのCPUとして振る舞えることが、こうしたタイプのプログラムの実行に有利に働いているようです。Core 2 Duo 2.4GHzの環境ではCPUの全能力を必要としていたAppleScriptのプログラムが、余裕で動いてしまう様にはほれぼれします>Core i7

yyyeyoyycyayee2010-04-27-10504e.jpeg
▲MacBook Pro Core i7 2.66GHzで本サンプルが稼働中の負荷

本サンプルの本当の目的は、並列処理時の子プログラムからメインプログラムへの状況・結果通知のテストでした。それぞれの子プログラムが並列でてんでバラバラにメッセージを投げた場合でも、イベントを取りこぼさずに正しく処理できることを確認できました。

パラメータをファイルに書き出して、そのファイルをメインプログラムでオープンさせるようにイベントを投げるというシンプルな処理ですが、並列処理時や長期間に渡る処理を行わせた場合の時間計測など、さまざまな用途が考えられます。実際に、ぜんぜん速度を要求されない「アドレスブック.app(のプラグインAppleScript)からAppleScript Studioアプリへのデータ転送」に、このファイルシステム経由のイベント送信を使って実装したことがあります。

本当は、こんなに短い間隔で連続的に子プログラム→メインプログラムの方向でイベントを送るようなシーンは考えていませんでした。かなりまとまった処理を子プログラムに行わせて、子プログラム側の処理終了後にその実行結果なりエラー内容なりを親プログラム側に伝えられればよい、というスタンスです。

さらに……本来であれば子プログラムの生成については、ダイナミックにその数を増減させられるのですが、GUIのプログラムに経過を表示する関係で、本サンプルでは6プロセス固定で行っています。

とても高価なため自分の手元にはないのですが、もっとCPUコア数の多いMacProなどで処理を行った場合の速度などが分かると有意義でしょう。速いマシンでの実行結果(秒数)については、コメント欄にて教えていただけると助かります。

02/17 AppleScriptにおける「パス」について(5)

もしも、現在お使いの環境がMac OS X 10.4.11などの古い環境(Mac OS X 10.5より前)の場合には、Mac OS Xのバグについても考慮しておく必要があります。

Mac OS X 10.5以前は、フォルダ名に濁点や半濁点を含む文字列があった場合……

  例:ぴよぴよ(半濁点を含む)
  例:びっくり(濁点を含む)

ファイル名/フォルダ名をFinderから取得して文字ごとに取り出すような処理をすると、「濁点と文字が分離されてしまう」というバグがありました。

filename.jpg

▲「Webログ」フォルダを選択した場合の実行結果(Mac OS X 10.4.11)

長い間、US Appleの担当者に散々文句を言って、ようやく10.5で直った次第です(ただ、その一方で10.5以降ではカタカナと平仮名の区別ができなくなるというおそるべきバグが……)。

回避方法は、一度stringにcastしてからunicode textにcastする、というものです。ものすごく、意味不明な処理を書かなくてはならず、本当にイライラするところですが……動かないよりはマシということで、長年この記述を行ってきました。いまでも、Mac OS X 10.4.11上で動作させる必要のあるAppleScriptは、そのような処理を書いています。

02/17 AppleScriptにおける「パス」について(4)

Mac OS X上でパスを記述する際に、気をつけるべき点があります。それが、ローカライズド・フォルダ。基本的かつ重要なフォルダについては、それぞれの言語環境に合わせてローカライズした名前をユーザーに見せる仕組みがローカライズド・フォルダですが……裏を返せば、Finder上で見えてる名前と本当の名前が違う場合がある、というわけです。

ふだん見ている「書類」や「デスクトップ」といったフォルダ名は、本当のフォルダ名ではありません。Finder上でカスタムアイコンが付いているようなフォルダは軒並みローカライズド・フォルダで表示されており、実際には「Documents」とか「Desktop」という名前がついています。

このあたり、AppleScript上でパスを記述する際に問題になるかもしれません(普段から英語で表記していたので、その存在を忘れていました)。

「フォルダの本当の名前」を確認するのに一番よいのは、Terminal.appを起動して確認するとか……あるいは、ちょっとしたAppleScriptを記述して

  set a to choose folder

実際にAppleScriptから「本当の名前」を確認してみることです。

02/17 AppleScriptにおける「パス」について(3)

昨今のOSはほとんどそうですが、Mac OS XはマルチユーザーのOS(複数ユーザーの使用を前提としたOS)であるため、ユーザーごとにホームディレクトリが異なります。また、Finder上でハードディスクの名前もユーザーが自由に変えられるようになっています。

このため、ホームディレクトリへのパスや、その他の重要なフォルダへのパスについても、プログラム上に直接文字列で書いておくと、他のマシンや他のユーザー環境で実行できなくなってしまいます。

そこで、途中までのパスについてはコンピュータからpath to命令で取得し、その後のパスのみ文字列で書いておき、それらを結合するというのが一般的なパスの記述方法です。

set picFol to path to pictures folder from user domain
–> alias “Cherry:Users:maro:Pictures:”

さらに、上記サンプルで出てきていますが、「ドメイン」の概念が必要になります。
たとえば、「ライブラリ」フォルダというものは、

  Macintosh HD:システム:ライブラリ:
  Macintosh HD:ライブラリ:
  Macintosh HD:Users:hiyoko:ライブラリ:

の3種類があって、それぞれ「system domain」、「local domain」、「user domain」のドメイン指定によって指定できます。

スクリプト名:ライブラリフォルダを各ドメインで取得
set libFol to path to library folder from system domain
–> alias "Macintosh HD:System:Library:"

set libFol to path to library folder from local domain
–> alias "Macintosh HD:Library:"

set libFol to path to library folder from user domain
–> alias "Macintosh HD:Users:maro:Library:"

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

たいていは、自分のユーザーディレクトリの下にあるフォルダを扱う場合がほとんどなので、user domainを指定することになるでしょう。

02/17 AppleScriptにおける「パス」について(2)

AppleScriptにおけるパス記述方式は、Mac OS形式、POSIX path形式、URL形式の3つがあります。出現頻度および重要性もこの並びのとおりです。

■Mac OS形式

フォルダ名を「:」で区切って記述する、Classic Mac OSから引き継がれたパス記述形式。AppleScriptの世界でもっともよく使われます。「東京都練馬区中村橋」という住所をMac OS形式で記述すれば、

  東京都:練馬区:中村橋:

になります(中村橋がファイルではなくフォルダの場合)。「中村橋」がファイルである場合には、

  東京都:練馬区:中村橋

になります。

ハードディスクの名称が「Macintosh HD」で、ユーザーが「maro」の場合のホームディレクトリは、

  Macintosh HD:Users:maro:

と表現されます。aliasはこの形式で表現されます。

■POSIX path形式

UNIXの世界で用いられるパス記述形式です。ディレクトリ(フォルダ)階層を「/」で区切ります。また、パスの途中にスペースなどが入る場合には、パス全体をクォートでくくる必要があります。

さきほどの東京都練馬区中村橋は、

  /東京都/練馬区/中村橋/

のように表現されます。特筆すべき点は、このPOSIX pathはルート「/」を頂点とするツリー構造であり、HDDの名前が変わろうがその構造は変わりません。

ハードディスクの名称が「Macintosh HD」で、ユーザーが「maro」の場合のホームディレクトリは、

  /Users/maro/

ハードディスクの名称が「Hiyoko」で、ユーザーが「maro」の場合のホームディレクトリも、

  /Users/maro/

という形式で表現されます。AppleScriptの世界におけるパスはMac OS形式で統一されることが望ましいところですが、shellコマンドを呼び出すような場合には必要になりますし、一部の出来の悪いアプリケーションがこのpathを必要とします(CocoaではPOSIX pathが使われるため)。パス処理関連で代表的な出来の悪いアプリケーションとして、Adobe Acrobat DistillerとKeynoteがあります。

とくに、Adobe Acrobat Distillerでは、POSIX形式のパスにクォート処理を行って渡すとエラーになるなど、本当にチェックしてから発売しているのか疑問に思えるほどです。

path1.jpg

Keynoteでは、ファイルのオープンにaliasを要求するのに、画像を連続配置するmake image slidesコマンドではPOSIX pathを要求します。

path2.jpg

path3.jpg

こうした、出来の悪いアプリケーションが存在することを意識したうえで、Mac OS形式とPOSIX path形式を使い分ける必要があります。また、これらの形式は相互変換できるように命令が用意されています。

■URL形式

ほとんど登場する機会はないのですが、Webブラウザなどにファイルを指定するような場合に使います。基本的にはPOSIX pathと同じですが、先頭にプロトコルを示す「file://」が付くことと、マシンのローカルに存在していることを示すために、localhostからパスが始まります。

ハードディスクの名称が「Macintosh HD」で、ユーザーが「maro」の場合のホームディレクトリは、

  file://localhost/Users/maro/

という形式で表現されます。URL形式では空白文字や一部の特殊記号、日本語の文字列などはURLエンコードされるため、パスの途中に空白文字を含むフォルダが存在する場合でも、クォート処理を行う必要はありません。

set a to choose folder

tell application “Finder”
  set aInfo to URL of a
end tell

–> “file://localhost/Users/maro/“

なお、AppleScriptの標準命令ではURL形式からMac OS形式に戻すことができません。本Blog上に掲載している「URLエンコードされたファイルパスを元のテキスト(POSIX Path)に戻す」サブルーチンを用いてPOSIX pathに変換し、そこからaliasに変換することはできます。

02/16 AppleScriptで扱う「パス」について(1)

AppleScriptにかぎらず、HDD内のどこかに入っているファイルとか、自分のホームフォルダの「書類」フォルダとか、意図したものを指し示して処理を行いたいという場合がものすごく多くあります。

一般的に、そういうファイルシステム中における階層的な位置情報を「パス」と呼びます。英語でいうとpathです。

path.jpg

パスは住所の表記によく似ています。ただ、人に伝える住所はちょっと間違いがあってもなんとかなりますが、プログラム中に記述するパスには一切間違いが許されません。

「東京都練馬区中村橋」という住所があったとして、人に伝える場合には「とうきょうと 練馬く中村橋」でも伝わりますが、プログラム中ではこれが1文字間違っただけでも別の住所と見なされることになります。たとえ、空白文字(スペース)が抜けたとか、そうしたささいな間違いも許容されません(1バイトのアルファベットの大文字小文字の違いは同一視されます。これは、Mac OS Xが標準採用しているHFS plusの仕様です)。

そんなわけで、プログラム中にパスを書いておくのは、けっこう骨が折れる作業です。

さらに、プログラムを他人のマシン上でも動作させることを考えると、HDDの名称やユーザー名も違う可能性が高いので、ユーザーの環境情報を取得しつつ、パスを組み立てるという作業が必要になります。このあたり、昔のClassic Mac OSでは必要のなかった話ですが、Mac OS Xになって少々複雑に(ちょっとだけですが)なった話です。

02/10 フォルダの選択(内容確認つき)v2

ちょっと気の利いた複数folder選択」を発展させ、選択したフォルダの一覧を確認表示するようにしたAppleScriptです。

最大の見どころは、フォルダパスを一覧表示するさいに、どのパスかを示す文字の長さをそろえて、フォルダパスの内容確認を行いやすくした「umekusaKit」です。

dialog1.jpg

これで、選択した複数パスの一覧を表示し、処理前に確認するように書いておけば、ささいなミスもなくなるに違いありません。

ちなみに、この一覧表示で「Cancel」ボタンをクリックすると……

dialog2.jpg

のように、処理を中断します。

どうも、プログラムを見直してみると……もっと汎用的なサブルーチンに仕立てて、もっと簡単に利用できるようにできそうな気がします。まだ、手直しできる余地が大きそうですが、使用頻度はあまり高くなさそうなので……このままかもしれません。

スクリプト名:フォルダの選択(内容確認つき)v2
property aFol : missing value
property bFol : missing value
property cFol : missing value

set a1Mes to “コマの書類の親フォルダ”
set b1Mes to “ページアップ先のフォルダ”
set c1Mes to “ページ台紙が入っているフォルダ”

set a2Mes to “(komaFolderServerPath)を選択”
set b2Mes to “(pageUpOutputTargetPath)を選択”
set c2Mes to “(pageDaishiFolder)を選択”

if aFol = missing value then
  set aFol to choose folder with prompt (a1Mes & a2Mes)
else
  set aFol to choose folder default location aFol with prompt (a1Mes & a2Mes)
end if

if bFol = missing value then
  set bFol to choose folder with prompt (b1Mes & b2Mes)
else
  set bFol to choose folder default location bFol with prompt (b1Mes & b2Mes)
end if

if cFol = missing value then
  set cFol to choose folder with prompt (c1Mes & c2Mes)
else
  set cFol to choose folder default location cFol with prompt (c1Mes & c2Mes)
end if

–設定内容の確認表示
set aaList to {a1Mes, b1Mes, c1Mes}
set {r1, r2, r3} to makeSameString(aaList, “ ”) of umekusaKit –ウメクサ文字には全角スペースを指定

set aList to {r1 & “ = ” & aFol as string, r2 & “ = ” & bFol as string, r3 & “ = ” & cFol as string}
set defaultItem to contents of item 1 of aList

–最終確認
set aRes to choose from list aList with prompt “この設定でよいですか?” default items defaultItem with title “指定内容の最終確認”
if aRes = false then
  display dialog “処理を中止します” buttons {“OK”} default button 1 with icon 1 with title “処理中止”
  
return
end if

–このあとにメインの処理が入る

–文字リストをchoose from listで表示させる際に長さを揃えるために作った
script umekusaKit
  –最大の長さの文字列に合わせて、ウメクサ文字を後ろに追加する
  
–fillCharは1文字であることを期待している
  
–全角文字と半角文字が「混在しない」ことを期待している。あるいは、半角文字の文字長が一緒とか
  
on makeSameString(aList, fillChar)
    –リスト中の文字列長の最大値を求める
    
set lenList to {}
    
repeat with i in aList
      set the end of lenList to length of (contents of i)
    end repeat
    
set maxLen to item (maximumItemNoFromList(lenList) of me) of lenList
    
    
–最大長に合わせて各項目の末尾にfillCharを追加する
    
set retList to {}
    
repeat with i in aList
      set newStr to contents of i
      
set aLen to length of i
      
repeat with ii from 1 to (maxLen - aLen)
        set newStr to newStr & fillChar
      end repeat
      
set the end of retList to newStr
    end repeat
    
    
return retList
  end makeSameString
  
  
–最大値の項目番号を取得する
  
on maximumItemNoFromList(nList)
    script o
      property nl : nList
      
property itemNo : 1
    end script
    
    
set max to item 1 of o’s nl
    
repeat with i from 2 to (count nList)
      set n to item i of o’s nl
      
if n > max then
        set max to n
        
set o’s itemNo to i
      end if
    end repeat
    
return o’s itemNo
  end maximumItemNoFromList
end script

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

02/04 Preview.appで用紙サイズおよび用紙方向を指定して印刷 v2

GUI Scripting経由でPreview.appで印刷を実行。プリンタ名、用紙サイズ、用紙方向を指定して印刷を行うAppleScriptです。

GUI Scriptingを使用しているため、「システム環境設定」の「ユニバーサルアクセス」で、「補助装置にアクセスできるようにする」をオンにしている必要があります。

ただし、使用プリンタは各ユーザー環境によって異なるため、そのまま使えるものとは思わないでください。

また、Preview.appで「画像オープン→印刷→画像クローズ」といった連続したフローを実現するものでもありません。Preview.appで画像をオープンすると、WindowのIDが画像をオープンするたびに+1されるようで、画像を1枚しかオープンしていないのにwindow 2を指定する必要があるとか、そういう状況を考慮した作りにはなっていません。

スクリプト名:Preview.appで用紙サイズおよび用紙方向を指定して印刷 v2
(*
2010/1/19

v2
Preview.appでは、書類をオープンするたびにWindowのインデックスがインクリメントされるらしく、
1枚目の書類はwindow 1でアクセスできるが、2枚目の書類(1枚しかウィンドウは開いていない)に
対しては、Window 2でアクセスする必要がある。これに対処する必要がある(問題未解決)

v1
シンプルタイプのプリントダイアログから拡張プリントダイアログに大きさをひろげたケースと、
最初から拡張プリントダイアログが表示されているケースでは、ポップアップメニューのIDが異なる。

拡張ダイアログが表示されていなかったら、拡張表示にして一度キャンセルし、再試行するようにした(10回まで)

*)

–set directionF to "V" –用紙方向=たて方向
set directionF to "H" –用紙方向=たて方向

set printerName to "PS-NX650"
set paperSize to "A4"

printViaGUI(directionF, printerName, paperSize) of me

on printViaGUI(directionF, printerName, paperSize)
  activate application "Preview"
  
tell application "System Events"
    tell process "プレビュー"
      
      
set goF to false
      
repeat 10 times
        –メニューから印刷を実行
        
click menu item "プリント…" of menu 1 of menu bar item "ファイル" of menu bar 1
        
        
tell window 1
          tell sheet 1
            –拡張プリントダイアログを表示させる
            
set sheetClosed to value of checkbox 1
            
if sheetClosed = 0 then
              click checkbox 1
              
click button "キャンセル"
            else
              set goF to true
              
exit repeat
            end if
          end tell
        end tell
        
      end repeat
      
      
if goF = false then return
    end tell
  end tell
  
  
  
  
  
activate application "Preview"
  
tell application "System Events"
    tell process "プレビュー"
      
      
–メニューから印刷を実行
      
click menu item "プリント…" of menu 1 of menu bar item "ファイル" of menu bar 1
      
      
tell window 1
        tell sheet 1
          
          
–出力先のプリンタを指定
          
tell pop up button 4 –「プリンタ」ポップアップボタン
            set aTitle to value
            
if aTitle is not equal to printerName then
              click
              
tell menu 1
                set mList to title of every menu item
                
set aRes1 to retIHitItem(mList, printerName as Unicode text) of me
                
if aRes1 = false then
                  return mList
                end if
                
click menu item aRes1
                
              end tell
            end if
          end tell
          
          
          
–用紙サイズを変更
          
tell pop up button 1
            
            
set aTitle to value as string
            
if aTitle is not equal to paperSize then
              click
              
tell menu 1
                set mList to value of every menu item
                
                
set aLen to length of mList
                
select menu item 1 –最終アイテムを選択しておく
                
                
set aRes2 to retIHitItem(mList, paperSize as string) of me
                
if aRes2 = false then
                  return false
                end if
                
click menu item aRes2
              end tell
            end if
          end tell
          
          
–用紙方向を指定
          
if directionF = "V" then
            click radio button 1 of radio group 3
          else if directionF = "H" then
            click radio button 2 of radio group 3
          end if
          
          
–「用紙サイズに合わせる」をクリック
          
click radio button 2 of radio group 1
          
          
–「プリント」ボタンをクリック
          
–click button "プリント"–ここをコメントアウトすると印刷を行う。
          
beep
        end tell
      end tell
    end tell
  end tell
end printViaGUI

–指定リスト中の何アイテム目かを調べて返す
on retIHitItem(aList, anItem)
  set aCount to 1
  
set hitF to false
  
repeat with i in aList
    set j to contents of i
    
if j = anItem then
      set hitF to true
      
exit repeat
    end if
    
set aCount to aCount + 1
  end repeat
  
  
if hitF = false then return false
  
  
return aCount
end retIHitItem

–GUI Scriptingの命令が続く中でdelayコマンドをエラーになるので、
–wait部分だけサブルーチン化
on waitSec(aNum)
  delay aNum
end waitSec

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

02/03 Preview.appでプリンタと用紙サイズを指定して印刷

一般的なAppleScriptの命令を使用して、特定のプリンタへの印刷は実行できますが、用紙サイズまでは指定できません。そこで、仕方なくGUI Scriptingを使用して印刷を実行することになります。

ここで、「仕方なく」という前置きが入るのは、GUI Scriptingという仕組みが通常のAppleScriptによる命令よりも遅く、また確実性が劣るためです。確実性が高くないといっても、OSやアプリのクラッシュを伴うようなレベルではなく、アプリケーション側の状態を詳細に調べながら操作しないと「思い通りに動かない」という話です。

■GUI Scriptingを使えるようにするために

GUI Scriptingが動作するためには、システム環境設定の「ユニバーサルアクセス」で「補助装置にアクセスできるようにする」にチェックが入っている必要があります。GUI Scriptingは使用に注意を要する機能であるため、デフォルト状態ではオフになっています。

sysprop.jpg

このチェックボックスをオンにすれば、GUI Scripting系の命令を実行できるようになります(プログラムを作成するだけであれば、オンにする必要はありません)。

■GUI Scriptingとは何なのか?

OS標準で搭載されているSystem Events.appが持っている「Process Suites」という命令群が、GUI Scripting(=UI Element Scripting)の命令です。他のSystem Eventsの命令については、システム環境設定/ユニバーサルアクセスでオンにしなくても普通に使えます。

sys2.jpg

一般的なAppleScriptでは、アプケーション内部のオブジェクトを経由して機能を呼び出すのに対し、GUI Scriptingはアプリケーションの外部からGUI部品を指定して命令を行います。このため、あるボタンを押そうとしたときに、もちろんオンになっていないと押せないですし、アプリケーション内部の状態がそのボタンがオンになる条件を満たしている必要もあります。

簡単な記述でアプリケーションをコントロールできるというのがGUI Scriptingの「理想」ではあるものの、実際に書いてみると……通常のAppleScriptとはまた別のマニアックなノウハウが必要な記法、と見るべきです。内部命令を経由しないでGUI ScriptingだけですべてのAppleScriptのプログラムを作成するというのが「幻想」というより「無理」であることはよく覚えておいてほしいと思います。通常のAppleScriptの「補助」的に使うのがGUI Scriptingの正しい使い方です。

■GUI Scriptingをはじめる前に

いろいろと前置きが長くなってしまいましたが、アプリケーションを無理矢理操作するため、「奥の手」としてGUI Scriptingが必要になるケースは確実にあります。そういう意味では、不完全なソリューションではあるものの、「じゃあいらないんだね?」とGUI Scriptingになくなられると困ります。ものすごく困ります。

GUI Scriptingでは、画面上のGUI部品を指定して命令を記述することになりますが、この部品の番号や並び順を知るためには、どうしてもツールが必要です。

(1)PreFab UI Browser
仕事でScriptを書く必要があるとか、確実かつ迅速にGUI Scriptingによるプログラムを作成する場合には必要です。ただし、このソフトでカバーできるのは8割程度であり、あとの残り2割をカバーするためには後述の「Accessibility Inspector」が必要になります(同ツールはXcode Toolsに含まれるため、Xcode Toolsのインストールが必須です)。

sys4.jpg

(2)Accessibility Inspector
Xcode Toolsをインストールすると、一緒に入ってきます。PreFab UI Browserでカバーできない一部のGUI部品の情報を調べるために必要です。具体的に言ってしまうと、PreFab UI Browserではポップアップメニューの内容までは追えません。とくに、ポップアップボタン「ではない」GUI部品からポップアップメニューを表示させるようなイレギュラーなプログラムでは本ツールの併用が必要です。

sys3.jpg

(3)「強引GUI Scripting」のためのツール
記述のために情報を調べるツールではなく、AppleScriptから命令して強引にマウスカーソルを移動させたりクリック動作させるようなツールです。ツールによって一長一短があるので、複数用意して用途に応じて使い分けるのが理想的です。プロのScripterはこの手のツールをいくつも手元に置いてあった、いざという時に備えています。何を使っているか、どの程度のAppleScriptライブラリを書きためているか等の情報はもちろんのこと、存在自体が門外不出のノウハウといえます。

これらのツールがそろってはじめて安心してGUI Scriptingのプログラムを作りはじめられます。……ない環境では、とても書く気になれません。

■アプリケーションの挙動を追う

GUI Scriptingでは、アプリケーションの状態を正確に認識した上で記述を行う必要があります。特定のウィンドウ上の特定のTabの中にあるGUI部品を操作する場合には、そのTabが表示されていなければなりません。ポップアップメニュー上の項目を選択するためには、ポップアップメニューを表示させておかなければなりません。通常のAppleScriptによるアプリケーション内部オブジェクト経由のコントロールの方が簡単と思えるほど、アプリケーションによってGUI部品の挙動が異なるため、正確な状況把握は欠かせません(PreFab UI Browser必須)。

■GUI部品の指定はIDか名前で

GUI ScriptingでGUI部品を指定する際には、window 1やmenu 1のように番号(ID)で指定する場合と、button “OK”のように名前で指定する場合があります。

名前で指定できたほうが、何を意味しているのか分りやすくてよさそうなものなのですが、名前で指定できるケースはあまり多くありませんし、だいたい……現在使用中の言語(日本語とか、英語とか)から別の言語環境でScriptを実行するような場合にはまったく動かなくなってしまいます。

このため、IDで指定することが多いのですが……アプリケーションがバージョンアップしたら、そのIDそのものも変わってしまうことが多々あり……GUI部品を検索して実行するような対策を行うケースもあります。

■実際に「プレビュー.app」のGUI Scriptingを行ってみよう

まずは、プレビュー.appで何も画像を開いていない状態で作業をはじめましょう。何もオープンしていない状態と画像をオープンしている状態でWindowの枚数がどう違うか、確認を行います。

何か画像をプレビュー.appでオープンすると、visibleなWindowの枚数が増えます。invisibleなwindowはほかにも何枚か用意しているようなので、ここでvisibleを指定しないで枚数を数えるとひどい目に遭います。

Windowの枚数だけでなく、画像をオープンしていない状態ではメニューの「プリント」の項目が有効になっていません。このため、「プリント」をGUI Scriptingで操作しようとしても、実行できません。このあたりで(オープンされている画像の有無を)判断するという手もあるわけです。

画像をオープンしている状態でメニューから「プリント」を実行できることを確認できたら、プリントダイアログの制御に入ります。

プリントダイアログは、デフォルトの小さいダイアログと拡張ダイアログで切り換えができるようになっており、この切り換えボタンの状態を取得して、「いま、どちらのダイアログが表示されているのか」を調べなければなりません。拡張ダイアログが表示されていないと、用紙サイズなどの詳細な情報にアクセスできません。

sys5.jpg

sys6.jpg

拡張ダイアログを表示して、用紙サイズやプリンタ名などを指定できるようになって、ようやくこれで出来上がった……と、安心していたのもつかの間……標準ダイアログの状態から拡張ダイアログに切り替えた後で、ポップアップメニューの操作が行えない。最初から拡張ダイアログが出ている分には問題ないのに……。

結論からいえば、同じGUI部品でも標準ダイアログと拡張の上でIDが異なっていました。さりとて、GUI部品に名前がついておらず名前による指定ができなかったので……拡張ダイアログが「出ていない」場合には、拡張ダイアログに切り換えたあとで、一度プリントをキャンセルし、再度プリント操作を行って拡張ダイアログ上の各種GUI部品をコントロールする……という方法で対処しました。

ちょっとしたことであれば、GUI Scriptingを少々かじったぐらいで処理できるかもしれません。ただし、そこに柔軟性を加えようとか、確実に動くようにしたい場合には、このぐらいの処理が必要になります。少々極端な例に見えるかもしれませんが、決して珍しいレベルではありません。

PreFab UI Browserを使って状態やIDを確認し、試行錯誤した末に最適な方法を見つけるなど、地道な……何かプログラミングとは別の作業のような気もするのですが、GUI Scriptingの実戦的なノウハウというのは、こういう感じのものです。

02/03 タイマー割り込み処理

AppleScriptには、タイマー割り込みの機能があります。タイマー割り込みとは、一定間隔で処理を行うことで……メインの処理を行っていても(割り込みを行って)、途中でその処理を行います。

タイマー割り込みがあるあたり、単なるスクリプト言語の処理系「らしからぬ」雰囲気ですが……たしかに、タイマー割り込みが活躍するケースは多いので、この機能があるのは助かります。

ただし、いくつか知っておくべきことがあります。

(1)タイマー割り込みは、AppleScriptをアプリケーション形式で保存して実行したときにしか効かない

timer1.jpg

AppleScriptエディタ上で実行したり、Script Menuから呼び出した場合には使用できません。アプリケーションとして保存し、アプリケーションとして起動された場合に限ります。

(2)タイマー割り込みは、「実行後、自動的に終了しない」をオンにしないと意味がない

timer2.jpg

すぐに終わるようにしている場合には、タイマー割り込みは効きません。

(3)タイマー割り込みは、秒単位でしか呼び出せない
0.1秒単位とか、1/64秒間隔で呼び出すといったことはできません。仮にできたとしても、あまり短い間隔で呼び出されると逆にメイン側の処理効率が落ちます(AppleScriptObjCでやってみたら、CPU負荷が増えてしまいました)。

(4)時間が長くかかる割り込み処理を実行中に、後続の割り込み処理を行わせてはいけない
10秒間隔で割り込み処理を呼び出すようにしているときに、実行完了に1分かかる処理を行うような場合……割り込み処理が完了する前に後続の割り込み処理が呼び出されてしまいます。

処理内容に矛盾が生じてしまったり、不具合が生じてしまう可能性があるため、後続の割り込みをブロックし、処理が終わったら割り込みを許可するようにしなくてはなりません。このあたり、割り込み処理の「基本」なのですが……AppleScriptの処理系では面倒を見てくれないので、自前で書いておく必要があります。

スクリプト名:タイマー割り込み
property enableF : true –タイマー割り込みイネーブルフラグ
property intervalSec : 1 –タイマー割り込み発生間隔(秒)
property vList : {“Agnes”, “Albert”, “Alex”, “Bad News”, “Bahh”, “Bells”, “Boing”, “Bruce”, “Bubbles”, “Cellos”, “Deranged”, “Fred”, “Good News”, “Hysterical”, “Junior”, “Kathy”, “Pipe Organ”, “Princess”, “Ralph”, “Trinoids”, “Vicki”, “Victoria”, “Whisper”, “Zarvox”}

–起動処理
on run
  set intervalSec to 5 –割り込み間隔を5秒に変更
  
set enableF to true –割り込みイネーブルフラグをtrue(割り込み可能)に変更
end run

–割り込みサービスハンドラ
on idle
  –割り込み処理メインで処理が終了するまで、タイマー割り込みは許可しない
  
if enableF = true then
    set enableF to false
    
    
idleMain() of me
    
    
–タイマー割り込みメインの処理が終わったら、割り込みを許可する
    
set enableF to true
  end if
  
  
–次回の割り込みを呼び出す相対秒数をリターンする(必須)
  
return intervalSec
  
end idle

–タイマー割り込み処理メイン
on idleMain()
  set aChar to contents of (some item of vList) –乱数選択
  
repeat 3 times
    say “Say Hello to piomal software.” using (aChar as text)
  end repeat
end idleMain

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

01/30 ちょっと気の利いた複数folder選択

仕事用にかなり凝ったAppleScriptのプログラムを作成したような場合には、処理対象のデータを自由に指定できるよう、処理先をchoose folderやchoose fileで指定することがあります。プログラム内にパスを固定で記述するよりも自由度が高く、ユーザーフレンドリーな処理になります。

ただ……1つのAppleScript中でいくつもフォルダを選択する必要があったりすると、そのたびにフォルダ選択ダイアログで処理対象のフォルダを指定しなければなりません。しかも、Folder Aを選択したあとでFolder Bを選択しようとすると、Folder Aの場所からスタートしなくてはなりません。

これはけっこう面倒です。

dialog1.jpeg

dialog2.jpeg

そこで、choose folderで一度フォルダを選択したら、propertyの値に保持しておいて、各フォルダ選択ダイアログで個別に記録。二度目からはそれぞれ前回指定したフォルダを勝手に指定するよう、些細な改善を行ってみました。

その結果、大幅な操作性の改善を行うことができ、いくつフォルダ選択ダイアログが出てきても、うっとおしくなくなりました(たいていにおいて、前回と同じ場所を指定することのほうが多く、目視で確認してリターンキーを押していくだけ)。

スクリプト名:ちょっと気の利いた複数folder選択
property folderA : missing value
property folderB : missing value

if folderA is not equal to missing value then
  –前回の選択内容を使用
  
set folderA to choose folder default location folderA with prompt “folder Aを選択”
else
  –はじめて実行する場合など
  
set folderA to choose folder with prompt “folder Aを選択”
end if

if folderB is not equal to missing value then
  –前回の選択内容を使用
  
set folderB to choose folder default location folderB with prompt “folder Bを選択”
else
  –はじめて実行する場合など
  
set folderB to choose folder with prompt “folder Bを選択”
end if

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

01/27 lengthの同義語number of items

AppleScriptで配列変数(AppleScript的には「リスト型変数」)の要素数をカウントする場合には、「length of リスト型変数」で取得しますが、AppleのサンプルScriptを読んでいたら、「number of items of リスト型変数」という記述があるのを見つけました。

AppleScriptには同義語やら「使っても使わなくてもかまわない語」(theとか)などがあるので、人によってプログラムの書き方がバラバラで、記述の流派が違うとまったくプログラムを読めない(理解できない)場合があります。

とりあえず、numbers of itemsでも要素数をカウントできることは確認しました。

スクリプト名:number of itemsのじっけん(1)
set aList to {1, 2, 3, 4}

set aLen to number of items of aList
–> 4

set bLen to length of aList
–> 4

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

実際のところ、スピードはどうなんでしょう? なんとなく、感覚的にlength ofのほうが速そうな感じはするのですが……この程度の命令だと一瞬で終わってしまうので、1000万回実行してようやく意味のある差が出てきました。

気持ち分length ofのほうが速いですが、そもそも1000万回もループを回すような処理はAppleScriptでは行わないでしょうから、そんなに気にしなくてよさそうです(計測はMacBook Pro Core 2 Duo 2.4GHzにて実施)。

スクリプト名:number of itemsのじっけん(2)
set aList to {1, 2, 3, 4}

set sTime to current date

repeat 10000000 times
  set bLen to length of aList
end repeat

set eTime to current date
set elapsedTime to eTime - sTime

–1000万回ループ時に
–number of items で23秒
–length で14秒

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

12/14 オブジェクトの複数同時指定

AppleScriptにおける高速化手法のうちの1つに、「複数オブジェクト同時アクセス」と呼ぶべきものがあります。

たとえば、Finderで指定フォルダ中の特定のアプリケーション書類を取得して、別のフォルダに移動したいといった場合に、取得した書類パスのリストをもとにループさせるとそれなりに時間がかかりますが、実はFinderのファイル移動命令のパラメータとして「リストに入ったパスそのもの」が使えるので、複数のファイルを一度に移動させたりできます。

同様に、Mail.appの特定のメッセージを別のメールボックス(フォルダ)に移動させる際に、「複数のメッセージが入ったリスト」を指定でき、一度の移動命令で複数のメールを移動させることができます。

AppleScript Studioでは、Window上のGUI部品……たとえば400個ボタンに対して同時にvisibleを指定したり、titleを指定したりできます(AppleScriptObjCでは……まだ、そこまでテストできていません)。

こうした複数オブジェクト同時アクセスを利用すれば、AppleScriptで格段に高速な処理を実現できます。

どのアプリケーションで複数オブジェクト同時指定ができるのかについては、とりあえず思いつくものはこの程度です。実際に試してみないと使えるかどうか分らない……記述できるかどうかは、あくまでアプリケーション側の対応度次第。AppleScriptが難解だとよく言われますが、実際にはアプリケーション側のScripting対応機能がまちまちであるために、一貫性がない言語と感じられてしまうことが理由になっていることでしょう。そして、「アプリケーションごとにScripting対応度がまちまちだ」という傾向はScripting言語がAppleScript以外のJavaScriptやRubyであっても大差はないものと思われます。

12/06 AppleScriptでwaitを入れる

AppleScriptでwaitを入れる(一定時間待つ)処理については、1993〜1994年のAppleScript登場当初から標準装備されている「delay」コマンドで行えるわけですが、意外と知られていないようです。

  delay 数値

で、指定秒数だけ待ちます。この、一見何をするでもない「待つ」という処理が、実はものすごく重要でさまざまな問題の解決方法になることがあります。とくに、アプリケーションに対してイベントを送って命令するAppleScriptのような処理系になれば、なおさらです。

コマンドを送ったけれど、相手側のアプリケーション内部の計算が間に合わない。だけど、アプリケーション側はコマンド実行に対して「OK」の応答をすでに行ってしまい、それを真に受けて次の命令を実行すると期待どおりの結果が得られなかったりエラーになったりする………そこで、マシンの処理性能とアプリケーションの処理内容を勘案して適度にdelayコマンドでウェイトを入れると……何事もなかったかのように処理を実行できる。そんな場面に遭遇したことが何度かあります(GUI Scriptingにその傾向が強い)。

なお、昔はdelayコマンドで整数値しか指定できませんでしたが、いまは0.1刻みで小数の値も指定できるようになっており、

 delay 0.1

といった記述もできます(AppleScriptのdateオブジェクトで秒単位以下の時間計測は行えないので、本当に0.1秒待っているのかは不明)。

Intel MacになってAppleScriptの処理速度が大幅に向上したこともあり、delayに小数値が指定できるようになったことは意義深いのですが、そうであればなおのこと、タイマ割り込み用のon idleハンドラで小数単位での秒数間隔の割り込みが入れられるようにしてほしいものです。

AppleScriptObjCではon idleハンドラそのものではないものの、小数秒単位での割り込みルーチンなどがAppleScriptだけで実装した例も発表されており、実際にサンプルでテストしてみたりしていますが…………やっぱり、AppleScriptで0.1秒間隔の割り込みを入れると処理が重くなって、CPUのロードアベレージが20%程度(Core 2 Duo 2.4GHz)は行ってしまうので、やや抑え気味に0.2秒単位での割り込みで抑えていたりします。

11/08 継続記号(continuation mark)の使用

AppleScriptには、プログラムの1行が長くなった場合に、可読性を維持するため途中で改行を行う「継続記号」(continuation mark)というものがあります。

継続記号は、キーボードから[option]+[L]で入力できます。

continu1.jpg

継続記号が入っても、プログラムの意味が変わるわけではありません。

継続記号が入力できるのは、あくまで命令やデータの区切り目であり、ダブルクォートで区切られた文字列データの途中などには入力できません。

ただし、例によってMac OS X 10.4までのAppleScript Studio環境でこの継続記号を使おうとしても、構文確認時にハネられてきたので、本Blogでも極力使わないようにしています。