Skip to content

Read and write HCL

The core reads and writes only YAML. HCL comes from a sibling module, config-hcl.

go get gitlab.com/phpboyscout/go/config-hcl
import (
    "gitlab.com/phpboyscout/go/config"
    confighcl "gitlab.com/phpboyscout/go/config-hcl"
)

store, err := config.NewStore(ctx,
    config.WithFiles(fsys, "/etc/app.yaml"),                    // YAML, the default
    config.WithBackend(confighcl.New(fsys, "/etc/app.hcl")),    // outranks it
)

When you do not need this

If you pick the format, YAML costs nothing extra and the core reads it.

Reach for HCL when the file is shared with tooling that already speaks it, so your configuration sits alongside the rest of a stack rather than translating it.

HCL as a configuration format — not Terraform

This module serves HCL the way Nomad, Consul, Vault, Packer and purpose-built HCL DSLs use it, and deliberately not Terraform. A document that reaches outside itself for a value — a var.* reference, ${local.x} interpolation, or a function call — is refused at load with config.ErrBackendUnsafe naming the construct:

port = var.base_port + 1   # refused: no correct answer without a variable context

There is no correct answer without a variable context a configuration library does not have and should not invent, and a value computed from a half-context is worse than an error because it looks right. Point it at a .tf file and it says so, rather than silently dropping the configuration it could not understand.

Blocks and labels are path segments

A block's type and labels become dotted-key segments:

migration "multi_state" "move_redis" {
  enabled = true
}
enabled := store.View().GetBool("migration.multi_state.move_redis.enabled")

Two blocks sharing a type and every label collide (an error, not last-one-wins), and repeated unlabelled blocks of one type are refused — they have no dotted-key representation.

Writing preserves the file

Because every accepted document is expression-free, HCL is exactly as safe to edit as YAML. Writing is HashiCorp's hclwrite — a targeted edit that preserves comments, block order and formatting, changing only the value asked for. Set("server.port", 9090) on

# public listener port — needs a firewall change
server {
  host = "localhost" # dev only
  port = 8080
}

changes port to 9090 and leaves every comment and the block untouched. Setting or removing a block attribute or a top-level attribute is supported; writing into a sub-path of an object-valued attribute, or creating a new nested block from a dotted path, is refused rather than guessed.

What it costs

Modules added 16 — 7 for hclparse, hclwrite and cty, 9 for the config graph

This is the heaviest adapter: the config graph plus HashiCorp's HCL toolchain (hashicorp/hcl/v2, zclconf/go-cty) and what those bring. An allowlist test in the module states the full set. No filesystem library: you supply the config.FS.