Starlark という、設定言語 (configuration language) として作られた Python 方言があります。 この記事では、google/go-starlark を使って、Go ツールで Starlark スクリプトを扱う方法を説明します。
Starlark は先日ブログにも書いた Tilt の設定ファイルでも使われています。これがきっかけで調べてみたところ、なかなか面白かったので紹介します。 haibara-works.hatenablog.com haibara-works.hatenablog.com
Starlark とは
Starlark の機能・仕様が議論されているリポジトリには、次のような説明があります。
Starlark (formerly known as Skylark) is a language intended for use as a configuration language. It was designed for the Bazel build system, but may be useful for other projects as well.
Bazel の設定を書くために作られた Python 方言 *1 ということですね。
言語仕様・各種実装・関連ツールなどはこちらにまとまっています。
設定ファイルとしてよく使われる YAML や JSON は machine readable な形式なのに対して、Starlark (の元になった Python) は human readable な文法を持っていることが嬉しいところでしょう。 他にも、分岐・ループ・関数などを使うことができるため、冗長な設定を DRY に書くこともできるでしょう。これは Terraform に似ていますね。
Starlark の言語仕様の詳細についてはここでは触れませんが、こちらに詳しくまとまっています。
Starlark の実装
Starlark の実装は以下の3種類が主に知られています。
- google/starlark-go
- facebook/starlark-rust
- bazelbuild/bazel (このリポジトリ内のパッケージ starlark/java が Starlark の実装)
そのため、Go/Rust/Java で書かれたソフトウェアであれば、これらの Starlark 実装を使えそうです。 それ以外の言語についても、starlark-go や starlark-rust のビルド済みバイナリをバインディングすれば、Starlark 実装を取り入れることができると思います。
今回は、素直に starkark-go を使って、Go で Starkark スクリプトを読み込むことにします。
starlark-go を使ってみる
google/go-starlark は、Starlark 言語の Go 実装です。
ミニマムのサンプルコードがこちらです。
package main import ( "log" "go.starlark.net/starlark" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "dummy.star" src := ` a = 2 b = 3 c = a * b print("{} x {} = {}".format(a, b, c)) ` predeclared := starlark.StringDict{} _, err := starlark.ExecFileOptions(opts, thread, filename, src, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } }
実行すると以下の出力が得られます。
> go run main.go 2 x 3 = 6
これは、変数 script に格納された Starlark スクリプトを starlark.ExecFileOptions*2 (Go Doc) で実行する例です。
この starlark.ExecFileOptionsが引数が多く取っ付きにくいのですが、説明していきます。
starlark.ExecFileOptions が受け取る引数を、以下にまとめます。
| 位置 | 名前 | 型 | 説明 |
|---|---|---|---|
| 1 | opts |
*syntax.FileOptions |
Starlark スクリプトの静的解析に関するオプション。Go Doc |
| 2 | thread |
*starlark.Thread |
Starlark スクリプト実行のための、スレッドの状態を保持する構造体。print や load などの Starlark 関数を注入する際にも使われる。 Go Doc |
| 3 | filename |
string |
Starlark スクリプトのファイル名。src に nil が指定された場合、ここで指定されたファイルパスから Starlark スクリプトが参照される。src が nil でない場合には、filename はエラーメッセージの表示に使われる。 ref |
| 4 | src |
interface{} (実際に受け入れられるのは nil または string / []byte / io.Reader / syntax.FilePortion) |
Starlark スクリプトそのものの文字列。これに nil を渡した場合は、filename で指定されたファイルが Starlark スクリプトとして参照される。 |
| 5 | predeclared |
starlark.StringDict |
Starlark スクリプトに提供される名前空間。これを通してGo アプリ側で定義した変数や関数を Starlark スクリプトから参照できるようになる。 Go Doc |
上記のサンプルコードでは、opts / thread / predeclared は構造体のゼロ値、src には Starlark スクリプトの文字列、filename にはダミーの値として dummy.star をそれぞれ渡しています。
また、starlark.ExecFileOptions の戻り値を以下にまとめます。
| 位置 | 型 | 説明 |
|---|---|---|
| 1 | starlark.StringDict |
Starlark スクリプトを実行した後のグローバル変数が Map で返される。 |
| 2 | error |
一般的なエラーのほか、Starlark スクリプトの評価に失敗した際には starlark.EvalError 型のエラーが返される。 |
次にそれぞれの引数の値を変えた、いろいろなケースを見ていきます。
Starlark スクリプトをファイルから読み出す
前のサンプルコードでは、Starlark スクリプトを文字列として Go コードに抱えていましたが、ファイルから読み出すオプションもあります。 以下の Go コードと Starlark スクリプトのファイルを用意して実行します。
package main import ( "log" "go.starlark.net/starlark" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "example.star" predeclared := starlark.StringDict{} _, err := starlark.ExecFileOptions(opts, thread, filename, nil, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } }
a = 2 b = 3 c = a * b print("{} x {} = {}".format(a, b, c))
> go run main.go 2 x 3 = 6
Go で定義した変数を Starlark スクリプトから参照する
Go コード側で変数を定義して、それを Starlark スクリプトから読み出すこともできます。
package main import ( "log" "go.starlark.net/starlark" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "dummy.star" script := ` print(quote) ` predeclared := starlark.StringDict{ "quote": starlark.String("To be, or not to be: that is the question."), } _, err := starlark.ExecFileOptions(opts, thread, filename, script, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } }
> go run main.go To be, or not to be: that is the question.
Starkark スクリプト内で変数に代入した値を Go コードで受け取る
starlark.ExecFileOptions は、Starlark スクリプトを実行した後の各種グローバル変数の値を返します。
これを使えば、Starlark スクリプト内で変数に代入した値を、Go コードの後続処理で使うことができます。
package main import ( "fmt" "log" "go.starlark.net/starlark" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "dummy.star" script := ` quote = 'To be, or not to be: that is the question.' ` predeclared := starlark.StringDict{} globals, err := starlark.ExecFileOptions(opts, thread, filename, script, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } fmt.Println("[quote]", globals["quote"]) }
> go run main.go [quote] "To be, or not to be: that is the question."
Go で定義した独自関数を Starlark スクリプトからコールする
starlark.NewBuiltin を使えば、変数と同じように、Go コードで定義した関数を Starlark スクリプトからコールできるようになります。
関数への引数は starlark.UnpackArgs でパースします。関数の戻り値は、starlark.Float など、インターフェース starlark.Value の具象型に変換する必要があります。
package main import ( "log" "math" "go.starlark.net/starlark" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "dummy.star" script := ` x = 2.0 y = 3.0 print("{} ^ {} = {}".format(x, y, pow(x, y))) ` predeclared := starlark.StringDict{ "pow": starlark.NewBuiltin("pow", pow), } _, err := starlark.ExecFileOptions(opts, thread, filename, script, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } } // Usage: pow(x float, y float) float func pow(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var x float64 var y float64 if err := starlark.UnpackArgs(b.Name(), args, kwargs, "x", &x, "y", &y); err != nil { return nil, err } return starlark.Float(math.Pow(x, y)), nil }
> go run main.go 2.0 ^ 3.0 = 8.0
starlark-go が提供するモジュールを使う (json / math / proto / time)
Starlark は Python の方言でありますが、例えば pip でモジュールをインストールして使う、ということは直接はできません。
その代わりに、starlark-go のレポジトリ内で、いくつかのモジュールが定義されています (json / math / proto / time)。
starlark-go/lib at master · google/starlark-go · GitHub
そのうち、json モジュールを使うサンプルを以下に示します。
package main import ( "log" "go.starlark.net/lib/json" "go.starlark.net/starlark" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "dummy.star" script := ` m = {"key1": "value1", "key2": "value2"} jsonstr = json.encode(m) print(json.indent(jsonstr)) # pretty print ` predeclared := starlark.StringDict{ "json": json.Module, } _, err := starlark.ExecFileOptions(opts, thread, filename, script, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } }
> go run main.go
{
"key1": "value1",
"key2": "value2"
}
独自モジュールを定義して使う
モジュールは自分で作ることもできます。
package main import ( "log" "strings" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" "go.starlark.net/syntax" ) func main() { opts := &syntax.FileOptions{} thread := &starlark.Thread{} filename := "dummy.star" script := ` str = strings.repeat("hello! ", 3) print(str) ` predeclared := starlark.StringDict{ "strings": staringsModule, } _, err := starlark.ExecFileOptions(opts, thread, filename, script, predeclared) if err != nil { log.Fatalln("starlark.ExecFileOptions failed:", err.Error()) return } } var staringsModule = &starlarkstruct.Module{ Name: "strings", Members: starlark.StringDict{ "repeat": starlark.NewBuiltin("strings.encode", repeatFn), }, } func repeatFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var s string var n int if err := starlark.UnpackArgs("repeat", args, kwargs, "s", &s, "n", &n); err != nil { return nil, err } return starlark.String(strings.Repeat(s, n)), nil }
> go run main.go hello! hello! hello!
おわりに
様々なケースのサンプルコードを通して starlark-go について紹介しました。設定が複雑になりがちなツールを扱う際には、ひとつの選択肢となるかもしれませんね。
*1:プログラミング言語の ”方言” というのは独特な言い回しですよね。一般的には「XX言語の一部の機能や仕様が変更された別の言語」のことを「XX言語の方言」と表現されることが多いでしょう。 それに対して「XX言語の機能や仕様のうち、限られたものを持っている別の言語」は、「XX言語のサブセット」と表現されることが多いでしょう。
*2:starlark.ExecFile というメソッドもありますが、これは deprecated になっています。 starlark package - go.starlark.net/starlark - Go Packages