Skip to content

🧰 Logic-less CLI templating tool

License

Notifications You must be signed in to change notification settings

tarampampam/mustpl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

46 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

banner

badge-version badge-tests badge-release badge-image-size badge-license

Sometimes, you might need to generate data using templates, and this tool allows you to do it in the simplest way. All it takes is the template itself, the data for it (the values that are inserted in the template), and this tool.

πŸ‘‰ The simplest example

Let's imagine, that you have such a Nginx template:

# File `nginx.tpl`

server {
  listen      {{ port }};
  server_name {{ server_name }};

  location / {
    root  /usr/share/nginx/html;
    index index.html index.htm;
  }
}

All that is required for its rendering is:

$ PORT_NUM="8080"
$ mustpl -d '{"port": "${PORT_NUM:-8888}", "server_name": "example.com"}' ./nginx.tpl

server {
  listen      8080;
  server_name example.com;

  location / {
    root  /usr/share/nginx/html;
    index index.html index.htm;
  }
}

πŸ”₯ Features List

  • Zero external dependencies
  • Mustache templating engine under the hood
  • Can be used in a scratch Docker image (empty file system)
  • Distributed, and compiled for many architectures, including a Docker image
  • Extremely lightweight (~55KB compressed, statically linked) and fast (written in pure C)
  • Supports substitution from environment variables into the template, with default values fallback (${ENV_NAME:-default value})
  • Can be used as a Docker entrypoint (can start another application without PID changing - mustpl ... -- nginx -g 'daemon off;')

🧩 Installation

Download the latest binary file for your architecture (only Linux-like platforms are supported at the moment) from the releases page. For example, let's install it on the amd64 architecture (e.g., Debian, Ubuntu, etc):

$ curl -SsL -o ./mustpl https://github.com/tarampampam/mustpl/releases/latest/download/mustpl-linux-amd64
$ chmod +x ./mustpl

# optionally, install the binary file globally:
$ sudo install -g root -o root -t /usr/local/bin -v ./mustpl
$ rm ./mustpl
$ mustpl --help
πŸ›Έ Compilation from sources

All you need to compile is gcc:

$ sudo apt install gcc

# on linux alpine:
$ apk add make gcc musl-dev

Get the sources and switch to the latest version:

$ git clone https://github.com/tarampampam/mustpl.git ./mustpl
$ cd ./mustpl
$ git fetch --tags
$ git checkout $(git describe --tags `git rev-list --tags --max-count=1`)

And compile:

$ make version=1.1.1 # set your version
$ ./mustpl --help

πŸ‹ Docker image

Additionally, you can use our docker image:

Registry Image
GitHub Container Registry ghcr.io/tarampampam/mustpl
Docker Hub tarampampam/mustpl

⚠ Using the latest tag for the docker image is highly discouraged because of possible backward-incompatible changes during major upgrades. Please, use tags in X.Y.Z format

πŸ” What's inside the Docker image?

To watch the docker image content you can use the dive:

$ docker run --rm -it \
    -v "/var/run/docker.sock:/var/run/docker.sock:ro" \
    wagoodman/dive:latest \
      ghcr.io/tarampampam/mustpl:latest

dive

πŸ” Which platforms are supported?

The following platforms for this image are available:

$ docker run --rm mplatform/mquery ghcr.io/tarampampam/mustpl:latest
Image: ghcr.io/tarampampam/mustpl:latest
 * Manifest List: Yes (Image type: application/vnd.docker.distribution.manifest.list.v2+json)
 * Supported platforms:
   - linux/386
   - linux/amd64
   - linux/arm/v6
   - linux/arm/v7
   - linux/arm64
   - linux/ppc64le
   - linux/s390x

To run locally:

$ docker run --rm -ti \
    -u "$(id -u):$(id -g)" \
    -v "$(pwd):/rootfs:rw" \
    -w /rootfs \
      ghcr.io/tarampampam/mustpl --help

Or add it to another image:

