<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[WeakPixel's Brain Dump]]></title><description><![CDATA[WeakPixel's Brain Dump]]></description><link>https://blog.weakpixel.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 01:11:31 GMT</lastBuildDate><atom:link href="https://blog.weakpixel.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[(Part2) Hate YAML? Build your next tool with HCL!]]></title><description><![CDATA[This is the second part of my HCL series. You find the first part here (Part 1)
In the second post of my HCL series, I want to extend our example with:

Cobra Commandline

Variables

Functions


Cobra
Cobra is my favourite library to build command-li...]]></description><link>https://blog.weakpixel.com/part2-hate-yaml-build-your-next-tool-with-hcl</link><guid isPermaLink="true">https://blog.weakpixel.com/part2-hate-yaml-build-your-next-tool-with-hcl</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[WeakPixel]]></dc:creator><pubDate>Sun, 10 Apr 2022 14:17:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649600054433/fRT5wZRD8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the second part of my HCL series. You find the first part <a target="_blank" href="https://blog.weakpixel.com/hate-yaml-build-your-next-tool-with-hcl">here (Part 1)</a></p>
<p>In the second post of my HCL series, I want to extend our example with:</p>
<ul>
<li><p>Cobra Commandline</p>
</li>
<li><p>Variables</p>
</li>
<li><p>Functions</p>
</li>
</ul>
<h2 id="heading-cobra">Cobra</h2>
<p><a target="_blank" href="https://github.com/spf13/cobra">Cobra</a> is my favourite library to build command-line tools.</p>
<p>We start with the example program from the first post (<a target="_blank" href="https://gist.github.com/weakpixel/356f44e8a3d0a74c6e542967ade5eb00">source</a>).</p>
<p>As I write before I want to introduce you to the Cobra command-line tool. To use it we have to add a new import:</p>
<pre><code class="lang-golang"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"os"</span>

    <span class="hljs-string">"github.com/spf13/cobra"</span>
    <span class="hljs-comment">// ...</span>
