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

## シバン

`osascript` コマンドを利用する。

    #!/usr/bin/osascript -l JavaScript

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

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

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

## 引数

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

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

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

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

## 環境変数

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

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

## 外部コマンド実行

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

```javascript
#!/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`を使用して関数定義する。

```javascript
function gets() {
  return $.NSString.alloc.initWithDataEncoding($.NSFileHandle.fileHandleWithStandardInput.availableData, $.NSUTF8StringEncoding).js
}
```

```javascript
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)
}
```

```javascript
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)
}
```

使用例。

```javascript
#!/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()`で呼び出す。

```javascript
#!/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](https://qiita-image-store.s3.amazonaws.com/0/91569/261f23a2-89a0-1b3f-6291-cec4c95f4d2e.jpeg)

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

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

```javascript
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`を用意した。


```javascript
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.js`で`Require()`のコードを読み込み`eval()`で宣言する形を取る。そうすると`Require()`は OSAS のライブラリ機構と無関係になり JavaScript の型情報を意図通りに扱うことが出来るようになる。

![02.jpg](https://qiita-image-store.s3.amazonaws.com/0/91569/5f1b64fa-78ca-015d-3af8-f8de423bf116.jpeg)

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

```javascript: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()`記述をコードから追い出す為に

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

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

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

追記：

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

```sh:/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
```

```js:hello.js
#!/usr/bin/env jxascript
ObjC.import('stdlib')
Sys = Require('Sys')
Sys.puts('hello again')
$.exit(0)
```

[jxa-cookbook]: https://github.com/dtinth/JXA-Cookbook/wiki/Importing-Scripts