Skip to content

shellbox-sh/shx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mac (BASH 3.2) BASH 4.3 BASH 4.4 BASH 5.0


📜 <%= "shell script templates" %>

Simple, easy-to-use template rendering engine ( written in BASH )

Download the latest version by clicking one of the download links above or:

curl -o- https://shx.shellbox.sh/installer.sh | bash

🏛️ Classic syntax

shx provides a well-known, friendly <%-style syntax:

<!-- index.shx -->
<html>
  <head>
    <%= $title %>
  </head>
  <body>
    <h1><%= $header %></h1>
    <ul>
      <% for item in "${items[@]}"; do %>
        <li><%= $item %></li>
      <% done %>
    </ul>
  </body>
</html>

💬 Render a string

Provide a simple string to shx render "[my template]":

$ export text="Hello, world!"

$ ./shx render "<h1><%= $text %></h1>"
<h1>Hello, world!</h1>

💾 Render a file

Provide a file path to shx render "[path to file]":

<!-- index.shx -->
<h1><%= $title %></h1>
$ export title="My Website"

$ ./shx render index.shx
<h1>Hello, world!</h1>

📚 Use as a library

Give shx visibility into your script's variables without export:

source shx.sh

title="Regular variable"

shx render "Title: <%= $title %>"
Title: Regular variable

Including access to function local variables:

source shx.sh

renderTemplate() {
  local greeting="$1"
  local name="$2"
  shx render "<%= $greeting %>, <%= $name %>!"
}

renderTemplate "Hello" "Rebecca"
Hello, Rebecca!

📎 Accepts argument list

You can provide arguments after shx render [template] which become available via the usual means, e.g. $* $@ $# $1 $2 $3 etc

<!-- template.shx -->
<%= $# %> arguments provided:
<% for argument in "$@"; do -%>
- <%= $argument %>
<% done %>
./shx render template.shx "First" "Second" "Third"
3 arguments provided:
- First
- Second
- Third

ℹ️ Using -%> prevents the character following -%> from being output (e.g. in this case a newline character)

🔄 Template Caching

By default, when used as a library, shx will cache the result of parsing each template file and store the compiled result.

source shx.sh

# template.shx will be parsed and evaluated
shx render path/to/template.shx

# Parsing and compilation is skipped!
# The template is evaluated directly from cache
shx render path/to/template.sh

To disable this functionality:

SHX_CACHE=false

Note: Caching only occurs when shx.sh is used as a sourced library


📓 Command Reference

shx compile

View Source
local __shx__outVariableName=''
[ "$1" = "--out" ] && { shift; __shx__outVariableName="$1"; shift; }

# Undocumented option, get the code for the template without evaluating it: --code
local __shx__providedTemplate="$1"; shift

[ -f "$__shx__providedTemplate" ] && __shx__providedTemplate="$(<"$__shx__providedTemplate")"

# Like most similar implementations across programming languages,
# the template render process builds up a script with lots of printf
# statements alongside the <% shell source %> code to run and
# then the result is created by evaluating the script.
#
# This is _not_ a side-effect-free/safe templating engine a la Liquid and friends
#
local __shx__outputScriptToEval=''
local __shx__stringBuilder=''
local __shx__stringBuilderComplete=false
local __shx__valueBlock=''
local __shx__valueBlockOpen=false
local __shx__codeBlockDefinition=''
local __shx__codeBlockDefinitionOpen=false
local __shx__heredocCount=0
local __shx__newLine=$'\n'

