mo/mo

1855 lines
52 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env bash
2015-01-27 12:05:06 +00:00
#
#/ 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.
#/ -- 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 "}}".
2023-04-22 01:59:31 +00:00
#/ 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 "{{".
#/ 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 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
2015-01-23 17:43:08 +00:00
# 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
2015-10-02 14:46:57 +00:00
# Public: Template parser function. Writes templates to stdout.
#
# $0 - Name of the mo file, used for getting the help message.
# $@ - Filenames to parse.
#
2015-10-02 14:46:57 +00:00
# Returns nothing.
mo() (
2023-04-22 01:59:31 +00:00
local moSource moFiles moDoubleHyphens moParsed moContent
2015-10-02 14:46:57 +00:00
# This function executes in a subshell; IFS is reset at the end.
2015-10-02 14:46:57 +00:00
IFS=$' \n\t'
# Enable a strict mode. This is also reset at the end.
set -eEu -o pipefail
moFiles=()
moDoubleHyphens=false
2015-10-02 14:46:57 +00:00
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")
2016-07-21 14:45:31 +00:00
else
case "$arg" in
-h|--h|--he|--hel|--help|-\?)
mo::usage "$0"
2016-07-21 14:45:31 +00:00
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
2016-07-21 14:45:31 +00:00
if [[ -f "$moSource" ]]; then
# shellcheck disable=SC1090
. "$moSource"
2016-07-21 14:45:31 +00:00
else
echo "No such file: $moSource" >&2
2016-07-21 14:45:31 +00:00
exit 1
fi
;;
-d | --debug)
MO_DEBUG=true
;;
2016-07-21 14:45:31 +00:00
--)
#: Set a flag indicating we've encountered double hyphens
moDoubleHyphens=true
2016-07-21 14:45:31 +00:00
;;
-*)
2023-04-10 16:39:47 +00:00
mo::error "Unknown option: $arg (See --help for options)"
;;
2016-07-21 14:45:31 +00:00
*)
#: Every arg that is not a flag or a option should be a file
moFiles=(${moFiles[@]+"${moFiles[@]}"} "$arg")
2016-07-21 14:45:31 +00:00
;;
esac
fi
done
2015-10-02 14:46:57 +00:00
fi
mo::debug "Debug enabled"
MO_OPEN_DELIMITER="${MO_OPEN_DELIMITER:-"{{"}"
MO_CLOSE_DELIMITER="${MO_CLOSE_DELIMITER:-"}}"}"
# 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_STANDALONE_CONTENT=$'\n'
2023-04-22 01:59:31 +00:00
MO_CURRENT=
mo::content moContent "${moFiles[@]}" || return 1
mo::parse moParsed "$moContent"
echo -n "$moParsed"
2015-10-02 14:46:57 +00:00
)
# 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 an error message and exit
2015-10-02 14:46:57 +00:00
#
# $1 - The error message to show
2023-04-22 01:59:31 +00:00
# $2 - Error code
#
# Returns nothing. Exits the program.
mo::error() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
2023-04-22 01:59:31 +00:00
# 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.
2015-10-02 14:46:57 +00:00
#
# $1 - Filename that has the help message
#
2015-10-02 14:46:57 +00:00
# Returns nothing.
mo::usage() {
while read -r line; do
if [[ "${line:0:2}" == "#/" ]]; then
echo "${line:3}"
fi
2023-04-08 04:10:47 +00:00
done < "$MO_ORIGINAL_COMMAND"
echo ""
echo "MO_VERSION=$MO_VERSION"
}
# Internal: Fetches the content to parse into MO_UNPARSED. Can be a list of
2015-10-02 14:46:57 +00:00
# partials for files or the content from stdin.
#
2023-04-22 01:59:31 +00:00
# $1 - Destination variable name
# $2-@ - File names (optional), read from stdin otherwise
#
2015-10-02 14:46:57 +00:00
# Returns nothing.
mo::content() {
2023-04-22 01:59:31 +00:00
local moTarget moContent moFilename
2023-04-22 01:59:31 +00:00
moTarget=$1
shift
moContent=""
2023-04-22 01:59:31 +00:00
if [[ "${#@}" -gt 0 ]]; then
for moFilename in "$@"; do
mo::debug "Using template to load content from file: $moFilename"
2015-10-02 14:46:57 +00:00
#: This is so relative paths work from inside template files
2023-04-22 01:59:31 +00:00
moContent="$moContent$MO_OPEN_DELIMITER>$moFilename$MO_CLOSE_DELIMITER"
done
else
mo::debug "Will read content from stdin"
2023-04-22 01:59:31 +00:00
mo::contentFile moContent || return 1
fi
2023-04-22 01:59:31 +00:00
local "$moTarget" && mo::indirect "$moTarget" "$moContent"
}
# Internal: Read a file into MO_UNPARSED.
#
2023-04-22 01:59:31 +00:00
# $1 - Destination variable name.
# $2 - Filename to load - if empty, defaults to /dev/stdin
2015-10-02 14:46:57 +00:00
#
# Returns nothing.
mo::contentFile() {
2023-04-22 01:59:31 +00:00
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.
2023-04-22 01:59:31 +00:00
moFile=${2:-/dev/stdin}
if [[ -e "$moFile" ]]; then
mo::debug "Loading content: $moFile"
2023-04-22 01:59:31 +00:00
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"
2023-04-22 01:59:31 +00:00
moContent=""
fi
2023-04-22 01:59:31 +00:00
local "$1" && mo::indirect "$1" "$moContent"
}
2015-10-02 14:46:57 +00:00
# Internal: Send a variable up to the parent of the caller of this function.
#
2015-10-02 14:46:57 +00:00
# $1 - Variable name
# $2 - Value
#
# Examples
#
# callFunc () {
# local "$1" && mo::indirect "$1" "the value"
2015-10-02 14:46:57 +00:00
# }
# callFunc dest
# echo "$dest" # writes "the value"
2015-10-02 14:46:57 +00:00
#
# Returns nothing.
mo::indirect() {
unset -v "$1"
printf -v "$1" '%s' "$2"
}
2015-10-02 14:46:57 +00:00
# Internal: Send an array as a variable up to caller of a function
#
# $1 - Variable name
2015-10-07 20:23:50 +00:00
# $2-@ - Array elements
2015-10-02 14:46:57 +00:00
#
# Examples
#
2015-10-02 14:46:57 +00:00
# callFunc () {
# local myArray=(one two three)
# local "$1" && mo::indirectArray "$1" "${myArray[@]}"
2015-10-02 14:46:57 +00:00
# }
# callFunc dest
# echo "${dest[@]}" # writes "one two three"
2015-10-02 14:46:57 +00:00
#
# 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 moLast moR moN moT
moLast=""
moR=$'\r'
moN=$'\n'
moT=$'\t'
while [[ "$MO_UNPARSED" != "$moLast" ]]; do
moLast=$MO_UNPARSED
MO_UNPARSED=${MO_UNPARSED# }
MO_UNPARSED=${MO_UNPARSED#"$moR"}
MO_UNPARSED=${MO_UNPARSED#"$moN"}
MO_UNPARSED=${MO_UNPARSED#"$moT"}
done
}
# 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"
}
2023-04-22 01:59:31 +00:00
# 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
#
# Returns nothing.
mo::parse() {
local moOldParsed moOldUnparsed moResult
mo::debug "Starting parse of ${#2} bytes"
moOldParsed=${MO_PARSED:-}
moOldUnparsed=${MO_UNPARSED:-}
MO_PARSED=""
MO_UNPARSED="$2"
mo::parseInternal
moResult="$MO_PARSED$MO_UNPARSED"
MO_PARSED=$moOldParsed
MO_UNPARSED=$moOldUnparsed
local "$1" && mo::indirect "$1" "$moResult"
}
# Internal: Parse MO_UNPARSED, writing content to MO_PARSED. Interpolates
# mustache tags.
#
2023-04-22 01:59:31 +00:00
# No arguments
#
# Returns nothing.
2023-04-22 01:59:31 +00:00
mo::parseInternal() {
local moChunk
2023-04-22 01:59:31 +00:00
mo::debug "Starting parse, current: $MO_CURRENT"
while [[ -n "$MO_UNPARSED" ]]; do
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
2023-04-22 01:59:31 +00:00
mo::parseBlock false
;;
'^'*)
# Display section if named thing does not exist
2023-04-22 01:59:31 +00:00
mo::parseBlock true
;;
'>'*)
# Load partial - get name of file relative to cwd
2023-04-22 01:59:31 +00:00
mo::parsePartial
;;
'/'*)
# Closing tag
2023-04-22 01:59:31 +00:00
mo::errorNear "Unbalanced close tag" "$MO_UNPARSED"
;;
'!'*)
# Comment - ignore the tag content entirely
mo::parseComment
2023-04-22 01:59:31 +00:00
;;
2015-01-27 14:29:08 +00:00
'='*)
# Change delimiters
# Any two non-whitespace sequences separated by whitespace.
mo::parseDelimiter
;;
'&'*)
# Unescaped - mo doesn't escape/unescape
MO_UNPARSED=${MO_UNPARSED#&}
mo::trimUnparsed
2023-04-22 01:59:31 +00:00
mo::parseValue
;;
*)
# Normal environment variable, string, subexpression,
# current value, key, or function call
2023-04-22 01:59:31 +00:00
mo::parseValue
;;
esac
fi
done
}
# Internal: Handle parsing a block
#
2023-04-22 01:59:31 +00:00
# $1 - Invert condition ("true" or "false")
#
# Returns nothing
mo::parseBlock() {
2023-04-22 01:59:31 +00:00
local moInvertBlock moTokens moTokensString
2023-04-22 01:59:31 +00:00
moInvertBlock=$1
MO_UNPARSED=${MO_UNPARSED:1}
2023-04-22 01:59:31 +00:00
mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"}
mo::tokensToString moTokensString "${moTokens[@]:1}"
2023-04-22 01:59:31 +00:00
mo::debug "Parsing block: $moTokensString"
if mo::standaloneCheck "$MO_STANDALONE_CONTENT"; 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
#
2023-04-22 01:59:31 +00:00
# $1 - Invert condition ("true" or "false")
# $2-@ - The parsed tokens from inside the block tags
#
# Returns nothing
mo::parseBlockFunction() {
2023-04-22 01:59:31 +00:00
local moTarget moInvertBlock moTokens moTemp moUnparsed moTokensString
2023-04-22 01:59:31 +00:00
moInvertBlock=$1
moTokensString=$2
shift 2
2023-04-22 01:59:31 +00:00
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
2023-04-22 01:59:31 +00:00
mo::evaluateFunction moResult "$moTemp" "${moTokens[@]:1}"
MO_PARSED="$MO_PARSED$moResult"
fi
2023-04-22 01:59:31 +00:00
mo::debug "Done parsing block function: $moTokensString"
}
# Internal: Handle parsing a block whose first argument is an array
#
2023-04-22 01:59:31 +00:00
# $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
2023-04-22 01:59:31 +00:00
moInvertBlock=$1
moTokensString=$2
shift 2
2023-04-22 01:59:31 +00:00
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
2023-04-22 01:59:31 +00:00
# Restore the delimiter before parsing
MO_OPEN_DELIMITER=$moOpenDelimiterBefore
MO_CLOSE_DELIMITER=$moCloseDelimiterBefore
moCurrent=$MO_CURRENT
MO_CURRENT=$moArrayName
2023-04-22 01:59:31 +00:00
mo::parse moParsed "$moTemp"
MO_CURRENT=$moCurrent
2023-04-22 01:59:31 +00:00
MO_PARSED="$MO_PARSED$moParsed"
fi
else
2023-04-22 01:59:31 +00:00
if [[ "$moInvertBlock" != "true" ]]; then
# Process for each element in the array
2023-04-22 01:59:31 +00:00
moUnparsed=$MO_UNPARSED
for moArrayIndex in "${moArrayIndexes[@]}"; do
2023-04-22 01:59:31 +00:00
# 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"
2023-04-22 01:59:31 +00:00
mo::parse moParsed "$moTemp" "$moArrayName"
MO_CURRENT=$moCurrent
2023-04-22 01:59:31 +00:00
MO_PARSED="$MO_PARSED$moParsed"
done
2023-04-22 01:59:31 +00:00
MO_UNPARSED=$moUnparsed
fi
fi
2023-04-22 01:59:31 +00:00
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
#
2023-04-22 01:59:31 +00:00
# $1 - Invert condition ("true" or "false")
# $2-@ - The parsed tokens from inside the block tags
#
# Returns nothing
mo::parseBlockValue() {
2023-04-22 01:59:31 +00:00
local moInvertBlock moTokens moResult moUnparsed moOpenDelimiterBefore moOpenDelimiterAfter moCloseDelimiterBefore moCloseDelimiterAfter moParsed moTemp moTokensString moCurrent
2023-04-22 01:59:31 +00:00
moInvertBlock=$1
moTokensString=$2
shift 2
2023-04-22 01:59:31 +00:00
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
2023-04-22 01:59:31 +00:00
mo::evaluateListOfSingles moResult "${moTokens[@]}"
if mo::isTruthy "$moResult" "$moInvertBlock"; then
mo::debug "Block is truthy: $moResult"
2023-04-22 01:59:31 +00:00
# Restore the delimiter before parsing
MO_OPEN_DELIMITER=$moOpenDelimiterBefore
MO_CLOSE_DELIMITER=$moCloseDelimiterBefore
moCurrent=$MO_CURRENT
MO_CURRENT=${moTokens[1]}
mo::parse moParsed "$moTemp"
MO_PARSED="$MO_PARSED$moParsed"
MO_CURRENT=$moCurrent
fi
2023-04-22 01:59:31 +00:00
MO_OPEN_DELIMITER=$moOpenDelimiterAfter
MO_CLOSE_DELIMITER=$moCloseDelimiterAfter
mo::debug "Done parsing block value: $moTokensString"
}
# Internal: Handle parsing a partial
#
2023-04-22 01:59:31 +00:00
# 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() {
2023-04-22 01:59:31 +00:00
local moFilename moResult moIndentation moN moR
MO_UNPARSED=${MO_UNPARSED:1}
mo::trimUnparsed
mo::chomp moFilename "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}"
MO_UNPARSED="${MO_UNPARSED#*"$MO_CLOSE_DELIMITER"}"
2023-04-08 04:10:47 +00:00
moIndentation=""
if mo::standaloneCheck "$MO_STANDALONE_CONTENT"; then
2023-04-08 04:10:47 +00:00
moN=$'\n'
moR=$'\r'
moIndentation="$moN${MO_PARSED//"$moR"/"$moN"}"
moIndentation=${moIndentation##*"$moN"}
mo::debug "Adding indentation to partial: '$moIndentation'"
mo::standaloneProcess
fi
2023-04-22 01:59:31 +00:00
mo::debug "Parsing partial: $moFilename"
2023-04-22 01:59:31 +00:00
# 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
2023-04-22 01:59:31 +00:00
if ! mo::contentFile moPartialContent "${moFilename##*/}"; then
exit 1
fi
mo::indentLines moPartialContent "$moIndentation" "$moPartialContent"
2023-04-22 01:59:31 +00:00
# Delimiters are reset when loading a new partial
MO_OPEN_DELIMITER="{{"
MO_CLOSE_DELIMITER="}}"
MO_STANDALONE_CONTENT=""
mo::parse moPartialParsed "$moPartialContent"
2023-04-08 04:10:47 +00:00
2023-04-22 01:59:31 +00:00
# Fix bash handling of subshells and keep trailing whitespace.
echo -n "$moPartialParsed."
)" || exit 1
) || exit 1
2023-04-22 01:59:31 +00:00
if [[ -z "$moResult" ]]; then
mo::debug "Error detected when trying to read the file"
exit 1
fi
2023-04-22 01:59:31 +00:00
MO_PARSED="$MO_PARSED${moResult%.}"
}
# Internal: Handle parsing a comment
#
# Returns nothing
mo::parseComment() {
2023-04-22 01:59:31 +00:00
local moContent moContent
MO_UNPARSED=${MO_UNPARSED#*"$MO_CLOSE_DELIMITER"}
mo::debug "Parsing comment"
if mo::standaloneCheck "$MO_STANDALONE_CONTENT"; then
mo::standaloneProcess
fi
}
# Internal: Handle parsing the change of delimiters
#
# Returns nothing
mo::parseDelimiter() {
2023-04-22 01:59:31 +00:00
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 "$MO_STANDALONE_CONTENT"; then
mo::standaloneProcess
fi
MO_OPEN_DELIMITER="$moOpen"
MO_CLOSE_DELIMITER="$moClose"
}
# Internal: Handle parsing value or function call
#
2023-04-22 01:59:31 +00:00
# No arguments.
#
# Returns nothing
mo::parseValue() {
2023-04-22 01:59:31 +00:00
local moUnparsedOriginal moTokens
moUnparsedOriginal=$MO_UNPARSED
2023-04-22 01:59:31 +00:00
mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
mo::evaluate moResult "${moTokens[@]:1}"
2023-04-22 01:59:31 +00:00
MO_PARSED="$MO_PARSED$moResult"
if [[ "${MO_UNPARSED:0:${#MO_CLOSE_DELIMITER}}" != "$MO_CLOSE_DELIMITER" ]]; then
2023-04-22 01:59:31 +00:00
mo::errorNear "Did not find closing tag" "$moUnparsedOriginal"
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() {
if declare -F "$1" &> /dev/null; then
return 0
fi
return 1
}
# Internal: Determine if a given environment variable exists and if it is
# an array.
#
# $1 - Name of environment variable
#
# Be extremely careful. Even if strict mode is enabled, it is not honored
# in newer versions of Bash. Any errors that crop up here will not be
# caught automatically.
#
# Examples
#
# var=(abc)
# if moIsArray var; then
# echo "This is an array"
# echo "Make sure you don't accidentally use \$var"
# fi
#
# Returns 0 if the name is not empty, 1 otherwise.
mo::isArray() {
# Namespace this variable so we don't conflict with what we're testing.
local moTestResult
moTestResult=$(declare -p "$1" 2>/dev/null) || return 1
[[ "${moTestResult:0:10}" == "declare -a" ]] && return 0
[[ "${moTestResult:0:10}" == "declare -A" ]] && return 0
return 1
}
# Internal: Determine if an array index exists.
#
# $1 - Variable name to check
# $2 - The index to check
#
# Has to check if the variable is an array and if the index is valid for that
# type of array.
#
# Returns true (0) if everything was ok, 1 if there's any condition that fails.
mo::isArrayIndexValid() {
local moDeclare moTest
moDeclare=$(declare -p "$1")
moTest=""
if [[ "${moDeclare:0:10}" == "declare -a" ]]; then
# Numerically indexed array - must check if the index looks like a
# number because using a string to index a numerically indexed array
# will appear like it worked.
if [[ "$2" == "0" ]] || [[ "$2" =~ ^[1-9][0-9]*$ ]]; then
# Index looks like a number
eval "moTest=\"\${$1[$2]+ok}\""
fi
elif [[ "${moDeclare:0:10}" == "declare -A" ]]; then
# Associative array
eval "moTest=\"\${$1[$2]+ok}\""
fi
if [[ -n "$moTest" ]]; then
return 0;
fi
return 1
}
# Internal: Determine if a variable is assigned, even if it is assigned an empty
# value.
2015-10-02 14:46:57 +00:00
#
# $1 - Variable name to check.
#
2023-04-08 04:10:47 +00:00
# Can not use logic like this in case invalid variable names are passed.
# [[ "${!1-a}" == "${!1-b}" ]]
#
# Returns true (0) if the variable is set, 1 if the variable is unset.
mo::isVarSet() {
2023-04-08 04:10:47 +00:00
if ! declare -p "$1" &> /dev/null; then
return 1
fi
return 0
}
# 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
2023-04-08 04:10:47 +00:00
mo::debug "Value is falsy, test result: $moTruthy inverse: $2"
return 1
fi
2023-04-08 04:10:47 +00:00
mo::debug "Value is truthy, test result: $moTruthy inverse: $2"
return 0
}
2023-04-22 01:59:31 +00:00
# Internal: Convert token list to values
#
# $1 - Destination variable name
2023-04-22 01:59:31 +00:00
# $2-@ - Tokens to convert
#
# Sample call:
#
2023-04-22 01:59:31 +00:00
# mo::evaluate dest NAME username VALUE abc123 PAREN 2
2015-10-02 14:46:57 +00:00
#
# Returns nothing.
mo::evaluate() {
2023-04-22 02:18:55 +00:00
local moTarget moStack moValue moType moIndex moCombined moResult
moTarget=$1
2023-04-22 01:59:31 +00:00
shift
# Phase 1 - remove all command tokens (PAREN, BRACE)
moStack=()
while [[ $# -gt 0 ]]; do
case "$1" in
2023-04-22 02:18:55 +00:00
PAREN|BRACE)
2023-04-22 01:59:31 +00:00
moType=$1
moValue=$2
mo::debug "Combining $moValue tokens"
moIndex=$((${#moStack[@]} - (2 * moValue)))
2023-04-22 02:18:55 +00:00
mo::evaluateListOfSingles moCombined "${moStack[@]:$moIndex}"
2023-04-22 01:59:31 +00:00
if [[ "$moType" == "PAREN" ]]; then
moStack=("${moStack[@]:0:$moIndex}" NAME "$moCombined")
else
moStack=("${moStack[@]:0:$moIndex}" VALUE "$moCombined")
fi
;;
*)
moStack=("${moStack[@]}" "$1" "$2")
;;
esac
2023-04-22 01:59:31 +00:00
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
2023-04-22 01:59:31 +00:00
# Concatenate
mo::debug "Concatenating ${#moStack[@]} stack items"
mo::evaluateListOfSingles moResult "${moStack[@]}"
fi
local "$moTarget" && mo::indirect "$moTarget" "$moResult"
}
# Internal: Convert an argument list to individual values.
#
# $1 - Destination variable name
2023-04-22 01:59:31 +00:00
# $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:
2015-10-02 14:46:57 +00:00
#
# mo::evaluateListOfSingles dest NAME username VALUE abc123
#
2015-10-02 14:46:57 +00:00
# Returns nothing.
mo::evaluateListOfSingles() {
2023-04-22 01:59:31 +00:00
local moResult moTarget moTemp
moTarget=$1
2023-04-22 01:59:31 +00:00
shift
moResult=""
while [[ $# -gt 1 ]]; do
2023-04-22 01:59:31 +00:00
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
2015-01-23 17:43:08 +00:00
#
# $1 - Name of variable for result
2023-04-22 01:59:31 +00:00
# $2 - Type of argument, either NAME or VALUE
# $3 - Argument
2015-01-23 17:43:08 +00:00
#
# Returns nothing
mo::evaluateSingle() {
2023-04-22 01:59:31 +00:00
local moResult moType moArg
2015-01-23 17:43:08 +00:00
2023-04-22 01:59:31 +00:00
moType=$2
moArg=$3
mo::debug "Evaluating $moType: $moArg ($MO_CURRENT)"
if [[ "$moType" == "VALUE" ]]; then
moResult=$moArg
elif [[ "$moArg" == "." ]]; then
2023-04-22 01:59:31 +00:00
mo::evaluateVariable moResult ""
elif [[ "$moArg" == "@key" ]]; then
2023-04-22 01:59:31 +00:00
mo::evaluateKey moResult
elif mo::isFunction "$moArg"; then
mo::evaluateFunction moResult "" "$moArg"
2015-01-23 17:43:08 +00:00
else
2023-04-22 01:59:31 +00:00
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() {
2023-04-22 01:59:31 +00:00
local moResult
2023-04-22 01:59:31 +00:00
if [[ "$MO_CURRENT" == *.* ]]; then
moResult="${MO_CURRENT#*.}"
else
2023-04-22 01:59:31 +00:00
moResult="${MO_CURRENT}"
2015-01-23 17:43:08 +00:00
fi
local "$1" && mo::indirect "$1" "$moResult"
2015-01-23 17:43:08 +00:00
}
# Internal: Handle a variable name
#
# $1 - Destination variable name
# $2 - Variable name
#
# Returns nothing.
mo::evaluateVariable() {
2023-04-22 01:59:31 +00:00
local moResult moArg moNameParts
moArg=$2
moResult=""
2023-04-22 01:59:31 +00:00
mo::findVariableName moNameParts "$moArg"
mo::debug "Evaluate variable ($moArg, $MO_CURRENT): ${moNameParts[*]}"
if [[ -z "${moNameParts[1]}" ]]; then
2023-04-22 01:59:31 +00:00
if mo::isArray "${moNameParts[0]}"; then
eval mo::join moResult "," "\${${moNameParts[0]}[@]}"
else
2023-04-22 01:59:31 +00:00
if mo::isVarSet "${moNameParts[0]}"; then
moResult=${moNameParts[0]}
moResult="${!moResult}"
elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then
2023-04-22 01:59:31 +00:00
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"
}
2015-01-23 17:43:08 +00:00
# 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")
#
2023-04-22 01:59:31 +00:00
# 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
2023-04-22 01:59:31 +00:00
# '' c => c
# '' c.0 => c.0
# Returns nothing.
mo::findVariableName() {
2023-04-22 01:59:31 +00:00
local moVar moNameParts moResultBase moResultIndex moCurrent
moVar=$2
moResultBase=$moVar
moResultIndex=""
2023-04-22 01:59:31 +00:00
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#*.}
2023-04-22 01:59:31 +00:00
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
2015-10-02 14:46:57 +00:00
#
# $1 - Variable name to receive the joined content
# $2 - Joiner
# $3-@ - Elements to join
2015-01-23 17:43:08 +00:00
#
2015-10-02 14:46:57 +00:00
# Returns nothing.
2023-04-10 16:39:47 +00:00
mo::join() {
local joiner part result target
2015-01-23 17:43:08 +00:00
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
2023-04-22 01:59:31 +00:00
mo::evaluateSingle moTemp "$1" "$2"
moArgs=(${moArgs[@]+"${moArgs[@]}"} "$moTemp")
shift 2
2015-01-23 17:43:08 +00:00
done
2023-04-08 04:10:47 +00:00
mo::escape moFunctionCall "$moFunction"
if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then
2023-04-08 04:10:47 +00:00
mo::debug "Function arguments are allowed"
for moTemp in "${moArgs[@]}"; do
mo::escape moTemp "$moTemp"
moFunctionCall="$moFunctionCall $moTemp"
done
fi
2023-04-08 04:10:47 +00:00
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.
2023-04-22 01:59:31 +00:00
moContent=$(
export MO_FUNCTION_ARGS=("${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
2023-04-22 01:59:31 +00:00
# it on a line. There must be a new line before and there must be a newline
# after or the end of a string
#
# $1 - The content before the tag.
#
# 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=${1//"$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"
2023-04-09 00:06:04 +00:00
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"
2023-04-09 00:06:04 +00:00
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"
2023-04-09 00:06:04 +00:00
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 moContent moLast moT moR moN
moT=$'\t'
moR=$'\r'
moN=$'\n'
moLast=
mo::debug "Standalone tag - processing content before and after tag"
while [[ "$moLast" != "$MO_PARSED" ]]; do
moLast=$MO_PARSED
MO_PARSED=${MO_PARSED% }
MO_PARSED=${MO_PARSED%"$moT"}
done
moLast=
while [[ "$moLast" != "$MO_UNPARSED" ]]; do
moLast=$MO_UNPARSED
MO_UNPARSED=${MO_UNPARSED# }
MO_UNPARSED=${MO_UNPARSED#"$moT"}
done
MO_UNPARSED=${MO_UNPARSED#"$moR"}
MO_UNPARSED=${MO_UNPARSED#"$moN"}
}
# Internal: Apply indentation before any line that has content in MO_UNPARSED.
2023-04-08 04:10:47 +00:00
#
# $1 - Destination variable name.
# $2 - The indentation string.
# $3 - The content that needs the indentation string prepended on each line.
2023-04-08 04:10:47 +00:00
#
# 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"
fi
if [[ -z "$moContent" ]]; then
mo::debug "Not applying indentation, empty contents"
local "$1" && mo::indirect "$1" "$moContent"
fi
moResult=
2023-04-08 04:10:47 +00:00
moN=$'\n'
moR=$'\r'
mo::debug "Applying indentation: '${moIndentation}'"
2023-04-08 04:10:47 +00:00
while [[ -n "$moContent" ]]; do
moChunk=${moContent%%"$moN"*}
moChunk=${moChunk%%"$moR"*}
moContent=${moContent:${#moChunk}}
2023-04-08 04:10:47 +00:00
if [[ -n "$moChunk" ]]; then
moResult="$moResult$moIndentation$moChunk"
fi
2023-04-08 04:10:47 +00:00
moResult="$moResult${moContent:0:1}"
moContent=${moContent:1}
done
local "$1" && mo::indirect "$1" "$moResult"
2023-04-08 04:10:47 +00:00
}
# 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"
}
2023-04-22 01:59:31 +00:00
# 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 moStandaloneTemp
2023-04-22 01:59:31 +00:00
moTarget=$1
moTagStack=("$2")
mo::debug "Get content until close tag: ${moTagStack[0]}"
moResult=""
while [[ -n "$MO_UNPARSED" ]] && [[ "${#moTagStack[@]}" -gt 0 ]]; do
2023-04-22 01:59:31 +00:00
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
if mo::standaloneCheck "$moResult"; then
moResultTemp=$MO_PARSED
MO_PARSED=$moResult
mo::standaloneProcess
moResult=$MO_PARSED
MO_PARSED=$moResultTemp
fi
2023-04-22 01:59:31 +00:00
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}"
2023-04-22 01:59:31 +00:00
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.
2023-04-22 01:59:31 +00:00
#
# 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
2023-04-22 01:59:31 +00:00
#
# Returns nothing
mo::tokenizeTagContents() {
local moResult moTerminator moTemp moUnparsedOriginal moTokenCount
2023-04-22 01:59:31 +00:00
moTerminator=$2
moResult=()
moUnparsedOriginal=$MO_UNPARSED
moTokenCount=0
2023-04-22 01:59:31 +00:00
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[@]}"
2023-04-22 01:59:31 +00:00
return
;;
'('*)
2023-04-22 02:18:55 +00:00
# Do not tokenize the open paren - treat this as RPL
MO_UNPARSED=${MO_UNPARSED:1}
2023-04-22 01:59:31 +00:00
mo::tokenizeTagContents moTemp ')'
moResult=("${moResult[@]}" "${moTemp[@]:1}" PAREN "${moTemp[0]}")
2023-04-22 01:59:31 +00:00
MO_UNPARSED=${MO_UNPARSED:1}
;;
'{'*)
2023-04-22 02:18:55 +00:00
# Do not tokenize the open brace - treat this as RPL
MO_UNPARSED=${MO_UNPARSED:1}
2023-04-22 01:59:31 +00:00
mo::tokenizeTagContents moTemp '}'
moResult=("${moResult[@]}" "${moTemp[@]:1}" BRACE "${moTemp[0]}")
2023-04-22 01:59:31 +00:00
MO_UNPARSED=${MO_UNPARSED:1}
;;
')'* | '}'*)
mo::errorNear "Unbalanced closing parenthesis or brace" "$MO_UNPARSED"
;;
"'"*)
mo::tokenizeTagContentsSingleQuote moTemp
moResult=("${moResult[@]}" "${moTemp[@]}")
;;
'"'*)
mo::tokenizeTagContentsDoubleQuote moTemp
moResult=("${moResult[@]}" "${moTemp[@]}")
;;
*)
mo::tokenizeTagContentsName moTemp
moResult=("${moResult[@]}" "${moTemp[@]}")
;;
esac
mo::debug "Got chunk: ${moTemp[0]} ${moTemp[1]}"
moTokenCount=$((moTokenCount + 1))
2023-04-22 01:59:31 +00:00
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%%\}*}
2023-04-22 01:59:31 +00:00
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=
2023-04-22 01:59:31 +00:00
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"
2023-04-22 01:59:31 +00:00
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*)
2023-04-22 01:59:31 +00:00
moResult="$moResult"$'\n'
MO_UNPARSED=${MO_UNPARSED:2}
;;
\\r*)
2023-04-22 01:59:31 +00:00
moResult="$moResult"$'\r'
MO_UNPARSED=${MO_UNPARSED:2}
;;
\\t*)
2023-04-22 01:59:31 +00:00
moResult="$moResult"$'\t'
MO_UNPARSED=${MO_UNPARSED:2}
;;
\\v*)
moResult="$moResult"$'\v'
MO_UNPARSED=${MO_UNPARSED:2}
;;
2023-04-22 01:59:31 +00:00
\\*)
moResult="$moResult${MO_UNPARSED:1:1}"
MO_UNPARSED=${MO_UNPARSED:2}
;;
*)
moResult="$moResult${MO_UNPARSED:0:1}"
2023-04-22 01:59:31 +00:00
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=
2023-04-22 01:59:31 +00:00
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}"
2023-04-22 01:59:31 +00:00
MO_UNPARSED=${MO_UNPARSED:1}
;;
esac
done
}
# Save the original command's path for usage later
2016-10-25 11:11:33 +00:00
MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}"
MO_VERSION="3.0.0"
2015-10-02 14:46:57 +00:00
# If sourced, load all functions.
# If executed, perform the actions as expected.
if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
mo "$@"
fi