</code></pre>
<p>Next, rename the <code>main()</code> function to <code>newRunCommand()</code> and refactor it to return a <a target="_blank" href="https://pkg.go.dev/github.com/spf13/cobra#Command">cobra.Command</a></p>
<pre><code class="lang-golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">newRunCommand</span><span class="hljs-params">()</span> *<span class="hljs-title">cobra</span>.<span class="hljs-title">Command</span></span> {
    <span class="hljs-comment">// contains all variables given by the user with --var "key=value"</span>
    vars := []<span class="hljs-keyword">string</span>{}
    cmd := cobra.Command{
        Use: <span class="hljs-string">"run"</span>
        Short: <span class="hljs-string">"Executes tasks"</span>,
        RunE: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(cmd *cobra.Command, args []<span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
            config := &amp;Config{}
            err := hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), <span class="hljs-literal">nil</span>, config)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> err
            }
            <span class="hljs-keyword">for</span> _, task := <span class="hljs-keyword">range</span> config.Tasks {
                fmt.Printf(<span class="hljs-string">"Task: %s\n"</span>, task.Name)
                <span class="hljs-keyword">for</span> _, step := <span class="hljs-keyword">range</span> task.Steps {
                    fmt.Printf(<span class="hljs-string">"    Step: %s %s\n"</span>, step.Type, step.Name)
                    <span class="hljs-keyword">var</span> runner Runner
                    <span class="hljs-keyword">switch</span> step.Type {
                    <span class="hljs-keyword">case</span> <span class="hljs-string">"mkdir"</span>:
                        runner = &amp;MkdirStep{}
                    <span class="hljs-keyword">case</span> <span class="hljs-string">"exec"</span>:
                        runner = &amp;ExecStep{}
                    <span class="hljs-keyword">default</span>:
                        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"unknown step type %q"</span>, step.Type)
                    }

                    diags := gohcl.DecodeBody(step.Remain, <span class="hljs-literal">nil</span>, runner)
                    <span class="hljs-keyword">if</span> diags.HasErrors() {
                        <span class="hljs-keyword">return</span> diags
                    }
                    err = runner.Run()
                    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                        <span class="hljs-keyword">return</span> err
                    }
                }
            }

            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
        },
    }
    <span class="hljs-comment">// Define an optional "var" flag for the commnd</span>
    cmd.Flags().StringArrayVar(&amp;vars, <span class="hljs-string">"var"</span>, <span class="hljs-literal">nil</span>, <span class="hljs-string">"Sets variable. Format &lt;name&gt;=&lt;value&gt;"</span>)
    <span class="hljs-keyword">return</span> &amp;cmd
}
</code></pre>
<p>The <code>Use</code> field describes the subcommand name. The <code>Short</code> field allows defining a short command description. The <code>RunE</code> implements the execution of the (sub-)command. It contains our HCL parsing code. Since <code>RunE</code> allows us to return an error we also have refactored the code to just return an error instead of using <code>os.Exit(1)</code>.</p>
<p>After that, we implement a new <code>main</code> function looking like:</p>
<pre><code class="lang-golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    root := cobra.Command{
        Use: <span class="hljs-string">"taskexec"</span>,
    }
    root.AddCommand(newRunCommand())
    err := root.Execute()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Println(err)
        os.Exit(<span class="hljs-number">1</span>)
    }
}
</code></pre>
<p>The root command is just an empty <code>cobra.Command</code>. To the root command we add our subcommand with <code>root.AddCommand(newRunCommand())</code>.</p>
<p>Let's try out what happens if we run our program:</p>
<pre><code class="lang-plaintext">go run main.go 
Usage:
  taskexec [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  run         Executes tasks

Flags:
  -h, --help   help for taskexec
</code></pre>
<p>Let's try to show the help for the subcommand:</p>
<pre><code class="lang-plaintext">go run main.go run -h
Executes tasks

Usage:
  taskexec run [flags]

Flags:
  -h, --help              help for run
      --var stringArray   Sets variable. Format &lt;name&gt;=&lt;value&gt;
</code></pre>
<p>Great! Next, we want to make use of the variables. To use variables in our HCL config, we must learn about the <code>hcl.EvalContext</code></p>
<h2 id="heading-evalcontext">EvalContext</h2>
<p>The <a target="_blank" href="https://pkg.go.dev/github.com/hashicorp/hcl2/hcl#EvalContext">hcl.</a>EvalContext allows us to define variables and functions</p>
<pre><code class="lang-golang"><span class="hljs-keyword">type</span> EvalContext <span class="hljs-keyword">struct</span> {
    Variables <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]cty.Value
    Functions <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]function.Function
}
</code></pre>
<p>For now, we focus on the variables. The <code>Variables</code> map allows us to define the variable name as key and as value a <code>cty.Value</code>. The <code>cty.Value</code> is part of the <code>github.com/zclconf/go-cty/cty</code> package. The package provides a dynamic type system.</p>
<p>You can read more about <code>cty</code> on the <a target="_blank" href="https://github.com/zclconf/go-cty">github project</a>.</p>
<p>Let's come back to <code>hcl.EvalContext</code>. Where is this context struct actually used? In our example code, we have two instances:</p>
<pre><code class="lang-golang">hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), 
    <span class="hljs-comment">/*&amp;hcl.EvalContext{}*/</span> <span class="hljs-literal">nil</span>, config)
</code></pre>
<p>and</p>
<pre><code class="lang-golang">diags := gohcl.DecodeBody(step.Remain,
     <span class="hljs-comment">/*&amp;hcl.EvalContext{}*/</span> <span class="hljs-literal">nil</span>, runner)
