git-vendor-name: mo git-vendor-dir: vendor/git.knownelement.com/ExternalVendorCode/mo git-vendor-repository: https://git.knownelement.com/ExternalVendorCode/mo.git git-vendor-ref: master
		
			
				
	
	
		
			1998 lines
		
	
	
		
			57 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1998 lines
		
	
	
		
			57 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
#!/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
 | 
						|
 | 
						|
    for moFunctionName in "${MO_FUNCTION_CACHE_HIT[@]}"; do
 | 
						|
        if [[ "$moFunctionName" == "$1" ]]; then
 | 
						|
            return 0
 | 
						|
        fi
 | 
						|
    done
 | 
						|
 | 
						|
    for moFunctionName in "${MO_FUNCTION_CACHE_MISS[@]}"; do
 | 
						|
        if [[ "$moFunctionName" == "$1" ]]; then
 | 
						|
            return 1
 | 
						|
        fi
 | 
						|
    done
 | 
						|
 | 
						|
    if declare -F "$1" &> /dev/null; then
 | 
						|
        MO_FUNCTION_CACHE_HIT=( ${MO_FUNCTION_CACHE_HIT[@]+"${MO_FUNCTION_CACHE_HIT[@]}"} "$1" )
 | 
						|
 | 
						|
        return 0
 | 
						|
    fi
 | 
						|
 | 
						|
    MO_FUNCTION_CACHE_MISS=( ${MO_FUNCTION_CACHE_MISS[@]+"${MO_FUNCTION_CACHE_MISS[@]}"} "$1" )
 | 
						|
 | 
						|
    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.
 | 
						|
#     [[ -v "$a" ]]
 | 
						|
#
 | 
						|
# Declaring a variable is not the same as assigning the variable.
 | 
						|
#     export x
 | 
						|
#     declare -p x   # Output: declare -x x
 | 
						|
#     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::isVarSet() {
 | 
						|
    if declare -p "$1" &> /dev/null && [[ -v "$1" ]]; then
 | 
						|
        return 0
 | 
						|
    fi
 | 
						|
 | 
						|
    return 1
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# 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"
 | 
						|
 | 
						|
    while [[ "${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.0.7"
 | 
						|
 | 
						|
# If sourced, load all functions.
 | 
						|
# If executed, perform the actions as expected.
 | 
						|
if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
 | 
						|
    mo "$@"
 | 
						|
fi
 |