gnikyt   /  Code ramblings
Ty King

Building a BASH template engine /

Earlier this year I migrated my blog away from Jekyll because it was such a large setup for such a small blog to maintain. I wanted something more “portable” without libraries, so I settled to build my blog and it’s generation with BASH.

Everything needed was built out: frontmatter processing, Markdown to HTML generation, post handling, page handling, category support, RSS support, sitemap support, and more.

Originally, I wrote a very basic template handler which essentially just sniffed the first few characters of a string to know an action to take, example: ${inc:file.html} would include a file into the template, ${var} would replace itself with the applicable variable, and so on. It began getting more complex where I required some basic if/else statements and looping, which was something difficult with the basic template handler I originally wrote.

I decided to rebuild it from scratch, taking inspiration from Mustache syntax, and ensuring it was more than just a simple find and replace operation as it was before.

The plan was to do the following:

  • Detect the applicable template tags
  • For each template tag:
    • Parse it character-by-character to determine:
      • It’s operation type
      • It’s optional ask
      • It’s optional contents
      • And more
  • Once determined, run the applicable operation to produce a result
  • Take the result and replace the original template tag with that result

Initial supports I targeted:

  • Variables: {{VAR}}
  • Variables with filters: {{VAR|UPPERCASE|REPLACE x y}}
  • If statements: {{#VAR}}I exist!{{/VAR}}
  • Unless statements: {{^VAR}}I do not exist!{{/VAR}}
  • Partial includes: {{>file.html}}
  • Looping: {{@FOREACH var}}{{KEY}}: {{VALUE}}{{/FOREACH var}}
  • Assignment/Capture: {{@ASSIGN XYZ}}{{VAR|UPPERCASE}} is cool!{{/@ASSIGN}}
  • Custom functions: {{@MONEY USD}}4500{{/MONEY USD}}

The method I chose, since BASH is limited, was to go character-by-character through the input.

  • If { was the current character, I peek ahead to know if the next character is also a {
  • If the current character and next are both {{, I know this is a variable or block
  • Next, the inside is parsed until }}
  • If determined to be a block, then everything after }} until {{/ is captured
  • Once everything is captured, depending on the operation, it would run specific functions to replace the input contents with the parsed contents

I built a library to handle this called be.

Example usage, with custom functions and filters:

#!/bin/bash

. ./be

be_bold() {
  local input
  input=$(cat -)
  echo "<strong>${input}</strong>"
}

be_replace() {
  local input
  input=$(cat -)
  echo "${input//$1/$2}"
}

be_capitalize() {
  local input
  input=$(cat -)
  echo "${input^}"
}

be_equals() {
  local block
  block=$(cat -)

  if [[ "$1" == "$2" ]]; then
    be <<< "$block"
  else
    # Failed, return nothing.
    echo ""
  fi
}

NAME="Joe"
LIKES="hockey,soccer"
TPL=$(cat <<EOF
  Well, well, hello {{NAME|APPEND e|APPEND e}}!
  {{@ASSIGN GOODBYE}}Goodbye, {{NAME|CAPITALIZE}}!{{/ASSIGN GOODBYE}}
  {{#LIKES}}
    So {{NAME|REPLACE "e" "ey"|BOLD}}, I heard you like:
    {{@FOREACH LIKES ,}}
      {{@ASSIGN IS_HOCKEY}}{{@EQUALS VALUE "hockey"}}true{{/EQUALS VALUE "hockey"}}{{/ASSIGN IS_HOCKEY}}
      {{KEY1}}) {{VALUE|CAPITALIZE}}{{#IS_HOCKEY}} (love!){{/IS_HOCKEY}}{{^LAST}}; and {{/LAST}}
    {{/FOREACH LIKES ,}}
    {{@RAW}}Won't be processed: {{NAME}}{{/RAW}}
  {{/LIKES}}
  {{GOODBYE}}
EOF
)

echo "$TPL" | be

Output:

  Well, well, hello Joeee!
  
  
    So <strong>Joey</strong>, I heard you like:
    
      
      1) Hockey (love!); and 
    
      
      2) Soccer
    
    Won't be processed: {{NAME}}

BASH is definately not the best solution for a template engine, however, you can view the entire solution on here and modify for your needs.

Anchors
Appendix

Copyright under CC-4.0.

Available in the following alternative formats: MD  /  TXT  /  PDF