270 lines
8.1 KiB
Bash
270 lines
8.1 KiB
Bash
|
|
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# `mustache.sh`, Mustache in POSIX shell.
|
|
|
|
set -e
|
|
|
|
# File descriptor 3 is commandeered for debug output, which may end up being
|
|
# forwarded to standard error.
|
|
[ -z "$MUSTACHE_DEBUG" ] && exec 3>/dev/null || exec 3>&2
|
|
|
|
# File descriptor 4 is commandeered for use as a sink for literal and
|
|
# variable output of (inverted) sections that are not destined for standard
|
|
# output because their condition is not met.
|
|
exec 4>/dev/null
|
|
|
|
# File descriptor 5 is commandeered for capturing input for list processing.
|
|
exec 5>/dev/null
|
|
|
|
# Consume standard input one character at a time to render `mustache`(5)
|
|
# templates with data from the environment.
|
|
mustache() {
|
|
|
|
# Initialize the file descriptor to be used to emit characters. At
|
|
# times this value will be 4 to send output to `/dev/null`.
|
|
_M_FD=1
|
|
|
|
# IFS must only contain '\n' so as to be able to read space and tab
|
|
# characters from standard input one-at-a-time. The easiest way to
|
|
# convince it to actually contain the correct byte, and only the
|
|
# correct byte, is to use a single-quoted literal newline.
|
|
IFS='
|
|
'
|
|
|
|
# Consuming standard input one character at a time is quite a feat
|
|
# within the confines of POSIX shell. Bash's `read` builtin has
|
|
# `-n` for limiting the number of characters consumed. Here it is
|
|
# faked using `sed`(1) to place each character on its own line.
|
|
# The subtlety is that real newline characters are chomped so they
|
|
# must be indirectly detected by checking for zero-length
|
|
# characters, which is done as the character is emitted.
|
|
_mustache_sed | _mustache
|
|
# TODO Replace the original value of IFS. Be careful if it's unset.
|
|
|
|
}
|
|
|
|
# Process the one-character-per-line stream from `sed` via a state machine.
|
|
# This function will be called recursively in subshell environments to
|
|
# isolate nested section tags from the outside environment.
|
|
_mustache() {
|
|
|
|
# Always start by assuming a character is a literal.
|
|
_M_STATE="literal"
|
|
|
|
# The `read` builtin consumes one line at a time but by now each line
|
|
# contains only a single character.
|
|
while read _M_C
|
|
do
|
|
echo " _M_C: $_M_C (${#_M_C}), _M_STATE: $_M_STATE" >&3
|
|
echo "$_M_C" >&5
|
|
case "$_M_STATE" in
|
|
|
|
# Consume a single character literal. In the event this
|
|
# character and the previous character have been opening
|
|
# braces, progress to the "tag" state and initialize the
|
|
# tag name to the empty string (this invariant is relied
|
|
# on by the "tag" state). If this is the first opening
|
|
# brace, wait and see. Otherwise, emit this character.
|
|
"literal")
|
|
if [ -z "$_M_PREV_C" ]
|
|
then
|
|
case "$_M_C" in
|
|
"{") ;;
|
|
"") echo;;
|
|
*) printf "%s" "$_M_C";;
|
|
esac
|
|
else
|
|
case "$_M_PREV_C$_M_C" in
|
|
"{{") _M_STATE="tag" _M_TAG="";;
|
|
?"{") ;;
|
|
*)
|
|
[ "$_M_PREV_C" = "{" ] && printf "%s" "{"
|
|
[ -z "$_M_C" ] && echo || printf "%s" "$_M_C";;
|
|
esac
|
|
fi >&$_M_FD;;
|
|
|
|
# Consume the tag type and tag.
|
|
"tag")
|
|
case "$_M_PREV_C$_M_C" in
|
|
|
|
# A third opening brace in a row could be treated as
|
|
# a literal and the beginning of tag, as it is here,
|
|
# or as the beginning of a tag which begins with an
|
|
# opening brace.
|
|
"{{") printf "{" >&$_M_FD;;
|
|
|
|
# Note the type of this tag, defaulting to "variable".
|
|
"{#"|"{^"|"{/"|"{!"|"{>") _M_TAG_TYPE="$_M_C" _M_TAG="";;
|
|
|
|
# A variable tag must note the first character of the
|
|
# variable name. Since it's possible that an opening
|
|
# brace comes in the middle of the tag, check that
|
|
# this is indeed the beginning of the tag.
|
|
"{"?)
|
|
if [ -z "$_M_TAG" ]
|
|
then
|
|
_M_TAG_TYPE="variable" _M_TAG="$_M_C"
|
|
fi;;
|
|
|
|
# Two closing braces in a row closes the tag. The
|
|
# state resets to "literal" and the tag is processed,
|
|
# possibly in a subshell.
|
|
"}}")
|
|
_M_STATE="literal"
|
|
_mustache_tag;;
|
|
|
|
# A single closing brace is ignored at first.
|
|
?"}") ;;
|
|
|
|
# If the variable continues, the closing brace becomes
|
|
# part of the variable name.
|
|
"}"?) _M_TAG="$_M_TAG}";;
|
|
|
|
# Any other character becomes part of the variable name.
|
|
*) _M_TAG="$_M_TAG$_M_C";;
|
|
|
|
esac;;
|
|
|
|
esac
|
|
|
|
# This character becomes the previous character.
|
|
_M_PREV_C="$_M_C"
|
|
|
|
done
|
|
|
|
}
|
|
|
|
# Paper over different versions of cat.
|
|
_mustache_cat() {
|
|
set +e
|
|
cat -A <"/dev/null" >"/dev/null" 2>&1
|
|
_M_STATUS="$?"
|
|
set -e
|
|
if [ "$_M_STATUS" -eq 1 ]
|
|
then cat -e
|
|
else cat -A
|
|
fi
|
|
}
|
|
|
|
# Execute a tag surrounded by backticks. Remove the backticks first.
|
|
_mustache_cmd() {
|
|
_M_CMD="$*"
|
|
_M_CMD="${_M_CMD#"\`"}"
|
|
_M_CMD="${_M_CMD%"\`"}"
|
|
sh -c "$_M_CMD"
|
|
}
|
|
|
|
# Print an error message and GTFO. The message is the concatenation
|
|
# of all the arguments to this function.
|
|
_mustache_die() {
|
|
echo "mustache.sh: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Paper over differences between GNU sed and BSD sed
|
|
_mustache_sed() {
|
|
_M_NEWLINE="
|
|
"
|
|
set +e
|
|
sed -r <"/dev/null" >"/dev/null" 2>&1
|
|
_M_STATUS="$?"
|
|
set -e
|
|
if [ "$_M_STATUS" -eq 1 ]
|
|
then sed -E "s/./&\\$_M_NEWLINE/g; s/\\\\/\\\\\\\\/g"
|
|
else sed -r "s/./&\\n/g; s/\\\\/\\\\\\\\/g"
|
|
fi
|
|
}
|
|
|
|
# Process a complete tag. Variables are emitted, sections are recursed
|
|
# into, comments are ignored, and (for now) partials raise an error.
|
|
_mustache_tag() {
|
|
case "$_M_TAG_TYPE" in
|
|
|
|
# Variable tags expand to the value of an environment variable
|
|
# or the empty string if the environment variable is unset.
|
|
#
|
|
# If the tag is surrounded by backticks, execute it as a shell
|
|
# command, instead, using standard output as its value.
|
|
#
|
|
# Since the variable tag has been completely consumed, return
|
|
# to the assumption that everything's a literal until proven
|
|
# otherwise for this character.
|
|
"variable")
|
|
case "$_M_TAG" in
|
|
"\`"*"\`") _mustache_cmd "$_M_TAG";;
|
|
*) eval printf "%s" "\"\$$_M_TAG\"";;
|
|
esac >&$_M_FD;;
|
|
|
|
# Section tags expand to the expanded value of the section's
|
|
# literals and tags if and only if the section tag is in the
|
|
# environment and non-empty. Inverted section tags expand
|
|
# if the section tag is empty or unset in the environment.
|
|
#
|
|
# If the tag is surrounded by backticks, execute it as a shell
|
|
# command, instead, and process the section once for each line
|
|
# of standard output (made available as `_M_LINE`).
|
|
#
|
|
# Sections not being expanded are redirected to `/dev/null`.
|
|
"#"|"^")
|
|
echo " # _M_TAG: $_M_TAG" >&3
|
|
_M_TAG_V="$(eval printf "%s" "\"\$$_M_TAG\"")"
|
|
case "$_M_TAG_TYPE" in
|
|
"#") [ -z "$_M_TAG_V" ] && _M_FD=4;;
|
|
"^") [ -n "$_M_TAG_V" ] && _M_FD=4;;
|
|
esac
|
|
case "$_M_TAG" in
|
|
"\`"*"\`")
|
|
_M_CAPTURE="$(_M_SECTION_TAG="$_M_TAG" _mustache 5>&1 >&4)"
|
|
echo " _M_CAPTURE: $_M_CAPTURE" | _mustache_cat >&3
|
|
_mustache_cmd "$_M_TAG" | while read _M_LINE
|
|
do
|
|
echo " _M_LINE: $_M_LINE" >&3
|
|
(
|
|
_M_SECTION_TAG="$_M_TAG"
|
|
echo "$_M_CAPTURE" | _mustache
|
|
)
|
|
done;;
|
|
*)
|
|
(
|
|
_M_SECTION_TAG="$_M_TAG"
|
|
_mustache
|
|
);;
|
|
esac
|
|
_M_FD=1;;
|
|
|
|
# Closing tags for (inverted) sections must match the expected
|
|
# tag name. Any redirections made when the (inverted) section
|
|
# opened are reset when the section closes.
|
|
"/")
|
|
echo " / _M_TAG: $_M_TAG, _M_SECTION_TAG: $_M_SECTION_TAG" >&3
|
|
if [ "$_M_TAG" != "$_M_SECTION_TAG" ]
|
|
then
|
|
_mustache_die "mismatched closing tag $_M_TAG," \
|
|
"expected $_M_SECTION_TAG"
|
|
fi
|
|
exit;;
|
|
|
|
# Comments do nothing.
|
|
"!") ;;
|
|
|
|
# TODO Partials.
|
|
">") _mustache_die "{{>$_M_TAG}} syntax not implemented";;
|
|
|
|
esac
|
|
}
|