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.
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"><stdio.h></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">"hello, world!</span><span class="se">\n</span><span class="s">"</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"><stdio.h></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">"hello, world!</span><span class="se">\n</span><span class="s">"</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: