HCL 2 is the most promising configuration language I have ever met, but the lack of document makes it hard to use, especially for developers who want to build applications using HCL 2 as config format. This article will show how to use and fully appreciate the benefits of HCL 2.
In the following content,
HCLmeansHCL v2, please don't confuse withHCL v1
Prerequisites
To fully understand the following content, you may need the following prerequisites:
- Basic golang development experience
 - Familiar with other configuration languages: 
YAML,JSONand so on 
Introduction
HCL is a toolkit for creating structured configuration languages that are both human- and machine-friendly, for use with command-line tools. Although intended to be generally useful, it is primarily targeted towards devops tools, servers, etc.
HCL has been widely used in all hashicorp products: terraform, vault, consul, nomad, vagrant and packer. Users can configure them like following:
io_mode = "async"
service "http" "web_proxy" {
  listen_addr = "127.0.0.1:8080"
  
  process "main" {
    command = ["/usr/local/bin/awesome-app", "server"]
  }
  process "mgmt" {
    command = ["/usr/local/bin/awesome-app", "mgmt"]
  }
}
instead of
{
  "io_mode": "async",
  "service": {
    "http": {
      "web_proxy": {
        "listen_addr": "127.0.0.1:8080",
        "process": {
          "main": {
            "command": ["/usr/local/bin/awesome-app", "server"]
          },
          "mgmt": {
            "command": ["/usr/local/bin/awesome-app", "mgmt"]
          }
        }
      }
    }
  }
}
or
id_mode: "async"
service:
    http:
      web_proxy:
        listen_addr: "127.0.0.1:8080"
        process:
          - main:
              command: 
                - "/usr/local/bin/awesome-app"
                - "server"
          - mgmt:
              command:
                - "/usr/local/bin/awesome-app"
                - "mgmt"
HCL v2 combines HCL 1.0 and HIL, so that we can interpolate values directly:
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
HCL is both user and developer-friendly, not registered or missing block will be warned, so the developer doesn't need to guess the reason why config parse failed:
TestParse: config_test.go:45: test.hcl:1,1-1: Missing required argument; The argument "name" is required, but no definition was found., and 1 other diagnostic(s)
Syntax
To get started quickly, we will not cover every syntax in HCL Native Syntax Specification. Instead, we focused on the most used subset of structural language.
Attributes and Blocks
HCL is built around two constructs: attributes and blocks.
An attribute means to assign a value to a name.
io_mode = "async"
debug = false
max_size = 1024 * 1024
ratio = 0.7
placehold = null
command = ["/usr/local/bin/awesome-app", "server"]
rules = {
  mainland: "mainland",
  default: "oversea"
}
The identifier before the equals sign is the attribute name, and the expression after the equals sign is the attribute's value.
A block creates a child body annotated by a type and optional labels, and block's content consists of a collection of attributes and blocks.
service "http" "web_proxy" {
  listen_addr = "127.0.0.1:8080"
  
  process {
    command = ["/usr/local/bin/awesome-app", "server"]
  }
}
service here defines a type with two required labels: every service following should have two labels. A particular block type may have any number of required labels, or it may require none as with the nested process block type.
A block's body content is delimited by { and }. Within the block body, further attributes and blocks may be nested, creating a hierarchy of blocks and their associated attributes.
Identifiers
identifier is the name for attribute and block types.
Identifiers can contain letters, digits, underscores (_), and hyphens (-). The first character of an identifier must not be a digit, to avoid ambiguity with literal numbers.
Comments
#begins a single-line comment, ending at the end of the line.//also begins a single-line comment, as an alternative to #./*and*/start and end delimiters for a comment that might span over multiple lines.
Other tips
- MUST be UTF-8 encoding
 - Invalid or non-normalized UTF-8 encoding is always a parse error
 - No limit for line endings, but prefer 
LFfor most case 
Using in a project
In this section, we will use hcl in a project. This project is designed to route DNS requests to different upstream via rules, different upstream could have different config.
All example code is in
github.com/Xuanwo/hcl2-example
Config design
The config format we desired could be:
listen = "127.0.0.1:53"
upstream "oversea" {
  type = "dot"
  addr = "185.222.222.222:853"
  tls_server_name = "public-dns-a.dns.sb"
}
upstream "mainland" {
  type = "udp"
  addr = "114.114.114.114:53"
}
rules = {
  to_mainland: "mainland",
  default: "oversea"
}
Implementation
Firstly, we need to declare our config struct:
type Config struct {
   Listen    string            `hcl:"listen"`
   Upstreams []*Upstream       `hcl:"upstream,block"`
   Rules     map[string]string `hcl:"rules"`
}
type Upstream struct {
   Name    string   `hcl:",label"`
   Type    string   `hcl:"type"`
   Addr    string   `hcl:"addr"`
   Options hcl.Body `hcl:",remain"`
}
The tags are formatted as in the following example:
ThingType string `hcl:"thing_type,attr"`
The first is the name of the corresponding construct in configuration, while the second is a keyword giving the kind of construct expected. gohcl supports the following keywords:
attr: default if empty, indicates that the value is to be populated from an attributeblock: indicates that the value is to populated from a blocklabel: indicates that the value is to populated from a block labeloptional: is the same as attr, but the field is optionalremain: indicates that the value is to be populated from the remaining body after populating other fields
More tips:
remain's corresponding type should behcl.Bodyorhcl.Attributes- If there is no 
remainfield, any attributes or blocks not matched will cause an error - All fields are required as default except they have an 
optionalkeyword 
Secondly, we need to parse the config to get a hcl.Body:
var diags hcl.Diagnostics
file, diags := hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
    return nil, fmt.Errorf("config parse: %w", diags)
}
srcis the config file's content in[]byteslicefilenameis used for debugging
Diagnostic is the struct used by hcl for representing information to be presented to a user about an error or anomaly in parsing or evaluating configuration, and Diagnostics is a slice of Diagnostic. All hcl functions will return Diagnostics instead of error, developers should check error by diags.HasErrors() instead of err != nil.
Finally, we can decode body into a struct:
c = &Config{}
diags = gohcl.DecodeBody(file.Body, nil, c)
if diags.HasErrors() {
    return nil, fmt.Errorf("config parse: %w", diags)
}
To support different upstream type, we may want to delay the hcl.Body parse so that we can get strongly typed config struct:
type client struct {
   cfg *Upstream
   // DoT related config
   TLSServerName string `hcl:"tls_server_name,optional"`
}
func NewClient(cfg *Upstream) (*client, error) {
   c := &client{cfg: cfg}
   var diags hcl.Diagnostics
   diags = gohcl.DecodeBody(cfg.Options, nil, c)
   if diags.HasErrors() {
      return nil, fmt.Errorf("new domain list: %w", diags)
   }
   return c, nil
}
One great feature for hcl is the clear error message, take the TestParseMissingField as an example, we are missing a addr here:
=== RUN   TestParseMissingField
    TestParseMissingField: main_test.go:35: config parse: testdata/2.hcl:9,20-20: Missing required argument; The argument "addr" is required, but no definition was found.
--- FAIL: TestParseMissingField (0.00s)
FAIL
Conclusion
HCL is a strong type, strictly restricted, human readable, developer friendly configuration language which suitable for rich configuration application.
References
- Terraform's Configuration Syntax introduce (partial contents and structure copied here for no more web pages)
 - HCL Native Syntax Specification