</code></pre>
<h2 id="heading-variables">Variables</h2>
<p>In our command, we have defined a <code>vars</code> slice which contains the user-defined variables in the format:</p>
<pre><code class="lang-plaintext">--var "key=value" ...
</code></pre>
<p>So let's get started and create <code>hcl.EvalContext</code> and populate it with the <code>vars</code> parameters from the command line.</p>
<pre><code class="lang-golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">newEvalContext</span><span class="hljs-params">(vars []<span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(*hcl.EvalContext, error)</span></span> {
    varMap := <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]cty.Value{}
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> vars {
        el := strings.Split(v, <span class="hljs-string">"="</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(el) != <span class="hljs-number">2</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"invalid format: %s"</span>, v)
        }
        varMap[el[<span class="hljs-number">0</span>]] = cty.StringVal(el[<span class="hljs-number">1</span>])
    }

    ctx := &amp;hcl.EvalContext{}
    ctx.Variables = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]cty.Value{
        <span class="hljs-string">"var"</span>: cty.ObjectVal(varMap),
    }
    <span class="hljs-keyword">return</span> ctx, <span class="hljs-literal">nil</span>
}
</code></pre>
<p>We use the <code>newEvalContext()</code> function in our subcommand to create the EvalContext and use the context in all places where we decode the HCL document:</p>
<pre><code class="lang-golang"><span class="hljs-comment">// ...</span>
RunE: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(cmd *cobra.Command, args []<span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    ctx, err := newEvalContext(vars)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    config := &amp;Config{}
    err = hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), ctx, config)
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">for</span> _, task := <span class="hljs-keyword">range</span> config.Tasks {
        fmt.Printf(<span class="hljs-string">"Task: %s\n"</span>, task.Name)
        <span class="hljs-keyword">for</span> _, step := <span class="hljs-keyword">range</span> task.Steps {
            <span class="hljs-comment">// ...</span>
            diags := gohcl.DecodeBody(step.Remain, ctx, runner)
            <span class="hljs-comment">// ...</span>
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
},
<span class="hljs-comment">// ...</span>
</code></pre>
<p>And finally, we change our <code>exampleHCL</code> to make use of variables:</p>
<pre><code class="lang-golang">exampleHCL = <span class="hljs-string">`
    task "first_task" {
        step "mkdir" "build_dir" {
            path = var.buildDir
        }
        step "exec" "list_build_dir" {
            command = "ls ${var.buildDir}"
        }
    }
`</span>
</code></pre>
<p>Let's try to execute the command without defining the <code>buildDir</code> variable:</p>
<pre><code class="lang-plaintext">go run main.go run 
...
example.hcl:4,15-24: Unsupported attribute; This object does not have an attribute named "buildDir"., and 1 other diagnostic(s)
exit status 1
</code></pre>
<p>Good, it fails with a detailed error message.</p>
<p>Now we try to execute the command with the needed variable:</p>
<pre><code class="lang-plaintext">go run main.go run --var buildDir=./build
Task: first_task
    Step: mkdir build_dir
    Step: exec list_build_dir
</code></pre>
<p>And it works as expected!</p>
<p>You can see the full source code <a target="_blank" href="https://gist.github.com/weakpixel/c92f8427b6197a501c1a8d0595e5b5db">here</a></p>
<h2 id="heading-functions">Functions</h2>
<p>Next, we want to explore how e.g. Terraform provides these nice inline functions which makes life so much easier to deal with input variables. It might not make much sense in our example but let's try to implement a function that converts all cased letters into uppercase:</p>
<pre><code class="lang-plaintext">helloValue = "${upper("hello")} World"
</code></pre>
<p>To implement a function we must add a new module to our import <code>"github.com/zclconf/go-cty/cty/function"</code>. We have to use the <code>function.Spec</code> struct to create with <code>function.New</code> our function implementation:</p>
<pre><code class="lang-golang"><span class="hljs-keyword">var</span> upperFn = function.New(&amp;function.Spec{
    <span class="hljs-comment">// Define the required parameters.</span>
    Params: []function.Parameter{
        {
            Name:             <span class="hljs-string">"str"</span>,
            Type:             cty.String,
            AllowDynamicType: <span class="hljs-literal">true</span>,
        },
    },
    <span class="hljs-comment">// Define the return type</span>
    Type: function.StaticReturnType(cty.String),
    <span class="hljs-comment">// Function implementation:</span>
    Impl: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []cty.Value, retType cty.Type)</span> <span class="hljs-params">(cty.Value, error)</span></span> {
        in := args[<span class="hljs-number">0</span>].AsString()
        out := strings.ToUpper(in)
        <span class="hljs-keyword">return</span> cty.StringVal(out), <span class="hljs-literal">nil</span>
    },
})
</code></pre>
<p>And last we add the new function to our EvalContext:</p>
<pre><code class="lang-golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">newEvalContext</span><span class="hljs-params">(vars []<span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(*hcl.EvalContext, error)</span></span> {

    <span class="hljs-comment">// ...</span>

    ctx.Functions = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]function.Function{
        <span class="hljs-string">"upper"</span>: upperFn,
    }
    <span class="hljs-keyword">return</span> ctx, <span class="hljs-literal">nil</span>
}
</code></pre>
<p>Update the <code>exampleHCL</code> to make use of our brand new defined function:</p>
<pre><code class="lang-plaintext">exampleHCL = `
    task "first_task" {
        step "mkdir" "build_dir" {
            path = upper(var.buildDir)
        }
        step "exec" "list_build_dir" {
            command = "ls ${ upper(var.buildDir) }"
        }
    }
`
</code></pre>
<p>Add some debug output to our example Step execution (mkdir, exec) and run the program:</p>
<pre><code class="lang-plaintext">go run main.go run --var "buildDir=./build"

