HCLWrite: A better way to modify attributes?

Hello everyone,

Firstly, sorry if I’m in the wrong category.

I’ve been using the Golang hclwrite library for a few weeks now and was wondering if there was an easier way to handle my use case.

I am not using hclwrite to create terraform files, but to modify them. And I often have the problem that I need to add attributes to an object.

Let’s take the following example:

locals {
  an_attribute = {
    A = "AAA"
  }

  another_one = {
    "C" = "CCC"
  }

  and_again = {
    an_object = {
      a_key = "A Value"
    }
  }
}

In these three scenarios, I manage to access the attributes with this code:

package main

import (
  "fmt"
  "github.com/hashicorp/hcl/v2"
  "github.com/hashicorp/hcl/v2/hclwrite"
  "log"
  "os"
)

func main() {
  filePath := "terraform/file.tf"

  content, err := os.ReadFile(filePath)
  if err != nil {
    log.Fatal(err)
    return
  }

  file, diags := hclwrite.ParseConfig(content, filePath, hcl.Pos{Line: 1, Column: 1})
  if diags.HasErrors() {
      log.Fatal(diags.Error())
      return
  }

  body := file.Body()
  block := body.FirstMatchingBlock("locals", nil)

  an_attribute := block.Body().GetAttribute("an_attribute")
  another_one := block.Body().GetAttribute("another_one")
  and_again := block.Body().GetAttribute("and_again")

  fmt.Println(an_attribute.BuildTokens(nil).WriteTo(os.Stdout))
  fmt.Println(another_one.BuildTokens(nil).WriteTo(os.Stdout))
  fmt.Println(and_again.BuildTokens(nil).WriteTo(os.Stdout))
}

But if I want to add/modify an attribute to one of my objects, I have to deal with tokens.
Which is tedious. Is there an easier way, or should I continue down this path of pain?

It would be nice to be able to convert the attribute value to capsule type for example.

Thanks in advance!

Hi @GeorgeDuckman,

In your example you are only reading some existing attributes, and not writing anything. Can you say more about what modification you are planning to make? Straightforward modifications such as setting an attribute to a literal value do not typically involve working directly with raw tokens, but you will need to use tokens if you want to construct a more complex expression of a type that hclwrite doesn’t yet fully support.

Hi,

Sorry if I wasn’t clear enough.

My modification involves adding an attribute of to my attributes. If we take the example from before :


locals {
  an_attribute = {
    A = "AAA"
  }

  another_one = {
    "C" = "CCC"
  }

  and_again = {
    an_object = {
      a_key = "A Value"
    }
  }
}

Let’s say I want to add another value to my attribute an_attribute :

locals {
  an_attribute = {
    A = "AAA"
    B = "BBB"
  }

  ...

}

I have to write the logic to check if the value already exists, to modify it or to append it.

package main

import (
  "bytes"
  "github.com/hashicorp/hcl/v2"
  "github.com/hashicorp/hcl/v2/hclsyntax"
  "github.com/hashicorp/hcl/v2/hclwrite"
  "log"
  "os"
)

func main() {
  filePath := "terraform/file.tf"

  fileContent, err := os.ReadFile(filePath)
  if err != nil {
    log.Fatal(err)
    return
  }

  file, diags := hclwrite.ParseConfig(fileContent, filePath, hcl.Pos{Line: 1, Column: 1})
  if diags.HasErrors() {
    log.Fatal(diags.Error())
    return
  }

  body := file.Body()
  block := body.FirstMatchingBlock("locals", nil)

  attributeName := "an_attribute"
  an_attribute := block.Body().GetAttribute(attributeName)
  content := an_attribute.Expr().BuildTokens(nil)
  contentNoBraces := content[1 : len(content)-1]

  newKey := []byte("B")
  newValue := []byte("BBB")

  if keyAlreadyExists(contentNoBraces, newKey) {
    content = updateAttributeValue(content, newKey, newValue)
  } else {
    newTokens := hclwrite.Tokens{
      {Type: hclsyntax.TokenIdent, Bytes: newKey},
      {Type: hclsyntax.TokenEqual, Bytes: []byte("=")},
      {Type: hclsyntax.TokenOQuote, Bytes: []byte("\"")},
      {Type: hclsyntax.TokenQuotedLit, Bytes: newValue},
      {Type: hclsyntax.TokenCQuote, Bytes: []byte("\"")},
      {Type: hclsyntax.TokenNewline, Bytes: []byte("\n")},
    }
    contentNoBraces = append(contentNoBraces, newTokens...)
    content = append(hclwrite.Tokens{{Type: hclsyntax.TokenOBrace, Bytes: []byte("{")}}, contentNoBraces...)
    content = append(content, hclwrite.Tokens{{Type: hclsyntax.TokenCBrace, Bytes: []byte("}")}}...)
  }

  block.Body().SetAttributeRaw(attributeName, content)
  file.WriteTo(os.Stdout)
}

func keyAlreadyExists(tokens hclwrite.Tokens, key []byte) bool {
  for _, token := range tokens {
    if token.Type == hclsyntax.TokenIdent {
      if bytes.Equal(token.Bytes, key) {
        return true
      }
    }
  }
  return false
}

func updateAttributeValue(tokens hclwrite.Tokens, key, newValue []byte) hclwrite.Tokens {
  for i := 0; i < len(tokens); i++ {
    if tokens[i].Type == hclsyntax.TokenIdent && bytes.Equal(tokens[i].Bytes, key) {
      tokens[i+3].Bytes = newValue
      break
    }
  }
  return tokens
}

That’s fine for simple attributes like an_attribute or another_one.
But for more complex attributes like and_again, where I might just want to change part of an existing object or add a new one.
Of course, not every object is the same, so I have to write the logic several times.

Is there a better way of doing what I’m trying to do?
I hope I’ve explained my problem more clearly.

Thank you.

For what you’ve described, I would suggest first decoding the file using the normal hclsyntax package API to determine if any updates are needed. You can use that to work with the value of an_attribute rather than with the physical syntax it’s written with. This will get you to a cty.Value representing the current value of the attribute, which you can then use as the basis for a new value (another cty.Value) if needed.

If that initial analysis determines that an update is needed then you can parse the same source again using hclwrite and then use SetAttributeValue with your new value. That method will automatically construct tokens to represent the value you provided.


There are also some third-party tools/libraries that wrap hclwrite to provide a higher-level API for certain operations. I’ve not made extensive use of either of these and so I cannot vouch for their behavior, but you could consider either using these directly to solve your problem or using what they’re doing as an example for your own code:

Thanks for your quick answers ! I’m going to try them

Have a nice day, I close the subject