# We legit loop thru all the characters.
local __shx__cursor=0
while [ "$__shx__cursor" -lt "${#__shx__providedTemplate}" ]
do
  if [ "${__shx__providedTemplate:$__shx__cursor:3}" = "<%=" ]
  then
    [ "$__shx__codeBlockDefinitionOpen" = true ] && { echo "shx [RenderError] <%= was started but there is a <% block already open with content: '$__shx__codeBlockDefinition'" >&2; return 1; }
    [ "$__shx__valueBlockOpen" = true ] && { echo "shx [RenderError] <%= was started but there is another <%= already open with content: '$__shx__valueBlock'" >&2; return 1; }
    __shx__valueBlockOpen=true
    __shx__stringBuilderComplete=true
    : "$(( __shx__cursor += 2 ))"
  elif [ "${__shx__providedTemplate:$__shx__cursor:2}" = "<%" ]
  then
    [ "$__shx__codeBlockDefinitionOpen" = true ] && { echo "shx [RenderError] %> block was closed but there is another <% currently open with content: '$__shx__codeBlockDefinition'" >&2; return 1; }
    [ "$__shx__valueBlockOpen" = true ] && { echo "shx [RenderError] %> block was closed but there is a <%= currently open with content: '$__shx__valueBlock'" >&2; return 1; }
    __shx__codeBlockDefinitionOpen=true
    __shx__stringBuilderComplete=true
    : "$(( __shx__cursor++ ))"
  elif [ "${__shx__providedTemplate:$__shx__cursor:3}" = "-%>" ]
  then
    if [ "$__shx__valueBlockOpen" = true ]
    then
      __shx__valueBlockOpen=false
      __shx__valueBlock="${__shx__valueBlock# }"
      __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"${__shx__valueBlock% }\"${__shx__newLine}"
      __shx__valueBlock=''
    elif [ "$__shx__codeBlockDefinitionOpen" = true ]
    then
      __shx__codeBlockDefinitionOpen=false
      __shx__codeBlockDefinition="${__shx__codeBlockDefinition# }"
      __shx__outputScriptToEval+="${__shx__newLine}${__shx__codeBlockDefinition% }${__shx__newLine}"
      __shx__codeBlockDefinition=''
    else
      echo "shx [RenderError] unexpected %> encountered, no <% or <%= blocks are currently open" >&2
      return 1
    fi
    : "$(( __shx__cursor += 3 ))"
  elif [ "${__shx__providedTemplate:$__shx__cursor:2}" = "%>" ]
  then
    if [ "$__shx__valueBlockOpen" = true ]
    then
      __shx__valueBlockOpen=false
      __shx__valueBlock="${__shx__valueBlock# }"
      __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"${__shx__valueBlock% }\"${__shx__newLine}"
      __shx__valueBlock=''
    elif [ "$__shx__codeBlockDefinitionOpen" = true ]
    then
      __shx__codeBlockDefinitionOpen=false
      __shx__codeBlockDefinition="${__shx__codeBlockDefinition# }"
      __shx__outputScriptToEval+="${__shx__newLine}${__shx__codeBlockDefinition% }${__shx__newLine}"
      __shx__codeBlockDefinition=''
    else
      echo "shx [RenderError] unexpected %> encountered, no <% or <%= blocks are currently open" >&2
      return 1
    fi
    : "$(( __shx__cursor++ ))"
  elif [ "$__shx__valueBlockOpen" = true ]
  then
    __shx__valueBlock+="${__shx__providedTemplate:$__shx__cursor:1}"
  elif [ "$__shx__codeBlockDefinitionOpen" = true ]
  then
    __shx__codeBlockDefinition+="${__shx__providedTemplate:$__shx__cursor:1}"
  else 
    __shx__stringBuilder+="${__shx__providedTemplate:$__shx__cursor:1}"
  fi

  if [ "$__shx__stringBuilderComplete" = true ]
  then
    __shx__stringBuilderComplete=false
    if [ -n "$__shx__stringBuilder" ]
    then
      : "$(( __shx__heredocCount++ ))"
      __shx__outputScriptToEval+="${__shx__newLine}IFS= read -r -d '' __SHX_HEREDOC_$__shx__heredocCount << 'SHX_PRINT_BLOCK'${__shx__newLine}"
      __shx__outputScriptToEval+="$__shx__stringBuilder"
      __shx__outputScriptToEval+="${__shx__newLine}SHX_PRINT_BLOCK"
      __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"\${__SHX_HEREDOC_$__shx__heredocCount%$'\\n'}\""
      __shx__outputScriptToEval+="${__shx__newLine}unset __SHX_HEREDOC_$__shx__heredocCount"
      __shx__stringBuilder=''
    fi
  fi

  : "$(( __shx__cursor++ ))"