FROM nginx:alpine

COPY --from=ghcr.io/tarampampam/mustpl /bin/mustpl /bin/mustpl

RUN mustpl --help

πŸ›  CLI Overview

Usage: mustpl [OPTION...] <template-file> [-- <exec-command>]

{{ mustach }} templating tool. For more details about the templating engine and
rules, please, follow this link: https://mustache.github.io/mustache.5.html

You can use environment variables in your template data using the following
format: ${ENV_NAME} or ${ENV_NAME:-default value}. Only those formats are
allowed (not $ENV_NAME).

 Template data:
  -d, --data=<json-string>   Template data in JSON-string format (has higher
                             priority than --data-file flag)
  -f, --data-file=<file>     Read template data from the JSON file

 Output:
  -o, --out=<out-file>       Write output to the file instead of standard
                             output

  -?, --help                 Give this help list
      --usage                Give a short usage message
  -V, --version              Print program version

🦾 What the mustpl can do

🌟 For detailed information about the templating engine please refer to the following links - mustache manual and the library repository

For example, you have the following template data (data.json):

{
  "name": "Chris",
  "value": 10000,
  "taxed_value": 6000,
  "in_ca": true
}

And template (template.txt):

<!DOCTYPE html>
<html>
<head><title>Hello {{name}}</title></head>
<body>
  <p>You have just won <strong>{{value}}</strong> dollars!</p>

