aboutsummaryrefslogtreecommitdiffstats
path: root/build/mo
diff options
context:
space:
mode:
Diffstat (limited to 'build/mo')
-rwxr-xr-xbuild/mo2037
1 files changed, 2037 insertions, 0 deletions
diff --git a/build/mo b/build/mo
new file mode 100755
index 0000000..8247a2d
--- /dev/null
+++ b/build/mo
@@ -0,0 +1,2037 @@
+#!/usr/bin/env bash
+#
+#/ Mo is a mustache template rendering software written in bash. It inserts
+#/ environment variables into templates.
+#/
+#/ Simply put, mo will change {{VARIABLE}} into the value of that
+#/ environment variable. You can use {{#VARIABLE}}content{{/VARIABLE}} to
+#/ conditionally display content or iterate over the values of an array.
+#/
+#/ Learn more about mustache templates at https://mustache.github.io/
+#/
+#/ Simple usage:
+#/
+#/ mo [OPTIONS] filenames...
+#/
+#/ Options:
+#/
+#/ --allow-function-arguments
+#/ Permit functions to be called with additional arguments. Otherwise,
+#/ the only way to get access to the arguments is to use the
+#/ MO_FUNCTION_ARGS environment variable.
+#/ -d, --debug
+#/ Enable debug logging to stderr.
+#/ -u, --fail-not-set
+#/ Fail upon expansion of an unset variable. Will silently ignore by
+#/ default. Alternately, set MO_FAIL_ON_UNSET to a non-empty value.
+#/ -x, --fail-on-function
+#/ Fail when a function returns a non-zero status code instead of
+#/ silently ignoring it. Alternately, set MO_FAIL_ON_FUNCTION to a
+#/ non-empty value.
+#/ -f, --fail-on-file
+#/ Fail when a file (from command-line or partial) does not exist.
+#/ Alternately, set MO_FAIL_ON_FILE to a non-empty value.
+#/ -e, --false
+#/ Treat the string "false" as empty for conditionals. Alternately,
+#/ set MO_FALSE_IS_EMPTY to a non-empty value.
+#/ -h, --help
+#/ This message.
+#/ -s=FILE, --source=FILE
+#/ Load FILE into the environment before processing templates.
+#/ Can be used multiple times. The file must be a valid shell script
+#/ and should only contain variable assignments.
+#/ -o=DELIM, --open=DELIM
+#/ Set the opening delimiter. Default is "{{".
+#/ -c=DELIM, --close=DELIM
+#/ Set the closing delimiter. Default is "}}".
+#/ -- Indicate the end of options. All arguments after this will be
+#/ treated as filenames only. Use when filenames may start with
+#/ hyphens.
+#/
+#/ Mo uses the following environment variables:
+#/
+#/ MO_ALLOW_FUNCTION_ARGUMENTS - When set to a non-empty value, this allows
+#/ functions referenced in templates to receive additional options and
+#/ arguments.
+#/ MO_CLOSE_DELIMITER - The string used when closing a tag. Defaults to "}}".
+#/ Used internally.
+#/ MO_CLOSE_DELIMITER_DEFAULT - The default value of MO_CLOSE_DELIMITER. Used
+#/ when resetting the close delimiter, such as when parsing a partial.
+#/ MO_CURRENT - Variable name to use for ".".
+#/ MO_DEBUG - When set to a non-empty value, additional debug information is
+#/ written to stderr.
+#/ MO_FUNCTION_ARGS - Arguments passed to the function.
+#/ MO_FAIL_ON_FILE - If a filename from the command-line is missing or a
+#/ partial does not exist, abort with an error.
+#/ MO_FAIL_ON_FUNCTION - If a function returns a non-zero status code, abort
+#/ with an error.
+#/ MO_FAIL_ON_UNSET - When set to a non-empty value, expansion of an unset env
+#/ variable will be aborted with an error.
+#/ MO_FALSE_IS_EMPTY - When set to a non-empty value, the string "false" will
+#/ be treated as an empty value for the purposes of conditionals.
+#/ MO_OPEN_DELIMITER - The string used when opening a tag. Defaults to "{{".
+#/ Used internally.
+#/ MO_OPEN_DELIMITER_DEFAULT - The default value of MO_OPEN_DELIMITER. Used
+#/ when resetting the open delimiter, such as when parsing a partial.
+#/ MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate a
+#/ help message.
+#/ MO_PARSED - Content that has made it through the template engine.
+#/ MO_STANDALONE_CONTENT - The unparsed content that preceeded the current tag.
+#/ When a standalone tag is encountered, this is checked to see if it only
+#/ contains whitespace. If this and the whitespace condition after a tag is
+#/ met, then this will be reset to $'\n'.
+#/ MO_UNPARSED - Template content yet to make it through the parser.
+#/
+#/ Mo is under a MIT style licence with an additional non-advertising clause.
+#/ See LICENSE.md for the full text.
+#/
+#/ This is open source! Please feel free to contribute.
+#/
+#/ https://github.com/tests-always-included/mo
+
+#: Disable these warnings for the entire file
+#:
+#: VAR_NAME was modified in a subshell. That change might be lost.
+# shellcheck disable=SC2031
+#:
+#: Modification of VAR_NAME is local (to subshell caused by (..) group).
+# shellcheck disable=SC2030
+
+# Public: Template parser function. Writes templates to stdout.
+#
+# $0 - Name of the mo file, used for getting the help message.
+# $@ - Filenames to parse.
+#
+# Returns nothing.
+mo() (
+ local moSource moFiles moDoubleHyphens moParsed moContent
+
+ #: This function executes in a subshell; IFS is reset at the end.
+ IFS=$' \n\t'
+
+ #: Enable a strict mode. This is also reset at the end.
+ set -eEu -o pipefail
+ moFiles=()
+ moDoubleHyphens=false
+ MO_OPEN_DELIMITER_DEFAULT="{{"
+ MO_CLOSE_DELIMITER_DEFAULT="}}"
+ MO_FUNCTION_CACHE_HIT=()
+ MO_FUNCTION_CACHE_MISS=()
+
+ if [[ $# -gt 0 ]]; then
+ for arg in "$@"; do
+ if $moDoubleHyphens; then
+ #: After we encounter two hyphens together, all the rest
+ #: of the arguments are files.
+ moFiles=(${moFiles[@]+"${moFiles[@]}"} "$arg")
+ else
+ case "$arg" in
+ -h|--h|--he|--hel|--help|-\?)
+ mo::usage "$0"
+ exit 0
+ ;;
+
+ --allow-function-arguments)
+ MO_ALLOW_FUNCTION_ARGUMENTS=true
+ ;;
+
+ -u | --fail-not-set)
+ MO_FAIL_ON_UNSET=true
+ ;;
+
+ -x | --fail-on-function)
+ MO_FAIL_ON_FUNCTION=true
+ ;;
+
+ -p | --fail-on-file)
+ MO_FAIL_ON_FILE=true
+ ;;
+
+ -e | --false)
+ MO_FALSE_IS_EMPTY=true
+ ;;
+
+ -s=* | --source=*)
+ if [[ "$arg" == --source=* ]]; then
+ moSource="${arg#--source=}"
+ else
+ moSource="${arg#-s=}"
+ fi
+
+ if [[ -e "$moSource" ]]; then
+ # shellcheck disable=SC1090
+ . "$moSource"
+ else
+ echo "No such file: $moSource" >&2
+ exit 1
+ fi
+ ;;
+
+ -o=* | --open=*)
+ if [[ "$arg" == --open=* ]]; then
+ MO_OPEN_DELIMITER_DEFAULT="${arg#--open=}"
+ else
+ MO_OPEN_DELIMITER_DEFAULT="${arg#-o=}"
+ fi
+ ;;
+
+ -c=* | --close=*)
+ if [[ "$arg" == --close=* ]]; then
+ MO_CLOSE_DELIMITER_DEFAULT="${arg#--close=}"
+ else
+ MO_CLOSE_DELIMITER_DEFAULT="${arg#-c=}"
+ fi
+ ;;
+
+ -d | --debug)
+ MO_DEBUG=true
+ ;;
+
+ --)
+ #: Set a flag indicating we've encountered double hyphens
+ moDoubleHyphens=true
+ ;;
+
+ -*)
+ mo::error "Unknown option: $arg (See --help for options)"
+ ;;
+
+ *)
+ #: Every arg that is not a flag or a option should be a file
+ moFiles=(${moFiles[@]+"${moFiles[@]}"} "$arg")
+ ;;
+ esac
+ fi
+ done
+ fi
+
+ mo::debug "Debug enabled"
+ MO_OPEN_DELIMITER="$MO_OPEN_DELIMITER_DEFAULT"
+ MO_CLOSE_DELIMITER="$MO_CLOSE_DELIMITER_DEFAULT"
+ mo::content moContent ${moFiles[@]+"${moFiles[@]}"} || return 1
+ mo::parse moParsed "$moContent"
+ echo -n "$moParsed"
+)
+
+
+# Internal: Show a debug message
+#
+# $1 - The debug message to show
+#
+# Returns nothing.
+mo::debug() {
+ if [[ -n "${MO_DEBUG:-}" ]]; then
+ echo "DEBUG ${FUNCNAME[1]:-?} - $1" >&2
+ fi
+}
+
+
+# Internal: Show a debug message and internal state information
+#
+# No arguments
+#
+# Returns nothing.
+mo::debugShowState() {
+ if [[ -z "${MO_DEBUG:-}" ]]; then
+ return
+ fi
+
+ local moState moTemp moIndex moDots
+
+ mo::escape moTemp "$MO_OPEN_DELIMITER"
+ moState="open: $moTemp"
+ mo::escape moTemp "$MO_CLOSE_DELIMITER"
+ moState="$moState close: $moTemp"
+ mo::escape moTemp "$MO_STANDALONE_CONTENT"
+ moState="$moState standalone: $moTemp"
+ mo::escape moTemp "$MO_CURRENT"
+ moState="$moState current: $moTemp"
+ moIndex=$((${#MO_PARSED} - 20))
+ moDots=...
+
+ if [[ "$moIndex" -lt 0 ]]; then
+ moIndex=0
+ moDots=
+ fi
+
+ mo::escape moTemp "${MO_PARSED:$moIndex}"
+ moState="$moState parsed: $moDots$moTemp"
+
+ moDots=...
+
+ if [[ "${#MO_UNPARSED}" -le 20 ]]; then
+ moDots=
+ fi
+
+ mo::escape moTemp "${MO_UNPARSED:0:20}$moDots"
+ moState="$moState unparsed: $moTemp"
+
+ echo "DEBUG ${FUNCNAME[1]:-?} - $moState" >&2
+}
+
+# Internal: Show an error message and exit
+#
+# $1 - The error message to show
+# $2 - Error code
+#
+# Returns nothing. Exits the program.
+mo::error() {
+ echo "ERROR: $1" >&2
+ exit "${2:-1}"
+}
+
+
+# Internal: Show an error message with a snippet of context and exit
+#
+# $1 - The error message to show
+# $2 - The starting point
+# $3 - Error code
+#
+# Returns nothing. Exits the program.
+mo::errorNear() {
+ local moEscaped
+
+ mo::escape moEscaped "${2:0:40}"
+ echo "ERROR: $1" >&2
+ echo "ERROR STARTS NEAR: $moEscaped"
+ exit "${3:-1}"
+}
+
+
+# Internal: Displays the usage for mo. Pulls this from the file that
+# contained the `mo` function. Can only work when the right filename
+# comes is the one argument, and that only happens when `mo` is called
+# with `$0` set to this file.
+#
+# $1 - Filename that has the help message
+#
+# Returns nothing.
+mo::usage() {
+ while read -r line; do
+ if [[ "${line:0:2}" == "#/" ]]; then
+ echo "${line:3}"
+ fi
+ done < "$MO_ORIGINAL_COMMAND"
+ echo ""
+ echo "MO_VERSION=$MO_VERSION"
+}
+
+
+# Internal: Fetches the content to parse into MO_UNPARSED. Can be a list of
+# partials for files or the content from stdin.
+#
+# $1 - Destination variable name
+# $2-@ - File names (optional), read from stdin otherwise
+#
+# Returns nothing.
+mo::content() {
+ local moTarget moContent moFilename
+
+ moTarget=$1
+ shift
+ moContent=""
+
+ if [[ "${#@}" -gt 0 ]]; then
+ for moFilename in "$@"; do
+ mo::debug "Using template to load content from file: $moFilename"
+ #: This is so relative paths work from inside template files
+ moContent="$moContent$MO_OPEN_DELIMITER>$moFilename$MO_CLOSE_DELIMITER"
+ done
+ else
+ mo::debug "Will read content from stdin"
+ mo::contentFile moContent || return 1
+ fi
+
+ local "$moTarget" && mo::indirect "$moTarget" "$moContent"
+}
+
+
+# Internal: Read a file into MO_UNPARSED.
+#
+# $1 - Destination variable name.
+# $2 - Filename to load - if empty, defaults to /dev/stdin
+#
+# Returns nothing.
+mo::contentFile() {
+ local moFile moResult moContent
+
+ #: The subshell removes any trailing newlines. We forcibly add
+ #: a dot to the content to preserve all newlines. Reading from
+ #: stdin with a `read` loop does not work as expected, so `cat`
+ #: needs to stay.
+ moFile=${2:-/dev/stdin}
+
+ if [[ -e "$moFile" ]]; then
+ mo::debug "Loading content: $moFile"
+ moContent=$(
+ set +Ee
+ cat -- "$moFile"
+ moResult=$?
+ echo -n '.'
+ exit "$moResult"
+ ) || return 1
+ moContent=${moContent%.} #: Remove last dot
+ elif [[ -n "${MO_FAIL_ON_FILE-}" ]]; then
+ mo::error "No such file: $moFile"
+ else
+ mo::debug "File does not exist: $moFile"
+ moContent=""
+ fi
+
+ local "$1" && mo::indirect "$1" "$moContent"
+}
+
+
+# Internal: Send a variable up to the parent of the caller of this function.
+#
+# $1 - Variable name
+# $2 - Value
+#
+# Examples
+#
+# callFunc () {
+# local "$1" && mo::indirect "$1" "the value"
+# }
+# callFunc dest
+# echo "$dest" # writes "the value"
+#
+# Returns nothing.
+mo::indirect() {
+ unset -v "$1"
+ printf -v "$1" '%s' "$2"
+}
+
+
+# Internal: Send an array as a variable up to caller of a function
+#
+# $1 - Variable name
+# $2-@ - Array elements
+#
+# Examples
+#
+# callFunc () {
+# local myArray=(one two three)
+# local "$1" && mo::indirectArray "$1" "${myArray[@]}"
+# }
+# callFunc dest
+# echo "${dest[@]}" # writes "one two three"
+#
+# Returns nothing.
+mo::indirectArray() {
+ unset -v "$1"
+
+ #: IFS must be set to a string containing space or unset in order for
+ #: the array slicing to work regardless of the current IFS setting on
+ #: bash 3. This is detailed further at
+ #: https://github.com/fidian/gg-core/pull/7
+ eval "$(printf "IFS= %s=(\"\${@:2}\") IFS=%q" "$1" "$IFS")"
+}
+
+
+# Internal: Trim leading characters from MO_UNPARSED
+#
+# Returns nothing.
+mo::trimUnparsed() {
+ local moI moC
+
+ moI=0
+ moC=${MO_UNPARSED:0:1}
+
+ while [[ "$moC" == " " || "$moC" == $'\r' || "$moC" == $'\n' || "$moC" == $'\t' ]]; do
+ moI=$((moI + 1))
+ moC=${MO_UNPARSED:$moI:1}
+ done
+
+ if [[ "$moI" != 0 ]]; then
+ MO_UNPARSED=${MO_UNPARSED:$moI}
+ fi
+}
+
+
+# Internal: Remove whitespace and content after whitespace
+#
+# $1 - Name of the destination variable
+# $2 - The string to chomp
+#
+# Returns nothing.
+mo::chomp() {
+ local moTemp moR moN moT
+
+ moR=$'\r'
+ moN=$'\n'
+ moT=$'\t'
+ moTemp=${2%% *}
+ moTemp=${moTemp%%"$moR"*}
+ moTemp=${moTemp%%"$moN"*}
+ moTemp=${moTemp%%"$moT"*}
+
+ local "$1" && mo::indirect "$1" "$moTemp"
+}
+
+
+# Public: Parses text, interpolates mustache tags. Utilizes the current value
+# of MO_OPEN_DELIMITER, MO_CLOSE_DELIMITER, and MO_STANDALONE_CONTENT. Those
+# three variables shouldn't be changed by user-defined functions.
+#
+# $1 - Destination variable name - where to store the finished content
+# $2 - Content to parse
+# $3 - Preserve standalone status/content - truthy if not empty. When set to a
+# value, that becomes the standalone content value
+#
+# Returns nothing.
+mo::parse() {
+ local moOldParsed moOldStandaloneContent moOldUnparsed moResult
+
+ #: The standalone content is a trick to make the standalone tag detection
+ #: possible. When it's set to content with a newline and if the tag supports
+ #: it, the standalone content check happens. This check ensures only
+ #: whitespace is after the last newline up to the tag, and only whitespace
+ #: is after the tag up to the next newline. If that is the case, remove
+ #: whitespace and the trailing newline. By setting this to $'\n', we're
+ #: saying we are at the beginning of content.
+ mo::debug "Starting parse of ${#2} bytes"
+ moOldParsed=${MO_PARSED:-}
+ moOldUnparsed=${MO_UNPARSED:-}
+ MO_PARSED=""
+ MO_UNPARSED="$2"
+
+ if [[ -z "${3:-}" ]]; then
+ moOldStandaloneContent=${MO_STANDALONE_CONTENT:-}
+ MO_STANDALONE_CONTENT=$'\n'
+ else
+ MO_STANDALONE_CONTENT=$3
+ fi
+
+ MO_CURRENT=${MO_CURRENT:-}
+ mo::parseInternal
+ moResult="$MO_PARSED$MO_UNPARSED"
+ MO_PARSED=$moOldParsed
+ MO_UNPARSED=$moOldUnparsed
+
+ if [[ -z "${3:-}" ]]; then
+ MO_STANDALONE_CONTENT=$moOldStandaloneContent
+ fi
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Internal: Parse MO_UNPARSED, writing content to MO_PARSED. Interpolates
+# mustache tags.
+#
+# No arguments
+#
+# Returns nothing.
+mo::parseInternal() {
+ local moChunk
+
+ mo::debug "Starting parse"
+
+ while [[ -n "$MO_UNPARSED" ]]; do
+ mo::debugShowState
+ moChunk=${MO_UNPARSED%%"$MO_OPEN_DELIMITER"*}
+ MO_PARSED="$MO_PARSED$moChunk"
+ MO_STANDALONE_CONTENT="$MO_STANDALONE_CONTENT$moChunk"
+ MO_UNPARSED=${MO_UNPARSED:${#moChunk}}
+
+ if [[ -n "$MO_UNPARSED" ]]; then
+ MO_UNPARSED=${MO_UNPARSED:${#MO_OPEN_DELIMITER}}
+ mo::trimUnparsed
+
+ case "$MO_UNPARSED" in
+ '#'*)
+ #: Loop, if/then, or pass content through function
+ mo::parseBlock false
+ ;;
+
+ '^'*)
+ #: Display section if named thing does not exist
+ mo::parseBlock true
+ ;;
+
+ '>'*)
+ #: Load partial - get name of file relative to cwd
+ mo::parsePartial
+ ;;
+
+ '/'*)
+ #: Closing tag
+ mo::errorNear "Unbalanced close tag" "$MO_UNPARSED"
+ ;;
+
+ '!'*)
+ #: Comment - ignore the tag content entirely
+ mo::parseComment
+ ;;
+
+ '='*)
+ #: Change delimiters
+ #: Any two non-whitespace sequences separated by whitespace.
+ mo::parseDelimiter
+ ;;
+
+ '&'*)
+ #: Unescaped - mo doesn't escape/unescape
+ MO_UNPARSED=${MO_UNPARSED#&}
+ mo::trimUnparsed
+ mo::parseValue
+ ;;
+
+ *)
+ #: Normal environment variable, string, subexpression,
+ #: current value, key, or function call
+ mo::parseValue
+ ;;
+ esac
+ fi
+ done
+}
+
+
+# Internal: Handle parsing a block
+#
+# $1 - Invert condition ("true" or "false")
+#
+# Returns nothing
+mo::parseBlock() {
+ local moInvertBlock moTokens moTokensString
+
+ moInvertBlock=$1
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
+ MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"}
+ mo::tokensToString moTokensString "${moTokens[@]:1}"
+ mo::debug "Parsing block: $moTokensString"
+
+ if mo::standaloneCheck; then
+ mo::standaloneProcess
+ fi
+
+ if [[ "${moTokens[1]}" == "NAME" ]] && mo::isFunction "${moTokens[2]}"; then
+ mo::parseBlockFunction "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}"
+ elif [[ "${moTokens[1]}" == "NAME" ]] && mo::isArray "${moTokens[2]}"; then
+ mo::parseBlockArray "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}"
+ else
+ mo::parseBlockValue "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}"
+ fi
+}
+
+
+# Internal: Handle parsing a block whose first argument is a function
+#
+# $1 - Invert condition ("true" or "false")
+# $2-@ - The parsed tokens from inside the block tags
+#
+# Returns nothing
+mo::parseBlockFunction() {
+ local moTarget moInvertBlock moTokens moTemp moUnparsed moTokensString
+
+ moInvertBlock=$1
+ moTokensString=$2
+ shift 2
+ moTokens=(${@+"$@"})
+ mo::debug "Parsing block function: $moTokensString"
+ mo::getContentUntilClose moTemp "$moTokensString"
+ #: Pass unparsed content to the function.
+ #: Keep the updated delimiters if they changed.
+
+ if [[ "$moInvertBlock" != "true" ]]; then
+ mo::evaluateFunction moResult "$moTemp" "${moTokens[@]:1}"
+ MO_PARSED="$MO_PARSED$moResult"
+ fi
+
+ mo::debug "Done parsing block function: $moTokensString"
+}
+
+
+# Internal: Handle parsing a block whose first argument is an array
+#
+# $1 - Invert condition ("true" or "false")
+# $2-@ - The parsed tokens from inside the block tags
+#
+# Returns nothing
+mo::parseBlockArray() {
+ local moInvertBlock moTokens moResult moArrayName moArrayIndexes moArrayIndex moTemp moUnparsed moOpenDelimiterBefore moCloseDelimiterBefore moOpenDelimiterAfter moCloseDelimiterAfter moParsed moTokensString moCurrent
+
+ moInvertBlock=$1
+ moTokensString=$2
+ shift 2
+ moTokens=(${@+"$@"})
+ mo::debug "Parsing block array: $moTokensString"
+ moOpenDelimiterBefore=$MO_OPEN_DELIMITER
+ moCloseDelimiterBefore=$MO_CLOSE_DELIMITER
+ mo::getContentUntilClose moTemp "$moTokensString"
+ moOpenDelimiterAfter=$MO_OPEN_DELIMITER
+ moCloseDelimiterAfter=$MO_CLOSE_DELIMITER
+ moArrayName=${moTokens[1]}
+ eval "moArrayIndexes=(\"\${!${moArrayName}[@]}\")"
+
+ if [[ "${#moArrayIndexes[@]}" -lt 1 ]]; then
+ #: No elements
+ if [[ "$moInvertBlock" == "true" ]]; then
+ #: Restore the delimiter before parsing
+ MO_OPEN_DELIMITER=$moOpenDelimiterBefore
+ MO_CLOSE_DELIMITER=$moCloseDelimiterBefore
+ moCurrent=$MO_CURRENT
+ MO_CURRENT=$moArrayName
+ mo::parse moParsed "$moTemp" "blockArrayInvert$MO_STANDALONE_CONTENT"
+ MO_CURRENT=$moCurrent
+ MO_PARSED="$MO_PARSED$moParsed"
+ fi
+ else
+ if [[ "$moInvertBlock" != "true" ]]; then
+ #: Process for each element in the array
+ moUnparsed=$MO_UNPARSED
+
+ for moArrayIndex in "${moArrayIndexes[@]}"; do
+ #: Restore the delimiter before parsing
+ MO_OPEN_DELIMITER=$moOpenDelimiterBefore
+ MO_CLOSE_DELIMITER=$moCloseDelimiterBefore
+ moCurrent=$MO_CURRENT
+ MO_CURRENT=$moArrayName.$moArrayIndex
+ mo::debug "Iterate over array using element: $MO_CURRENT"
+ mo::parse moParsed "$moTemp" "blockArray$MO_STANDALONE_CONTENT"
+ MO_CURRENT=$moCurrent
+ MO_PARSED="$MO_PARSED$moParsed"
+ done
+
+ MO_UNPARSED=$moUnparsed
+ fi
+ fi
+
+ MO_OPEN_DELIMITER=$moOpenDelimiterAfter
+ MO_CLOSE_DELIMITER=$moCloseDelimiterAfter
+ mo::debug "Done parsing block array: $moTokensString"
+}
+
+
+# Internal: Handle parsing a block whose first argument is a value
+#
+# $1 - Invert condition ("true" or "false")
+# $2-@ - The parsed tokens from inside the block tags
+#
+# Returns nothing
+mo::parseBlockValue() {
+ local moInvertBlock moTokens moResult moUnparsed moOpenDelimiterBefore moOpenDelimiterAfter moCloseDelimiterBefore moCloseDelimiterAfter moParsed moTemp moTokensString moCurrent
+
+ moInvertBlock=$1
+ moTokensString=$2
+ shift 2
+ moTokens=(${@+"$@"})
+ mo::debug "Parsing block value: $moTokensString"
+ moOpenDelimiterBefore=$MO_OPEN_DELIMITER
+ moCloseDelimiterBefore=$MO_CLOSE_DELIMITER
+ mo::getContentUntilClose moTemp "$moTokensString"
+ moOpenDelimiterAfter=$MO_OPEN_DELIMITER
+ moCloseDelimiterAfter=$MO_CLOSE_DELIMITER
+
+ #: Variable, value, or list of mixed things
+ mo::evaluateListOfSingles moResult "${moTokens[@]}"
+
+ if mo::isTruthy "$moResult" "$moInvertBlock"; then
+ mo::debug "Block is truthy: $moResult"
+ #: Restore the delimiter before parsing
+ MO_OPEN_DELIMITER=$moOpenDelimiterBefore
+ MO_CLOSE_DELIMITER=$moCloseDelimiterBefore
+ moCurrent=$MO_CURRENT
+ MO_CURRENT=${moTokens[1]}
+ mo::parse moParsed "$moTemp" "blockValue$MO_STANDALONE_CONTENT"
+ MO_PARSED="$MO_PARSED$moParsed"
+ MO_CURRENT=$moCurrent
+ fi
+
+ MO_OPEN_DELIMITER=$moOpenDelimiterAfter
+ MO_CLOSE_DELIMITER=$moCloseDelimiterAfter
+ mo::debug "Done parsing block value: $moTokensString"
+}
+
+
+# Internal: Handle parsing a partial
+#
+# No arguments.
+#
+# Indentation will be applied to the entire partial's contents before parsing.
+# This indentation is based on the whitespace that ends the previously parsed
+# content.
+#
+# Returns nothing
+mo::parsePartial() {
+ local moFilename moResult moIndentation moN moR moTemp moT
+
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::trimUnparsed
+ mo::chomp moFilename "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}"
+ MO_UNPARSED="${MO_UNPARSED#*"$MO_CLOSE_DELIMITER"}"
+ moIndentation=""
+
+ if mo::standaloneCheck; then
+ moN=$'\n'
+ moR=$'\r'
+ moT=$'\t'
+ moIndentation="$moN${MO_PARSED//"$moR"/"$moN"}"
+ moIndentation=${moIndentation##*"$moN"}
+ moTemp=${moIndentation// }
+ moTemp=${moTemp//"$moT"}
+
+ if [[ -n "$moTemp" ]]; then
+ moIndentation=
+ fi
+
+ mo::debug "Adding indentation to partial: '$moIndentation'"
+ mo::standaloneProcess
+ fi
+
+ mo::debug "Parsing partial: $moFilename"
+
+ #: Execute in subshell to preserve current cwd and environment
+ moResult=$(
+ #: It would be nice to remove `dirname` and use a function instead,
+ #: but that is difficult when only given filenames.
+ cd "$(dirname -- "$moFilename")" || exit 1
+ echo "$(
+ local moPartialContent moPartialParsed
+
+ if ! mo::contentFile moPartialContent "${moFilename##*/}"; then
+ exit 1
+ fi
+
+ #: Reset delimiters before parsing
+ mo::indentLines moPartialContent "$moIndentation" "$moPartialContent"
+ MO_OPEN_DELIMITER="$MO_OPEN_DELIMITER_DEFAULT"
+ MO_CLOSE_DELIMITER="$MO_CLOSE_DELIMITER_DEFAULT"
+ mo::parse moPartialParsed "$moPartialContent"
+
+ #: Fix bash handling of subshells and keep trailing whitespace.
+ echo -n "$moPartialParsed."
+ )" || exit 1
+ ) || exit 1
+
+ if [[ -z "$moResult" ]]; then
+ mo::debug "Error detected when trying to read the file"
+ exit 1
+ fi
+
+ MO_PARSED="$MO_PARSED${moResult%.}"
+}
+
+
+# Internal: Handle parsing a comment
+#
+# No arguments.
+#
+# Returns nothing
+mo::parseComment() {
+ local moContent moContent
+
+ MO_UNPARSED=${MO_UNPARSED#*"$MO_CLOSE_DELIMITER"}
+ mo::debug "Parsing comment"
+
+ if mo::standaloneCheck; then
+ mo::standaloneProcess
+ fi
+}
+
+
+# Internal: Handle parsing the change of delimiters
+#
+# No arguments.
+#
+# Returns nothing
+mo::parseDelimiter() {
+ local moContent moOpen moClose
+
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::trimUnparsed
+ mo::chomp moOpen "$MO_UNPARSED"
+ MO_UNPARSED=${MO_UNPARSED:${#moOpen}}
+ mo::trimUnparsed
+ mo::chomp moClose "${MO_UNPARSED%%="$MO_CLOSE_DELIMITER"*}"
+ MO_UNPARSED=${MO_UNPARSED#*="$MO_CLOSE_DELIMITER"}
+ mo::debug "Parsing delimiters: $moOpen $moClose"
+
+ if mo::standaloneCheck; then
+ mo::standaloneProcess
+ fi
+
+ MO_OPEN_DELIMITER="$moOpen"
+ MO_CLOSE_DELIMITER="$moClose"
+}
+
+
+# Internal: Handle parsing value or function call
+#
+# No arguments.
+#
+# Returns nothing
+mo::parseValue() {
+ local moUnparsedOriginal moTokens
+
+ moUnparsedOriginal=$MO_UNPARSED
+ mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
+ mo::evaluate moResult "${moTokens[@]:1}"
+ MO_PARSED="$MO_PARSED$moResult"
+
+ if [[ "${MO_UNPARSED:0:${#MO_CLOSE_DELIMITER}}" != "$MO_CLOSE_DELIMITER" ]]; then
+ mo::errorNear "Did not find closing tag" "$moUnparsedOriginal"
+ fi
+
+ if mo::standaloneCheck; then
+ mo::standaloneProcess
+ fi
+
+ MO_UNPARSED=${MO_UNPARSED:${#MO_CLOSE_DELIMITER}}
+}
+
+
+# Internal: Determine if the given name is a defined function.
+#
+# $1 - Function name to check
+#
+# Be extremely careful. Even if strict mode is enabled, it is not honored
+# in newer versions of Bash. Any errors that crop up here will not be
+# caught automatically.
+#
+# Examples
+#
+# moo () {
+# echo "This is a function"
+# }
+# if mo::isFunction moo; then
+# echo "moo is a defined function"
+# fi
+#
+# Returns 0 if the name is a function, 1 otherwise.
+mo::isFunction() {
+ local moFunctionName
+
+ # Need to test for the array length, otherwise Mac will report an
+ # unbound variable
+ if [[ "${#MO_FUNCTION_CACHE_HIT[@]}" -gt 0 ]]; then
+ for moFunctionName in "${MO_FUNCTION_CACHE_HIT[@]}"; do
+ if [[ "$moFunctionName" == "$1" ]]; then
+ return 0
+ fi
+ done
+ fi
+
+ if [[ "${#MO_FUNCTION_CACHE_MISS[@]}" -gt 0 ]]; then
+ for moFunctionName in "${MO_FUNCTION_CACHE_MISS[@]}"; do
+ if [[ "$moFunctionName" == "$1" ]]; then
+ return 1
+ fi
+ done
+ fi
+
+ if declare -F "$1" &> /dev/null; then
+ if [[ "${#MO_FUNCTION_CACHE_HIT[@]}" -gt 0 ]]; then
+ MO_FUNCTION_CACHE_HIT=( ${MO_FUNCTION_CACHE_HIT[@]+"${MO_FUNCTION_CACHE_HIT[@]}"} "$1" )
+ else
+ MO_FUNCTION_CACHE_HIT=( "$1" )
+ fi
+
+ return 0
+ fi
+
+ if [[ "${#MO_FUNCTION_CACHE_MISS[@]}" -gt 0 ]]; then
+ MO_FUNCTION_CACHE_MISS=( ${MO_FUNCTION_CACHE_MISS[@]+"${MO_FUNCTION_CACHE_MISS[@]}"} "$1" )
+ else
+ MO_FUNCTION_CACHE_MISS=( "$1" )
+ fi
+
+ return 1
+}
+
+
+# Internal: Determine if a given environment variable exists and if it is
+# an array.
+#
+# $1 - Name of environment variable
+#
+# Be extremely careful. Even if strict mode is enabled, it is not honored
+# in newer versions of Bash. Any errors that crop up here will not be
+# caught automatically.
+#
+# Examples
+#
+# var=(abc)
+# if moIsArray var; then
+# echo "This is an array"
+# echo "Make sure you don't accidentally use \$var"
+# fi
+#
+# Returns 0 if the name is not empty, 1 otherwise.
+mo::isArray() {
+ #: Namespace this variable so we don't conflict with what we're testing.
+ local moTestResult
+
+ moTestResult=$(declare -p "$1" 2>/dev/null) || return 1
+ [[ "${moTestResult:0:10}" == "declare -a" ]] && return 0
+ [[ "${moTestResult:0:10}" == "declare -A" ]] && return 0
+
+ return 1
+}
+
+
+# Internal: Determine if an array index exists.
+#
+# $1 - Variable name to check
+# $2 - The index to check
+#
+# Has to check if the variable is an array and if the index is valid for that
+# type of array.
+#
+# Returns true (0) if everything was ok, 1 if there's any condition that fails.
+mo::isArrayIndexValid() {
+ local moDeclare moTest
+
+ moDeclare=$(declare -p "$1")
+ moTest=""
+
+ if [[ "${moDeclare:0:10}" == "declare -a" ]]; then
+ #: Numerically indexed array - must check if the index looks like a
+ #: number because using a string to index a numerically indexed array
+ #: will appear like it worked.
+ if [[ "$2" == "0" ]] || [[ "$2" =~ ^[1-9][0-9]*$ ]]; then
+ #: Index looks like a number
+ eval "moTest=\"\${$1[$2]+ok}\""
+ fi
+ elif [[ "${moDeclare:0:10}" == "declare -A" ]]; then
+ #: Associative array
+ eval "moTest=\"\${$1[$2]+ok}\""
+ fi
+
+ if [[ -n "$moTest" ]]; then
+ return 0;
+ fi
+
+ return 1
+}
+
+
+# Internal: Determine if a variable is assigned, even if it is assigned an empty
+# value.
+#
+# $1 - Variable name to check.
+#
+# Can not use logic like this in case invalid variable names are passed.
+# [[ "${!1-a}" == "${!1-b}" ]]
+#
+# Using logic like this gives false positives. Also, this is not supported on
+# Bash 3.2 and the script parsing will error before any commands are executed.
+# [[ -v "$a" ]]
+#
+# Declaring a variable is not the same as assigning the variable.
+# export x
+# declare -p x # Output: declare -x x
+# # Bash 3.2 returns error code 1 and outputs:
+# # bash: declare: x: not found
+# export y=""
+# declare -p y # Output: declare -x y=""
+# unset z
+# declare -p z # Error code 1 and output: bash: declare: z: not found
+#
+# Returns true (0) if the variable is set, 1 if the variable is unset.
+MO_VAR_TEST="ok"
+
+# This must not use `[[` because otherwise Bash 3.2 will stop execution and
+# always write to stderr without the ability to capture it.
+if test -v "MO_VAR_TEST" &> /dev/null; then
+ mo::debug "Using declare -p and [[ -v ]] for variable checks"
+ # More recent Bash
+ mo::isVarSet() {
+ # Do not convert this to [[, otherwise Bash 3.2 will fail to parse the
+ # script.
+ if declare -p "$1" &> /dev/null && test -v "$1"; then
+ return 0
+ fi
+
+ return 1
+ }
+else
+ mo::debug "Using declare -p for variable checks"
+ # Bash 3.2
+ mo::isVarSet() {
+ # If the variable is exported and not assigned, declare -p will error.
+ if declare -p "$1" &> /dev/null; then
+ return 0
+ fi
+
+ return 1
+ }
+fi
+unset MO_VAR_TEST
+
+
+# Internal: Determine if a value is considered truthy.
+#
+# $1 - The value to test
+# $2 - Invert the value, either "true" or "false"
+#
+# Returns true (0) if truthy, 1 otherwise.
+mo::isTruthy() {
+ local moTruthy
+
+ moTruthy=true
+
+ if [[ -z "${1-}" ]]; then
+ moTruthy=false
+ elif [[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${1-}" == "false" ]]; then
+ moTruthy=false
+ fi
+
+ #: XOR the results
+ #: moTruthy inverse desiredResult
+ #: true false true
+ #: true true false
+ #: false false false
+ #: false true true
+ if [[ "$moTruthy" == "$2" ]]; then
+ mo::debug "Value is falsy, test result: $moTruthy inverse: $2"
+ return 1
+ fi
+
+ mo::debug "Value is truthy, test result: $moTruthy inverse: $2"
+ return 0
+}
+
+
+# Internal: Convert token list to values
+#
+# $1 - Destination variable name
+# $2-@ - Tokens to convert
+#
+# Sample call:
+#
+# mo::evaluate dest NAME username VALUE abc123 PAREN 2
+#
+# Returns nothing.
+mo::evaluate() {
+ local moTarget moStack moValue moType moIndex moCombined moResult
+
+ moTarget=$1
+ shift
+
+ #: Phase 1 - remove all command tokens (PAREN, BRACE)
+ moStack=()
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ PAREN|BRACE)
+ moType=$1
+ moValue=$2
+ mo::debug "Combining $moValue tokens"
+ moIndex=$((${#moStack[@]} - (2 * moValue)))
+ mo::evaluateListOfSingles moCombined "${moStack[@]:$moIndex}"
+
+ if [[ "$moType" == "PAREN" ]]; then
+ moStack=("${moStack[@]:0:$moIndex}" NAME "$moCombined")
+ else
+ moStack=("${moStack[@]:0:$moIndex}" VALUE "$moCombined")
+ fi
+ ;;
+
+ *)
+ moStack=(${moStack[@]+"${moStack[@]}"} "$1" "$2")
+ ;;
+ esac
+
+ shift 2
+ done
+
+ #: Phase 2 - check if this is a function or if we should just concatenate values
+ if [[ "${moStack[0]:-}" == "NAME" ]] && mo::isFunction "${moStack[1]}"; then
+ #: Special case - if the first argument is a function, then the rest are
+ #: passed to the function.
+ mo::debug "Evaluating function: ${moStack[1]}"
+ mo::evaluateFunction moResult "" "${moStack[@]:1}"
+ else
+ #: Concatenate
+ mo::debug "Concatenating ${#moStack[@]} stack items"
+ mo::evaluateListOfSingles moResult ${moStack[@]+"${moStack[@]}"}
+ fi
+
+ local "$moTarget" && mo::indirect "$moTarget" "$moResult"
+}
+
+
+# Internal: Convert an argument list to individual values.
+#
+# $1 - Destination variable name
+# $2-@ - A list of argument types and argument name/value.
+#
+# This assumes each value is separate from the rest. In contrast, mo::evaluate
+# will pass all arguments to a function if the first value is a function.
+#
+# Sample call:
+#
+# mo::evaluateListOfSingles dest NAME username VALUE abc123
+#
+# Returns nothing.
+mo::evaluateListOfSingles() {
+ local moResult moTarget moTemp
+
+ moTarget=$1
+ shift
+ moResult=""
+
+ while [[ $# -gt 1 ]]; do
+ mo::evaluateSingle moTemp "$1" "$2"
+ moResult="$moResult$moTemp"
+ shift 2
+ done
+
+ mo::debug "Evaluated list of singles: $moResult"
+
+ local "$moTarget" && mo::indirect "$moTarget" "$moResult"
+}
+
+
+# Internal: Evaluate a single argument
+#
+# $1 - Name of variable for result
+# $2 - Type of argument, either NAME or VALUE
+# $3 - Argument
+#
+# Returns nothing
+mo::evaluateSingle() {
+ local moResult moType moArg
+
+ moType=$2
+ moArg=$3
+ mo::debug "Evaluating $moType: $moArg ($MO_CURRENT)"
+
+ if [[ "$moType" == "VALUE" ]]; then
+ moResult=$moArg
+ elif [[ "$moArg" == "." ]]; then
+ mo::evaluateVariable moResult ""
+ elif [[ "$moArg" == "@key" ]]; then
+ mo::evaluateKey moResult
+ elif mo::isFunction "$moArg"; then
+ mo::evaluateFunction moResult "" "$moArg"
+ else
+ mo::evaluateVariable moResult "$moArg"
+ fi
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Internal: Return the value for @key based on current's name
+#
+# $1 - Name of variable for result
+#
+# Returns nothing
+mo::evaluateKey() {
+ local moResult
+
+ if [[ "$MO_CURRENT" == *.* ]]; then
+ moResult="${MO_CURRENT#*.}"
+ else
+ moResult="${MO_CURRENT}"
+ fi
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Internal: Handle a variable name
+#
+# $1 - Destination variable name
+# $2 - Variable name
+#
+# Returns nothing.
+mo::evaluateVariable() {
+ local moResult moArg moNameParts
+
+ moArg=$2
+ moResult=""
+ mo::findVariableName moNameParts "$moArg"
+ mo::debug "Evaluate variable ($moArg, $MO_CURRENT): ${moNameParts[*]}"
+
+ if [[ -z "${moNameParts[1]}" ]]; then
+ if mo::isArray "${moNameParts[0]}"; then
+ eval mo::join moResult "," "\${${moNameParts[0]}[@]}"
+ else
+ if mo::isVarSet "${moNameParts[0]}"; then
+ moResult=${moNameParts[0]}
+ moResult="${!moResult}"
+ elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then
+ mo::error "Environment variable not set: ${moNameParts[0]}"
+ fi
+ fi
+ else
+ if mo::isArray "${moNameParts[0]}"; then
+ eval "set +u;moResult=\"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\""
+ else
+ mo::error "Unable to index a scalar as an array: $moArg"
+ fi
+ fi
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Internal: Find the name of a variable to use
+#
+# $1 - Destination variable name, receives an array
+# $2 - Variable name from the template
+#
+# The array contains the following values
+# [0] - Variable name
+# [1] - Array index, or empty string
+#
+# Example variables
+# a="a"
+# b="b"
+# c=("c.0" "c.1")
+# d=([b]="d.b" [d]="d.d")
+#
+# Given these inputs (function input, current value), produce these outputs
+# a c => a
+# a c.0 => a
+# b d => d.b
+# b d.d => d.b
+# a d => d.a
+# a d.d => d.a
+# c.0 d => c.0
+# d.b d => d.b
+# '' c => c
+# '' c.0 => c.0
+# Returns nothing.
+mo::findVariableName() {
+ local moVar moNameParts moResultBase moResultIndex moCurrent
+
+ moVar=$2
+ moResultBase=$moVar
+ moResultIndex=""
+
+ if [[ -z "$moVar" ]]; then
+ moResultBase=${MO_CURRENT%%.*}
+
+ if [[ "$MO_CURRENT" == *.* ]]; then
+ moResultIndex=${MO_CURRENT#*.}
+ fi
+ elif [[ "$moVar" == *.* ]]; then
+ mo::debug "Find variable name; name has dot: $moVar"
+ moResultBase=${moVar%%.*}
+ moResultIndex=${moVar#*.}
+ elif [[ -n "$MO_CURRENT" ]]; then
+ moCurrent=${MO_CURRENT%%.*}
+ mo::debug "Find variable name; look in array: $moCurrent"
+
+ if mo::isArrayIndexValid "$moCurrent" "$moVar"; then
+ moResultBase=$moCurrent
+ moResultIndex=$moVar
+ fi
+ fi
+
+ local "$1" && mo::indirectArray "$1" "$moResultBase" "$moResultIndex"
+}
+
+
+# Internal: Join / implode an array
+#
+# $1 - Variable name to receive the joined content
+# $2 - Joiner
+# $3-@ - Elements to join
+#
+# Returns nothing.
+mo::join() {
+ local joiner part result target
+
+ target=$1
+ joiner=$2
+ result=$3
+ shift 3
+
+ for part in "$@"; do
+ result="$result$joiner$part"
+ done
+
+ local "$target" && mo::indirect "$target" "$result"
+}
+
+
+# Internal: Call a function.
+#
+# $1 - Variable for output
+# $2 - Content to pass
+# $3 - Function to call
+# $4-@ - Additional arguments as list of type, value/name
+#
+# Returns nothing.
+mo::evaluateFunction() {
+ local moArgs moContent moFunctionResult moTarget moFunction moTemp moFunctionCall
+
+ moTarget=$1
+ moContent=$2
+ moFunction=$3
+ shift 3
+ moArgs=()
+
+ while [[ $# -gt 1 ]]; do
+ mo::evaluateSingle moTemp "$1" "$2"
+ moArgs=(${moArgs[@]+"${moArgs[@]}"} "$moTemp")
+ shift 2
+ done
+
+ mo::escape moFunctionCall "$moFunction"
+
+ if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then
+ mo::debug "Function arguments are allowed"
+
+ if [[ ${#moArgs[@]} -gt 0 ]]; then
+ for moTemp in "${moArgs[@]}"; do
+ mo::escape moTemp "$moTemp"
+ moFunctionCall="$moFunctionCall $moTemp"
+ done
+ fi
+ fi
+
+ mo::debug "Calling function: $moFunctionCall"
+
+ #: Call the function in a subshell for safety. Employ the trick to preserve
+ #: whitespace at the end of the output.
+ moContent=$(
+ export MO_FUNCTION_ARGS=(${moArgs[@]+"${moArgs[@]}"})
+ echo -n "$moContent" | eval "$moFunctionCall ; moFunctionResult=\$? ; echo -n '.' ; exit \"\$moFunctionResult\""
+ ) || {
+ moFunctionResult=$?
+ if [[ -n "${MO_FAIL_ON_FUNCTION-}" && "$moFunctionResult" != 0 ]]; then
+ mo::error "Function failed with status code $moFunctionResult: $moFunctionCall" "$moFunctionResult"
+ fi
+ }
+
+ local "$moTarget" && mo::indirect "$moTarget" "${moContent%.}"
+}
+
+
+# Internal: Check if a tag appears to have only whitespace before it and after
+# it on a line. There must be a new line before and there must be a newline
+# after or the end of a string
+#
+# No arguments.
+#
+# Returns 0 if this is a standalone tag, 1 otherwise.
+mo::standaloneCheck() {
+ local moContent moN moR moT
+
+ moN=$'\n'
+ moR=$'\r'
+ moT=$'\t'
+
+ #: Check the content before
+ moContent=${MO_STANDALONE_CONTENT//"$moR"/"$moN"}
+
+ #: By default, signal to the next check that this one failed
+ MO_STANDALONE_CONTENT=""
+
+ if [[ "$moContent" != *"$moN"* ]]; then
+ mo::debug "Not a standalone tag - no newline before"
+
+ return 1
+ fi
+
+ moContent=${moContent##*"$moN"}
+ moContent=${moContent//"$moT"/}
+ moContent=${moContent// /}
+
+ if [[ -n "$moContent" ]]; then
+ mo::debug "Not a standalone tag - non-whitespace detected before tag"
+
+ return 1
+ fi
+
+ #: Check the content after
+ moContent=${MO_UNPARSED//"$moR"/"$moN"}
+ moContent=${moContent%%"$moN"*}
+ moContent=${moContent//"$moT"/}
+ moContent=${moContent// /}
+
+ if [[ -n "$moContent" ]]; then
+ mo::debug "Not a standalone tag - non-whitespace detected after tag"
+
+ return 1
+ fi
+
+ #: Signal to the next check that this tag removed content
+ MO_STANDALONE_CONTENT=$'\n'
+
+ return 0
+}
+
+
+# Internal: Process content before and after a tag. Remove prior whitespace up
+# to the previous newline. Remove following whitespace up to and including the
+# next newline.
+#
+# No arguments.
+#
+# Returns nothing.
+mo::standaloneProcess() {
+ local moI moTemp
+
+ mo::debug "Standalone tag - processing content before and after tag"
+ moI=$((${#MO_PARSED} - 1))
+ mo::debug "zero done ${#MO_PARSED}"
+ mo::escape moTemp "$MO_PARSED"
+ mo::debug "$moTemp"
+
+ # Mac appears to allow getting characters from before the start of the string
+ while [[ "$moI" -ge 0 ]] && [[ "${MO_PARSED:$moI:1}" == " " || "${MO_PARSED:$moI:1}" == $'\t' ]]; do
+ moI=$((moI - 1))
+ done
+
+ if [[ $((moI + 1)) != "${#MO_PARSED}" ]]; then
+ MO_PARSED="${MO_PARSED:0:${moI}+1}"
+ fi
+
+ moI=0
+
+ while [[ "${MO_UNPARSED:${moI}:1}" == " " || "${MO_UNPARSED:${moI}:1}" == $'\t' ]]; do
+ moI=$((moI + 1))
+ done
+
+ if [[ "${MO_UNPARSED:${moI}:1}" == $'\r' ]]; then
+ moI=$((moI + 1))
+ fi
+
+ if [[ "${MO_UNPARSED:${moI}:1}" == $'\n' ]]; then
+ moI=$((moI + 1))
+ fi
+
+ if [[ "$moI" != 0 ]]; then
+ MO_UNPARSED=${MO_UNPARSED:${moI}}
+ fi
+}
+
+
+# Internal: Apply indentation before any line that has content in MO_UNPARSED.
+#
+# $1 - Destination variable name.
+# $2 - The indentation string.
+# $3 - The content that needs the indentation string prepended on each line.
+#
+# Returns nothing.
+mo::indentLines() {
+ local moContent moIndentation moResult moN moR moChunk
+
+ moIndentation=$2
+ moContent=$3
+
+ if [[ -z "$moIndentation" ]]; then
+ mo::debug "Not applying indentation, empty indentation"
+
+ local "$1" && mo::indirect "$1" "$moContent"
+ return
+ fi
+
+ if [[ -z "$moContent" ]]; then
+ mo::debug "Not applying indentation, empty contents"
+
+ local "$1" && mo::indirect "$1" "$moContent"
+ return
+ fi
+
+ moResult=
+ moN=$'\n'
+ moR=$'\r'
+
+ mo::debug "Applying indentation: '${moIndentation}'"
+
+ while [[ -n "$moContent" ]]; do
+ moChunk=${moContent%%"$moN"*}
+ moChunk=${moChunk%%"$moR"*}
+ moContent=${moContent:${#moChunk}}
+
+ if [[ -n "$moChunk" ]]; then
+ moResult="$moResult$moIndentation$moChunk"
+ fi
+
+ moResult="$moResult${moContent:0:1}"
+ moContent=${moContent:1}
+ done
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Internal: Escape a value
+#
+# $1 - Destination variable name
+# $2 - Value to escape
+#
+# Returns nothing
+mo::escape() {
+ local moResult
+
+ moResult=$2
+ moResult=$(declare -p moResult)
+ moResult=${moResult#*=}
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Internal: Get the content up to the end of the block by minimally parsing and
+# balancing blocks. Returns the content before the end tag to the caller and
+# removes the content + the end tag from MO_UNPARSED. This can change the
+# delimiters, adjusting MO_OPEN_DELIMITER and MO_CLOSE_DELIMITER.
+#
+# $1 - Destination variable name
+# $2 - Token string to match for a closing tag
+#
+# Returns nothing.
+mo::getContentUntilClose() {
+ local moChunk moResult moTemp moTokensString moTokens moTarget moTagStack moResultTemp
+
+ moTarget=$1
+ moTagStack=("$2")
+ mo::debug "Get content until close tag: ${moTagStack[0]}"
+ moResult=""
+
+ while [[ -n "$MO_UNPARSED" ]] && [[ "${#moTagStack[@]}" -gt 0 ]]; do
+ moChunk=${MO_UNPARSED%%"$MO_OPEN_DELIMITER"*}
+ moResult="$moResult$moChunk"
+ MO_UNPARSED=${MO_UNPARSED:${#moChunk}}
+
+ if [[ -n "$MO_UNPARSED" ]]; then
+ moResultTemp="$MO_OPEN_DELIMITER"
+ MO_UNPARSED=${MO_UNPARSED:${#MO_OPEN_DELIMITER}}
+ mo::getContentTrim moTemp
+ moResultTemp="$moResultTemp$moTemp"
+ mo::debug "First character within tag: ${MO_UNPARSED:0:1}"
+
+ case "$MO_UNPARSED" in
+ '#'*)
+ #: Increase block
+ moResultTemp="$moResultTemp${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::getContentTrim moTemp
+ mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER"
+ moResultTemp="$moResultTemp${moTemp[0]}"
+ moTagStack=("${moTemp[1]}" "${moTagStack[@]}")
+ ;;
+
+ '^'*)
+ #: Increase block
+ moResultTemp="$moResultTemp${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::getContentTrim moTemp
+ mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER"
+ moResultTemp="$moResultTemp${moTemp[0]}"
+ moTagStack=("${moTemp[1]}" "${moTagStack[@]}")
+ ;;
+
+ '>'*)
+ #: Partial - ignore
+ moResultTemp="$moResultTemp${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::getContentTrim moTemp
+ mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER"
+ moResultTemp="$moResultTemp${moTemp[0]}"
+ ;;
+
+ '/'*)
+ #: Decrease block
+ moResultTemp="$moResultTemp${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::getContentTrim moTemp
+ mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER"
+
+ if [[ "${moTagStack[0]}" == "${moTemp[1]}" ]]; then
+ moResultTemp="$moResultTemp${moTemp[0]}"
+ moTagStack=("${moTagStack[@]:1}")
+
+ if [[ "${#moTagStack[@]}" -eq 0 ]]; then
+ #: Erase all portions of the close tag
+ moResultTemp=""
+ fi
+ else
+ mo::errorNear "Unbalanced closing tag, expected: ${moTagStack[0]}" "${moTemp[0]}${MO_UNPARSED}"
+ fi
+ ;;
+
+ '!'*)
+ #: Comment - ignore
+ mo::getContentComment moTemp
+ moResultTemp="$moResultTemp$moTemp"
+ ;;
+
+ '='*)
+ #: Change delimiters
+ mo::getContentDelimiter moTemp
+ moResultTemp="$moResultTemp$moTemp"
+ ;;
+
+ '&'*)
+ #: Unescaped - bypass one then ignore
+ moResultTemp="$moResultTemp${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::getContentTrim moTemp
+ moResultTemp="$moResultTemp$moTemp"
+ mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER"
+ moResultTemp="$moResultTemp${moTemp[0]}"
+ ;;
+
+ *)
+ #: Normal variable - ignore
+ mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER"
+ moResultTemp="$moResultTemp${moTemp[0]}"
+ ;;
+ esac
+
+ moResult="$moResult$moResultTemp"
+ fi
+ done
+
+ MO_STANDALONE_CONTENT="$MO_STANDALONE_CONTENT$moResult"
+
+ if mo::standaloneCheck; then
+ moResultTemp=$MO_PARSED
+ MO_PARSED=$moResult
+ mo::standaloneProcess
+ moResult=$MO_PARSED
+ MO_PARSED=$moResultTemp
+ fi
+
+ local "$moTarget" && mo::indirect "$moTarget" "$moResult"
+}
+
+
+# Internal: Convert a list of tokens to a string
+#
+# $1 - Destination variable for the string
+# $2-$@ - Token list
+#
+# Returns nothing.
+mo::tokensToString() {
+ local moTarget moString moTokens
+
+ moTarget=$1
+ shift 1
+ moTokens=("$@")
+ moString=$(declare -p moTokens)
+ moString=${moString#*=}
+
+ local "$moTarget" && mo::indirect "$moTarget" "$moString"
+}
+
+
+# Internal: Trims content from MO_UNPARSED, returns trimmed content.
+#
+# $1 - Destination variable
+#
+# Returns nothing.
+mo::getContentTrim() {
+ local moChar moResult
+
+ moChar=${MO_UNPARSED:0:1}
+ moResult=""
+
+ while [[ "$moChar" == " " ]] || [[ "$moChar" == $'\r' ]] || [[ "$moChar" == $'\t' ]] || [[ "$moChar" == $'\n' ]]; do
+ moResult="$moResult$moChar"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ moChar=${MO_UNPARSED:0:1}
+ done
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Get the content up to and including a close tag
+#
+# $1 - Destination variable
+#
+# Returns nothing.
+mo::getContentComment() {
+ local moResult
+
+ mo::debug "Getting content for comment"
+ moResult=${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}
+ MO_UNPARSED=${MO_UNPARSED:${#moResult}}
+
+ if [[ "$MO_UNPARSED" == "$MO_CLOSE_DELIMITER"* ]]; then
+ moResult="$moResult$MO_CLOSE_DELIMITER"
+ MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"}
+ fi
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Get the content up to and including a close tag. First two non-whitespace
+# tokens become the new open and close tag.
+#
+# $1 - Destination variable
+#
+# Returns nothing.
+mo::getContentDelimiter() {
+ local moResult moTemp moOpen moClose
+
+ mo::debug "Getting content for delimiter"
+ moResult=""
+ mo::getContentTrim moTemp
+ moResult="$moResult$moTemp"
+ mo::chomp moOpen "$MO_UNPARSED"
+ MO_UNPARSED="${MO_UNPARSED:${#moOpen}}"
+ moResult="$moResult$moOpen"
+ mo::getContentTrim moTemp
+ moResult="$moResult$moTemp"
+ mo::chomp moClose "${MO_UNPARSED%%="$MO_CLOSE_DELIMITER"*}"
+ MO_UNPARSED="${MO_UNPARSED:${#moClose}}"
+ moResult="$moResult$moClose"
+ mo::getContentTrim moTemp
+ moResult="$moResult$moTemp"
+ MO_OPEN_DELIMITER="$moOpen"
+ MO_CLOSE_DELIMITER="$moClose"
+
+ local "$1" && mo::indirect "$1" "$moResult"
+}
+
+
+# Get the content up to and including a close tag. First two non-whitespace
+# tokens become the new open and close tag.
+#
+# $1 - Destination variable, an array
+# $2 - Terminator string
+#
+# The array contents:
+# [0] The raw content within the tag
+# [1] The parsed tokens as a single string
+#
+# Returns nothing.
+mo::getContentWithinTag() {
+ local moUnparsed moTokens
+
+ moUnparsed=${MO_UNPARSED}
+ mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
+ MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"}
+ mo::tokensToString moTokensString "${moTokens[@]:1}"
+ moParsed=${moUnparsed:0:$((${#moUnparsed} - ${#MO_UNPARSED}))}
+
+ local "$1" && mo::indirectArray "$1" "$moParsed" "$moTokensString"
+}
+
+
+# Internal: Parse MO_UNPARSED and retrieve the content within the tag
+# delimiters. Converts everything into an array of string values.
+#
+# $1 - Destination variable for the array of contents.
+# $2 - Stop processing when this content is found.
+#
+# The list of tokens are in RPN form. The first item in the resulting array is
+# the number of actual tokens (after combining command tokens) in the list.
+#
+# Given: a 'bc' "de\"\n" (f {g 'h'})
+# Result: ([0]=4 [1]=NAME [2]=a [3]=VALUE [4]=bc [5]=VALUE [6]=$'de\"\n'
+# [7]=NAME [8]=f [9]=NAME [10]=g [11]=VALUE [12]=h
+# [13]=BRACE [14]=2 [15]=PAREN [16]=2
+#
+# Returns nothing
+mo::tokenizeTagContents() {
+ local moResult moTerminator moTemp moUnparsedOriginal moTokenCount
+
+ moTerminator=$2
+ moResult=()
+ moUnparsedOriginal=$MO_UNPARSED
+ moTokenCount=0
+ mo::debug "Tokenizing tag contents until terminator: $moTerminator"
+
+ while true; do
+ mo::trimUnparsed
+
+ case "$MO_UNPARSED" in
+ "")
+ mo::errorNear "Did not find matching terminator: $moTerminator" "$moUnparsedOriginal"
+ ;;
+
+ "$moTerminator"*)
+ mo::debug "Found terminator"
+ local "$1" && mo::indirectArray "$1" "$moTokenCount" ${moResult[@]+"${moResult[@]}"}
+ return
+ ;;
+
+ '('*)
+ #: Do not tokenize the open paren - treat this as RPL
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::tokenizeTagContents moTemp ')'
+ moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]:1}" PAREN "${moTemp[0]}")
+ MO_UNPARSED=${MO_UNPARSED:1}
+ ;;
+
+ '{'*)
+ #: Do not tokenize the open brace - treat this as RPL
+ MO_UNPARSED=${MO_UNPARSED:1}
+ mo::tokenizeTagContents moTemp '}'
+ moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]:1}" BRACE "${moTemp[0]}")
+ MO_UNPARSED=${MO_UNPARSED:1}
+ ;;
+
+ ')'* | '}'*)
+ mo::errorNear "Unbalanced closing parenthesis or brace" "$MO_UNPARSED"
+ ;;
+
+ "'"*)
+ mo::tokenizeTagContentsSingleQuote moTemp
+ moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]}")
+ ;;
+
+ '"'*)
+ mo::tokenizeTagContentsDoubleQuote moTemp
+ moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]}")
+ ;;
+
+ *)
+ mo::tokenizeTagContentsName moTemp
+ moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]}")
+ ;;
+ esac
+
+ mo::debug "Got chunk: ${moTemp[0]} ${moTemp[1]}"
+ moTokenCount=$((moTokenCount + 1))
+ done
+}
+
+
+# Internal: Get the contents of a variable name.
+#
+# $1 - Destination variable name for the token list (array of strings)
+#
+# Returns nothing
+mo::tokenizeTagContentsName() {
+ local moTemp
+
+ mo::chomp moTemp "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}"
+ moTemp=${moTemp%%(*}
+ moTemp=${moTemp%%)*}
+ moTemp=${moTemp%%\{*}
+ moTemp=${moTemp%%\}*}
+ MO_UNPARSED=${MO_UNPARSED:${#moTemp}}
+ mo::trimUnparsed
+ mo::debug "Parsed default token: $moTemp"
+
+ local "$1" && mo::indirectArray "$1" "NAME" "$moTemp"
+}
+
+
+# Internal: Get the contents of a tag in double quotes. Parses the backslash
+# sequences.
+#
+# $1 - Destination variable name for the token list (array of strings)
+#
+# Returns nothing.
+mo::tokenizeTagContentsDoubleQuote() {
+ local moResult moUnparsedOriginal
+
+ moUnparsedOriginal=$MO_UNPARSED
+ MO_UNPARSED=${MO_UNPARSED:1}
+ moResult=
+ mo::debug "Getting double quoted tag contents"
+
+ while true; do
+ if [[ -z "$MO_UNPARSED" ]]; then
+ mo::errorNear "Unbalanced double quote" "$moUnparsedOriginal"
+ fi
+
+ case "$MO_UNPARSED" in
+ '"'*)
+ MO_UNPARSED=${MO_UNPARSED:1}
+ local "$1" && mo::indirectArray "$1" "VALUE" "$moResult"
+ return
+ ;;
+
+ \\b*)
+ moResult="$moResult"$'\b'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\e*)
+ #: Note, \e is ESC, but in Bash $'\E' is ESC.
+ moResult="$moResult"$'\E'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\f*)
+ moResult="$moResult"$'\f'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\n*)
+ moResult="$moResult"$'\n'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\r*)
+ moResult="$moResult"$'\r'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\t*)
+ moResult="$moResult"$'\t'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\v*)
+ moResult="$moResult"$'\v'
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ \\*)
+ moResult="$moResult${MO_UNPARSED:1:1}"
+ MO_UNPARSED=${MO_UNPARSED:2}
+ ;;
+
+ *)
+ moResult="$moResult${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ ;;
+ esac
+ done
+}
+
+
+# Internal: Get the contents of a tag in single quotes. Only gets the raw
+# value.
+#
+# $1 - Destination variable name for the token list (array of strings)
+#
+# Returns nothing.
+mo::tokenizeTagContentsSingleQuote() {
+ local moResult moUnparsedOriginal
+
+ moUnparsedOriginal=$MO_UNPARSED
+ MO_UNPARSED=${MO_UNPARSED:1}
+ moResult=
+ mo::debug "Getting single quoted tag contents"
+
+ while true; do
+ if [[ -z "$MO_UNPARSED" ]]; then
+ mo::errorNear "Unbalanced single quote" "$moUnparsedOriginal"
+ fi
+
+ case "$MO_UNPARSED" in
+ "'"*)
+ MO_UNPARSED=${MO_UNPARSED:1}
+ local "$1" && mo::indirectArray "$1" VALUE "$moResult"
+ return
+ ;;
+
+ *)
+ moResult="$moResult${MO_UNPARSED:0:1}"
+ MO_UNPARSED=${MO_UNPARSED:1}
+ ;;
+ esac
+ done
+}
+
+
+# Save the original command's path for usage later
+MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}"
+MO_VERSION="3.1.0"
+
+# If sourced, load all functions.
+# If executed, perform the actions as expected.
+if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
+ mo "$@"
+fi