Go言語でJavaScriptを使ってオリジナルなコマンドシェルを作る

# 概要
MongoDBのCLIツールであるmongoシェルを使っていると、JavaScriptをコマンドシェル（REPL）をとして使うのも良い思います。それならNode.jsでも良いかもしれませんが、Go言語はシングルバイナリなところが気に入っています。今回使用するJavaScriptエンジンはECMAScript 5.1の仕様かつNode.jsとは違いI/O関係など実装されていないので、実用的にするためにはかなりGo言語で拡張しなければなりません。しかし、いろいろ拡張することでGo言語の勉強にもなり、実用的なツールも手にすることも可能です。

# 環境

- Windows 10
- Go 1.12 
- Go JavaScript パッケージ 
     - github.com/dop251/goja (ECMAScript 5.1)
     - Javascriptエンジンはほかに `otto`, ~~`go-duptake`~~ `go-duktape`などがある

# 最低限の実装

- コマンドの入力・実行。実行結果の表示
- console.logもprintもないのでprintの実装
- exitコマンドで終了
- 継続行のサポート
    - 継続行のpromptは`...> `とします
    - 継続のキャンセルは`...`と入力します

実行例


```javascript
go run main.go
> for(var i=0; i < 5; i++) {
...> print(i)
...> }
0
1
2
3
4
undefined
> doc={a:1, b:"xxxx"}
{"a":1,"b":"xxxx"}
> Object.keys(doc)
["a","b"]
> exit
```


実行例、特に実用的なものは実装していないので簡単なものしかできません。JavaScriptの仕様はECMAScript 5.1なので現在使わているのものより古いです。


## ソースコード

```go:main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"

	"github.com/dop251/goja"
)

type jsRuntime struct {
	runtime   *goja.Runtime
	stringify goja.Callable
	program   *goja.Program
}

func scanCommand(scanner *bufio.Scanner, prompt string) bool {
	fmt.Print(prompt)
	return scanner.Scan()
}

func (js *jsRuntime) jsprint(vals ...goja.Value) {
	rt := js.runtime
	format := "%v"
	for _, val := range vals {
		str, ok := val.Export().(string)
		if ok {
			fmt.Printf(format, str)
			format = " %v"
			continue
		}
		v, err := js.stringify(goja.Undefined(), val)
		if err != nil {
			rt.Interrupt(rt.NewGoError(err))
			return
		}
		fmt.Printf(format, v)
		format = " %v"
	}
	fmt.Println()
}

func (js *jsRuntime) setStringify() {
	rt := js.runtime
	json := rt.Get("JSON")
	jsonObj, ok := json.(*goja.Object)
	if !ok {
		panic("JSON not defined")
	}
	stringifyProp := jsonObj.Get("stringify")
	stringify, ok := goja.AssertFunction(stringifyProp)
	if !ok {
		panic("JSON.stringify not defined")
	}
	js.stringify = stringify
}

func initialSetting() *jsRuntime {
	rt := goja.New()
	js := &jsRuntime{runtime: rt}
	rt.Set("print", js.jsprint)
	js.setStringify()
	return js
}

func (js *jsRuntime) execute() (goja.Value, error) {
	return js.runtime.RunProgram(js.program)
}

const (
	execInput = iota
	contInput
	errorInput
)

func (js *jsRuntime) compile(cmd string) int {
	program, err := goja.Compile("console", cmd, false)
	if err != nil {
		idx := strings.Index(err.Error(), "Unexpected end of input")
		if idx == -1 {
			fmt.Println(err)
			return errorInput
		}
		return contInput
	}
	js.program = program
	return execInput
}

func main() {
	prompt := "> "
	js := initialSetting()
	scanner := bufio.NewScanner(os.Stdin)
	var cmds []string
	for scanCommand(scanner, prompt) {
		cmd := scanner.Text()
		if cmd == "exit" {
			break
		} else if cmd == "..." {
			cmds = nil
			prompt = "> "
			continue
		}
		cmds = append(cmds, cmd)
		switch js.compile(strings.Join(cmds, "\n")) {
		case execInput:
			ret, err := js.execute()
			if err == nil {
				js.jsprint(ret)
			} else {
				fmt.Println(err)
			}
			cmds = nil
			prompt = "> "
		case contInput:
			prompt = "...> "
		case errorInput:
			cmds = nil
			prompt = "> "
		}
	}
}
```





### 解説


- コンソールから入力されたTEXTを`scanner`で取得してCompileしてエラーが無ければRunProgramで実行します。
    - `exit`が入力されたらプログラムを終了します
    - `RunProgram()`の戻り値あるいはエラーを表示します
