📜 <%= “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>"
💾 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
📚 Use as a library
Give shx
visibility into your script’s variables without export
:
source shx.sh
title = "Regular variable"
shx render "Title: <%= $title %>"
Including access to function local
variables:
source shx.sh
renderTemplate() {
local greeting = " $1 "
local name = " $2 "
shx render "<%= $greeting %>, <%= $name %>!"
}
renderTemplate "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:
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