以下の内容はhttps://haibara-works.hatenablog.com/entry/2025/01/11/130401より取得しました。


Python 方言の設定言語 Starlark で Go ツールをコンフィグする

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.

github.com

Bazel の設定を書くために作られた Python 方言 *1 ということですね。

言語仕様・各種実装・関連ツールなどはこちらにまとまっています。

github.com

設定ファイルとしてよく使われる YAMLJSON は machine readable な形式なのに対して、Starlark (の元になった Python) は human readable な文法を持っていることが嬉しいところでしょう。 他にも、分岐・ループ・関数などを使うことができるため、冗長な設定を DRY に書くこともできるでしょう。これは Terraform に似ていますね。

Starlark の言語仕様の詳細についてはここでは触れませんが、こちらに詳しくまとまっています。

github.com

Starlark の実装

Starlark の実装は以下の3種類が主に知られています。

そのため、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 スクリプト実行のための、スレッドの状態を保持する構造体。printload などの Starlark 関数を注入する際にも使われる。 Go Doc
3 filename string Starlark スクリプトのファイル名。srcnil が指定された場合、ここで指定されたファイルパスから Starlark スクリプトが参照される。srcnil でない場合には、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




以上の内容はhttps://haibara-works.hatenablog.com/entry/2025/01/11/130401より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14