1062 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1062 lines
		
	
	
		
			31 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:
 | 
						|
#/
 | 
						|
#/    -u, --fail-not-set
 | 
						|
#/          - Fail upon expansion of an unset variable.
 | 
						|
#/    -e, --false
 | 
						|
#/          - Treat the string "false" as empty for conditionals.
 | 
						|
#/    -h, --help
 | 
						|
#/          - This message.
 | 
						|
#/    -s=FILE, --source=FILE
 | 
						|
#/          - Load FILE into the environment before processing templates.
 | 
						|
#
 | 
						|
# 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
 | 
						|
 | 
						|
 | 
						|
# Public: Template parser function.  Writes templates to stdout.
 | 
						|
#
 | 
						|
# $0             - Name of the mo file, used for getting the help message.
 | 
						|
# --allow-function-arguments
 | 
						|
#                - Permit functions in templates to be called with additional
 | 
						|
#                  arguments. This puts template data directly in to the path
 | 
						|
#                  of an eval statement. Use with caution. Not listed in the
 | 
						|
#                  help because it only makes sense when mo is sourced.
 | 
						|
# -u, --fail-not-set
 | 
						|
#                - Fail upon expansion of an unset variable.  Default behavior
 | 
						|
#                  is to silently ignore and expand into empty string.
 | 
						|
# -e, --false    - Treat "false" as an empty value.  You may set the
 | 
						|
#                  MO_FALSE_IS_EMPTY environment variable instead to a non-empty
 | 
						|
#                  value to enable this behavior.
 | 
						|
# -h, --help     - Display a help message.
 | 
						|
# -s=FILE, --source=FILE
 | 
						|
#                - Source a file into the environment before processint
 | 
						|
#                  template files.
 | 
						|
# --             - Used to indicate the end of options.  You may optionally
 | 
						|
#                  use this when filenames may start with two hyphens.
 | 
						|
# $@             - Filenames to parse.
 | 
						|
#
 | 
						|
# 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. This puts the content from the
 | 
						|
#                       template directly into an eval statement. Use with
 | 
						|
#                       extreme care.
 | 
						|
# MO_FUNCTION_ARGS    - Arguments passed to the function
 | 
						|
# 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_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate
 | 
						|
#                       a help message.
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
mo() (
 | 
						|
    # This function executes in a subshell so IFS is reset.
 | 
						|
    # Namespace this variable so we don't conflict with desired values.
 | 
						|
    local moContent f2source files doubleHyphens
 | 
						|
 | 
						|
    IFS=$' \n\t'
 | 
						|
    files=()
 | 
						|
    doubleHyphens=false
 | 
						|
 | 
						|
    if [[ $# -gt 0 ]]; then
 | 
						|
        for arg in "$@"; do
 | 
						|
            if $doubleHyphens; then
 | 
						|
                # After we encounter two hyphens together, all the rest
 | 
						|
                # of the arguments are files.
 | 
						|
                files=("${files[@]}" "$arg")
 | 
						|
            else
 | 
						|
                case "$arg" in
 | 
						|
                    -h|--h|--he|--hel|--help|-\?)
 | 
						|
                        moUsage "$0"
 | 
						|
                        exit 0
 | 
						|
                        ;;
 | 
						|
 | 
						|
                    --allow-function-arguments)
 | 
						|
                        # shellcheck disable=SC2030
 | 
						|
                        MO_ALLOW_FUNCTION_ARGUMENTS=true
 | 
						|
                        ;;
 | 
						|
 | 
						|
                    -u | --fail-not-set)
 | 
						|
                        # shellcheck disable=SC2030
 | 
						|
                        MO_FAIL_ON_UNSET=true
 | 
						|
                        ;;
 | 
						|
 | 
						|
                    -e | --false)
 | 
						|
                        # shellcheck disable=SC2030
 | 
						|
                        MO_FALSE_IS_EMPTY=true
 | 
						|
                        ;;
 | 
						|
 | 
						|
                    -s=* | --source=*)
 | 
						|
                        if [[ "$arg" == --source=* ]]; then
 | 
						|
                            f2source="${arg#--source=}"
 | 
						|
                        else
 | 
						|
                            f2source="${arg#-s=}"
 | 
						|
                        fi
 | 
						|
 | 
						|
                        if [[ -f "$f2source" ]]; then
 | 
						|
                            # shellcheck disable=SC1090
 | 
						|
                            . "$f2source"
 | 
						|
                        else
 | 
						|
                            echo "No such file: $f2source" >&2
 | 
						|
                            exit 1
 | 
						|
                        fi
 | 
						|
                        ;;
 | 
						|
 | 
						|
                    --)
 | 
						|
                        # Set a flag indicating we've encountered double hyphens
 | 
						|
                        doubleHyphens=true
 | 
						|
                        ;;
 | 
						|
 | 
						|
                    *)
 | 
						|
                        # Every arg that is not a flag or a option should be a file
 | 
						|
                        files=(${files[@]+"${files[@]}"} "$arg")
 | 
						|
                        ;;
 | 
						|
                esac
 | 
						|
            fi
 | 
						|
        done
 | 
						|
    fi
 | 
						|
 | 
						|
    moGetContent moContent "${files[@]}" || return 1
 | 
						|
    moParse "$moContent" "" true
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
# Internal: Call a function.
 | 
						|