done

if [ -n "$__shx__stringBuilder" ]
then
    __shx__outputScriptToEval+="${__shx__newLine}IFS= read -r -d '' __SHX_HEREDOC_$__shx__heredocCount << 'SHX_PRINT_BLOCK'${__shx__newLine}"
  __shx__outputScriptToEval+="$__shx__stringBuilder"
  __shx__outputScriptToEval+="${__shx__newLine}SHX_PRINT_BLOCK"
  __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"\${__SHX_HEREDOC_$__shx__heredocCount%$'\\\n'}\""
  __shx__outputScriptToEval+="${__shx__newLine}unset __SHX_HEREDOC_$__shx__heredocCount"
  __shx__stringBuilder=''
fi

[ "$__shx__codeBlockDefinitionOpen" = true ] && { echo "shx [RenderError] <% block was not closed: '$__shx__codeBlockDefinition'" >&2; return 1; }
[ "$__shx__valueBlockOpen" = true ] && { echo "shx [RenderError] <%= was not closed: '$__shx__valueBlock'" >&2; return 1; }

# local __shx__COMPILED_TEMPLATE="$( printf '%s' "$__shx__outputScriptToEval" )"
local __shx__COMPILED_TEMPLATE="$__shx__outputScriptToEval"

if [ "$__shx__printCodeOnly" = true ]
then
  echo "$__shx__COMPILED_TEMPLATE"
  return 0
fi

unset __shx__cursor
unset __shx__outputScriptToEval
unset __shx__stringBuilder
unset __shx__stringBuilderComplete
unset __shx__valueBlock
unset __shx__valueBlockOpen
unset __shx__codeBlockDefinition
unset __shx__codeBlockDefinitionOpen
unset __shx__heredocCount
unset __shx__printCodeOnly
unset __shx__newLine

if [ -n "$__shx__outVariableName" ]
then
  printf -v "$__shx__outVariableName" '%s' "$__shx__COMPILED_TEMPLATE"
else
  printf '%s' "$__shx__COMPILED_TEMPLATE"
fi

Compiles a provided template to a result which can be stored and later evaluated via shx evaluate.

This allows you to perform template parsing only once and evaluate templates without the computational penalty of parsing.

Note: alternatively you may want to consider Template caching

Description
$1 Template to compile (string or path to file)

The first wo arguments may be --out [variableName],in which case the compiled result will not beprinted and will instead be assigned to the provided variable.
$3 Template to compile (string or path to file)
if $1 and $2 are --out [variableName] respectively

Compile Template

template="<h1><%= $1 %></h1>"
compiledTemplate="$( shx compile "$template" )"

# Later ...

shx evaluate "$compiledTemplate" "My Title"
# => "<h1>My Title</h1>"

Store in Variable

template="<h1><%= $1 %></h1>"
shx compile "$template" compiledTemplate
# ^--- the compilated template is stored in $compiledTemplate

# Later ...

shx evaluate "$compiledTemplate" "My Title"
# => "<h1>My Title</h1>"

shx evaluate

View Source
local __shx__COMPILED_TEMPLATE="$1"; shift
eval "$__shx__COMPILED_TEMPLATE"

Evaluates a previously compiled template.

Description
$1 Compiled template provided via shx compile

shx render

View Source
# Undocumented option, get the code for the template without evaluating it: --code
local __shx__printCodeOnly=false
[ "$1" = "--code" ] && { __shx__printCodeOnly=true; shift; }

# Shift so that templates can properly read in provided "$1" "$@" etc to the `render` function
local __shx__originalTemplateArgument="$1"; shift
local __shx__providedTemplate="$__shx__originalTemplateArgument"