Task: first_task
    Step: mkdir build_dir
        Path:./build
    Step: exec list_build_dir
        Command: ls ./BUILD
</code></pre>
<p>and as expected we have an upper-case build directory.</p>
<p>If you don't want to implement all the functions yourself or you need some inspiration to implement a function you find want you looking for here:</p>
<ul>
<li><p>https://pkg.go.dev/github.com/zclconf/go-cty/cty/function/stdlib</p>
</li>
<li><p>https://github.com/hashicorp/terraform/blob/main/internal/lang/functions.go</p>
</li>
</ul>
<h2 id="heading-resources">Resources</h2>
<p>Resources:</p>
<ul>
<li><p><a target="_blank" href="https://blog.weakpixel.com/hate-yaml-build-your-next-tool-with-hcl">Part 1 - Hate YAML? Build your next tool with HCL!</a></p>
</li>
<li><p><a target="_blank" href="https://gist.github.com/weakpixel/40147127fdcac0e11f74967a9ef5aaad">Full Source Code Gist</a></p>
</li>
</ul>
<h2 id="heading-full-source-code">Full Source Code</h2>
<pre><code class="lang-golang"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"strings"</span>

    <span class="hljs-string">"github.com/spf13/cobra"</span>
    <span class="hljs-string">"github.com/zclconf/go-cty/cty"</span>

    <span class="hljs-string">"github.com/hashicorp/hcl/v2"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/gohcl"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/hclsimple"</span>

    <span class="hljs-string">"github.com/zclconf/go-cty/cty/function"</span>
)

<span class="hljs-keyword">var</span> (
    exampleHCL = <span class="hljs-string">`
        task "first_task" {
            step "mkdir" "build_dir" {
                path = upper(var.buildDir)
            }
            step "exec" "list_build_dir" {
                command = "ls ${ upper(var.buildDir) }"
            }
        }
    `</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    root := cobra.Command{
        Use: <span class="hljs-string">"taskexec"</span>,
    }
    root.AddCommand(newRunCommand())
    err := root.Execute()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Println(err)
        os.Exit(<span class="hljs-number">1</span>)
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">newRunCommand</span><span class="hljs-params">()</span> *<span class="hljs-title">cobra</span>.<span class="hljs-title">Command</span></span> {
    vars := []<span class="hljs-keyword">string</span>{}
    cmd := cobra.Command{
        Use:   <span class="hljs-string">"run"</span>,
        Short: <span class="hljs-string">"Executes tasks"</span>,
        RunE: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(cmd *cobra.Command, args []<span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
            ctx, err := newEvalContext(vars)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> err
            }
            config := &amp;Config{}
            err = hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), ctx, config)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> err
            }
            <span class="hljs-keyword">for</span> _, task := <span class="hljs-keyword">range</span> config.Tasks {
                fmt.Printf(<span class="hljs-string">"Task: %s\n"</span>, task.Name)
                <span class="hljs-keyword">for</span> _, step := <span class="hljs-keyword">range</span> task.Steps {
                    fmt.Printf(<span class="hljs-string">"    Step: %s %s\n"</span>, step.Type, step.Name)
                    <span class="hljs-keyword">var</span> runner Runner
                    <span class="hljs-keyword">switch</span> step.Type {
                    <span class="hljs-keyword">case</span> <span class="hljs-string">"mkdir"</span>:
                        runner = &amp;MkdirStep{}
                    <span class="hljs-keyword">case</span> <span class="hljs-string">"exec"</span>:
                        runner = &amp;ExecStep{}
                    <span class="hljs-keyword">default</span>:
                        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"unknown step type %q"</span>, step.Type)
                    }

                    diags := gohcl.DecodeBody(step.Remain, ctx, runner)
                    <span class="hljs-keyword">if</span> diags.HasErrors() {
                        <span class="hljs-keyword">return</span> diags
                    }
                    err = runner.Run()
                    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                        <span class="hljs-keyword">return</span> err
                    }
                }
            }

            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
        },
    }

    cmd.Flags().StringArrayVar(&amp;vars, <span class="hljs-string">"var"</span>, <span class="hljs-literal">nil</span>, <span class="hljs-string">"Sets variable. Format &lt;name&gt;=&lt;value&gt;"</span>)

    <span class="hljs-keyword">return</span> &amp;cmd
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">newEvalContext</span><span class="hljs-params">(vars []<span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(*hcl.EvalContext, error)</span></span> {
    varMap := <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]cty.Value{}
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> vars {
        el := strings.Split(v, <span class="hljs-string">"="</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(el) != <span class="hljs-number">2</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"invalid format: %s"</span>, v)
        }
        varMap[el[<span class="hljs-number">0</span>]] = cty.StringVal(el[<span class="hljs-number">1</span>])
    }

    ctx := &amp;hcl.EvalContext{}
    ctx.Variables = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]cty.Value{
        <span class="hljs-string">"var"</span>: cty.ObjectVal(varMap),
    }
    ctx.Functions = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]function.Function{
        <span class="hljs-string">"upper"</span>: upperFn,
    }
    <span class="hljs-keyword">return</span> ctx, <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">var</span> upperFn = function.New(&amp;function.Spec{
    <span class="hljs-comment">// Define the required parameters.</span>
    Params: []function.Parameter{
        {
            Name:             <span class="hljs-string">"str"</span>,
            Type:             cty.String,
            AllowDynamicType: <span class="hljs-literal">true</span>,
        },
    },
    <span class="hljs-comment">// Define the return type</span>
    Type: function.StaticReturnType(cty.String),
    <span class="hljs-comment">// Function implementation:</span>
    Impl: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []cty.Value, retType cty.Type)</span> <span class="hljs-params">(cty.Value, error)</span></span> {
        in := args[<span class="hljs-number">0</span>].AsString()
        out := strings.ToUpper(in)
        <span class="hljs-keyword">return</span> cty.StringVal(out), <span class="hljs-literal">nil</span>
    },
})

