LoginSignup
13
13

More than 5 years have passed since last update.

JXAでコマンドラインツールを書く為のメモ

Last updated at Posted at 2016-05-28

シバン

osascript コマンドを利用する。

#!/usr/bin/osascript -l JavaScript

コマンドの正常・異常終了

コマンド終了の為にObjective-Cライブラリをインポートして $.exit() を利用可能にする。

ObjC.import('stdlib')
error = 0
$.exit(error)

引数

引数を扱う方法は二つ。簡単な方法はrun()ハンドラーを定義すること。その関数の引数がコマンドラインの引数となる。

function run(arguments) {
  arguments.forEach(function(arg, idx) {
    console.log(arg)
  })
}

NSProcessInfoを使えばObjective-CやSwiftと同等に処理できる。

$.NSProcessInfo.processInfo.arguments.js.forEach(function(arg, idx){
  console.log(arg.js)
})

環境変数

環境変数もNSProcessInfoを使用する。

environment = ObjC.deepUnwrap($.NSProcessInfo.processInfo.environment)
for(var prop in environment){
  console.log(`${prop}: ${environment[prop]}`)
}

外部コマンド実行

JXA は外部コマンドを実行する doShellScript() が用意されている。使用例は以下の通り。

#!/usr/bin/osascript -l JavaScript
ObjC.import('stdlib')
app = Application.currentApplication()
app.includeStandardAdditions = true
cmd = 'ls -l /usr/bin | sort | head -n 5'
result = app.doShellScript(cmd, {
    administratorPrivileges: false,
    withPrompt: '',
    alteringLineEndings: false
})
console.log(result)
$.exit(0)

標準入出力・エラー出力

console.log()は標準エラー出力だったり標準入出力は特に用意されていないようなのでNSFileHandleを使用して関数定義する。