{{#in_ca}}
  <p style="font-size: .7em">Well, <i>{{taxed_value}} dollars</i>, after taxes.</p>
{{/in_ca}}
</body>
</html>

Let's do the magic!

$ mustpl -f ./data.json ./template.txt

<!DOCTYPE html>
<html>
<head><title>Hello Chris</title></head>
<body>
  <p>You have just won <strong>10000</strong> dollars!</p>

  <p style="font-size: .7em">Well, <i>6000 dollars</i>, after taxes.</p>
</body>
</html>

βœ… Conditions

You can test the value of the selected key using the following operators:

  • key=value (matching test)
  • key=!value (not matching test)
  • key>value (greater)
  • key>=value (greater or equal)
  • key<value (lesser)
  • key<=value (lesser or equal)
{
  "person": {
    "name": "Harry",
    "age": 37
  },
  "lang": "fr",
  "l10n": {
    "en": "Hello",
    "fr": "Salut"
  }
}
{{#person.name=Harry}}
Hello Harry!
{{/person.name=Harry}}

{{^person.name=John}}
The person's name is not John.
{{/person.name=John}}

{{#person.age>40}}
He's over 40 years old.
{{/person.age>40}}{{^person.age>40}}
He's definitely not more than 40 years old.
{{/person.age>40}}

{{#lang=fr}}{{ l10n.fr }}{{/lang=fr}}{{#lang=!fr}}{{ l10n.en }}{{/lang=!fr}} {{ person.name }}!

Render only if equals:
- {{ person.age=36 }}
- {{ person.age=37 }}
- {{ person.age=38 }}

It will be rendered as follows:

Hello Harry!

The person's name is not John.


He's definitely not more than 40 years old.

Salut Harry!

Render only if equals:
-
- 37
-

πŸ”„ Loops

Okay, but what about the loops? Here you go (the value of the current field can be accessed using the single dot {{ . }}):

{
  "servers": [
    {
      "listen": 8080,
      "names": [
        "example.com"
      ],
      "is_default": true,
      "home": "/www/example.com"
    },
    {
      "listen": 1088,
      "names": [
        "127-0-0-1.nip.io",
        "127-0-0-2.nip.io"
      ],
      "home": "/www/local"
    }
  ]
}
{{#servers}}
server { {{! just a comment, will not be rendered }}
  listen      {{ listen }};
  server_name{{#names}} {{ . }}{{/names}}{{#is_default}} default_server{{/is_default}};

  location / {
    root  {{ home }};
    index index.html index.htm;
  }
}
{{/servers}}
$ mustpl -f ./data.json ./template.txt
server {
  listen      8080;
  server_name example.com default_server;

  location / {
    root  /www/example.com;
    index index.html index.htm;
  }
}
server {
  listen      1088;
  server_name 127-0-0-1.nip.io 127-0-0-2.nip.io;

  location / {
    root  /www/local;
    index index.html index.htm;
  }
}

In addition, you can use the pattern {{#X.*}} ... {{/X.*}} to iterate on fields of X :

{
  "people": {
    "John": 27,
    "Mark": "32"
  }
}
{{#people.*}}
- {{ * }} is {{ . }} years old
{{/people.*}}

Produces:

- John is 27 years old
- Mark is 32 years old

Here the single star {{ * }} is replaced by the iterated key and the single dot {{ . }} is replaced by its value.

🚩 Template data provided using options

You can provide your template data from cli using the -d (--data) flag:

server {
  listen      8080;
  server_name{{#names}} {{ . }}{{/names}};

  location / {
    root  /var/www/data;
    index index.html index.htm;
  }
}
$ mustpl -d '{"names": ["example.com", "google.com"]}' ./template.txt
server {
  listen      8080;
  server_name example.com google.com;

  location / {
    root  /var/www/data;
    index index.html index.htm;
  }
}

πŸ“Ž Environment variables

Environment variables can be used in the following format: ${ENV_NAME:-default value} (inside template data file too; template from the example above is used):

$ SERVER_NAME_1=example.com ./mustpl -d '{"names": [{"name": "${SERVER_NAME_1:-fallback.com}"}, {"name": "${SERVER_NAME_X:-unset.com}"}]}' ./tmp/template.txt
server {
  listen      8080;
  server_name example.com unset.com;

  location / {
    root  /var/www/data;
    index index.html index.htm;
  }
}

πŸ›° exec and the PID magic

As you probably know, the main process inside the docker container should have PID == 1 for the correct signals processing from the docker daemon. That's why basically entry-point scripts have the following code:

#!/bin/sh
set -e

if [ -n "$MY_OPTION" ]; then
  sed -i "s~foo~bar ${MY_OPTION}~" /etc/app.cfg # modify the config
fi;

exec "$@" # <-- that's it!
# ...

COPY docker-entrypoint.sh ./docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

CMD ["/bin/app", "--another", "flags"]

From the man exec:

The exec() family of functions replaces the current process image with a new process image

So, the application /bin/app will have a PID == 1 that was previously assigned to the bash (because the bash is the image entrypoint). mustpl uses the same technique, let's create the following files for the example:

data.json:

{
  "my_option": "${MY_OPTION:-default value}"
}

template.txt:

Hello {{ my_option }}!

Dockerfile:

FROM alpine:latest

COPY --from=ghcr.io/tarampampam/mustpl /bin/mustpl /bin/mustpl

COPY ./data.json /data.json
COPY ./template.txt /template.txt

ENTRYPOINT ["mustpl", "-f", "/data.json", "-o", "/rendered.txt", "/template.txt", "--"]

CMD ["sleep", "infinity"]

Next, build the image:

$ docker build --tag test:local .

And then run it:

$ docker run --rm --name mustpl_example -e "MY_OPTION=foobar" test:local

In the separate terminal run:

$ docker exec mustpl_example ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 sleep infinity # <-- our CMD with a PID == 1
    7 root      0:00 ps aux

$ docker exec mustpl_example cat /rendered.txt
Hello foobar! # <-- our environment variable value

$ docker kill mustpl_example

This approach is easier than using sed, awk, and other tools to modify the configuration files before running the main application. But despite this, no one is restricting you from using the entrypoint scripts πŸ˜‰

πŸ“° Changes log

Release date Commits since latest release

Changes log can be found here.

πŸ‘Ύ Support

Issues Issues

If you find any bugs in the project, please create an issue in the current repository.

πŸ“– License

This is open-sourced software licensed under the MIT License.