Skip to content

Instantly share code, notes, and snippets.

@uchcode
Last active April 14, 2022 13:22
Show Gist options
  • Save uchcode/73d438052cd4ede3abe0 to your computer and use it in GitHub Desktop.
Save uchcode/73d438052cd4ede3abe0 to your computer and use it in GitHub Desktop.
Building OS X shell script with JavaScript

JXAでシェルスクリプト

OS X 10.10 から JavaScript for Automation (JXA) が利用可能になった。ここでは、その Automation 機能をシェルスクリプトとして利用するステップを記述する。

シバン

シェルスクリプトとして利用するので、その前提はターミナルでテキストファイルとして記述する。その際にはファイルを実行するコマンドが何かを記載するシバンが必要。JXA の場合は osascript コマンドを利用する。

#!/usr/bin/env osascript -l JavaScript

標準ライブラリー

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

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

引数

引数を扱う方法は幾つかある。その中で一番単純な方法はメイン処理の run() を定義すること。その関数の引数がコマンドラインの引数となる。

function run(args) {
    args.forEach(function(arg, idx) {
    	switch (arg) {
        case 'foo':
    	    break
        default:
        	break
        }
    })
}

DoShellScript()

JXA は外部コマンドを実行する doShellScript() が用意されている。利用は以下の手順で。

app = Application.currentApplication()
app.includeStandardAdditions = true
cmd = 'ls -l /usr/bin | sort | head -n 5'
app.doShellScript(cmd, {
    administratorPrivileges: false,
    withPrompt: '',
    alteringLineEndings: false
})

標準入出力・エラー出力

JXA は Objective-Cオブジェクトを簡単に呼べるので、 NSFileHandle を利用する。

std = {
    input: function() {
        var input = $.NSFileHandle.fileHandleWithStandardInput.availableData
        return $.NSString.alloc.initWithDataEncoding(input, $.NSUTF8StringEncoding).js
    },
    output: function(str) {
        var data = $(str).dataUsingEncoding($.NSUTF8StringEncoding)
        if (data) $.NSFileHandle.fileHandleWithStandardOutput.writeData(data)
    },
    error: function(str) {
        var data = $(str).dataUsingEncoding($.NSUTF8StringEncoding)
        if (data) $.NSFileHandle.fileHandleWithStandardError.writeData(data)
    }
}

console を拡張してもいいかもしれない。

console.input = function() {
    var input = $.NSFileHandle.fileHandleWithStandardInput.availableData
    return $.NSString.alloc.initWithDataEncoding(input, $.NSUTF8StringEncoding).js
}

console.output = function(str) {
    var data = $(str).dataUsingEncoding($.NSUTF8StringEncoding)
    if (data) $.NSFileHandle.fileHandleWithStandardOutput.writeData(data)
}

console.error = function(str) {
    var data = $(str).dataUsingEncoding($.NSUTF8StringEncoding)
    if (data) $.NSFileHandle.fileHandleWithStandardError.writeData(data)
}

使い方の例:

function run(args) {
    console.log(`arguments: ${args}`)
    do {
        std.output('input answer: ')
        var input = std.input()
        console.log( input )
    } while (input != '\n')
    std.output('これはスタンダードメッセージ\n')
    std.error('これはエラーメッセージ')
}

シェルスクリプト例

具体的なサンプルとして jupyter のコントロールコマンド jupyterctl を挙げる。

jupyterctl.js

#!/usr/bin/env osascript -l JavaScript
ObjC.import('stdlib')

app = Application.currentApplication()
app.includeStandardAdditions = true

function sh(command, promptText) {
    try {
        return app.doShellScript(command, { 
            administratorPrivileges: promptText ? true : false,
            withPrompt: promptText ? promptText : '',
            alteringLineEndings: false
        })
    } catch(e) {
        console.log(e)
        return null
    }
}

function start() {
    return sh("/bin/bash --login -c 'cd ~/usr/workspace/anyenv/python/; jupyter-notebook --no-browser >> ~/usr/log/jupyter.log 2>&1' &>/dev/null &")
}

function stop() {
    return sh("ps aux | grep [j]upyter-notebook | awk '{print $2}' | sort | xargs kill")
}

function status() {
    return sh('ps aux | grep [j]upyter-notebook') != null
}

function run(args) {
    var error = 0
    if (args.length != 1) { 
        args = ['help']
    }
    args.forEach(function(arg, i) {
        switch (arg) {
        case 'start':
            error = Jupyter.start() != null ? 0 : 1
            break
        case 'stop':
            error = Jupyter.stop() != null ? 0 : 1
            break
        case 'restart':
            if (Jupyter.stop() == null) { error = 1; break }
            delay(1)
            error = Jupyter.start() != null ? 0 : 1
            break
        case 'status':
            var status = Jupyter.status(); if (status == null) {
                error = 1; break
            }
            if (status) {
                console.log('jupyter is running...')
            } else {
                console.log('jupyter is stop.')
            }
            break
        default:
            console.log('Usage: jupyterctl {start|stop|restart|status|help}')
            break
        }
    })
    $.exit(error)
}

ライブラリーとコマンドに分割

~/Library/Script Libraries/Common.scpt

app = Application.currentApplication()
app.includeStandardAdditions = true

function sh(command, promptText) {
    try {
        return app.doShellScript(command, { 
            administratorPrivileges: promptText ? true : false,
            withPrompt: promptText ? promptText : '',
            alteringLineEndings: false
        })
    } catch(e) {
        console.log(e)
        return null
    }
}

~/Library/Script Libraries/Jupyter.scpt

CommonLib = Library('Common')
sh = CommonLib.sh

function start() {
    return sh("/bin/bash --login -c 'cd ~/usr/workspace/anyenv/python/; jupyter-notebook --no-browser >> ~/usr/log/jupyter.log 2>&1' &>/dev/null &")
}

function stop() {
    return sh("ps aux | grep [j]upyter-notebook | awk '{print $2}' | sort | xargs kill")
}

function status() {
    return sh('ps aux | grep [j]upyter-notebook') != null
}

jupyterctl

#!/usr/bin/env osascript -l JavaScript
ObjC.import('stdlib')

Jupyter = Library('Jupyter')

function run(args) {
    var error = 0
    if (args.length != 1) { 
        args = ['help']
    }
    args.forEach(function(arg, i) {
        switch (arg) {
        case 'start':
            error = Jupyter.start() != null ? 0 : 1
            break
        case 'stop':
            error = Jupyter.stop() != null ? 0 : 1
            break
        case 'restart':
            if (Jupyter.stop() == null) { error = 1; break }
            delay(1)
            error = Jupyter.start() != null ? 0 : 1
            break
        case 'status':
            var status = Jupyter.status(); if (status == null) {
                error = 1; break
            }
            if (status) {
                console.log('jupyter is running...')
            } else {
                console.log('jupyter is stop.')
            }
            break
        default:
            console.log('Usage: jupyterctl {start|stop|restart|status|help}')
            break
        }
    })
    $.exit(error)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment