Home

Awesome

Mo - Mustache Templates in Bash

Mustache templates are simple, logic-less templates. Because of their simplicity, they are able to be ported to many languages. The syntax is quite simple.

Hello, {{NAME}}.

I hope your {{TIME_PERIOD}} was fun.

The above file is demo/fun-trip.mo. Let's try using this template some data from bash's environment. Go to your checked out copy of the project and run a command like this:

NAME=Tyler TIME_PERIOD=weekend ./mo demo/fun-trip.mo

Your result?

Hello, Tyler.

I hope your weekend was fun.

This bash version supports conditionals, functions (both as filters and as values), as well as indexed arrays (for iteration). You are able to leverage these additional features by adding more information into the environment. It is easiest to do this when you source mo. See the demo scripts for further examples.

Requirements

If you intend to develop this and run the official specs, you also need node.js.

Installation

There are a few ways you can install this tool. How you install it depends on how you want to use it.

Globally; For Everyone

You can install this file in /usr/local/bin/ or /usr/bin/ by simply downloading it, changing the permissions, then moving it to the right location. Double check that your system's PATH includes the destination folder, otherwise users may have a hard time starting the command.

# Download
curl -sSL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo

# Make executable
chmod +x mo

# Move to the right folder
sudo mv mo /usr/local/bin/

# Test
echo "works" | mo

Locally; For Yourself

This is very similar to installing it globally but it does not require root privileges. It is very important that your PATH includes the destination folder otherwise it won't work. Some local folders that are typically used are ~/bin/ and ~/.local/bin/.

# Download
curl -sSL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo

# Make executable
chmod +x mo

# Ensure destination folder exists
mkdir -p ~/.local/bin/

# Move to the right folder
mv mo ~/.local/bin/

# Test
echo "works" | mo

As A Library; For A Tool

Bash scripts can source mo to include the functionality in their own routines. This usage typically would have mo saved to a lib/ folder in an application and your other scripts would use . lib/mo to bring it into your project.

# Download
curl -sSL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo

# Move into your project folder
mv mo ~/projects/amazing-things/lib/

To allow it to work this way, you either should source the file (. "lib/mo") or make it executable (chmod +x lib/mo) and run it from your scripts.

How to Use

If you only plan using strings and numbers, nothing could be simpler. In your shell script you can choose to export the variables. The below script is demo/using-strings.

#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory
export TEST="This is a test"
echo "Your message:  {{TEST}}" | ../mo

The result? "Your message: This is a test".

Using arrays adds a slight level of complexity. You must source mo. Look at demo/using-arrays.

#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory
export ARRAY=( one two "three three three" four five )
. ../mo # This loads the "mo" function
cat << EOF | mo
Here are the items in the array:
{{#ARRAY}}
    * {{.}}
{{/ARRAY}}
EOF

The result? You get a list of the five elements in the array. It is vital that you source mo and run the function when you want arrays to work because you can not execute a command and have arrays passed to that command's environment. Instead, we first source the file to load the function and then run the function directly.

There are more scripts available in the demos directory that could help illustrate how you would use this program.

There are additional features that the program supports. Try using mo --help to see what is available.

Please note that this command is written in Bash and pulls data from either the environment or (when using --source) from a text file that will be sourced and loaded into the environment, which means you will need to have Bash-style variables defined. Please see the examples in demo/ for different ways you can use mo.

Enhancements

In addition to many of the features built-in to Mustache, mo includes a number of unique features that make it a bit more powerful.

Loop @key

mo implements Handlebar's @key references for outputting the key inside of a loop:

Env:

myarr=( foo bar )

# Bash v4+
declare -A myassoc
myassoc[hello]="mo"
myassoc[world]="is great"

Template:

{{#myarr}}
 - {{@key}} {{.}}
{{/myarr}}

{{#myassoc}}
 * {{@key}} {{.}}
{{/myassoc}}

Output:

 - 0 foo
 - 1 bar

 * hello mo
 * world is great

Helpers / Function Arguments

Function Arguments are not a part of the official Mustache implementation, and are more often associated with Handlebar's Helper functionality.

mo allows for passing strings to functions.

{{myfunc foo bar}}

For security reasons, these arguments are not immediately available to function calls without a flag.

with --allow-function-arguments

myfunc() {
    # Outputs "foo, bar"
    echo "$1, $2";
}

Using $MO_FUNCTION_ARGS

myfunc() {
    # Outputs "foo, bar"
    echo "${MO_FUNCTION_ARGS[0]}, ${MO_FUNCTION_ARGS[1]}";
}

Triple Mustache, Parenthesis, and Quotes

Normally, triple mustache syntax, such as {{{var}}} will avoid HTML escaping of the variable. Because HTML escaping is not supported in mo, this is now used differently. Anything within braces will be looked up and the values will be concatenated together and the result will be treated as a value. Anything in parenthesis will be looked up, concatenated, and treated as a name. Also, anything in single quotes is passed as a value; double quoted things first are unescaped and then passed as a value.

# Example input
var=abc
user=admin
admin=Administrator
u=user
abc=([0]=zero [1]=one [2]=two)
Mustache syntaxResulting outputNotes
{{var}}abcNormal behavior
{{var us}}abcusConcatenation
{{'var'}}varPassing as a value
{{"a\tb"}}a bThere was an escaped tab in the value
{{u}}userNormal behavior
{{{u}}}userLook up "$u", treat as the value {{'user'}}
{{(u)}}adminLook up "$u", treat as the name {{user}}
{{var user}}abcuserConcatenation
{{(var '.1')}}oneLook up "$var", treat as "abc", then concatenate ".1" and look up {{abc.1}}

In double-quoted strings, the following escape sequences are defined.

Environment Variables and Functions

There are several functions and variables used to process templates. mo reserves variables that start with MO_ for variables exposing data or configuration, functions starting with mo::, and local variables starting with mo[A-Z]. You are welcome to use internal functions, though only ones that are marked as "Public" should not change their interface. Scripts may also read any of the variables.

Functions are all executed in a subshell, with another subshell for lambdas. Thus, your lambda can't affect the parsing of a template. There's more information about lambdas when talking about tests that fail.

Concessions

I admit that implementing everything in bash just doesn't make a lot of sense. For example, the following things just don't work because they don't really mesh with the "bash way".

Pull requests to solve the following issues would be helpful.

Mustache Syntax

General Scripting Issues

Developing

Check out the code and hack away. Please add tests to show off bugs before fixing them. New functionality should also be covered by a test.

First, make sure you install Node.js. After that, run npm run install-tests to get the dependencies and the repository of YAML tests. Run npm run test to run the JavaScript tests. There's over 100 of them, which is great. Not all of them will pass, but that's discussed later.

When submitting patches, make sure to run them past ShellCheck and ensure no problems are found. Also please use Bash 3 syntax if you are manipulating arrays.

Porting and Backporting

In case of problems, setting MO_DEBUG to a non-empty value will give you LOTS of output.

MO_DEBUG=1 ./mo my-template

Failed Specs

It is acceptable for some of the official spec tests to fail. The spec runner has specific exclusions and overrides to test similar functionality that avoid the following issues.

For lambdas, these examples may help.

# Retrieve content into a variable.
content=$(cat)

# Retrieve all content and do not trim newlines at the end.
content=$(cat; echo -n '.')
content=${content%.}

# Parse content using the current delimiters
mo::parse results "This is my content. Hello, {{username}}"
echo -n "$results"

# Parse content using the default delimiters
MO_OPEN_DELIMITER=$MO_OPEN_DELIMITER_DEFAULT
MO_CLOSE_DELIMITER=$MO_CLOSE_DELIMITER_DEFAULT
mo::parse results "This is my content. Hello, {{username}}"
echo -n "$results"

Future Enhancements

There's a few places in the code marked with TODO to signify areas that could use improvement. Care to help? Keep in mind that this uses bash exclusively, so it might not look the prettiest.

License

This program is licensed under an MIT license with an additional non-advertising clause. See LICENSE.md for the full text.