<span class="hljs-keyword">type</span> Config <span class="hljs-keyword">struct</span> {
    Tasks []*Task <span class="hljs-string">`hcl:"task,block"`</span>
}
<span class="hljs-keyword">type</span> Task <span class="hljs-keyword">struct</span> {
    Name  <span class="hljs-keyword">string</span>  <span class="hljs-string">`hcl:"name,label"`</span>
    Steps []*Step <span class="hljs-string">`hcl:"step,block"`</span>
}

<span class="hljs-keyword">type</span> Step <span class="hljs-keyword">struct</span> {
    Type   <span class="hljs-keyword">string</span>   <span class="hljs-string">`hcl:"type,label"`</span>
    Name   <span class="hljs-keyword">string</span>   <span class="hljs-string">`hcl:"name,label"`</span>
    Remain hcl.Body <span class="hljs-string">`hcl:",remain"`</span>
}

<span class="hljs-keyword">type</span> ExecStep <span class="hljs-keyword">struct</span> {
    Command <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"command"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *ExecStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    fmt.Println(<span class="hljs-string">"\tCommand: "</span> + s.Command)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">type</span> MkdirStep <span class="hljs-keyword">struct</span> {
    Path <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"path"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *MkdirStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    fmt.Println(<span class="hljs-string">"\tPath:"</span> + s.Path)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">type</span> Runner <span class="hljs-keyword">interface</span> {
    Run() error
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Hate YAML? Build your next tool with HCL!]]></title><description><![CDATA[In this post, I want to show how you can implement your own tool using the HCL format. The HCL configuration format is used by all the amazing HasiCorp tools like Terraform, Vault, and Nomad.
Most modern applications and tools use YAML these days whi...]]></description><link>https://blog.weakpixel.com/hate-yaml-build-your-next-tool-with-hcl</link><guid isPermaLink="true">https://blog.weakpixel.com/hate-yaml-build-your-next-tool-with-hcl</guid><category><![CDATA[Go Language]]></category><category><![CDATA[tools]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[WeakPixel]]></dc:creator><pubDate>Wed, 30 Mar 2022 12:33:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648738341051/9iHpl0PP0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, I want to show how you can implement your own tool using the HCL format. The HCL configuration format is used by all the amazing HasiCorp tools like <a target="_blank" href="https://www.terraform.io/">Terraform</a>, <a target="_blank" href="https://www.vaultproject.io/">Vault</a>, and <a target="_blank" href="https://www.nomadproject.io/">Nomad</a>.</p>
<p>Most modern applications and tools use YAML these days which is generally an easy-to-read format but can also be the cause of a lot of pain because of the white space sensitivity. HCL on the other hand provides an easy-to-read format with a clear structure and additional features like variable interpolation and inline function calls.</p>
<p>Let's start with a really simple example to parse HCL.</p>
<pre><code class="lang-plaintext">task "first_task" {
    exec "list_current_dir" {
        command = "ls ."
    }

    exec "list_var_dir" {
        command = "ls /var"
    }
}
</code></pre>
<p>To map the HCL to our structs we can use struct tags:</p>
<pre><code class="lang-golang"><span class="hljs-keyword">type</span> Config <span class="hljs-keyword">struct</span> {
    Tasks []*Task <span class="hljs-string">`hcl:"task,block"`</span>
}
<span class="hljs-keyword">type</span> Task <span class="hljs-keyword">struct</span> {
    Name  <span class="hljs-keyword">string</span>     <span class="hljs-string">`hcl:"name,label"`</span>
    Steps []*ExecStep <span class="hljs-string">`hcl:"exec,block"`</span>
}