#
# Begin Cache Lookup
#
local __shx__cacheEncodedItem_indexOfCompiledTemplate=''
if [ -f "$__shx__providedTemplate" ] && [ "$SHX_CACHE" = true ]
then
  # Build up the new cache lookup field (may have MTIME file changes)
  declare -a __shx__cacheLookupIndex=()

  # Loop Thru Every Item in the Cache, including it's Filename, Mtime,
  # and index to compiled template in the cache
  local __shx__cacheEncodedItem=''
  while IFS="" read -r __shx__cacheEncodedItem
  do
    local __shx__cacheUpdatedEncodedItem=''
    local __shx__cacheEncodedItem_filename="${__shx__cacheEncodedItem##*|}"

    # Found the item
    if [ "$__shx__cacheEncodedItem_filename" = "$__shx__providedTemplate" ]
    then
      # Get and check the mtime
      local __shx__currentTemplateFileMtime="$( date +"%s" -r "$__shx__providedTemplate" 2>/dev/null || stat -x "$__shx__providedTemplate" | grep "Modify" )"

      # MTIME
      local __shx__cacheEncodedItem_mtime="${__shx__cacheEncodedItem#*>}"
      __shx__cacheEncodedItem_mtime="${__shx__cacheEncodedItem_mtime%%|*}"

      # Index
      __shx__cacheEncodedItem_indexOfCompiledTemplate="${__shx__cacheEncodedItem%%*<}"
      __shx__cacheEncodedItem_indexOfCompiledTemplate="${__shx__cacheEncodedItem_indexOfCompiledTemplate%>*}"

      if [ "$__shx__currentTemplateFileMtime" = "$__shx__cacheEncodedItem_mtime" ]
      then
        # Equal! Just eval the previously compiled template
        eval "${_SHX_COMPILED_TEMPLATE_CACHE[$__shx__cacheEncodedItem_indexOfCompiledTemplate]}" && return $?
      else
        # Present but not equal, note to update it via its index
        # Update the item with the new MTIME
        local __shx__cacheUpdatedEncodedItem="$__shx__cacheEncodedItem_indexOfCompiledTemplate>$__shx__currentTemplateFileMtime|$__shx__cacheEncodedItem_filename"
      fi
    fi

    if [ -n "$__shx__cacheUpdatedEncodedItem" ]
    then
      __shx__cacheLookupIndex+=("$__shx__cacheUpdatedEncodedItem\n")
    else
      __shx__cacheLookupIndex+=("$__shx__cacheEncodedItem\n")
    fi
  done < <( printf "${_SHX_COMPILED_TEMPLATE_CACHE[0]}" )

  # Update the cache index
  _SHX_COMPILED_TEMPLATE_CACHE[0]="${__shx__cacheLookupIndex[*]}"

  # If no template was found and eval'd and returned from the cache, grab a new one from the filesystem
  __shx__providedTemplate="$(<"$__shx__providedTemplate")"
fi
#
# End Cache Lookup
#

# Like most similar implementations across programming languages,
# the template render process builds up a script with lots of printf
# statements alongside the <% shell source %> code to run and
# then the result is created by evaluating the script.
#
# This is _not_ a side-effect-free/safe templating engine a la Liquid and friends
#
local __shx__outputScriptToEval=''
local __shx__stringBuilder=''
local __shx__stringBuilderComplete=false
local __shx__valueBlock=''
local __shx__valueBlockOpen=false
local __shx__codeBlockDefinition=''
local __shx__codeBlockDefinitionOpen=false
local __shx__heredocCount=0
local __shx__newLine=$'\n'