function gets() {
  return $.NSString.alloc.initWithDataEncoding($.NSFileHandle.fileHandleWithStandardInput.availableData, $.NSUTF8StringEncoding).js
}
function puts(obj, opt) {
  if (obj === false) return $.NSFileHandle.fileHandleWithStandardOutput.writeData($('false\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (obj === 0) return $.NSFileHandle.fileHandleWithStandardOutput.writeData($('0\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (!obj) return $.NSFileHandle.fileHandleWithStandardOutput.writeData($('\n').dataUsingEncoding($.NSUTF8StringEncoding))
  const term = opt != null && opt.terminator !== null ? opt.terminator : '\n'
  const data = obj ? $(obj.toString()+term).dataUsingEncoding($.NSUTF8StringEncoding) : null
  if (data) $.NSFileHandle.fileHandleWithStandardOutput.writeData(data)
}
function errs(obj, opt) {
  if (obj === false) return $.NSFileHandle.fileHandleWithStandardError.writeData($('false\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (obj === 0) return $.NSFileHandle.fileHandleWithStandardError.writeData($('0\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (!obj) return $.NSFileHandle.fileHandleWithStandardError.writeData($('\n').dataUsingEncoding($.NSUTF8StringEncoding))
  const term = opt != null && opt.terminator !== null ? opt.terminator : '\n'
  const data = obj ? $(obj.toString()+term).dataUsingEncoding($.NSUTF8StringEncoding) : null
  if (data) $.NSFileHandle.fileHandleWithStandardError.writeData(data)
}

使用例。

#!/usr/bin/osascript -l JavaScript
ObjC.import('stdlib')
function gets() {
  return $.NSString.alloc.initWithDataEncoding($.NSFileHandle.fileHandleWithStandardInput.availableData, $.NSUTF8StringEncoding).js
}
function puts(obj, opt) {
  if (obj === false) return $.NSFileHandle.fileHandleWithStandardOutput.writeData($('false\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (obj === 0) return $.NSFileHandle.fileHandleWithStandardOutput.writeData($('0\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (!obj) return $.NSFileHandle.fileHandleWithStandardOutput.writeData($('\n').dataUsingEncoding($.NSUTF8StringEncoding))
  const term = opt != null && opt.terminator !== null ? opt.terminator : '\n'
  const data = obj ? $(obj.toString()+term).dataUsingEncoding($.NSUTF8StringEncoding) : null
  if (data) $.NSFileHandle.fileHandleWithStandardOutput.writeData(data)
}
function errs(obj, opt) {
  if (obj === false) return $.NSFileHandle.fileHandleWithStandardError.writeData($('false\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (obj === 0) return $.NSFileHandle.fileHandleWithStandardError.writeData($('0\n').dataUsingEncoding($.NSUTF8StringEncoding))
  if (!obj) return $.NSFileHandle.fileHandleWithStandardError.writeData($('\n').dataUsingEncoding($.NSUTF8StringEncoding))
  const term = opt != null && opt.terminator !== null ? opt.terminator : '\n'
  const data = obj ? $(obj.toString()+term).dataUsingEncoding($.NSUTF8StringEncoding) : null
  if (data) $.NSFileHandle.fileHandleWithStandardError.writeData(data)
}
do {
  puts('input answer: ', { terminator: '' })
  input = gets()
  puts(input)
} while (input != '\n')
puts('this is standard message.')
errs('this is error message.')
$.exit(0)

ライブラリシステム

OSAS用のLibrary()が用意されているのでこれを使用する。OSAS用の機構なので AppleScript で定義したライブラリも利用可能。

先ほどの標準入出力で示した関数をそのまま~/Library/Script Libraries/StandardIO.jsに記述。作成したスクリプトをLibrary()で呼び出す。

#!/usr/bin/osascript -l JavaScript
ObjC.import('stdlib')
stdio = Library('StandardIO.js')
puts = stdio.puts
puts('hello world.')
$.exit(0)

しかしこのLibrary()にはいくつかの問題が。

単純なライブラリならLibrary()の利用で問題はないが OSAS用の機構なので複雑な JavaScript を定義するのに向いていない。

致命的なのは JavaScript オブジェクトの型情報が失われて function が object になってしまうケースがあること。

01.jpg

他のJavaScript実装のようにimportがあれば良いのだが JXA には用意されていない(import キーワードとして予約済みなので将来のサポートが期待される)。

ObjC と AppleEvents の記述が容易に同居可能な魅力ある実装なので問題解決の為にRequire()を考えてみた。実装はJXA-Cookbookを参考にした。

function Require(fname) {
  function LibPath(fname, resourcePath) {
    const l = '/Script Libraries/'
    const n = resourcePath + l + fname
    const p = $(n).pathExtension.js ? n : n+'.js'
    return $(p).stringByStandardizingPath.js
  }
  const Finder = Application('Finder')
  var f
  f = LibPath(fname, $.NSBundle.mainBundle.resourcePath.js)
  if ( !Finder.exists( Path(f) ) ) {
    f = LibPath(fname, '~/Library')
    if ( !Finder.exists( Path(f) ) ) {
      f = LibPath(fname, '/Library')
      if ( !Finder.exists( Path(f) ) ) {
        throw `File '${fname}.js' not found in libs.`
      }
    }
  }
  const p = $(f)
  const u = $.NSUTF8StringEncoding
  var   e = $()
  const c = $.NSString.stringWithContentsOfFileEncodingError(p, u, e).js
  if (e.js) throw $.NSString.stringWithFormat('%@', e).js
  const module   = {exports: {}}
  const exports  = module.exports
  eval(c)
  return module.exports
}

このままでは先に示した通り型情報が失われてしまう。そこで~/Library/Script Libraries/JXAReader.jsを用意した。

function read(fname) {
  function LibPath(fname, resourcePath) {
    const l = '/Script Libraries/'
    const n = resourcePath + l + fname
    const p = $(n).pathExtension.js ? n : n+'.js'
    return $(p).stringByStandardizingPath.js
  }
  const Finder = Application('Finder')
  var f
  f = LibPath(fname, $.NSBundle.mainBundle.resourcePath.js)
  if ( !Finder.exists( Path(f) ) ) {
    f = LibPath(fname, '~/Library')
    if ( !Finder.exists( Path(f) ) ) {
      f = LibPath(fname, '/Library')
      if ( !Finder.exists( Path(f) ) ) {
        throw `read('${fname}'): File not found.`
      }
    }
  }
  const p = $(f)
  const u = $.NSUTF8StringEncoding
  var   e = $()
  const c = $.NSString.stringWithContentsOfFileEncodingError(p, u, e).js
  if (e.js) throw $.NSString.stringWithFormat('%@', e).js
  return c
}

JXAReader.jsRequire()のコードを読み込みeval()で宣言する形を取る。そうするとRequire()は OSAS のライブラリ機構と無関係になり JavaScript の型情報を意図通りに扱うことが出来るようになる。

02.jpg

以下のコードも意図通りに動く。

hello.js
#!/usr/bin/osascript -l JavaScript
eval( Library('JXAReader.js').read('Require.js') )
ObjC.import('stdlib')
Sys = Require('Sys')
Sys.puts('hello again')
$.exit(0)

最後にeval()記述をコードから追い出す為に

(echo "eval( Library('JXAReader.js').read('Require.js') )" && cat hello.js) | osascript -l JavaScript

と実行すれば今後書くコードにスニペットを貼り付けるような手間は無くなる。

不恰好でデバッグもしづらいという問題は残るが、これで一先ずは node.js 相当のライブラリ構築が可能なはず。

追記:

JXAReader()は流石に手間がかかりすぎるので素直にコマンドを作ることにした。

/usr/local/bin/jxascript
#!/usr/bin/env sh
(cat "$HOME/Library/Script Libraries/Require.js" && sed -e "s/^#!.*$//g" "$1") | /usr/bin/osascript -l JavaScript
hello.js
#!/usr/bin/env jxascript
ObjC.import('stdlib')
Sys = Require('Sys')
Sys.puts('hello again')
$.exit(0)
13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13