💻

github.com/pelletier/go-toml で遊ぶ

2021/08/08に公開

Hugo v0.87.0TOML の操作に github.com/pelletier/go-toml パッケージを採用することにしたらしい。

we have switched to using go-toml for all things TOML in Hugo. A big thanks to @pelletier for his work on the v2 version. It's both faster than what we had and TOML v1.0.0 compliant.
(via "Release v0.87.0 · gohugoio/hugo")

ちなみに,このパッケージは v2 系がベータ版公開されていて github.com/BurntSushi/toml パッケージより2倍以上速いとのこと(via "Release v2.0.0-beta.3 · pelletier/go-toml")。

Benchmark go-toml v1 BurntSushi/toml
Marshal/HugoFrontMatter 2.0x 2.0x
Marshal/ReferenceFile/map 1.8x 2.0x
Marshal/ReferenceFile/struct 2.7x 2.7x
Unmarshal/HugoFrontMatter 3.0x 2.6x
Unmarshal/ReferenceFile/map 3.0x 3.1x
Unmarshal/ReferenceFile/struct 5.9x 6.6x

というわけで pelletier/go-toml を使って軽く遊んでみよう。

gpgpdump 解析結果を TOML で出力する

拙作の github.com/spiegel-im-spiegel/gpgpdump パッケージは OpenPGP パケットの解析結果を TOML 形式で出力することができる[1]。たとえば BurntSushi/toml パッケージと組み合わせて

sample0.go
// +build run

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"

    "github.com/BurntSushi/toml"
    "github.com/spiegel-im-spiegel/gpgpdump/parse"
    "github.com/spiegel-im-spiegel/gpgpdump/parse/context"
)

const openpgpStr = `
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2

iF4EARMIAAYFAlTDCN8ACgkQMfv9qV+7+hg2HwEA6h2iFFuCBv3VrsSf2BREQaT1
T1ZprZqwRPOjiLJg9AwA/ArTwCPz7c2vmxlv7sRlRLUI6CdsOqhuO1KfYXrq7idI
=ZOTN
-----END PGP SIGNATURE-----
`

func main() {
    p, err := parse.New(
        context.New(
            context.Set(context.ARMOR, true),
            context.Set(context.UTC, true),
        ),
        strings.NewReader(openpgpStr),
    )
    if err != nil {
        fmt.Fprintf(os.Stderr, "%+v", err)
        return
    }
    res, err := p.Parse()
    if err != nil {
        fmt.Fprintf(os.Stderr, "%+v", err)
        return
    }
    buf := &bytes.Buffer{}
    if err := toml.NewEncoder(buf).Encode(res); err != nil {
        fmt.Fprintf(os.Stderr, "%+v", err)
        return
    }
    if _, err = io.Copy(os.Stdout, buf); err != nil {
        fmt.Fprintf(os.Stderr, "%+v", err)
        return
    }
}

てな感じに書ける。出力結果は

$ go run sample0.go 
[[Packet]]
  name = "Signature Packet (tag 2)"
  note = "94 bytes"

  [[Packet.Item]]
    name = "Version"
    value = "4"
    note = "current"

  [[Packet.Item]]
    name = "Signiture Type"
    value = "Signature of a canonical text document (0x01)"

  [[Packet.Item]]
    name = "Public-key Algorithm"
    value = "ECDSA public key algorithm (pub 19)"

  [[Packet.Item]]
    name = "Hash Algorithm"
    value = "SHA2-256 (hash 8)"

  [[Packet.Item]]
    name = "Hashed Subpacket"
    note = "6 bytes"

    [[Packet.Item.Item]]
      name = "Signature Creation Time (sub 2)"
      value = "2015-01-24T02:52:15Z"

  [[Packet.Item]]
    name = "Unhashed Subpacket"
    note = "10 bytes"

    [[Packet.Item.Item]]
      name = "Issuer (sub 16)"
      value = "0x31fbfda95fbbfa18"

  [[Packet.Item]]
    name = "Hash left 2 bytes"
    dump = "36 1f"

  [[Packet.Item]]
    name = "ECDSA value r"
    note = "256 bits"

  [[Packet.Item]]
    name = "ECDSA value s"
    note = "252 bits"

という感じ。これを pelletier/go-toml パッケージに置き換えてみよう。インポート・パスで

sample1.go
import "github.com/pelletier/go-toml/v2"

と v2 系を指定して BurntSushi/toml パッケージを使った部分を書き換える。

sample1.go
b, err := toml.Marshal(res)
if err != nil {
    fmt.Fprintf(os.Stderr, "%+v", err)
    return
}
if _, err = bytes.NewReader(b).WriteTo(os.Stdout); err != nil {
    fmt.Fprintf(os.Stderr, "%+v", err)
    return
}

