#!/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