#
 | 
						|
# $1 - Function to call
 | 
						|
# $2 - Content to pass
 | 
						|
# $3 - Additional arguments as a single string
 | 
						|
#
 | 
						|
# This can be dangerous, especially if you are using tags like
 | 
						|
# {{someFunction ; rm -rf / }}
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moCallFunction() {
 | 
						|
    local moArgs moFunctionArgs
 | 
						|
 | 
						|
    moArgs=()
 | 
						|
    moTrimWhitespace moFunctionArgs "$3"
 | 
						|
 | 
						|
    # shellcheck disable=SC2031
 | 
						|
    if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then
 | 
						|
        moArgs=$3
 | 
						|
    fi
 | 
						|
 | 
						|
    echo -n "$2" | MO_FUNCTION_ARGS="$moFunctionArgs" eval "$1" "$moArgs"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Scan content until the right end tag is found.  Creates an array
 | 
						|
# with the following members:
 | 
						|
#
 | 
						|
#   [0] = Content before end tag
 | 
						|
#   [1] = End tag (complete tag)
 | 
						|
#   [2] = Content after end tag
 | 
						|
#
 | 
						|
# Everything using this function uses the "standalone tags" logic.
 | 
						|
#
 | 
						|
# $1 - Name of variable for the array
 | 
						|
# $2 - Content
 | 
						|
# $3 - Name of end tag
 | 
						|
# $4 - If -z, do standalone tag processing before finishing
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moFindEndTag() {
 | 
						|
    local content remaining scanned standaloneBytes tag
 | 
						|
 | 
						|
    #: Find open tags
 | 
						|
    scanned=""
 | 
						|
    moSplit content "$2" '{{' '}}'
 | 
						|
 | 
						|
    while [[ "${#content[@]}" -gt 1 ]]; do
 | 
						|
        moTrimWhitespace tag "${content[1]}"
 | 
						|
 | 
						|
        #: Restore content[1] before we start using it
 | 
						|
        content[1]='{{'"${content[1]}"'}}'
 | 
						|
 | 
						|
        case $tag in
 | 
						|
            '#'* | '^'*)
 | 
						|
                #: Start another block
 | 
						|
                scanned="${scanned}${content[0]}${content[1]}"
 | 
						|
                moTrimWhitespace tag "${tag:1}"
 | 
						|
                moFindEndTag content "${content[2]}" "$tag" "loop"
 | 
						|
                scanned="${scanned}${content[0]}${content[1]}"
 | 
						|
                remaining=${content[2]}
 | 
						|
                ;;
 | 
						|
 | 
						|
            '/'*)
 | 
						|
                #: End a block - could be ours
 | 
						|
                moTrimWhitespace tag "${tag:1}"
 | 
						|
                scanned="$scanned${content[0]}"
 | 
						|
 | 
						|
                if [[ "$tag" == "$3" ]]; then
 | 
						|
                    #: Found our end tag
 | 
						|
                    if [[ -z "${4-}" ]] && moIsStandalone standaloneBytes "$scanned" "${content[2]}" true; then
 | 
						|
                        #: This is also a standalone tag - clean up whitespace
 | 
						|
                        #: and move those whitespace bytes to the "tag" element
 | 
						|
                        standaloneBytes=( $standaloneBytes )
 | 
						|
                        content[1]="${scanned:${standaloneBytes[0]}}${content[1]}${content[2]:0:${standaloneBytes[1]}}"
 | 
						|
                        scanned="${scanned:0:${standaloneBytes[0]}}"
 | 
						|
                        content[2]="${content[2]:${standaloneBytes[1]}}"
 | 
						|
                    fi
 | 
						|
 | 
						|
                    local "$1" && moIndirectArray "$1" "$scanned" "${content[1]}" "${content[2]}"
 | 
						|
                    return 0
 | 
						|
                fi
 | 
						|
 | 
						|
                scanned="$scanned${content[1]}"
 | 
						|
                remaining=${content[2]}
 | 
						|
                ;;
 | 
						|
 | 
						|
            *)
 | 
						|
                #: Ignore all other tags
 | 
						|
                scanned="${scanned}${content[0]}${content[1]}"
 | 
						|
                remaining=${content[2]}
 | 
						|
                ;;
 | 
						|
        esac
 | 
						|
 | 
						|
        moSplit content "$remaining" '{{' '}}'
 | 
						|
    done
 | 
						|
 | 
						|
    #: Did not find our closing tag
 | 
						|
    scanned="$scanned${content[0]}"
 | 
						|
    local "$1" && moIndirectArray "$1" "${scanned}" "" ""
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Find the first index of a substring.  If not found, sets the
 | 
						|
# index to -1.
 | 
						|
#
 | 
						|