- 行を継続のためにはCompileしてみて`"Unexpected end of input"`というエラーなら入力を継続する
- 各関数で今実行中の`goja.Runtime`が必要なので`jsRuntime`構造体を定義します。
- Goの関数をJavaScriptで使えるようするには`*goja.Runtime`の`Set`を使いGlobalな関数として定義します。オブジェクトの関数として定義するには`*goja.Object`の`Set`を使います。
    - 今回定義したのは`print`関数だけです
    - `print`で直接`fmt.Printf`などを使うと`JSON`が正常に表示されないので、一度JavaScriptの`JSON.stringify()`を実行してから`fmt.Printf`で表示します
    - 文字列は`JSON.stringify`を使うと`"`引用符が付くので文字列だけ別処理にしました。
    - `setStringify()`で`JSON.stringify`をGo言語から使えるようにします。
- `Interrupt`はJavaScriptで`throw`のように働きます。





## (拡張1) load関数の実装

外部ファイルを実行するために`load(ファイル名)`を実装します。

`func initialSetting() *jsRuntime`に次のコードを追加します

```go
 rt.Set("load", js.jsload)
```

`js.jsload`の実装 

```go:jsload
func (js *jsRuntime) jsload(file string) goja.Value {
	rt := js.runtime
	f, err := os.Open(file)
	if err != nil {
		rt.Interrupt(rt.NewGoError(err))
		return goja.Null()
	}
	defer f.Close()
	text, err := ioutil.ReadAll(f)
	if err != nil {
		rt.Interrupt(rt.NewGoError(err))
		return goja.Null()
	}
	val, err := js.runtime.RunScript(file, string(text))
	if err != nil {
		rt.Interrupt(err)
		return goja.Null()
	}
	return val
}
```

`import	"io/ioutil"`パッケージを使ってファイルを入力します。
入力したファイルの実行は`RunScript`を使います。これは入力の継続の判定する必要がないためです。


`test.js`を作って実行します

```java:test.js
for(var i=0; i < 5; i++) {
    print(i)
}
```

```javascript
go run main.go
> load("test.js")
0
1
2
3
4
undefined
>
```

## (拡張2) Go言語のオブジェクト(構造体とそのメソッド)をJavaScriptから使う

オブジェクト`exampleObj`とします。これを構造体として定義し、そのメソッドも定義します。以下の例は`Method1`,`Method2`として両方とも円の面積を計算する同じ処理ですが戻り値が型が異なっています。メソッドは`goja`パッケージから参照されるのでメソッド名を大文字で始めます。また`exampleObj`を生成する関数`createExampleObj`をJavaScriptに設定します。

```go:initialSetting()に追加
	rt.Set("createExample", js.createExampleObj)
```


```go
type exampleObj struct {
	js *jsRuntime
}

func (js *jsRuntime) createExampleObj() *exampleObj {
	return &exampleObj{js: js}
}

func (o *exampleObj) Method1(r float64) float64 {
	return r * r * 3.14
}

func (o *exampleObj) Method2(r float64) goja.Value {
	return o.js.runtime.ToValue(r * r * 3.14)
}
```

実行

```javascript
go run main.go
> o=createExample()
{}
> o.Method1(1)
3.14
> o.Method2(1)
3.14
> o.Method2(2)
12.56
>
```

問題はないのですが、メソッド名が大文字で始まるのは入力コマンドとしては使いづらいです。そこでこれらのWrapperをJavaScriptで記述します。


```go:initialSetting()に追加
	_, err := rt.RunString(builtin)
	if err != nil {
		panic(err)
	}
```


```go
const builtin = `
example = function() {
	return new ExampleObj()
}
ExampleObj = function () {
	var _obj = createExample()
	this.getObj = function() {
		return _obj
	}
}
ExampleObj.prototype.method1 = function (r) {
	return this.getObj().Method1(r)
}
ExampleObj.prototype.method2 = function (r) {
	return this.getObj().Method2(r)
}
`
```

`_obj`を`ExampleObj`外から直接アクセスできないように`this._obj`にはしませんでした。
外部ファイルにして`load`で読み込むのも可能です。

実行

```javascript
go run main.go
> o=example()
{}
> o.method1(1)
3.14
> o.method1(2)
12.56
> o.method2(2)
12.56
> 
```

# 参照

