シバン
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 になってしまうケースがあること。
他の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.js
でRequire()
のコードを読み込みeval()
で宣言する形を取る。そうするとRequire()
は OSAS のライブラリ機構と無関係になり JavaScript の型情報を意図通りに扱うことが出来るようになる。
以下のコードも意図通りに動く。
#!/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/bin/env sh
(cat "$HOME/Library/Script Libraries/Require.js" && sed -e "s/^#!.*$//g" "$1") | /usr/bin/osascript -l JavaScript
#!/usr/bin/env jxascript
ObjC.import('stdlib')
Sys = Require('Sys')
Sys.puts('hello again')
$.exit(0)