概要
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は
...>
とします - 継続のキャンセルは
...
と入力します
- 継続行のpromptは
実行例
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なので現在使わているのものより古いです。
ソースコード
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
に次のコードを追加します
rt.Set("load", js.jsload)
js.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
を作って実行します
for(var i=0; i < 5; i++) {
print(i)
}
go run main.go
> load("test.js")
0
1
2
3
4
undefined
>
(拡張2) Go言語のオブジェクト(構造体とそのメソッド)をJavaScriptから使う
オブジェクトexampleObj
とします。これを構造体として定義し、そのメソッドも定義します。以下の例はMethod1
,Method2
として両方とも円の面積を計算する同じ処理ですが戻り値が型が異なっています。メソッドはgoja
パッケージから参照されるのでメソッド名を大文字で始めます。またexampleObj
を生成する関数createExampleObj
をJavaScriptに設定します。
rt.Set("createExample", js.createExampleObj)
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)
}
実行
go run main.go
> o=createExample()
{}
> o.Method1(1)
3.14
> o.Method2(1)
3.14
> o.Method2(2)
12.56
>
問題はないのですが、メソッド名が大文字で始まるのは入力コマンドとしては使いづらいです。そこでこれらのWrapperをJavaScriptで記述します。
_, err := rt.RunString(builtin)
if err != nil {
panic(err)
}
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
で読み込むのも可能です。
実行
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のコマンド例
$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>
テーブルオブジェクトを実装して次のように使う
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;
と入力します。
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
する項目名を指定するコマンドも実装可能でしょう。
script> t.find().limit(2).skip(1)
{"a":2,"b":"string 2"}
{"a":3,"b":"string 3"}
sqlite3
のtbl1
のデータをPostgreSQL
のpgtbl1
にコピーする
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コマンドで確認します
>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
パッケージを改造しました。
script> t = db.table("tbl1")
{"driver":"sqlite3","name":"tbl1"}
script> t.insert({a:3, b:"string 3"})
{}
としているものをmongoシェル風にdb.tbl1.insert
として使えるようになりました。通常はtbl1
はundefined
になりますので、undefined
のプロパティが参照されたときに特定のメソッドを呼び出すように改造しました。
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")
なども実装出来たら有用かもしれない。