これを実行してみよう。

$ go run sample1.go 
[['Packet,omitempty']]
name = 'Signature Packet (tag 2)'
'value,omitempty' = ''
'dump,omitempty' = ''
'note,omitempty' = '94 bytes'
[['Packet,omitempty'.'Item,omitempty']]
name = 'Version'
'value,omitempty' = '4'
'dump,omitempty' = ''
'note,omitempty' = 'current'
'Item,omitempty' = []
[['Packet,omitempty'.'Item,omitempty']]
name = 'Signiture Type'
'value,omitempty' = 'Signature of a canonical text document (0x01)'
'dump,omitempty' = ''
'note,omitempty' = ''
'Item,omitempty' = []
[['Packet,omitempty'.'Item,omitempty']]
name = 'Public-key Algorithm'
'value,omitempty' = 'ECDSA public key algorithm (pub 19)'
'dump,omitempty' = ''
'note,omitempty' = ''
'Item,omitempty' = []
[['Packet,omitempty'.'Item,omitempty']]
name = 'Hash Algorithm'
'value,omitempty' = 'SHA2-256 (hash 8)'
'dump,omitempty' = ''
'note,omitempty' = ''
'Item,omitempty' = []
[['Packet,omitempty'.'Item,omitempty']]
name = 'Hashed Subpacket'
'value,omitempty' = ''
'dump,omitempty' = ''
'note,omitempty' = '6 bytes'
[['Packet,omitempty'.'Item,omitempty'.'Item,omitempty']]
name = 'Signature Creation Time (sub 2)'
'value,omitempty' = '2015-01-24T02:52:15Z'
'dump,omitempty' = ''
'note,omitempty' = ''
'Item,omitempty' = []

[['Packet,omitempty'.'Item,omitempty']]
name = 'Unhashed Subpacket'
'value,omitempty' = ''
'dump,omitempty' = ''
'note,omitempty' = '10 bytes'
[['Packet,omitempty'.'Item,omitempty'.'Item,omitempty']]
name = 'Issuer (sub 16)'
'value,omitempty' = '0x31fbfda95fbbfa18'
'dump,omitempty' = ''
'note,omitempty' = ''
'Item,omitempty' = []

[['Packet,omitempty'.'Item,omitempty']]
name = 'Hash left 2 bytes'
'value,omitempty' = ''
'dump,omitempty' = '36 1f'
'note,omitempty' = ''
'Item,omitempty' = []
[['Packet,omitempty'.'Item,omitempty']]
name = 'ECDSA value r'
'value,omitempty' = ''
'dump,omitempty' = ''
'note,omitempty' = '256 bits'
'Item,omitempty' = []
[['Packet,omitempty'.'Item,omitempty']]
name = 'ECDSA value s'
'value,omitempty' = ''
'dump,omitempty' = ''
'note,omitempty' = '252 bits'
'Item,omitempty' = []

ありゃりゃーん。 Struct タグの omitempty を解釈してくれないんだな[2] orz

ま,まぁ,特定の構造体ではなく map[string]interface{} を使う手もあるのだが... ちなみに import 宣言を

import "github.com/pelletier/go-toml"

と v1 系にすれば BurntSushi/toml と同等の出力になる(でも遅い,多分)。

【おまけ】 jq 風のクエリ

pelletier/go-toml v1 系には jq 風のクエリを使って TOML の内容をフィルタリングする機能がある。これを使って以下のようなコードを書いてみた。

sample2.go
// +build run

package main

import (
    "flag"
    "fmt"
    "os"

    "github.com/pelletier/go-toml"
    "github.com/pelletier/go-toml/query"
)

func main() {
    flag.Parse()
    args := flag.Args()
    if len(args) != 1 {
        fmt.Fprintln(os.Stderr, os.ErrInvalid)
        return
    }

    info, err := toml.LoadReader(os.Stdin)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    results, err := query.CompileAndExecute(args[0], info)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    for _, item := range results.Values() {
        fmt.Println(item)
    }
}

たとえば,最初に書いた sample0.go の実行結果をこの sample2.go にパイプで繋げば

$ go run sample0.go | go run sample2.go "$.Packet[0].Item[0]"
name = "Version"
note = "current"
value = "4"

てな感じで評価できる。

参考

https://text.baldanders.info/release/gpgpdump/

脚注
  1. gpgpdump の解析結果を TOML で出力する機能はコマンドライン版ではドロップしてしまったが struct タグは残している。 ↩︎

  2. pelletier/go-toml/v2 の toml.Unmarshal() / toml.Decoder.Decode() 各関数も omitempty を解釈してくれないようだ。 ↩︎

GitHubで編集を提案

Discussion