# We legit loop thru all the characters.
local __shx__cursor=0
while [ "$__shx__cursor" -lt "${#__shx__providedTemplate}" ]
do
  if [ "${__shx__providedTemplate:$__shx__cursor:3}" = "<%=" ]
  then
    [ "$__shx__codeBlockDefinitionOpen" = true ] && { echo "shx [RenderError] <%= was started but there is a <% block already open with content: '$__shx__codeBlockDefinition'" >&2; return 1; }
    [ "$__shx__valueBlockOpen" = true ] && { echo "shx [RenderError] <%= was started but there is another <%= already open with content: '$__shx__valueBlock'" >&2; return 1; }
    __shx__valueBlockOpen=true
    __shx__stringBuilderComplete=true
    : "$(( __shx__cursor += 2 ))"
  elif [ "${__shx__providedTemplate:$__shx__cursor:2}" = "<%" ]
  then
    [ "$__shx__codeBlockDefinitionOpen" = true ] && { echo "shx [RenderError] %> block was closed but there is another <% currently open with content: '$__shx__codeBlockDefinition'" >&2; return 1; }
    [ "$__shx__valueBlockOpen" = true ] && { echo "shx [RenderError] %> block was closed but there is a <%= currently open with content: '$__shx__valueBlock'" >&2; return 1; }
    __shx__codeBlockDefinitionOpen=true
    __shx__stringBuilderComplete=true
    : "$(( __shx__cursor++ ))"
  elif [ "${__shx__providedTemplate:$__shx__cursor:3}" = "-%>" ]
  then
    if [ "$__shx__valueBlockOpen" = true ]
    then
      __shx__valueBlockOpen=false
      __shx__valueBlock="${__shx__valueBlock# }"
      __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"${__shx__valueBlock% }\"${__shx__newLine}"
      __shx__valueBlock=''
    elif [ "$__shx__codeBlockDefinitionOpen" = true ]
    then
      __shx__codeBlockDefinitionOpen=false
      __shx__codeBlockDefinition="${__shx__codeBlockDefinition# }"
      __shx__outputScriptToEval+="${__shx__newLine}${__shx__codeBlockDefinition% }${__shx__newLine}"
      __shx__codeBlockDefinition=''
    else
      echo "shx [RenderError] unexpected %> encountered, no <% or <%= blocks are currently open" >&2
      return 1
    fi
    : "$(( __shx__cursor += 3 ))"
  elif [ "${__shx__providedTemplate:$__shx__cursor:2}" = "%>" ]
  then
    if [ "$__shx__valueBlockOpen" = true ]
    then
      __shx__valueBlockOpen=false
      __shx__valueBlock="${__shx__valueBlock# }"
      __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"${__shx__valueBlock% }\"${__shx__newLine}"
      __shx__valueBlock=''
    elif [ "$__shx__codeBlockDefinitionOpen" = true ]
    then
      __shx__codeBlockDefinitionOpen=false
      __shx__codeBlockDefinition="${__shx__codeBlockDefinition# }"
      __shx__outputScriptToEval+="${__shx__newLine}${__shx__codeBlockDefinition% }${__shx__newLine}"
      __shx__codeBlockDefinition=''
    else
      echo "shx [RenderError] unexpected %> encountered, no <% or <%= blocks are currently open" >&2
      return 1
    fi
    : "$(( __shx__cursor++ ))"
  elif [ "$__shx__valueBlockOpen" = true ]
  then
    __shx__valueBlock+="${__shx__providedTemplate:$__shx__cursor:1}"
  elif [ "$__shx__codeBlockDefinitionOpen" = true ]
  then
    __shx__codeBlockDefinition+="${__shx__providedTemplate:$__shx__cursor:1}"
  else 
    __shx__stringBuilder+="${__shx__providedTemplate:$__shx__cursor:1}"
  fi

  if [ "$__shx__stringBuilderComplete" = true ]
  then
    __shx__stringBuilderComplete=false
    if [ -n "$__shx__stringBuilder" ]
    then
      : "$(( __shx__heredocCount++ ))"
      __shx__outputScriptToEval+="${__shx__newLine}IFS= read -r -d '' __SHX_HEREDOC_$__shx__heredocCount << 'SHX_PRINT_BLOCK'${__shx__newLine}"
      __shx__outputScriptToEval+="$__shx__stringBuilder"
      __shx__outputScriptToEval+="${__shx__newLine}SHX_PRINT_BLOCK"
      __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"\${__SHX_HEREDOC_$__shx__heredocCount%$'\\n'}\""
      __shx__outputScriptToEval+="${__shx__newLine}unset __SHX_HEREDOC_$__shx__heredocCount"
      __shx__stringBuilder=''
    fi
  fi

  : "$(( __shx__cursor++ ))"
