ラベル AppleScriptObjC の投稿を表示しています。 すべての投稿を表示
ラベル AppleScriptObjC の投稿を表示しています。 すべての投稿を表示

2010年9月13日月曜日

NSRange オブジェクト

AppleScriptObjC では Cocoa が呼び出せるので、Cocoa クラスのインスタンスを作成する時は alloc() すれば済みます。 ところが、NSRange のように、インライン関数 NSMakeRange() を使ってインスタンスを作成するものは、undocumented な部分が多く、よくわかっていません。なお、AppleScript で range と言うと … n thru m … のことですが、これは NSRange ではありません。

NSRange を使うサンプルコードを示します。このコードは、ファイル名のディレクトリ部分を得る ("/usr/local/bin/script.sh" が渡されたとき、"/usr/local/bin" を取得する) ものです。UNIX のコマンドで言うと、dirname に相当します。

property NSString : class "NSString"
property NSFileManager : class "NSFileManager"

on foo(filename)
  set str to ((NSString's alloc())'s initWithString_(filename))'s autorelease()
  set pc to str's pathComponents()
  set rng to {0, (pc's |count|()) - 1}
  set cwd to NSString's pathWithComponents_(pc's subarrayWithRange_(rng))
  set mng to NSFileManager's defaultManager()
  mng's changeCurrentDirectoryPath_(cwd)
end foo

このコードは、以下のように動きます。

  1. ファイル名の文字列 filename を NSString にして、変数 str に代入します。AppleScript で扱っている文字列は NSString ではないことがあるので、NSString の機能を使いたいときは、このような変換をします。
  2. pathComponents() で、str を / ごとに分解した文字列の入った NSArray を取得し、変数 pc に代入します。"/usr/local/bin/script.sh" は [ "/", "usr", "local", "bin", "script.sh" ] に分解されます。
  3. 変数 pc に入っている配列から、最後の要素 "script.sh" を取り除いた NSArray を作ります。この範囲 (0 から、配列の要素数 - 1) を NSRange で指定します。とは言え、単なるリストを作るだけです。リストの第一要素が開始位置、第二要素が長さを表しています。作成したリストを rng に代入しています。
  4. subarrayWithRange_() で、3 で作ったリストで範囲を指定し、配列から必要な部分を取り出します。これを元に新しい NSString を pathWithComponents_() で作成し、変数 cwd に代入します。
  5. NSFileManager を使って、カレントディレクトリを cwd に変更します。

結局、subarrayWithRange_() を呼び出す時に、AppleScriptObjC が強制的に型変換のようなことを行っているようなのですが、この辺のルールがよくわからないのです。とりあえず、動いているのでよしとします。

なお、Cocoa のメソッドを呼び出した時に、NSRange オブジェクトが返却された場合、AppleScriptObjC からはレコードとして見えるので、開始位置は |location| of aRange 、長さは |length| of aRange のようにアクセスします。

2010年6月28日月曜日

自作アクションでエラーを返す

AppleScriptObC で書いた Automator の自作アクションでエラーを返すには、次のようにやればよいようです。

script ErrorTest
  property parent : class "AMBundleAction"
  property NSDictionary : class "NSDictionary"
 
  on runWithInput_fromAction_error_(input, anAction, errorRef)
    my fatalError(-1750, "your own error message", errorRef)
    return missing value
  end runWithInput_fromAction_error_

  on fatalError(errorNum, msg, errorRef)
    set objsArray to {errorNum, msg}
    set keysArray to {current application's OSAScriptErrorNumberKey, current application's OSAScriptErrorMessageKey}
    set contents of errorRef to NSDictionary's dictionaryWithObjects_forKeys_(objsArray, keysArray)
  end fatalError
end script

errorRef は NSDictionary のポインタのポインタなので、contents of を使って、作ったオブジェクトを渡すところがキモです。

エラー番号 -1750 は MacError.h に errOSASystemError と宣言されていて、それを使うのがベストです。AppleScriptObjC の定数参照の要領で、current application's errOSASystemError とか書いてみたのですが、だめでした。MacError.h は CarbonCore のヘッダファイルなので、Carbon Framework をリンクしてみたのですが、やはりだめでした。

情報をお持ちの方がいらっしゃいましたら、コメントをください。

2010年6月20日日曜日

Automator アクションでの子プロセスの扱い

Automator のアクションで子プロセスを走らせたとき、子プロセスを管理するのはアクション側の責任になります。Automator のワークフローはユーザの操作によって停止することがありますが、その場合、子プロセスを終了させるのは、アクションの実装者の責任です。

AppleScriptObjC で自作アクションを書けますが、ナイーブに do shell script を使って子プロセスを起動させると、停止させる術がありません。NSTask を使って自分で管理するのが良さそうです。

ユーザの停止操作によって、AMAction から継承した stop メソッドが呼ばれますので、そこで動いている子プロセスを停止させます。テンプレートとしては、こんな感じでしょうか。

script my_Automator_Action

  property parent : class "AMBundleAction"
  property NSTask : class "NSTask"
  property NSFileHandle : class "NSFileHandle"
  property task : missing value
 
  -- @override
  on runWithInput_fromAction_error_(input, anAction, errorRef)
    ...
    set command to POSIX Path of "..."
    set args to {}
    set the end of args to "argument 1"
    set the end of args to "argument 2"
    ...
    my runExternalTask(command, args)
    ...
  end runWithInput_fromAction_error_

  -- @override
  on |stop|()
    if task is not missing value then
      task's terminate()
    end if
  end |stop|
 
  -- Instead of 'do shell script', use this method to prepare for abortion by user operations.
  on runExternalTask(command, args)
    set nulldev to NSFileHandle's fileHandleWithNullDevice()
    set task to (NSTask's alloc)'s init()
    task's setLaunchPath_(command)
    task's setArguments_(args)
    task's setStandardInput_(nulldev)
    task's setStandardOutput_(nulldev)
    task's setStandardError_(nulldev)
    task's |launch|()
    task's waitUntilExit()
    task's autorelease()
    set task to missing value
  end runExternalTask

end script

気付いた点をいくつか。

  • NSTask の setArguments: は NSArray を受け取ることになっていますが、以下のようにAppleScript の文字列のリストをそのまま渡しても問題ないようです。ただし、リストの中に数値などが入っていた場合、自動的に文字列に変換してくれず、エラーになります。
  • 標準出力と標準エラー出力を /dev/null へつないでおかないと、コンソールに全部出てしまいます。
  • do shell script は一旦シェルを経由するので、リダイレクトやパイプの指定ができます。同様のことを行うには、NSPipe や NSFileHandle 等を使って自力でやるか、一旦シェルを呼び出すようにします。
  • do shell script は標準出力の内容を戻り値として取得できますが、NSPipe を使って自力で取得する必要があります。

2010年1月24日日曜日

メモ: オリジナルの Automator アクションを作る

Mac OS X 10.6 では、Xcode 3.2 を使います。Mac OS X 10.6 から AppleScript Studio が deprecated になり、AppleScriptObjC を使うことになりました。簡単に言うと、AppleScript で Objective-C のクラスを書けるように、うまいこと対応を付けたものです。これによって、Automator の実行速度が上がったり、ユーザにとってはいいことずくめですが、問題は、今までの Automator アクションの作り方と大幅に変わってしまったことです。現時点では Google 先生が教えてくれる情報は、あまりアテになりません。Apple もドキュメンテーションをサボっていて、手探り状態です。

まず、知っておかないといけないのは、Xcode 3.2 が作ったテンプレートの違いです。Mac OS X 10.5 用のアクションは、以下のように、単に run ハンドラに input と parameters が渡されるだけでした。

on run {input, parameters}
  -- Add your code here, returning the data to be passed to the next action.

 return input
 
end run

これからは、myaction.script にはこんなコードが生成されます。

script myaction
 property parent : class "AMBundleAction"

 on runWithInput_fromAction_error_(input, anAction, errorRef)
  -- Add your code here, returning the data to be passed to the next action.

  return input
 end runWithInput_fromAction_error_
end script

script 〜 end script は見慣れないブロックです。これは、Objective-C のクラス定義で、myaction というクラスを宣言しています。2行目の property parent の行は AMBundleAction を継承をしていることを宣言しています。これにより、NSObject → AMAction → AMAppleScriptAction → AMBundleAction → myaction という継承ツリーを持ちます。on runWithInput_fromAction_error_ 〜 end runWithInput_fromAction_error_ はメソッドの定義です。オリジナルのアクションは、runWithInput:fromAction:error: をオーバーライドして作るという形になります。

AppleScript しか知らなくて、ピンと来ない人は、まずは、以下のようにテンプレートを埋めましょう。

script myaction
 property parent : class "AMBundleAction"
 
 on runWithInput_fromAction_error_(input, anAction, errorRef)
  -- Add your code here, returning the data to be passed to the next action.
  set input to input as list
  set output to {}
  repeat with i in input

   -- do something for i

  end repeat
  return output
 end runWithInput_fromAction_error_
 
end script

これで、入力ファイルに対して、順番に何かをする、という処理が書けると思います。アクションの出力は、ここではファイルと仮定しますが、POSIX File の文字列を output のリストに詰め込んで返却すれば OK です。

パラメタの参照

パラメタは以前は run ハンドラの引数に渡されていました。これからは、AMBundleAction から継承した parameters() メソッドを使って、パラメタの値が入ったレコードを取得します。個々のパラメタには AMDefaultParameters (Info.plist にあります) に設定したキーでアクセスできます。例えば、foo というキーを持つパラメタは foo of parameters() でアクセスできます。ただし、取得した値は、as text や as number などで、型をきっちり指定してください。パラメタと GUI のバインディングなどは、これまでと同じなので省略します。

アクションのバンドル

アクションのバンドルの中のファイル (リソース) の参照には path to resource は使えません。これだと Automator.app のバンドルを参照してしまいます。AMBundleAction から継承した bundle() メソッドを使って、一旦 NSBundle オブジェクトを取得したあと、NSBundle のメソッドを使ってアクセスします。名前の衝突を防ぐために、縦棒 (|) で囲って |bundle|() とやるとアクションのバンドルオブジェクトが取得できますので、それに対して、pathForResource:ofType:等を使ってリソースのパスを取得します。具体的には、以下のようになります。

set action_bundle to |bundle|()
set aPath to action_bundle's pathForResource_ofType_("image", "tiff")

エラーの返却方法などは、まだよく分かっていません。この辺になにやら書いてありますが、NSNumber と NSString を NSArray に突っ込んで返してるけど、NSError を返却するんじゃないの? という疑問が…。この情報も古いのかもしれません…。