diff --git a/mo b/mo deleted file mode 100644 index b53d48a..0000000 --- a/mo +++ /dev/null @@ -1,1997 +0,0 @@ -#!/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