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