Edited at

HCL を Parse する

More than 1 year has passed since last update.

Go 言語で、Viper を使わず、生の HCLinterface{} ではなく、特定の構造体にパースしたいと思ったので、サンプルを書いてみました。

Using HCL - Part 1 - Introductionを参考にしました。


ターゲットの HCL

とても簡単ですが、こちらのHCL

variable "azure" {

description = "This is Azure"
}

こちらは下記の json と等価です。やはり、HCLは100倍読みやすい、、、

{

"variable": {
"azure": {
"description": "This is Azure"
}
}


構造体

上記の HCL をパースするための構造体を定義します。

    type Directories struct {

Name string `hcl:",key"`
Description string
}

type Config struct {
Variable []Directories
}

実際に実施してみると、ポイントは二つありました。Config が起点になります。実際のdescription は一つしかありませんがDirectories は配列にしないとうまく Parse できません。

これは上記のブログが参考になります。

directory "config" {

source_dir = "/etc/eventstore"
dest_prefix = "escluster/config"
exclude = []
pre_backup_script = "before_backup.sh"
post_backup_script = "after_backup.sh"
pre_restore_script = "before_restore.sh"
post_restore_script = "after_restore.sh"
}

の部分は下記のようなオブジェクトに Parse されます。下記のオブジェクトを見ると、directory は、map になっており、そのValue の部分は、([]*hcl.Object) の形式になっています。ですので、この部分は、配列でないとうまくパースできないことになります。

  (*hcl.Object)(0x82035e840)({

Key: (string) (len=9) "directory",
Type: (hcl.ValueType) ValueTypeObject,
Value: ([]*hcl.Object) (len=1 cap=1) {
(*hcl.Object)(0x82035e810)({
Key: (string) (len=6) "config",
Type: (hcl.ValueType) ValueTypeObject,
Value: ([]*hcl.Object) (len=7 cap=7) {
(*hcl.Object)(0x82035e6c0)({
Key: (string) (len=10) "source_dir",
Type: (hcl.ValueType) ValueTypeString,
Value: (string) (len=15) "/etc/eventstore",
Next: (*hcl.Object)(<nil>)
}),

もう一点は、下記のazure を取得する方法です。こちらも上記の生オブジェクトの結果が参考になります。上記のサンプルは、config という値は、Keyに格納されています。

variable "azure" {

description = "This is Azure"
}

ですので、Nameを hcl の Key から取得します。

    type Directories struct {

Name string `hcl:",key"`


Decode の実行

Decode の実行は、Decode 関数で行います。

    var conf Config

:
err = hcl.Decode(&conf, string(b))

第一引数に、構造体のポインタを渡して、2つ目にパースしたい文字列を渡します。

これでめでたく Decode できました。

$ go run main.go

Decoding...

variable "azure" {
description = "This is Azure"
}

This is Azure
azure


コード全体

package main

import (
"fmt"
"io/ioutil"

"github.com/hashicorp/hcl"
)

/// This sample test read/write from HCL file.
func main() {

type Directories struct {
Name string `hcl:",key"`
Description string
}

type Config struct {
Variable []Directories
}

// var conf map[string]map[string]string
var conf Config
b, err := ioutil.ReadFile("sample.hcl")
if err != nil {
panic(err)
}

fmt.Println("Decoding...\n")
fmt.Println(string(b))
err = hcl.Decode(&conf, string(b))
fmt.Println(conf.Variable[0].Description)
fmt.Println(conf.Variable[0].Name)
if err != nil {
panic(err)
}
}


今回の反省と改善のヒント

こんな簡単なものに数時間かかった。考えられる原因は


  • しばらく触っておらず、go の文法(構造体とポインタ、ファイルアクセス)を忘れた

  • HCLのドキュメントやサンプルが見つからず、テストを読んだ

  • HCL の Decode の仕様がよくわからなかった(テストは、interface{} に対するパースになっている)

  • 上記のを理解するために、結局他人のブログで、生パースをしているコードと結果が参考になった

次回からの改善としては


  • Go の基本文法は忘れたのなら、まず思い出す。よくコードを書くようにする

hcl:", key" の記述は、jsonのパースの時に使われるものと同じノリでで、HCLに実装されていると思われる。参考。この記法は、Struct の Tagという記法。

* Tags in Golang

タグは、下記のようにリフレクションでLookupできる様子。

type T struct {

f string `one:"1" two:"2"blank:""`
}
func main() {
t := reflect.TypeOf(T{})
f, _ := t.FieldByName("f")
fmt.Println(f.Tag) // one:"1" two:"2"blank:""
v, ok := f.Tag.Lookup("one")

実際のコードだと、この付近で、tagを参照している。

* decoder.go#L588

多分、このあたりのコードを読んでよくわからない文法があったら、ハローワールドを作っていって理解したら次回はこの手のコードは高速に書けそう。