Hclwrite - insert block at specific position

Excuse me if I post this at the wrong forum, but I can’t find any specific one for HCL.

Looking at the hclwrite functions Insert and InsertNode, what do they expect as input for the pos *node argument?

An example would be great! I’m completely new to GO and I’ve been trying lots of stuff to the best of my abilities, but I struggle to being able to use the functions since I have no idea how to. However, being able to insert blocks at given positions is something that I’d really need to be able to do, so I’m really hoping for this functionality. :slightly_smiling_face:

Hi @aleqsss,

I don’t think the public API of hclwrite currently allows inserting items in arbitrary locations; it’s focused on either appending new items to the end of a body or replacing attribute values in-place.

What you’ve found and linked to here is part of the internal implementation details of hclwrite, which isn’t accessible to external callers. This package is designed in a pretty general way to allow meeting a variety of use-cases over time, but the low-level internal representation is not very ergonomic and we expect that it will probably need tweaks and changes over time as the scope of this package grows, so we intentionally limited the public API to only well-understood needs where it was clearer how to design an API that won’t need to be broken in future to support new features.

With the current scope of this package I think you will either need to only append new items and not try to constrain the order, or to rebuild a body after emptying it so that you can append everything again in the order you want. The latter is probably not straightforward and will probably require working with the raw tokens in some spots, rather than with the higher-level API.

If you are able to share more about your goals in a feature request in the HCL repository we could consider some new API, but I want to be up front with you that nobody on the Terraform team is currently focused on hclwrite and so the team will probably not be able to promptly design, implement, or merge new features at the moment.

Hi @apparentlymart,

Thank you for this answer, much appreciated since I struggle with how to edit some already existing HCL2 files.

As I know that you’re really competent, seeing your other replies in various forums over the years, I won’t question your answer. However, there are still some things I can’t help but wonder about.

Background story:
Since AppendBlock essentially seems to be using Append/AppendNode in the background via appendItem, I created the following (in attempt to “mimic” that behaviour):

func (b *Body) InsertBlock(pos *node, block *Block) *Block {
	b.insertItem(pos, block)
	return block
}
func (b *Body) insertItem(pos *node, c nodeContent) *node {
	nn := b.children.Insert(pos, c)
	b.items.Add(nn)
	return nn
}

And running this action on an empty node by specifying the position as nil, as stated here:

actually worked as expected. Hence my question what the Insert and InsertNode would expect as input for the pos *node argument. :slightly_smiling_face:

But perhaps my above tested implementation is crap for a ton of reasons, this was just a desperate attempt in trying to use the functions to be able to insert at a specific position.

I think I already now the answer, but I must ask for the piece of my mind (have been struggling and testing things with this for weeks); is there any way you see with the above code, to pass the data needed for the pos *node argument? Or do you think I should just let go of it and wait for my (to be created, as per your advice) feature request to be picked up? :slightly_smiling_face:

Hi @aleqsss,

Indeed, the key problem here is that node is not an exported type and so using it as part of the signature of an exported function is tricky: it is not possible for code outside of the package to directly construct a value of that type, or even to mention the name of the type when declaring a variable.

To make this work in the exported API would need to accept one of the types that wraps this internal node type and then make the insert function retrieve the node from it to pass to the internal function. I expect it’s possible to design such a thing, but I’ve not thought deeply about it so I’m not sure what exactly that would look like.

Roger that! Feature request it is then, thank you for the valuable input! :slightly_smiling_face:

Another question that is relevant to the topic:

Let’s say I have a Terraform configuration of 300 lines, if I run (Line: 1):

parsedConfig := hclwrite.ParseConfig(tfSourceFile, "HCL2config", hcl.Pos{Line: 1, Column: 1})
fmt.Printf("%s", parsedConfig.Bytes())

As expected I’ll get the whole configuration starting from line 1, as stated in the comments of the Pos struct:

type Pos struct {
    // Line is the source code line where this position points. Lines are
    // counted starting at 1 and incremented for each newline character
    // encountered.
    Line int 

However, if I run (Line: 100):

parsedConfig := hclwrite.ParseConfig(tfSourceFile, "HCL2config", hcl.Pos{Line: 100, Column: 1})
fmt.Printf("%s", parsedConfig.Bytes())

It still prints out the whole configuration, starting from line 1. I would expect it to only contain the part of the configuration starting from line 100. Do I use or interpret this argument incorrectly?

This is relevant to the topic, because I could perhaps potentially use this to set a starting point, and then prepend stuff to kind of get a functionality to insert at a specific position.

Hi @aleqsss,

That last two arguments to ParseConfig are essentially telling hclwrite where you got this data from: if you loaded an entire file from disk and passed it in then you should pass the name of that file and hcl.InitialPos, which is a predefined range value representing “the start of the file”, and is the same as your hcl.Pos{Line: 1, Column: 1}.

You’d only pass a position other than hcl.InitialPos here if for some reason you were passing only part of a file. If you want to do that then you’d need to do that before you call hclwrite.ParseConfig, trimming off the beginning of the byte slice to the appropriate point and then telling hclwrite what part of the file the first byte of your given source code represents.

hclwrite uses this information to return source location information in error messages, so passing an incorrect value for that argument as you did in your second example will just cause hclwrite to return incorrect source position values in error messages, assuming any error messages are returned.