done

if [ -n "$__shx__stringBuilder" ]
then
    __shx__outputScriptToEval+="${__shx__newLine}IFS= read -r -d '' __SHX_HEREDOC_$__shx__heredocCount << 'SHX_PRINT_BLOCK'${__shx__newLine}"
  __shx__outputScriptToEval+="$__shx__stringBuilder"
  __shx__outputScriptToEval+="${__shx__newLine}SHX_PRINT_BLOCK"
  __shx__outputScriptToEval+="${__shx__newLine}printf '%s' \"\${__SHX_HEREDOC_$__shx__heredocCount%$'\\\n'}\""
  __shx__outputScriptToEval+="${__shx__newLine}unset __SHX_HEREDOC_$__shx__heredocCount"
  __shx__stringBuilder=''
fi

[ "$__shx__codeBlockDefinitionOpen" = true ] && { echo "shx [RenderError] <% block was not closed: '$__shx__codeBlockDefinition'" >&2; return 1; }
[ "$__shx__valueBlockOpen" = true ] && { echo "shx [RenderError] <%= was not closed: '$__shx__valueBlock'" >&2; return 1; }

# local __shx__COMPILED_TEMPLATE="$( printf '%s' "$__shx__outputScriptToEval" )"
local __shx__COMPILED_TEMPLATE="$__shx__outputScriptToEval"

if [ "$__shx__printCodeOnly" = true ]
then
  echo "$__shx__COMPILED_TEMPLATE"
  return 0
fi

if [ -f "$__shx__originalTemplateArgument" ] && [ "$SHX_CACHE" = true ]
then
  if [ -n "$__shx__cacheEncodedItem_indexOfCompiledTemplate" ] # Existing item in the cache to update
  then
    _SHX_COMPILED_TEMPLATE_CACHE[$__shx__cacheEncodedItem_indexOfCompiledTemplate]="$__shx__COMPILED_TEMPLATE"
  else
    # Add a new item
    local __shx__actualMtime="$( date +"%s" -r "$__shx__originalTemplateArgument" 2>/dev/null || stat -x "$__shx__originalTemplateArgument" | grep "Modify" )"
    local __shx__itemIndexLine="${#_SHX_COMPILED_TEMPLATE_CACHE[@]}>$__shx__actualMtime|$__shx__originalTemplateArgument"
    _SHX_COMPILED_TEMPLATE_CACHE[0]+="$__shx__itemIndexLine\n"
    _SHX_COMPILED_TEMPLATE_CACHE+=("$__shx__COMPILED_TEMPLATE")
  fi
fi

unset __shx__cursor
unset __shx__outputScriptToEval
unset __shx__stringBuilder
unset __shx__stringBuilderComplete
unset __shx__valueBlock
unset __shx__valueBlockOpen
unset __shx__codeBlockDefinition
unset __shx__codeBlockDefinitionOpen
unset __shx__heredocCount
unset __shx__printCodeOnly
unset __shx__newLine
unset __shx__originalTemplateArgument
unset __shx__providedTemplate
unset __shx__cacheEncodedItem_indexOfCompiledTemplate
unset __shx__cacheLookupIndex
unset __shx__cacheEncodedItem
unset __shx__cacheUpdatedEncodedItem
unset __shx__cacheEncodedItem_filename
unset __shx__currentTemplateFileMtime
unset __shx__cacheEncodedItem_mtime
unset __shx__cacheUpdatedEncodedItem

eval "$__shx__COMPILED_TEMPLATE"

Render the provided template and evaluate the result, printing the template result to STDOUT.

Description
$1 Template to compile (string or path to file)
$@ Any number of arguments.
Arguments which will be available to the evaluated template,
e.g. $1 or $* or $@

Simple String

template='<% for arg in "$@"; do %>Arg:<%= $arg %> <% done %>'
shx render "$template" "Hello" "World!"
# => "Arg: Hello Arg: World!"

shx --version

View Source
echo "shx version $SHX_VERSION"

Displays the current version of shx.sh