- https://github.com/dop251/goja
- [gojaを使ってGoでJavaScriptの実行を試してみた] (https://qiita.com/koki_cheese/items/8744d5d7c23c31501e9e)

# まとめ

いろいろ工夫すれば使いやすいツールが作れるのではないでしょうか。

問題点

- 基本的なものもGoで拡張しないと使いえない
- GCは実装されていないようです。
- JavaScriptの仕様が古い
    - Proxyがない
    - 可変長引数を次の可変長引数に渡すには配列にして`apply`メソッドを使う



# Appnedix

## 試作の紹介

今、自分用に試作中のものを少し紹介します。

`database/sql`パッケージを使用してSQLをJavaScriptのコマンドとして実装して、データの受け取りはなるべくJSON化しています。現時点では`sqlite3`と`PostgreSQL`のドライバーを使って試作しています。これらはmongoシェルを参考にしています。
また、コマンドの入力用のパッケージとして https://github.com/chzyer/readline を使っています


さらに考えられるのは

- MongoDBのようにテーブルが存在しないときに自動的に作成する　
    - 最初に`insert`するJSONを用いて、`create table`を発行する。
    - データの型をどうするか。データベースごとにdefaultの設定を考える
- EXCELファイルやCSVファイルを読んでJavaScript上でJSONデータとして扱えるようにする
    - これらのファイルをDBにストアできる
- MongoDB
    - すでにmongoシェルがあるが、ここに実装すれば他のRDBと連携できる
    - mongoシェルの実装(C++, SpiderMonkey)は複雑なので改造は難しい。
- I/O関係の実装
- RESTfulクライアント
- https://github.com/gizak/termui/ のパッケージをJavaScriptから使えるようにして監視モニター

## sqlite3のコマンド例

```javascript
$go run main.go
script> db=sqlite3("sample.db")
{"driver":"sqlite3","conn":"sample.db"}
script> db.exec("create table tbl1(a integer, b varchar)")
{}
script> db.exec("insert into tbl1 values(?,?)", 1, "string 1")
{}
script> db.exec("insert into tbl1 values(?,?)", 2, "string 2")
{}
script> db.query("select * from tbl1")
{"a":1,"b":"string 1"}
{"a":2,"b":"string 2"}
script>
```

テーブルオブジェクトを実装して次のように使う

```javascript
script> t = db.table("tbl1")
{"driver":"sqlite3","name":"tbl1"}
script> t.insert({a:3, b:"string 3"})
{}
script> t.find()
{"a":1,"b":"string 1"}
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
script> t.find("a>1")
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
script> t.find().forEach(function(doc) {
...   if (doc.a > 1) {
...     print(JSON.stringify(doc))
...   }})
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
script>
```

`modesql()`でSQLモードにします。SQLモードを終わらせるには`exit;`と入力します。

```javascript
script> modesql(db)
sql> select * from tbl1 where a > 1;
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
sql> create table tbl2(c real ,d varchar);
sql> insert into tbl2 values(1.2, "string 1.2");
sql> select * from tbl2;
{"c":1.2,"d":"string 1.2"}
sql> exit;
script> t2=db.table("tbl2")
{"driver":"sqlite3","name":"tbl2"}
script> t2.find()
{"c":1.2,"d":"string 1.2"}
script>
```

`find`には`limit`,`skip`オプションを実装しました。このほかには`sort`や`select`する項目名を指定するコマンドも実装可能でしょう。

```javascript
script> t.find().limit(2).skip(1)
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
```

`sqlite3`の`tbl1`のデータを`PostgreSQL`の`pgtbl1`にコピーする

```javascript
script> pg = postgres()
{"driver":"postgres","conn":"dbname=test sslmode=disable"}
script> pg.exec("create table pgtbl1(a integer, b varchar)")
{}
script> pgt = pg.table("pgtbl1")
{"driver":"postgres","name":"pgtbl1"}
script> t.find().forEach(function(doc) {
...   pgt.insert(doc)
... })
script> pgt.find()
{"a":1,"b":"string 1"}
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
script>
```

PostgreSQLのpsqlコマンドで確認します

```text
>psql test
psql (11.0)
"help" でヘルプを表示します。

test=# select * from pgtbl1;
 a  |     b
----+-----------
  1 | string 1
  2 | string 2
  3 | string 3

(3 行)


test=#
```

また、mongoシェル風なテーブルの記述にしたかったので`goja`パッケージを改造しました。

```javascript
script> t = db.table("tbl1")
{"driver":"sqlite3","name":"tbl1"}
script> t.insert({a:3, b:"string 3"})
{}
```
としているものをmongoシェル風に`db.tbl1.insert`として使えるようになりました。通常は`tbl1`は`undefined`になりますので、`undefined`のプロパティが参照されたときに特定のメソッドを呼び出すように改造しました。

```javascript
script> db.tbl1.insert({a:4, b:"string 4"})
{}
script> db.tbl1.find()
{"a":1,"b":"string 1"}
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
{"a":4,"b":"string 4"}
script>
```

`db.tbl1.fromExcel("Book1.xlsx", "Sheet1")`や`db.tbl1.toExcel("Book1.xlsx", "Sheet1")`なども実装出来たら有用かもしれない。