HCL Blocks in order using the API

I’m using the HCL v2.4.0 to parse some custom HCL files. Overall I’ve been very happy with it. HCL seems to be a good “syntax” that works well for what I need.

I have situation where I want (1) either the blocks in order or (2) I’d like to get back their range information so I can sort them myself. I’m currently doing something like this (code is approximate):

type HCLFile struct {
  Services    []Service       `hcl:"service,block"`
  Apps        []Database      `hcl:"app,block"`
}

var hclfile HCLFile
parser := hclparse.NewParser()
f, diags := parser.ParseHCL(src, fileName)

diags = gohcl.DecodeBody(f.Body, nil, &hclfile)

I have a requirement to go through these in order. I’m assume each type goes into their respective array in order. But I’m not sure how to determine the overall order.

I saw this thread on HCL and the reply from @apparentlymart led me to his syntax cleaner.

That led me to look at a pattern using the hclwrite package and something like hclwrite.File.Body().Blocks(). That returns them all in order and I can use the Type() method to figure out what I have.

Is there a more elegant way to this? Also, is it possible to get back the Position information of each block? I can see Position down deeper in the data structures but I don’t see a way to bring it back with the blocks. I can get it back for attributes but I don’t see a way for the block itself.

(And sorry for a non-Terraform question in the Terraform forum but I didn’t see where else to put it)

Hi @billgraziano,

The higher-level abstractions provided by the gohcl and hcldec packages make some assumptions about what is significant and what is not which means that unfortunately they cannot represent all possible situations.

However, the lowest-level HCL decoding API does preserve overall ordering of blocks and so you should be able to get what you want here by taking a hybrid approach of decoding the toplevel with the low-level API while still being able to use gohcl to decode the contents of those blocks. Something like this:

rootSchema := &hcl.BodySchema{
    Blocks: []hcl.BlockHeaderSchema{
        { Type: "service" },
        { Type: "app" },
    },
}
content, diags := f.Body.Content(rootSchema)
// (handle diagnostics in some way...)

for _, block := range content.Blocks {
    switch block.Type {
    case "service":
        var service Service
        diags := gohcl.DecodeBody(block.Body, nil, &service)
        // (handle diags and do something with "service"...)
    case "app":
        var app Database
        diags := gohcl.DecodeBody(block.Body, nil, &app)
        // (handle diags and do something with "app"...)
    }
}

The BodyContent.Blocks field is a flat slice of pointers to hcl.Block objects in the original order given in the source, so you can then iterate over that slice to interleave the processing of blocks of different types.

There’s more information on this low-level API in the manual section Advanced Decoding with the Low-level API. This API is the one that gohcl and hcldec are both implemented in terms of, and exposes all of the details of the HCL infoset.

1 Like

Thanks for the reply. That looks interesting. I’ll see what I can build using this.

Just wanted to pop back in and say Thank You @apparentlymart. It looks like I can make it do what I need based on your comment. It gets me back different types in the proper order and with the Range.

I did find one thing weird though. It doesn’t seem to populate the labels. I can see them in the Labels field but they aren’t populated into the structure. Full working code is below. Maybe you can spot something obvious I did wrong :slight_smile: It may be that’s a quick of mixing API levels. It’s easy for me to work around. Mostly posted this to say Thank You!

package main

import (
	"fmt"
	"log"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclparse"
)

func main() {
	type Service struct {
		Name    string `hcl:"name,label"`
		Address string `hcl:"address"`
	}

	type App struct {
		ID   string `hcl:"id,label"`
		Port int    `hcl:"port"`
	}

	type HCLFile struct {
		Services []Service `hcl:"service,block"`
		Apps     []App     `hcl:"app,block"`
	}

	rootSchema := &hcl.BodySchema{
		Blocks: []hcl.BlockHeaderSchema{
			{
				Type:       "service",
				LabelNames: []string{"name"},
			},
			{
				Type:       "app",
				LabelNames: []string{"id"},
			},
		},
	}

	src := `
	service "abc" {
		address = "1.1.1.1:80"
	}

	app "id-37" {
		port = 8080
	}

	service "xyz" {
		address = "1.1.1.1:1230"
	}
	`
	parser := hclparse.NewParser()

	bb := []byte(src)
	f, diags := parser.ParseHCL(bb, "test.hcl")
	if diags.HasErrors() {
		log.Fatal(diags.Error())
	}

	content, diags := f.Body.Content(rootSchema)
	if diags.HasErrors() {
		log.Fatal(diags.Error())
	}

	//spew.Dump(content)
	//spew.Dump(content.Blocks[0])

	fmt.Printf("Blocks: %d\n", len(content.Blocks))
	for k, block := range content.Blocks {
		fmt.Println("-------------------------------------------------")
		fmt.Printf("key: %v block.type: %v\n", k, block.Type)
		fmt.Println("DefRange:", block.DefRange)
		fmt.Println("TypeRange:", block.TypeRange)
		fmt.Println("LabelRanges:", block.LabelRanges)
		fmt.Println("Labels:", block.Labels)
		switch block.Type {
		case "service":
			var svc Service
			diags := gohcl.DecodeBody(block.Body, nil, &svc)
			if diags.HasErrors() {
				log.Fatal(diags.Error())
			}
			// spew.Dump(svc)
			fmt.Println("Name (why empty?):", svc.Name)
			fmt.Println("Address:", svc.Address)
		case "app":
			var app App
			diags := gohcl.DecodeBody(block.Body, nil, &app)
			if diags.HasErrors() {
				log.Fatal(diags.Error())
			}
			// spew.Dump(svc)
			fmt.Println("ID (why empty?):", app.ID)
			fmt.Println(fmt.Sprintf("Port: %d", app.Port))
		}

	}
}

Hmm, yes… that’s because block.Body is the content of the block, rather than the block itself, and so it doesn’t have access to the block labels.

It doesn’t look like we have an API to decode the isolated block itself into a Go struct using gohcl right now, so with the API as it currently stands I think you’d need to manually copy the values from block.Labels into the appropriate fields of your struct after gohcl.DecodeBody returns.

gohcl could potentially offer another function gohcl.DecodeBlock to decode the block as a whole, including its labels, into the struct value. I don’t have time to work on that right now (I’m currently focused on Terraform project work) but if you’re motivated to work on it we could talk more about it in an issue in the HCL repository.

2 Likes