You may want to inform yourself about human rights in China.

On Piping a Go Text/Template to/From a Shell Command

date: 2023-08-14

This article shows how to send the content of a Go template to the stdin of an arbitrary command, and capture its output. To make things clearer, consider the following example: assume you’ve installed pygmentize(1), the command line interface for the famous Pygments syntax highlighter, and want to use it to highlight a chunk of code, say, in the context of a static site generator.

The idea is to first define a template containing the code to highlight:

{{- define "the-code-to-highlight" -}}
#include <stdio.h>

int
main(void)
{
	printf("hello, world!\n");
	return 0;
}
{{- end -}}

And then to launch the relevant command with the help of some glue code targeting the previously defined template, something like:

{{- run .this (sarr "pygmentize" "-f" "html" "-l" "C") "the-code-to-highlight" }}

Our final goal is to implement such a run function, and to clarify a little its interface.

Valves (and pipes) at the switch house east at Landschaftspark Duisburg-Nord, Duisburg, North Rhine-Westphalia, Germany

Valves (and pipes) at the switch house east at Landschaftspark Duisburg-Nord, Duisburg, North Rhine-Westphalia, Germany by Dietmar Rabich through wikimedia.org CC BY-SA-4.0

Getting started

I’ll essentially assume that you’re familiar with the text/template package; the documentation is comprehensive, and I’ve covered some specific elements that might feels unexpected earlier.

We’ll start by writing a template function processing a template referred to by its name. Note how the this parameter allows us to call say from within the code template; in the context of a previous article, we could then call any tool from the toolbox. This feature will be largely underused in this article, but it might come handy in more realistic scenarios.

But the this actually really is mandatory given the present code structure: were we to refer to e.g. tmpls instead of this within the highlight function, we wouldn’t be able to access to the code template, as it’s been defined on a different *template.Template.

package main

import (
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.New("").Funcs(template.FuncMap{
		"say" : func(s string) string {
			return s
		},
		"highlight" : func(this *template.Template, args any, x string) (string, error) {
			this = template.Must(this.Clone())

			var s strings.Builder
			err := this.ExecuteTemplate(&s, x, map[string]any{
				"this" : this,
				"args" : args,
			})

			return "<code><pre>"+s.String()+"</pre></code>", err
		},
	})
}

func main() {
	for _, x := range []string{"09.tmpl"} {
		this := template.Must(tmpls.Clone())
		err := template.Must(
			this.ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"this" : this,
			"args" : map[string]any{"val" : "line"},
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/09.tmpl */}}
{{- define "code" -}}
first line
second line
{{ say (print "third" " " .args.val) }}
{{- end -}}
{{- highlight .this .args "code" -}}
# stdout
<code><pre>first line
second line
third line</pre></code>

Note: We forward this one last time when executing the code template: we could keep forwarding it to other tools as needed. I’ve also added, for this example only, an .args parameter, just to demonstrate how to uniformly parametrize the code with a set of variables, as this may come handy in real life scenarios.

Piping to a specific command

We’re now relying on os/exec to execute a fixed command instead of wrapping the text between <pre><code> ... </code></pre>; note that I’m using a temporary file: we could use a real pipe, but it’s a bit more involved, as we would need some concurrency to avoid filling the pipe, in the general case.

If needed, this shouldn’t be too difficult to add.

package main

import (
//	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.New("").Funcs(template.FuncMap{
		"highlight" : func(this *template.Template, x, y string) (string, error) {
			this = template.Must(this.Clone())

			fn := "/tmp/test"
			f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0644)
			if err != nil {
				return "", err
			}

			err = this.ExecuteTemplate(f, x, map[string]any{
				"this" : this,
			})
			f.Close()
			if err != nil {
				return "", err
			}
			var s strings.Builder
			com := exec.Command("pygmentize", "-f", "html", "-l", y, fn)
			com.Stdout = &s

			if err := com.Run(); err != nil {
				return "", err
			}

			return s.String(), err
		},
	})
}

func main() {
	for _, x := range []string{"10.tmpl"} {
		this := template.Must(tmpls.Clone())
		err := template.Must(
			this.ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"this" : this,
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/10.tmpl */}}
{{- define "code" -}}
#include <stdio.h>

int
main(void)
{
	printf("hello, world!\n");
	return 0;
}
{{- end -}}
{{- highlight .this "code" "C" -}}
# stdout
<div class="highlight"><pre><span></span><span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;stdio.h&gt;</span>

<span class="kt">int</span>
<span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">	</span><span class="n">printf</span><span class="p">(</span><span class="s">&quot;hello, world!</span><span class="se">\n</span><span class="s">&quot;</span><span class="p">);</span>
<span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>

Piping to a generic command

We’re finally ready to write the run function; we need a sarr() function to create an array of string to represent the command, and that’s pretty much the only major change with the previous iteration.

package main

import (
//	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.New("").Funcs(template.FuncMap{
		"sarr" : func(xs ...string) []string {
			return xs
		},
		"run" : func(this *template.Template, cmd []string, x string) (string, error) {
			this = template.Must(this.Clone())

			fn := "/tmp/test"
			f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0644)
			if err != nil {
				return "", err
			}

			err = this.ExecuteTemplate(f, x, map[string]any{
				"this" : this,
			})
			f.Close()
			if err != nil {
				return "", err
			}
			var s strings.Builder
			com := exec.Command(cmd[0], append(cmd[1:], fn)...)
			com.Stdout = &s

			if err := com.Run(); err != nil {
				return "", err
			}

			return s.String(), err
		},
	})
}

func main() {
	for _, x := range []string{"11.tmpl"} {
		this := template.Must(tmpls.Clone())
		err := template.Must(
			this.ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"this" : this,
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/11.tmpl */}}
{{- define "code" -}}
#include <stdio.h>

int
main(void)
{
	printf("hello, world!\n");
	return 0;
}
{{- end -}}
{{- run .this (sarr "pygmentize" "-f" "html" "-l" "C") "code" }}
# stdout
<div class="highlight"><pre><span></span><span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;stdio.h&gt;</span>

<span class="kt">int</span>
<span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">	</span><span class="n">printf</span><span class="p">(</span><span class="s">&quot;hello, world!</span><span class="se">\n</span><span class="s">&quot;</span><span class="p">);</span>
<span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>

Note: We’re expecting the command to take as its last parameter the path to a file containing the code. It’s arbitrary, and we could be smarter about it. For example, we could mimic find(1)’s convention, and consider occurrences of {} in the command’s argument as a placeholder for the filename. Again, this shouldn’t be too difficult to add.

Remarks

While this can be convenient, assuming you’ve got a bunch of commands to run, and that their output doesn’t change much, this may not be the best approach.

I’ve found that having a Makefile (or a sh(1) script, or whatever) generating all the files (say, the .html for highlighted code and the files containing the stdout/stderr of the executed code) should be not only noticeably faster, as you don’t need to run everything always, but also allows you to version control everything: it’s a good idea to make sure that, say, some system update doesn’t silently break a bunch of code snippets on your static site for example.


Comments

By email, at mathieu.bivert chez:

email