# $1 - Destination variable for the index
 | 
						|
# $2 - Haystack
 | 
						|
# $3 - Needle
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moFindString() {
 | 
						|
    local pos string
 | 
						|
 | 
						|
    string=${2%%$3*}
 | 
						|
    [[ "$string" == "$2" ]] && pos=-1 || pos=${#string}
 | 
						|
    local "$1" && moIndirect "$1" "$pos"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Generate a dotted name based on current context and target name.
 | 
						|
#
 | 
						|
# $1 - Target variable to store results
 | 
						|
# $2 - Context name
 | 
						|
# $3 - Desired variable name
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moFullTagName() {
 | 
						|
    if [[ -z "${2-}" ]] || [[ "$2" == *.* ]]; then
 | 
						|
        local "$1" && moIndirect "$1" "$3"
 | 
						|
    else
 | 
						|
        local "$1" && moIndirect "$1" "${2}.${3}"
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Fetches the content to parse into a variable.  Can be a list of
 | 
						|
# partials for files or the content from stdin.
 | 
						|
#
 | 
						|
# $1   - Variable name to assign this content back as
 | 
						|
# $2-@ - File names (optional)
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moGetContent() {
 | 
						|
    local content filename target
 | 
						|
 | 
						|
    target=$1
 | 
						|
    shift
 | 
						|
    if [[ "${#@}" -gt 0 ]]; then
 | 
						|
        content=""
 | 
						|
 | 
						|
        for filename in "$@"; do
 | 
						|
            #: This is so relative paths work from inside template files
 | 
						|
            content="$content"'{{>'"$filename"'}}'
 | 
						|
        done
 | 
						|
    else
 | 
						|
        moLoadFile content /dev/stdin || return 1
 | 
						|
    fi
 | 
						|
 | 
						|
    local "$target" && moIndirect "$target" "$content"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Indent a string, placing the indent at the beginning of every
 | 
						|
# line that has any content.
 | 
						|
#
 | 
						|
# $1 - Name of destination variable to get an array of lines
 | 
						|
# $2 - The indent string
 | 
						|
# $3 - The string to reindent
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moIndentLines() {
 | 
						|
    local content fragment len posN posR result trimmed
 | 
						|
 | 
						|
    result=""
 | 
						|
 | 
						|
    #: Remove the period from the end of the string.
 | 
						|
    len=$((${#3} - 1))
 | 
						|
    content=${3:0:$len}
 | 
						|
 | 
						|
    if [[ -z "${2-}" ]]; then
 | 
						|
        local "$1" && moIndirect "$1" "$content"
 | 
						|
 | 
						|
        return 0
 | 
						|
    fi
 | 
						|
 | 
						|
    moFindString posN "$content" $'\n'
 | 
						|
    moFindString posR "$content" $'\r'
 | 
						|
 | 
						|
    while [[ "$posN" -gt -1 ]] || [[ "$posR" -gt -1 ]]; do
 | 
						|
        if [[ "$posN" -gt -1 ]]; then
 | 
						|
            fragment="${content:0:$posN + 1}"
 | 
						|
            content=${content:$posN + 1}
 | 
						|
        else
 | 
						|
            fragment="${content:0:$posR + 1}"
 | 
						|
            content=${content:$posR + 1}
 | 
						|
        fi
 | 
						|
 | 
						|
        moTrimChars trimmed "$fragment" false true " " $'\t' $'\n' $'\r'
 | 
						|
 | 
						|
        if [[ -n "$trimmed" ]]; then
 | 
						|
            fragment="$2$fragment"
 | 
						|
        fi
 | 
						|
 | 
						|
        result="$result$fragment"
 | 
						|
 | 
						|
        moFindString posN "$content" $'\n'
 | 
						|
        moFindString posR "$content" $'\r'
 | 
						|
 | 
						|
        # If the content ends in a newline, do not indent.
 | 
						|
        if [[ "$posN" -eq ${#content} ]]; then
 | 
						|
            # Special clause for \r\n
 | 
						|
            if [[ "$posR" -eq "$((posN - 1))" ]]; then
 | 
						|
                posR=-1
 | 
						|
            fi
 | 
						|
 | 
						|
            posN=-1
 | 
						|
        fi
 | 
						|
 | 
						|
        if [[ "$posR" -eq ${#content} ]]; then
 | 
						|
            posR=-1
 | 
						|
        fi
 | 
						|
    done
 | 
						|
 | 
						|
    moTrimChars trimmed "$content" false true " " $'\t'
 | 
						|
 | 
						|
    if [[ -n "$trimmed" ]]; then
 | 
						|
        content="$2$content"
 | 
						|
    fi
 | 
						|
 | 
						|
    result="$result$content"
 | 
						|
 | 
						|
    local "$1" && moIndirect "$1" "$result"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Send a variable up to the parent of the caller of this function.
 | 
						|
#
 | 
						|
# $1 - Variable name
 | 
						|
# $2 - Value
 | 
						|
#
 | 
						|
# Examples
 | 
						|
#
 | 
						|
#   callFunc () {
 | 
						|
#       local "$1" && moIndirect "$1" "the value"
 | 
						|
#   }
 | 
						|
#   callFunc dest
 | 
						|
#   echo "$dest"  # writes "the value"
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moIndirect() {
 | 
						|
    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" && moIndirectArray "$1" "${myArray[@]}"
 | 
						|
#   }
 | 
						|
#   callFunc dest
 | 
						|
#   echo "${dest[@]}" # writes "one two three"
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moIndirectArray() {
 | 
						|
    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: 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.
 | 
						|
moIsArray() {
 | 
						|
    # 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 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 moIsFunction moo; then
 | 
						|
#       echo "moo is a defined function"
 | 
						|
#   fi
 | 
						|
#
 | 
						|
# Returns 0 if the name is a function, 1 otherwise.
 | 
						|
moIsFunction() {
 | 
						|
    local functionList functionName
 | 
						|
 | 
						|
    functionList=$(declare -F)
 | 
						|
    functionList=( ${functionList//declare -f /} )
 | 
						|
 | 
						|
    for functionName in "${functionList[@]}"; do
 | 
						|
        if [[ "$functionName" == "$1" ]]; then
 | 
						|
            return 0
 | 
						|
        fi
 | 
						|
    done
 | 
						|
 | 
						|
    return 1
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Determine if the tag is a standalone tag based on whitespace
 | 
						|
# before and after the tag.
 | 
						|
#
 | 
						|
# Passes back a string containing two numbers in the format "BEFORE AFTER"
 | 
						|
# like "27 10".  It indicates the number of bytes remaining in the "before"
 | 
						|
# string (27) and the number of bytes to trim in the "after" string (10).
 | 
						|
# Useful for string manipulation:
 | 
						|
#
 | 
						|
# $1 - Variable to set for passing data back
 | 
						|
# $2 - Content before the tag
 | 
						|
# $3 - Content after the tag
 | 
						|
# $4 - true/false: is this the beginning of the content?
 | 
						|
#
 | 
						|
# Examples
 | 
						|
#
 | 
						|
#   moIsStandalone RESULT "$before" "$after" false || return 0
 | 
						|
#   RESULT_ARRAY=( $RESULT )
 | 
						|
#   echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}"
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moIsStandalone() {
 | 
						|
    local afterTrimmed beforeTrimmed char
 | 
						|
 | 
						|
    moTrimChars beforeTrimmed "$2" false true " " $'\t'
 | 
						|
    moTrimChars afterTrimmed "$3" true false " " $'\t'
 | 
						|
    char=$((${#beforeTrimmed} - 1))
 | 
						|
    char=${beforeTrimmed:$char}
 | 
						|
 | 
						|
    # If the content before didn't end in a newline
 | 
						|
    if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]]; then
 | 
						|
        # and there was content or this didn't start the file
 | 
						|
        if [[ -n "$char" ]] || ! $4; then
 | 
						|
            # then this is not a standalone tag.
 | 
						|
            return 1
 | 
						|
        fi
 | 
						|
    fi
 | 
						|
 | 
						|
    char=${afterTrimmed:0:1}
 | 
						|
 | 
						|
    # If the content after doesn't start with a newline and it is something
 | 
						|
    if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]] && [[ -n "$char" ]]; then
 | 
						|
        # then this is not a standalone tag.
 | 
						|
        return 2
 | 
						|
    fi
 | 
						|
 | 
						|
    if [[ "$char" == $'\r' ]] && [[ "${afterTrimmed:1:1}" == $'\n' ]]; then
 | 
						|
        char="$char"$'\n'
 | 
						|
    fi
 | 
						|
 | 
						|
    local "$1" && moIndirect "$1" "$((${#beforeTrimmed})) $((${#3} + ${#char} - ${#afterTrimmed}))"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Join / implode an array
 | 
						|
#
 | 
						|
# $1    - Variable name to receive the joined content
 | 
						|
# $2    - Joiner
 | 
						|
# $3-$* - Elements to join
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moJoin() {
 | 
						|
    local joiner part result target
 | 
						|
 | 
						|
    target=$1
 | 
						|
    joiner=$2
 | 
						|
    result=$3
 | 
						|
    shift 3
 | 
						|
 | 
						|
    for part in "$@"; do
 | 
						|
        result="$result$joiner$part"
 | 
						|
    done
 | 
						|
 | 
						|
    local "$target" && moIndirect "$target" "$result"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Read a file into a variable.
 | 
						|
#
 | 
						|
# $1 - Variable name to receive the file's content
 | 
						|
# $2 - Filename to load
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moLoadFile() {
 | 
						|
    local content len
 | 
						|
 | 
						|
    # The subshell removes any trailing newlines.  We forcibly add
 | 
						|
    # a dot to the content to preserve all newlines.
 | 
						|
    # As a future optimization, it would be worth considering removing
 | 
						|
    # cat and replacing this with a read loop.
 | 
						|
 | 
						|
    content=$(cat -- "$2" && echo '.') || return 1
 | 
						|
    len=$((${#content} - 1))
 | 
						|
    content=${content:0:$len}  # Remove last dot
 | 
						|
 | 
						|
    local "$1" && moIndirect "$1" "$content"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Process a chunk of content some number of times.  Writes output
 | 
						|
# to stdout.
 | 
						|
#
 | 
						|
# $1   - Content to parse repeatedly
 | 
						|
# $2   - Tag prefix (context name)
 | 
						|
# $3-@ - Names to insert into the parsed content
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moLoop() {
 | 
						|
    local content context contextBase
 | 
						|
 | 
						|
    content=$1
 | 
						|
    contextBase=$2
 | 
						|
    shift 2
 | 
						|
 | 
						|
    while [[ "${#@}" -gt 0 ]]; do
 | 
						|
        moFullTagName context "$contextBase" "$1"
 | 
						|
        moParse "$content" "$context" false
 | 
						|
        shift
 | 
						|
    done
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Parse a block of text, writing the result to stdout.
 | 
						|
#
 | 
						|
# $1 - Block of text to change
 | 
						|
# $2 - Current name (the variable NAME for what {{.}} means)
 | 
						|
# $3 - true when no content before this, false otherwise
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moParse() {
 | 
						|
    # Keep naming variables mo* here to not overwrite needed variables
 | 
						|
    # used in the string replacements
 | 
						|
    local moArgs moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag
 | 
						|
 | 
						|
    moCurrent=$2
 | 
						|
    moIsBeginning=$3
 | 
						|
 | 
						|
    # Find open tags
 | 
						|
    moSplit moContent "$1" '{{' '}}'
 | 
						|
 | 
						|
    while [[ "${#moContent[@]}" -gt 1 ]]; do
 | 
						|
        moTrimWhitespace moTag "${moContent[1]}"
 | 
						|
        moNextIsBeginning=false
 | 
						|
 | 
						|
        case $moTag in
 | 
						|
            '#'*)
 | 
						|
                # Loop, if/then, or pass content through function
 | 
						|
                # Sets context
 | 
						|
                moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
 | 
						|
                moTrimWhitespace moTag "${moTag:1}"
 | 
						|
 | 
						|
                # Split arguments from the tag name. Arguments are passed to
 | 
						|
                # functions.
 | 
						|
                moArgs=$moTag
 | 
						|
                moTag=${moTag%% *}
 | 
						|
                moTag=${moTag%%$'\t'*}
 | 
						|
                moArgs=${moArgs:${#moTag}}
 | 
						|
                moFindEndTag moBlock "$moContent" "$moTag"
 | 
						|
                moFullTagName moTag "$moCurrent" "$moTag"
 | 
						|
 | 
						|
                if moTest "$moTag"; then
 | 
						|
                    # Show / loop / pass through function
 | 
						|
                    if moIsFunction "$moTag"; then
 | 
						|
                        #: Consider piping the output to moGetContent
 | 
						|
                        #: so the lambda does not execute in a subshell?
 | 
						|
                        moContent=$(moCallFunction "$moTag" "${moBlock[0]}" "$moArgs")
 | 
						|
                        moParse "$moContent" "$moCurrent" false
 | 
						|
                        moContent="${moBlock[2]}"
 | 
						|
                    elif moIsArray "$moTag"; then
 | 
						|
                        eval "moLoop \"\${moBlock[0]}\" \"$moTag\" \"\${!${moTag}[@]}\""
 | 
						|
                    else
 | 
						|
                        moParse "${moBlock[0]}" "$moCurrent" true
 | 
						|
                    fi
 | 
						|
                fi
 | 
						|
 | 
						|
                moContent="${moBlock[2]}"
 | 
						|
                ;;
 | 
						|
 | 
						|
            '>'*)
 | 
						|
                # Load partial - get name of file relative to cwd
 | 
						|
                moPartial moContent "${moContent[@]}" "$moIsBeginning" "$moCurrent"
 | 
						|
                moNextIsBeginning=${moContent[1]}
 | 
						|
                moContent=${moContent[0]}
 | 
						|
                ;;
 | 
						|
 | 
						|
            '/'*)
 | 
						|
                # Closing tag - If hit in this loop, we simply ignore
 | 
						|
                # Matching tags are found in moFindEndTag
 | 
						|
                moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
 | 
						|
                ;;
 | 
						|
 | 
						|
            '^'*)
 | 
						|
                # Display section if named thing does not exist
 | 
						|
                moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
 | 
						|
                moTrimWhitespace moTag "${moTag:1}"
 | 
						|
                moFindEndTag moBlock "$moContent" "$moTag"
 | 
						|
                moFullTagName moTag "$moCurrent" "$moTag"
 | 
						|
 | 
						|
                if ! moTest "$moTag"; then
 | 
						|
                    moParse "${moBlock[0]}" "$moCurrent" false "$moCurrent"
 | 
						|
                fi
 | 
						|
 | 
						|
                moContent="${moBlock[2]}"
 | 
						|
                ;;
 | 
						|
 | 
						|
            '!'*)
 | 
						|
                # Comment - ignore the tag content entirely
 | 
						|
                # Trim spaces/tabs before the comment
 | 
						|
                moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
 | 
						|
                ;;
 | 
						|
 | 
						|
            .)
 | 
						|
                # Current content (environment variable or function)
 | 
						|
                moStandaloneDenied moContent "${moContent[@]}"
 | 
						|
                moShow "$moCurrent" "$moCurrent"
 | 
						|
                ;;
 | 
						|
 | 
						|
            '=')
 | 
						|
                # Change delimiters
 | 
						|
                # Any two non-whitespace sequences separated by whitespace.
 | 
						|
                # This tag is ignored.
 | 
						|
                moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
 | 
						|
                ;;
 | 
						|
 | 
						|
            '{'*)
 | 
						|
                # Unescaped - split on }}} not }}
 | 
						|
                moStandaloneDenied moContent "${moContent[@]}"
 | 
						|
                moContent="${moTag:1}"'}}'"$moContent"
 | 
						|
                moSplit moContent "$moContent" '}}}'
 | 
						|
                moTrimWhitespace moTag "${moContent[0]}"
 | 
						|
                moArgs=$moTag
 | 
						|
                moTag=${moTag%% *}
 | 
						|
                moTag=${moTag%%$'\t'*}
 | 
						|
                moArgs=${moArgs:${#moTag}}
 | 
						|
                moFullTagName moTag "$moCurrent" "$moTag"
 | 
						|
                moContent=${moContent[1]}
 | 
						|
 | 
						|
                # Now show the value
 | 
						|
                # Quote moArgs here, do not quote it later.
 | 
						|
                moShow "$moTag" "$moCurrent" "$moArgs"
 | 
						|
                ;;
 | 
						|
 | 
						|
            '&'*)
 | 
						|
                # Unescaped
 | 
						|
                moStandaloneDenied moContent "${moContent[@]}"
 | 
						|
                moTrimWhitespace moTag "${moTag:1}"
 | 
						|
                moFullTagName moTag "$moCurrent" "$moTag"
 | 
						|
                moShow "$moTag" "$moCurrent"
 | 
						|
                ;;
 | 
						|
 | 
						|
            *)
 | 
						|
                # Normal environment variable or function call
 | 
						|
                moStandaloneDenied moContent "${moContent[@]}"
 | 
						|
                moArgs=$moTag
 | 
						|
                moTag=${moTag%% *}
 | 
						|
                moTag=${moTag%%$'\t'*}
 | 
						|
                moArgs=${moArgs:${#moTag}}
 | 
						|
                moFullTagName moTag "$moCurrent" "$moTag"
 | 
						|
 | 
						|
                # Quote moArgs here, do not quote it later.
 | 
						|
                moShow "$moTag" "$moCurrent" "$moArgs"
 | 
						|
                ;;
 | 
						|
        esac
 | 
						|
 | 
						|
        moIsBeginning=$moNextIsBeginning
 | 
						|
        moSplit moContent "$moContent" '{{' '}}'
 | 
						|
    done
 | 
						|
 | 
						|
    echo -n "${moContent[0]}"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Process a partial.
 | 
						|
#
 | 
						|
# Indentation should be applied to the entire partial.
 | 
						|
#
 | 
						|
# This sends back the "is beginning" flag because the newline after a
 | 
						|
# standalone partial is consumed. That newline is very important in the middle
 | 
						|
# of content. We send back this flag to reset the processing loop's
 | 
						|
# `moIsBeginning` variable, so the software thinks we are back at the
 | 
						|
# beginning of a file and standalone processing continues to work.
 | 
						|
#
 | 
						|
# Prefix all variables.
 | 
						|
#
 | 
						|
# $1 - Name of destination variable. Element [0] is the content, [1] is the
 | 
						|
#      true/false flag indicating if we are at the beginning of content.
 | 
						|
# $2 - Content before the tag that was not yet written
 | 
						|
# $3 - Tag content
 | 
						|
# $4 - Content after the tag
 | 
						|
# $5 - true/false: is this the beginning of the content?
 | 
						|
# $6 - Current context name
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moPartial() {
 | 
						|
    # Namespace variables here to prevent conflicts.
 | 
						|
    local moContent moFilename moIndent moIsBeginning moPartial moStandalone moUnindented
 | 
						|
 | 
						|
    if moIsStandalone moStandalone "$2" "$4" "$5"; then
 | 
						|
        moStandalone=( $moStandalone )
 | 
						|
        echo -n "${2:0:${moStandalone[0]}}"
 | 
						|
        moIndent=${2:${moStandalone[0]}}
 | 
						|
        moContent=${4:${moStandalone[1]}}
 | 
						|
        moIsBeginning=true
 | 
						|
    else
 | 
						|
        moIndent=""
 | 
						|
        echo -n "$2"
 | 
						|
        moContent=$4
 | 
						|
        moIsBeginning=$5
 | 
						|
    fi
 | 
						|
 | 
						|
    moTrimWhitespace moFilename "${3:1}"
 | 
						|
 | 
						|
    # Execute in subshell to preserve current cwd and environment
 | 
						|
    (
 | 
						|
        # It would be nice to remove `dirname` and use a function instead,
 | 
						|
        # but that's difficult when you're only given filenames.
 | 
						|
        cd "$(dirname -- "$moFilename")" || exit 1
 | 
						|
        moUnindented="$(
 | 
						|
            moLoadFile moPartial "${moFilename##*/}" || exit 1
 | 
						|
            moParse "${moPartial}" "$6" true
 | 
						|
 | 
						|
            # Fix bash handling of subshells and keep trailing whitespace.
 | 
						|
            # This is removed in moIndentLines.
 | 
						|
            echo -n "."
 | 
						|
        )" || exit 1
 | 
						|
        moIndentLines moPartial "$moIndent" "$moUnindented"
 | 
						|
        echo -n "$moPartial"
 | 
						|
    ) || exit 1
 | 
						|
 | 
						|
    # If this is a standalone tag, the trailing newline after the tag is
 | 
						|
    # removed and the contents of the partial are added, which typically
 | 
						|
    # contain a newline. We need to send a signal back to the processing
 | 
						|
    # loop that the moIsBeginning flag needs to be turned on again.
 | 
						|
    #
 | 
						|
    # [0] is the content, [1] is that flag.
 | 
						|
    local "$1" && moIndirectArray "$1" "$moContent" "$moIsBeginning"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Show an environment variable or the output of a function to
 | 
						|
# stdout.
 | 
						|
#
 | 
						|
# Limit/prefix any variables used.
 | 
						|
#
 | 
						|
# $1 - Name of environment variable or function
 | 
						|
# $2 - Current context
 | 
						|
# $3 - Arguments string if $1 is a function
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moShow() {
 | 
						|
    # Namespace these variables
 | 
						|
    local moJoined moNameParts
 | 
						|
 | 
						|
    if moIsFunction "$1"; then
 | 
						|
        CONTENT=$(moCallFunction "$1" "" "$3")
 | 
						|
        moParse "$CONTENT" "$2" false
 | 
						|
        return 0
 | 
						|
    fi
 | 
						|
 | 
						|
    moSplit moNameParts "$1" "."
 | 
						|
 | 
						|
    if [[ -z "${moNameParts[1]-}" ]]; then
 | 
						|
        if moIsArray "$1"; then
 | 
						|
            eval moJoin moJoined "," "\${$1[@]}"
 | 
						|
            echo -n "$moJoined"
 | 
						|
        else
 | 
						|
            # shellcheck disable=SC2031
 | 
						|
            if moTestVarSet "$1"; then
 | 
						|
                echo -n "${!1}"
 | 
						|
            elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then
 | 
						|
                echo "Env variable not set: $1" >&2
 | 
						|
                exit 1
 | 
						|
            fi
 | 
						|
        fi
 | 
						|
    else
 | 
						|
        # Further subindexes are disallowed
 | 
						|
        eval "echo -n \"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\""
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Split a larger string into an array.
 | 
						|
#
 | 
						|
# $1 - Destination variable
 | 
						|
# $2 - String to split
 | 
						|
# $3 - Starting delimiter
 | 
						|
# $4 - Ending delimiter (optional)
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moSplit() {
 | 
						|
    local pos result
 | 
						|
 | 
						|
    result=( "$2" )
 | 
						|
    moFindString pos "${result[0]}" "$3"
 | 
						|
 | 
						|
    if [[ "$pos" -ne -1 ]]; then
 | 
						|
        # The first delimiter was found
 | 
						|
        result[1]=${result[0]:$pos + ${#3}}
 | 
						|
        result[0]=${result[0]:0:$pos}
 | 
						|
 | 
						|
        if [[ -n "${4-}" ]]; then
 | 
						|
            moFindString pos "${result[1]}" "$4"
 | 
						|
 | 
						|
            if [[ "$pos" -ne -1 ]]; then
 | 
						|
                # The second delimiter was found
 | 
						|
                result[2]="${result[1]:$pos + ${#4}}"
 | 
						|
                result[1]="${result[1]:0:$pos}"
 | 
						|
            fi
 | 
						|
        fi
 | 
						|
    fi
 | 
						|
 | 
						|
    local "$1" && moIndirectArray "$1" "${result[@]}"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Handle the content for a standalone tag.  This means removing
 | 
						|
# whitespace (not newlines) before a tag and whitespace and a newline after
 | 
						|
# a tag.  That is, assuming, that the line is otherwise empty.
 | 
						|
#
 | 
						|
# $1 - Name of destination "content" variable.
 | 
						|
# $2 - Content before the tag that was not yet written
 | 
						|
# $3 - Tag content (not used)
 | 
						|
# $4 - Content after the tag
 | 
						|
# $5 - true/false: is this the beginning of the content?
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moStandaloneAllowed() {
 | 
						|
    local bytes
 | 
						|
 | 
						|
    if moIsStandalone bytes "$2" "$4" "$5"; then
 | 
						|
        bytes=( $bytes )
 | 
						|
        echo -n "${2:0:${bytes[0]}}"
 | 
						|
        local "$1" && moIndirect "$1" "${4:${bytes[1]}}"
 | 
						|
    else
 | 
						|
        echo -n "$2"
 | 
						|
        local "$1" && moIndirect "$1" "$4"
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Handle the content for a tag that is never "standalone".  No
 | 
						|
# adjustments are made for newlines and whitespace.
 | 
						|
#
 | 
						|
# $1 - Name of destination "content" variable.
 | 
						|
# $2 - Content before the tag that was not yet written
 | 
						|
# $3 - Tag content (not used)
 | 
						|
# $4 - Content after the tag
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moStandaloneDenied() {
 | 
						|
    echo -n "$2"
 | 
						|
    local "$1" && moIndirect "$1" "$4"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Determines if the named thing is a function or if it is a
 | 
						|
# non-empty environment variable.  When MO_FALSE_IS_EMPTY is set to a
 | 
						|
# non-empty value, then "false" is also treated is an empty value.
 | 
						|
#
 | 
						|
# Do not use variables without prefixes here if possible as this needs to
 | 
						|
# check if any name exists in the environment
 | 
						|
#
 | 
						|
# $1                - Name of environment variable or function
 | 
						|
# $2                - Current value (our context)
 | 
						|
# MO_FALSE_IS_EMPTY - When set to a non-empty value, this will say the
 | 
						|
#                     string value "false" is empty.
 | 
						|
#
 | 
						|
# Returns 0 if the name is not empty, 1 otherwise.  When MO_FALSE_IS_EMPTY
 | 
						|
# is set, this returns 1 if the name is "false".
 | 
						|
moTest() {
 | 
						|
    # Test for functions
 | 
						|
    moIsFunction "$1" && return 0
 | 
						|
 | 
						|
    if moIsArray "$1"; then
 | 
						|
        # Arrays must have at least 1 element
 | 
						|
        eval "[[ \"\${#${1}[@]}\" -gt 0 ]]" && return 0
 | 
						|
    else
 | 
						|
        # If MO_FALSE_IS_EMPTY is set, then return 1 if the value of
 | 
						|
        # the variable is "false".
 | 
						|
        # shellcheck disable=SC2031
 | 
						|
        [[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${!1-}" == "false" ]] && return 1
 | 
						|
 | 
						|
        # Environment variables must not be empty
 | 
						|
        [[ -n "${!1-}" ]] && 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.
 | 
						|
#
 | 
						|
# Returns true (0) if the variable is set, 1 if the variable is unset.
 | 
						|
moTestVarSet() {
 | 
						|
    [[ "${!1-a}" == "${!1-b}" ]]
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Trim the leading whitespace only.
 | 
						|
#
 | 
						|
# $1   - Name of destination variable
 | 
						|
# $2   - The string
 | 
						|
# $3   - true/false - trim front?
 | 
						|
# $4   - true/false - trim end?
 | 
						|
# $5-@ - Characters to trim
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moTrimChars() {
 | 
						|
    local back current front last target varName
 | 
						|
 | 
						|
    target=$1
 | 
						|
    current=$2
 | 
						|
    front=$3
 | 
						|
    back=$4
 | 
						|
    last=""
 | 
						|
    shift 4 # Remove target, string, trim front flag, trim end flag
 | 
						|
 | 
						|
    while [[ "$current" != "$last" ]]; do
 | 
						|
        last=$current
 | 
						|
 | 
						|
        for varName in "$@"; do
 | 
						|
            $front && current="${current/#$varName}"
 | 
						|
            $back && current="${current/%$varName}"
 | 
						|
        done
 | 
						|
    done
 | 
						|
 | 
						|
    local "$target" && moIndirect "$target" "$current"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Internal: Trim leading and trailing whitespace from a string.
 | 
						|
#
 | 
						|
# $1 - Name of variable to store trimmed string
 | 
						|
# $2 - The string
 | 
						|
#
 | 
						|
# Returns nothing.
 | 
						|
moTrimWhitespace() {
 | 
						|
    local result
 | 
						|
 | 
						|
    moTrimChars result "$2" true true $'\r' $'\n' $'\t' " "
 | 
						|
    local "$1" && moIndirect "$1" "$result"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# 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.
 | 
						|
moUsage() {
 | 
						|
    grep '^#/' "${MO_ORIGINAL_COMMAND}" | cut -c 4-
 | 
						|
    echo ""
 | 
						|
    set | grep ^MO_VERSION=
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# Save the original command's path for usage later
 | 
						|
MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}"
 | 
						|
MO_VERSION="2.0.4"
 | 
						|
 | 
						|
# If sourced, load all functions.
 | 
						|
# If executed, perform the actions as expected.
 | 
						|
if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
 | 
						|
    mo "$@"
 | 
						|
fi
 |