mustache and mo: Logic-less Templating in Shell
The mustache CLI renders a Mustache template against a YAML/JSON data file, and mo renders Mustache using only Bash and environment variables.
Mustache is deliberately logic-less: tags interpolate, sections iterate, and nothing else. That predictability makes it a safe minimal templater for CI config.
What it does
The Ruby mustache gem CLI takes a YAML data file and a template and prints the rendered result. mo is a single Bash script that renders the same {{tag}} syntax, pulling values straight from exported environment variables, with no runtime beyond Bash.
Common usage
# ruby mustache gem: data file + template
mustache data.yml template.mustache > out.txt
# mo (pure bash): values come from the environment
export NAME=ci IMAGE=nginx:1.27
mo template.mustache > out.txtOptions
| Form | What it does |
|---|---|
| mustache DATA TEMPLATE | Render TEMPLATE using DATA (YAML with a frontmatter-style hash) |
| {{name}} | HTML-escaped interpolation of a value |
| {{{name}}} or {{&name}} | Unescaped (raw) interpolation |
| {{#section}}...{{/section}} | Iterate a list or render if truthy |
| mo (env) | mo reads variables from the environment, no data file |
In CI
mo is handy on minimal images because it needs nothing but Bash, but it only sees exported variables, so export your values first. For structured data (lists, nested objects) prefer the mustache gem with a YAML file; mo maps arrays from space-separated or newline-separated env values, which is easy to get wrong.
Common errors in CI
With mustache you often see undefined method [] for nil when the data file lacks a key the template references, or a Psych "did not find expected key" YAML parse error in the data file itself. With mo, unset variables render as empty (no error), and a stray {{ with no closing }} is emitted literally. Double vs triple braces matters: {{value}} HTML-escapes, so < becomes < and breaks non-HTML config; use {{{value}}} for raw config.