<span class="hljs-keyword">type</span> ExecStep <span class="hljs-keyword">struct</span> {
    Name    <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"name,label"`</span>
    Command <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"command"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *ExecStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p>And to decode the HCL we can use the <code>decode</code> function from the <code>hclsimple</code> package</p>
<pre><code class="lang-golang"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/hclsimple"</span>
)

<span class="hljs-keyword">var</span> (
    exampleHCL = <span class="hljs-string">`
        task "first_task" {
            exec "list_current_dir" {
                command = "ls ."
            }

            exec "list_var_dir" {
                command = "ls /var"
            }
        }
    `</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    config := &amp;Config{}
    err := hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), <span class="hljs-literal">nil</span>, config)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Println(err)
        os.Exit(<span class="hljs-number">1</span>)
    }
    <span class="hljs-keyword">for</span> _, task := <span class="hljs-keyword">range</span> config.Tasks {
        fmt.Printf(<span class="hljs-string">"Task: %s\n"</span>, task.Name)
        <span class="hljs-keyword">for</span> _, step := <span class="hljs-keyword">range</span> task.Steps {
            fmt.Printf(<span class="hljs-string">"    Step: %s %q\n"</span>, step.Name, step.Command)
            err = step.Run()
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                fmt.Println(err)
                os.Exit(<span class="hljs-number">1</span>)
            }
        }
    }
}
</code></pre>
<p>That was easy!</p>
<p>But what if I want to support different Step types? Let's say I want to support <code>mkdir</code> to easily create directories.</p>
<pre><code class="lang-plaintext">task "first_task" {
    mkdir "build_dir" {
        path = "./build"
    }
    exec "list_var_dir" {
        command = "ls /var"
    }
}
</code></pre>
<p>If I run our tool I get the following error:</p>
<pre><code class="lang-plaintext">example.hcl:3,4-9: Unsupported block type; Blocks of type "mkdir" are not expected here.
</code></pre>
<p>We could update our Task struct and add a block list for "mkdir":</p>
<pre><code class="lang-golang"><span class="hljs-keyword">type</span> Task <span class="hljs-keyword">struct</span> {
    Name      <span class="hljs-keyword">string</span>       <span class="hljs-string">`hcl:"name,label"`</span>
    ExecSteps []*ExecStep  <span class="hljs-string">`hcl:"exec,block"`</span>
    MkdirStep []*MkdirStep <span class="hljs-string">`hcl:"mkdir,block"`</span>
}
</code></pre>
<p>but obviously, we would lose the execution order since we have two separate lists. This is not going to work for us.</p>
<p>As an alternative solution, we could change our configuration and make the Step type a label:</p>
<pre><code class="lang-plaintext">task "first_task" {
    step "mkdir" "build_dir" {
        path = "./build/"
    }
    step "exec" "list_build_dir" {
        command = "ls ./build/"
    }
}
</code></pre>
<p>Let's reflect the configuration change to our structs.</p>
<pre><code class="lang-golang"><span class="hljs-keyword">type</span> Config <span class="hljs-keyword">struct</span> {
    Tasks []*Task <span class="hljs-string">`hcl:"task,block"`</span>
}

<span class="hljs-keyword">type</span> Task <span class="hljs-keyword">struct</span> {
    Name  <span class="hljs-keyword">string</span>  <span class="hljs-string">`hcl:"name,label"`</span>
    Steps []*Step <span class="hljs-string">`hcl:"step,block"`</span>
}

<span class="hljs-keyword">type</span> Step <span class="hljs-keyword">struct</span> {
    Type   <span class="hljs-keyword">string</span>   <span class="hljs-string">`hcl:"type,label"`</span>
    Name   <span class="hljs-keyword">string</span>   <span class="hljs-string">`hcl:"name,label"`</span>
    Remain hcl.Body <span class="hljs-string">`hcl:",remain"`</span>
}

<span class="hljs-keyword">type</span> ExecStep <span class="hljs-keyword">struct</span> {
    Command <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"command"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *ExecStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">type</span> MkdirStep <span class="hljs-keyword">struct</span> {
    Path <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"path"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *MkdirStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p>As you can see we have added a new <code>Step</code> struct and use it in the <code>Tasks</code> struct instead of the <code>ExecStep</code> The <code>Step</code> struct has an extra field called <code>Remain</code>. This field is required to capture all fields of the actual Step implementation. We will see later how we use the <code>Remain</code> field to decode the fields into our actual Step implementation.</p>
<p>Lastly, we add a new interface that allows us to run the Step implementation:</p>
<pre><code class="lang-golang"><span class="hljs-keyword">type</span> Runner <span class="hljs-keyword">interface</span> {
    Run() error
}
</code></pre>
<p>You can see above that all our Steps implement the Runner interface.</p>
<p>Now we have to adapt our parsing code:</p>
<pre><code class="lang-golang"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"os"</span>

    <span class="hljs-string">"github.com/hashicorp/hcl/v2"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/gohcl"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/hclsimple"</span>
)

<span class="hljs-keyword">var</span> (
    exampleHCL = <span class="hljs-string">`
        task "first_task" {
            step "mkdir" "build_dir" {
                path = "./build/"
            }
            step "exec" "list_build_dir" {
                command = "ls ./build/"
            }
        }
    `</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    config := &amp;Config{}
    err := hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), <span class="hljs-literal">nil</span>, config)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Println(err)
        os.Exit(<span class="hljs-number">1</span>)
    }
    <span class="hljs-keyword">for</span> _, task := <span class="hljs-keyword">range</span> config.Tasks {
        fmt.Printf(<span class="hljs-string">"Task: %s\n"</span>, task.Name)
        <span class="hljs-keyword">for</span> _, step := <span class="hljs-keyword">range</span> task.Steps {
            fmt.Printf(<span class="hljs-string">"    Step: %s %s\n"</span>, step.Type, step.Name)
            <span class="hljs-comment">// The actual step implementation </span>
            <span class="hljs-comment">// which implements the Runner interface</span>
            <span class="hljs-keyword">var</span> runner Runner

            <span class="hljs-comment">// Determine the step implementation</span>
            <span class="hljs-keyword">switch</span> step.Type {
            <span class="hljs-keyword">case</span> <span class="hljs-string">"mkdir"</span>:
                runner = &amp;MkdirStep{}
            <span class="hljs-keyword">case</span> <span class="hljs-string">"exec"</span>:
                runner = &amp;ExecStep{}
            <span class="hljs-keyword">default</span>:
                fmt.Printf(<span class="hljs-string">"Unknown step type %q\n"</span>, step.Type)
                os.Exit(<span class="hljs-number">1</span>)
            }

            <span class="hljs-comment">// Decode remaining fields into our step implementation.</span>
            diags := gohcl.DecodeBody(step.Remain, <span class="hljs-literal">nil</span>, runner)
            <span class="hljs-keyword">if</span> diags.HasErrors() {
                fmt.Println(diags)
                os.Exit(<span class="hljs-number">1</span>)
            }

            err = runner.Run()
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                fmt.Println(err)
                os.Exit(<span class="hljs-number">1</span>)
            }
        }
    }
}
</code></pre>
<p>The parsing does determine the Step type in the switch statement and creates an instance of the Struct. The struct is then the target of <code>gohcl.DecodeBody(step.Remain, nil, runner)</code> which decodes the remaining fields.</p>
<p>Voilà, we have an easy-to-extend Task execution engine.</p>
<h2 id="heading-what-is-next">What is Next?</h2>
<p>In <a target="_blank" href="https://blog.weakpixel.com/part2-hate-yaml-build-your-next-tool-with-hcl">Part 2</a> of the HCL series, we are going to implement Variables and Functions</p>
<h2 id="heading-resources">Resources</h2>
<p>Godocs:</p>
<ul>
<li><p><a target="_blank" href="https://pkg.go.dev/github.com/hashicorp/hcl/v2">hcl</a></p>
</li>
<li><p><a target="_blank" href="https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclsimple">hclsimple</a></p>
</li>
<li><p><a target="_blank" href="https://pkg.go.dev/github.com/hashicorp/hcl/v2/gohcl">gohcl</a></p>
</li>
</ul>
<p>Others:</p>
<ul>
<li><a target="_blank" href="https://buildmedia.readthedocs.org/media/pdf/hcl/guide/hcl.pdf">buildmedia.readthedocs.org/hcl.pdf</a></li>
</ul>
<p>Source Gists:</p>
<ul>
<li><p><a target="_blank" href="https://gist.github.com/weakpixel/8501a0dabeeb800ef38b6d1cc9fa1e72">Example 1</a></p>
</li>
<li><p><a target="_blank" href="https://gist.github.com/weakpixel/356f44e8a3d0a74c6e542967ade5eb00">Example 2</a></p>
</li>
</ul>
<h2 id="heading-full-source-code">Full Source Code</h2>
<pre><code class="lang-golang"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"os"</span>

    <span class="hljs-string">"github.com/hashicorp/hcl/v2"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/gohcl"</span>
    <span class="hljs-string">"github.com/hashicorp/hcl/v2/hclsimple"</span>
)

<span class="hljs-keyword">var</span> (
    exampleHCL = <span class="hljs-string">`
        task "first_task" {
            step "mkdir" "build_dir" {
                path = "./build/"
            }
            step "exec" "list_build_dir" {
                command = "ls ./build/"
            }
        }
    `</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    config := &amp;Config{}
    err := hclsimple.Decode(<span class="hljs-string">"example.hcl"</span>, []<span class="hljs-keyword">byte</span>(exampleHCL), <span class="hljs-literal">nil</span>, config)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Println(err)
        os.Exit(<span class="hljs-number">1</span>)
    }
    <span class="hljs-keyword">for</span> _, task := <span class="hljs-keyword">range</span> config.Tasks {
        fmt.Printf(<span class="hljs-string">"Task: %s\n"</span>, task.Name)
        <span class="hljs-keyword">for</span> _, step := <span class="hljs-keyword">range</span> task.Steps {
            fmt.Printf(<span class="hljs-string">"    Step: %s %s\n"</span>, step.Type, step.Name)

            <span class="hljs-keyword">var</span> runner Runner

            <span class="hljs-keyword">switch</span> step.Type {
            <span class="hljs-keyword">case</span> <span class="hljs-string">"mkdir"</span>:
                runner = &amp;MkdirStep{}
            <span class="hljs-keyword">case</span> <span class="hljs-string">"exec"</span>:
                runner = &amp;ExecStep{}
            <span class="hljs-keyword">default</span>:
                fmt.Printf(<span class="hljs-string">"Unknown step type %q\n"</span>, step.Type)
                os.Exit(<span class="hljs-number">1</span>)
            }

            diags := gohcl.DecodeBody(step.Remain, <span class="hljs-literal">nil</span>, runner)
            <span class="hljs-keyword">if</span> diags.HasErrors() {
                fmt.Println(diags)
                os.Exit(<span class="hljs-number">1</span>)
            }
            err = runner.Run()
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                fmt.Println(err)
                os.Exit(<span class="hljs-number">1</span>)
            }
        }
    }
}

<span class="hljs-keyword">type</span> Config <span class="hljs-keyword">struct</span> {
    Tasks []*Task <span class="hljs-string">`hcl:"task,block"`</span>
}
<span class="hljs-keyword">type</span> Task <span class="hljs-keyword">struct</span> {
    Name  <span class="hljs-keyword">string</span>  <span class="hljs-string">`hcl:"name,label"`</span>
    Steps []*Step <span class="hljs-string">`hcl:"step,block"`</span>
}

<span class="hljs-keyword">type</span> Step <span class="hljs-keyword">struct</span> {
    Type   <span class="hljs-keyword">string</span>   <span class="hljs-string">`hcl:"type,label"`</span>
    Name   <span class="hljs-keyword">string</span>   <span class="hljs-string">`hcl:"name,label"`</span>
    Remain hcl.Body <span class="hljs-string">`hcl:",remain"`</span>
}

<span class="hljs-keyword">type</span> ExecStep <span class="hljs-keyword">struct</span> {
    Command <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"command"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *ExecStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// Implement me</span>
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">type</span> MkdirStep <span class="hljs-keyword">struct</span> {
    Path <span class="hljs-keyword">string</span> <span class="hljs-string">`hcl:"path"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *MkdirStep)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// Implement me</span>
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">type</span> Runner <span class="hljs-keyword">interface</span> {
    Run() error
}
</code></pre>
]]></content:encoded></item></channel></rss>