anything-scripts

Disposable scripts to fix day to day problems.
git clone git://soucy.cc/anything-scripts.git
Log | Files | Refs

mo (31429B)


      1 #!/usr/bin/env bash
      2 #
      3 #/ Mo is a mustache template rendering software written in bash.  It inserts
      4 #/ environment variables into templates.
      5 #/
      6 #/ Simply put, mo will change {{VARIABLE}} into the value of that
      7 #/ environment variable.  You can use {{#VARIABLE}}content{{/VARIABLE}} to
      8 #/ conditionally display content or iterate over the values of an array.
      9 #/
     10 #/ Learn more about mustache templates at https://mustache.github.io/
     11 #/
     12 #/ Simple usage:
     13 #/
     14 #/    mo [--false] [--help] [--source=FILE] filenames...
     15 #/
     16 #/ --fail-not-set - Fail upon expansion of an unset variable.
     17 #/ --false        - Treat the string "false" as empty for conditionals.
     18 #/ --help         - This message.
     19 #/ --source=FILE  - Load FILE into the environment before processing templates.
     20 #
     21 # Mo is under a MIT style licence with an additional non-advertising clause.
     22 # See LICENSE.md for the full text.
     23 #
     24 # This is open source!  Please feel free to contribute.
     25 #
     26 # https://github.com/tests-always-included/mo
     27 
     28 
     29 # Public: Template parser function.  Writes templates to stdout.
     30 #
     31 # $0             - Name of the mo file, used for getting the help message.
     32 # --allow-function-arguments
     33 #                - Permit functions in templates to be called with additional
     34 #                  arguments. This puts template data directly in to the path
     35 #                  of an eval statement. Use with caution.
     36 # --fail-not-set - Fail upon expansion of an unset variable.  Default behavior
     37 #                  is to silently ignore and expand into empty string.
     38 # --false        - Treat "false" as an empty value.  You may set the
     39 #                  MO_FALSE_IS_EMPTY environment variable instead to a non-empty
     40 #                  value to enable this behavior.
     41 # --help         - Display a help message.
     42 # --source=FILE  - Source a file into the environment before processint
     43 #                  template files.
     44 # --             - Used to indicate the end of options.  You may optionally
     45 #                  use this when filenames may start with two hyphens.
     46 # $@             - Filenames to parse.
     47 #
     48 # Mo uses the following environment variables:
     49 #
     50 # MO_ALLOW_FUNCTION_ARGUMENTS
     51 #                     - When set to a non-empty value, this allows functions
     52 #                       referenced in templates to receive additional
     53 #                       options and arguments. This puts the content from the
     54 #                       template directly into an eval statement. Use with
     55 #                       extreme care.
     56 # MO_FAIL_ON_UNSET    - When set to a non-empty value, expansion of an unset
     57 #                       env variable will be aborted with an error.
     58 # MO_FALSE_IS_EMPTY   - When set to a non-empty value, the string "false"
     59 #                       will be treated as an empty value for the purposes
     60 #                       of conditionals.
     61 # MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate
     62 #                       a help message.
     63 #
     64 # Returns nothing.
     65 mo() (
     66     # This function executes in a subshell so IFS is reset.
     67     # Namespace this variable so we don't conflict with desired values.
     68     local moContent f2source files doubleHyphens
     69 
     70     IFS=$' \n\t'
     71     files=()
     72     doubleHyphens=false
     73 
     74     if [[ $# -gt 0 ]]; then
     75         for arg in "$@"; do
     76             if $doubleHyphens; then
     77                 # After we encounter two hyphens together, all the rest
     78                 # of the arguments are files.
     79                 files=("${files[@]}" "$arg")
     80             else
     81                 case "$arg" in
     82                     -h|--h|--he|--hel|--help|-\?)
     83                         moUsage "$0"
     84                         exit 0
     85                         ;;
     86 
     87                     --allow-function-arguments)
     88                         # shellcheck disable=SC2030
     89                         MO_ALLOW_FUNCTION_ARGUMENTS=true
     90                         ;;
     91 
     92                     --fail-not-set)
     93                         # shellcheck disable=SC2030
     94                         MO_FAIL_ON_UNSET=true
     95                         ;;
     96 
     97                     --false)
     98                         # shellcheck disable=SC2030
     99                         MO_FALSE_IS_EMPTY=true
    100                         ;;
    101 
    102                     --source=*)
    103                         f2source="${arg#--source=}"
    104 
    105                         if [[ -f "$f2source" ]]; then
    106                             # shellcheck disable=SC1090
    107                             . "$f2source"
    108                         else
    109                             echo "No such file: $f2source" >&2
    110                             exit 1
    111                         fi
    112                         ;;
    113 
    114                     --)
    115                         # Set a flag indicating we've encountered double hyphens
    116                         doubleHyphens=true
    117                         ;;
    118 
    119                     *)
    120                         # Every arg that is not a flag or a option should be a file
    121                         files=(${files[@]+"${files[@]}"} "$arg")
    122                         ;;
    123                 esac
    124             fi
    125         done
    126     fi
    127 
    128     moGetContent moContent "${files[@]}" || return 1
    129     moParse "$moContent" "" true
    130 )
    131 
    132 
    133 # Internal: Call a function.
    134 #
    135 # $1 - Function to call
    136 # $2 - Content to pass
    137 # $3 - Additional arguments as a single string
    138 #
    139 # This can be dangerous, especially if you are using tags like
    140 # {{someFunction ; rm -rf / }}
    141 #
    142 # Returns nothing.
    143 moCallFunction() {
    144     local moArgs
    145 
    146     moArgs=()
    147 
    148     # shellcheck disable=SC2031
    149     if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then
    150         moArgs=$3
    151     fi
    152 
    153     echo -n "$2" | eval "$1" "$moArgs"
    154 }
    155 
    156 
    157 # Internal: Scan content until the right end tag is found.  Creates an array
    158 # with the following members:
    159 #
    160 #   [0] = Content before end tag
    161 #   [1] = End tag (complete tag)
    162 #   [2] = Content after end tag
    163 #
    164 # Everything using this function uses the "standalone tags" logic.
    165 #
    166 # $1 - Name of variable for the array
    167 # $2 - Content
    168 # $3 - Name of end tag
    169 # $4 - If -z, do standalone tag processing before finishing
    170 #
    171 # Returns nothing.
    172 moFindEndTag() {
    173     local content remaining scanned standaloneBytes tag
    174 
    175     #: Find open tags
    176     scanned=""
    177     moSplit content "$2" '{{' '}}'
    178 
    179     while [[ "${#content[@]}" -gt 1 ]]; do
    180         moTrimWhitespace tag "${content[1]}"
    181 
    182         #: Restore content[1] before we start using it
    183         content[1]='{{'"${content[1]}"'}}'
    184 
    185         case $tag in
    186             '#'* | '^'*)
    187                 #: Start another block
    188                 scanned="${scanned}${content[0]}${content[1]}"
    189                 moTrimWhitespace tag "${tag:1}"
    190                 moFindEndTag content "${content[2]}" "$tag" "loop"
    191                 scanned="${scanned}${content[0]}${content[1]}"
    192                 remaining=${content[2]}
    193                 ;;
    194 
    195             '/'*)
    196                 #: End a block - could be ours
    197                 moTrimWhitespace tag "${tag:1}"
    198                 scanned="$scanned${content[0]}"
    199 
    200                 if [[ "$tag" == "$3" ]]; then
    201                     #: Found our end tag
    202                     if [[ -z "${4-}" ]] && moIsStandalone standaloneBytes "$scanned" "${content[2]}" true; then
    203                         #: This is also a standalone tag - clean up whitespace
    204                         #: and move those whitespace bytes to the "tag" element
    205                         standaloneBytes=( $standaloneBytes )
    206                         content[1]="${scanned:${standaloneBytes[0]}}${content[1]}${content[2]:0:${standaloneBytes[1]}}"
    207                         scanned="${scanned:0:${standaloneBytes[0]}}"
    208                         content[2]="${content[2]:${standaloneBytes[1]}}"
    209                     fi
    210 
    211                     local "$1" && moIndirectArray "$1" "$scanned" "${content[1]}" "${content[2]}"
    212                     return 0
    213                 fi
    214 
    215                 scanned="$scanned${content[1]}"
    216                 remaining=${content[2]}
    217                 ;;
    218 
    219             *)
    220                 #: Ignore all other tags
    221                 scanned="${scanned}${content[0]}${content[1]}"
    222                 remaining=${content[2]}
    223                 ;;
    224         esac
    225 
    226         moSplit content "$remaining" '{{' '}}'
    227     done
    228 
    229     #: Did not find our closing tag
    230     scanned="$scanned${content[0]}"
    231     local "$1" && moIndirectArray "$1" "${scanned}" "" ""
    232 }
    233 
    234 
    235 # Internal: Find the first index of a substring.  If not found, sets the
    236 # index to -1.
    237 #
    238 # $1 - Destination variable for the index
    239 # $2 - Haystack
    240 # $3 - Needle
    241 #
    242 # Returns nothing.
    243 moFindString() {
    244     local pos string
    245 
    246     string=${2%%$3*}
    247     [[ "$string" == "$2" ]] && pos=-1 || pos=${#string}
    248     local "$1" && moIndirect "$1" "$pos"
    249 }
    250 
    251 
    252 # Internal: Generate a dotted name based on current context and target name.
    253 #
    254 # $1 - Target variable to store results
    255 # $2 - Context name
    256 # $3 - Desired variable name
    257 #
    258 # Returns nothing.
    259 moFullTagName() {
    260     if [[ -z "${2-}" ]] || [[ "$2" == *.* ]]; then
    261         local "$1" && moIndirect "$1" "$3"
    262     else
    263         local "$1" && moIndirect "$1" "${2}.${3}"
    264     fi
    265 }
    266 
    267 
    268 # Internal: Fetches the content to parse into a variable.  Can be a list of
    269 # partials for files or the content from stdin.
    270 #
    271 # $1   - Variable name to assign this content back as
    272 # $2-@ - File names (optional)
    273 #
    274 # Returns nothing.
    275 moGetContent() {
    276     local content filename target
    277 
    278     target=$1
    279     shift
    280     if [[ "${#@}" -gt 0 ]]; then
    281         content=""
    282 
    283         for filename in "$@"; do
    284             #: This is so relative paths work from inside template files
    285             content="$content"'{{>'"$filename"'}}'
    286         done
    287     else
    288         moLoadFile content /dev/stdin || return 1
    289     fi
    290 
    291     local "$target" && moIndirect "$target" "$content"
    292 }
    293 
    294 
    295 # Internal: Indent a string, placing the indent at the beginning of every
    296 # line that has any content.
    297 #
    298 # $1 - Name of destination variable to get an array of lines
    299 # $2 - The indent string
    300 # $3 - The string to reindent
    301 #
    302 # Returns nothing.
    303 moIndentLines() {
    304     local content fragment len posN posR result trimmed
    305 
    306     result=""
    307 
    308     #: Remove the period from the end of the string.
    309     len=$((${#3} - 1))
    310     content=${3:0:$len}
    311 
    312     if [[ -z "${2-}" ]]; then
    313         local "$1" && moIndirect "$1" "$content"
    314 
    315         return 0
    316     fi
    317 
    318     moFindString posN "$content" $'\n'
    319     moFindString posR "$content" $'\r'
    320 
    321     while [[ "$posN" -gt -1 ]] || [[ "$posR" -gt -1 ]]; do
    322         if [[ "$posN" -gt -1 ]]; then
    323             fragment="${content:0:$posN + 1}"
    324             content=${content:$posN + 1}
    325         else
    326             fragment="${content:0:$posR + 1}"
    327             content=${content:$posR + 1}
    328         fi
    329 
    330         moTrimChars trimmed "$fragment" false true " " $'\t' $'\n' $'\r'
    331 
    332         if [[ -n "$trimmed" ]]; then
    333             fragment="$2$fragment"
    334         fi
    335 
    336         result="$result$fragment"
    337 
    338         moFindString posN "$content" $'\n'
    339         moFindString posR "$content" $'\r'
    340 
    341         # If the content ends in a newline, do not indent.
    342         if [[ "$posN" -eq ${#content} ]]; then
    343             # Special clause for \r\n
    344             if [[ "$posR" -eq "$((posN - 1))" ]]; then
    345                 posR=-1
    346             fi
    347 
    348             posN=-1
    349         fi
    350 
    351         if [[ "$posR" -eq ${#content} ]]; then
    352             posR=-1
    353         fi
    354     done
    355 
    356     moTrimChars trimmed "$content" false true " " $'\t'
    357 
    358     if [[ -n "$trimmed" ]]; then
    359         content="$2$content"
    360     fi
    361 
    362     result="$result$content"
    363 
    364     local "$1" && moIndirect "$1" "$result"
    365 }
    366 
    367 
    368 # Internal: Send a variable up to the parent of the caller of this function.
    369 #
    370 # $1 - Variable name
    371 # $2 - Value
    372 #
    373 # Examples
    374 #
    375 #   callFunc () {
    376 #       local "$1" && moIndirect "$1" "the value"
    377 #   }
    378 #   callFunc dest
    379 #   echo "$dest"  # writes "the value"
    380 #
    381 # Returns nothing.
    382 moIndirect() {
    383     unset -v "$1"
    384     printf -v "$1" '%s' "$2"
    385 }
    386 
    387 
    388 # Internal: Send an array as a variable up to caller of a function
    389 #
    390 # $1   - Variable name
    391 # $2-@ - Array elements
    392 #
    393 # Examples
    394 #
    395 #   callFunc () {
    396 #       local myArray=(one two three)
    397 #       local "$1" && moIndirectArray "$1" "${myArray[@]}"
    398 #   }
    399 #   callFunc dest
    400 #   echo "${dest[@]}" # writes "one two three"
    401 #
    402 # Returns nothing.
    403 moIndirectArray() {
    404     unset -v "$1"
    405 
    406     # IFS must be set to a string containing space or unset in order for
    407     # the array slicing to work regardless of the current IFS setting on
    408     # bash 3.  This is detailed further at
    409     # https://github.com/fidian/gg-core/pull/7
    410     eval "$(printf "IFS= %s=(\"\${@:2}\") IFS=%q" "$1" "$IFS")"
    411 }
    412 
    413 
    414 # Internal: Determine if a given environment variable exists and if it is
    415 # an array.
    416 #
    417 # $1 - Name of environment variable
    418 #
    419 # Be extremely careful.  Even if strict mode is enabled, it is not honored
    420 # in newer versions of Bash.  Any errors that crop up here will not be
    421 # caught automatically.
    422 #
    423 # Examples
    424 #
    425 #   var=(abc)
    426 #   if moIsArray var; then
    427 #      echo "This is an array"
    428 #      echo "Make sure you don't accidentally use \$var"
    429 #   fi
    430 #
    431 # Returns 0 if the name is not empty, 1 otherwise.
    432 moIsArray() {
    433     # Namespace this variable so we don't conflict with what we're testing.
    434     local moTestResult
    435 
    436     moTestResult=$(declare -p "$1" 2>/dev/null) || return 1
    437     [[ "${moTestResult:0:10}" == "declare -a" ]] && return 0
    438     [[ "${moTestResult:0:10}" == "declare -A" ]] && return 0
    439 
    440     return 1
    441 }
    442 
    443 
    444 # Internal: Determine if the given name is a defined function.
    445 #
    446 # $1 - Function name to check
    447 #
    448 # Be extremely careful.  Even if strict mode is enabled, it is not honored
    449 # in newer versions of Bash.  Any errors that crop up here will not be
    450 # caught automatically.
    451 #
    452 # Examples
    453 #
    454 #   moo () {
    455 #       echo "This is a function"
    456 #   }
    457 #   if moIsFunction moo; then
    458 #       echo "moo is a defined function"
    459 #   fi
    460 #
    461 # Returns 0 if the name is a function, 1 otherwise.
    462 moIsFunction() {
    463     local functionList functionName
    464 
    465     functionList=$(declare -F)
    466     functionList=( ${functionList//declare -f /} )
    467 
    468     for functionName in "${functionList[@]}"; do
    469         if [[ "$functionName" == "$1" ]]; then
    470             return 0
    471         fi
    472     done
    473 
    474     return 1
    475 }
    476 
    477 
    478 # Internal: Determine if the tag is a standalone tag based on whitespace
    479 # before and after the tag.
    480 #
    481 # Passes back a string containing two numbers in the format "BEFORE AFTER"
    482 # like "27 10".  It indicates the number of bytes remaining in the "before"
    483 # string (27) and the number of bytes to trim in the "after" string (10).
    484 # Useful for string manipulation:
    485 #
    486 # $1 - Variable to set for passing data back
    487 # $2 - Content before the tag
    488 # $3 - Content after the tag
    489 # $4 - true/false: is this the beginning of the content?
    490 #
    491 # Examples
    492 #
    493 #   moIsStandalone RESULT "$before" "$after" false || return 0
    494 #   RESULT_ARRAY=( $RESULT )
    495 #   echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}"
    496 #
    497 # Returns nothing.
    498 moIsStandalone() {
    499     local afterTrimmed beforeTrimmed char
    500 
    501     moTrimChars beforeTrimmed "$2" false true " " $'\t'
    502     moTrimChars afterTrimmed "$3" true false " " $'\t'
    503     char=$((${#beforeTrimmed} - 1))
    504     char=${beforeTrimmed:$char}
    505 
    506     # If the content before didn't end in a newline
    507     if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]]; then
    508         # and there was content or this didn't start the file
    509         if [[ -n "$char" ]] || ! $4; then
    510             # then this is not a standalone tag.
    511             return 1
    512         fi
    513     fi
    514 
    515     char=${afterTrimmed:0:1}
    516 
    517     # If the content after doesn't start with a newline and it is something
    518     if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]] && [[ -n "$char" ]]; then
    519         # then this is not a standalone tag.
    520         return 2
    521     fi
    522 
    523     if [[ "$char" == $'\r' ]] && [[ "${afterTrimmed:1:1}" == $'\n' ]]; then
    524         char="$char"$'\n'
    525     fi
    526 
    527     local "$1" && moIndirect "$1" "$((${#beforeTrimmed})) $((${#3} + ${#char} - ${#afterTrimmed}))"
    528 }
    529 
    530 
    531 # Internal: Join / implode an array
    532 #
    533 # $1    - Variable name to receive the joined content
    534 # $2    - Joiner
    535 # $3-$* - Elements to join
    536 #
    537 # Returns nothing.
    538 moJoin() {
    539     local joiner part result target
    540 
    541     target=$1
    542     joiner=$2
    543     result=$3
    544     shift 3
    545 
    546     for part in "$@"; do
    547         result="$result$joiner$part"
    548     done
    549 
    550     local "$target" && moIndirect "$target" "$result"
    551 }
    552 
    553 
    554 # Internal: Read a file into a variable.
    555 #
    556 # $1 - Variable name to receive the file's content
    557 # $2 - Filename to load
    558 #
    559 # Returns nothing.
    560 moLoadFile() {
    561     local content len
    562 
    563     # The subshell removes any trailing newlines.  We forcibly add
    564     # a dot to the content to preserve all newlines.
    565     # As a future optimization, it would be worth considering removing
    566     # cat and replacing this with a read loop.
    567 
    568     content=$(cat -- "$2" && echo '.') || return 1
    569     len=$((${#content} - 1))
    570     content=${content:0:$len}  # Remove last dot
    571 
    572     local "$1" && moIndirect "$1" "$content"
    573 }
    574 
    575 
    576 # Internal: Process a chunk of content some number of times.  Writes output
    577 # to stdout.
    578 #
    579 # $1   - Content to parse repeatedly
    580 # $2   - Tag prefix (context name)
    581 # $3-@ - Names to insert into the parsed content
    582 #
    583 # Returns nothing.
    584 moLoop() {
    585     local content context contextBase
    586 
    587     content=$1
    588     contextBase=$2
    589     shift 2
    590 
    591     while [[ "${#@}" -gt 0 ]]; do
    592         moFullTagName context "$contextBase" "$1"
    593         moParse "$content" "$context" false
    594         shift
    595     done
    596 }
    597 
    598 
    599 # Internal: Parse a block of text, writing the result to stdout.
    600 #
    601 # $1 - Block of text to change
    602 # $2 - Current name (the variable NAME for what {{.}} means)
    603 # $3 - true when no content before this, false otherwise
    604 #
    605 # Returns nothing.
    606 moParse() {
    607     # Keep naming variables mo* here to not overwrite needed variables
    608     # used in the string replacements
    609     local moArgs moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag
    610 
    611     moCurrent=$2
    612     moIsBeginning=$3
    613 
    614     # Find open tags
    615     moSplit moContent "$1" '{{' '}}'
    616 
    617     while [[ "${#moContent[@]}" -gt 1 ]]; do
    618         moTrimWhitespace moTag "${moContent[1]}"
    619         moNextIsBeginning=false
    620 
    621         case $moTag in
    622             '#'*)
    623                 # Loop, if/then, or pass content through function
    624                 # Sets context
    625                 moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
    626                 moTrimWhitespace moTag "${moTag:1}"
    627                 
    628                 # Split arguments from the tag name. Arguments are passed to
    629                 # functions.
    630                 moArgs=$moTag
    631                 moTag=${moTag%% *}
    632                 moTag=${moTag%%$'\t'*}
    633                 moArgs=${moArgs:${#moTag}}
    634                 moFindEndTag moBlock "$moContent" "$moTag"
    635                 moFullTagName moTag "$moCurrent" "$moTag"
    636 
    637                 if moTest "$moTag"; then
    638                     # Show / loop / pass through function
    639                     if moIsFunction "$moTag"; then
    640                         #: Consider piping the output to moGetContent
    641                         #: so the lambda does not execute in a subshell?
    642                         moContent=$(moCallFunction "$moTag" "${moBlock[0]}" "$moArgs")
    643                         moParse "$moContent" "$moCurrent" false
    644                         moContent="${moBlock[2]}"
    645                     elif moIsArray "$moTag"; then
    646                         eval "moLoop \"\${moBlock[0]}\" \"$moTag\" \"\${!${moTag}[@]}\""
    647                     else
    648                         moParse "${moBlock[0]}" "$moCurrent" true
    649                     fi
    650                 fi
    651 
    652                 moContent="${moBlock[2]}"
    653                 ;;
    654 
    655             '>'*)
    656                 # Load partial - get name of file relative to cwd
    657                 moPartial moContent "${moContent[@]}" "$moIsBeginning" "$moCurrent"
    658                 moNextIsBeginning=${moContent[1]}
    659                 moContent=${moContent[0]}
    660                 ;;
    661 
    662             '/'*)
    663                 # Closing tag - If hit in this loop, we simply ignore
    664                 # Matching tags are found in moFindEndTag
    665                 moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
    666                 ;;
    667 
    668             '^'*)
    669                 # Display section if named thing does not exist
    670                 moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
    671                 moTrimWhitespace moTag "${moTag:1}"
    672                 moFindEndTag moBlock "$moContent" "$moTag"
    673                 moFullTagName moTag "$moCurrent" "$moTag"
    674 
    675                 if ! moTest "$moTag"; then
    676                     moParse "${moBlock[0]}" "$moCurrent" false "$moCurrent"
    677                 fi
    678 
    679                 moContent="${moBlock[2]}"
    680                 ;;
    681 
    682             '!'*)
    683                 # Comment - ignore the tag content entirely
    684                 # Trim spaces/tabs before the comment
    685                 moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
    686                 ;;
    687 
    688             .)
    689                 # Current content (environment variable or function)
    690                 moStandaloneDenied moContent "${moContent[@]}"
    691                 moShow "$moCurrent" "$moCurrent"
    692                 ;;
    693 
    694             '=')
    695                 # Change delimiters
    696                 # Any two non-whitespace sequences separated by whitespace.
    697                 # This tag is ignored.
    698                 moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
    699                 ;;
    700 
    701             '{'*)
    702                 # Unescaped - split on }}} not }}
    703                 moStandaloneDenied moContent "${moContent[@]}"
    704                 moContent="${moTag:1}"'}}'"$moContent"
    705                 moSplit moContent "$moContent" '}}}'
    706                 moTrimWhitespace moTag "${moContent[0]}"
    707                 moArgs=$moTag
    708                 moTag=${moTag%% *}
    709                 moTag=${moTag%%$'\t'*}
    710                 moArgs=${moArgs:${#moTag}}
    711                 moFullTagName moTag "$moCurrent" "$moTag"
    712                 moContent=${moContent[1]}
    713 
    714                 # Now show the value
    715                 # Quote moArgs here, do not quote it later.
    716                 moShow "$moTag" "$moCurrent" "$moArgs"
    717                 ;;
    718 
    719             '&'*)
    720                 # Unescaped
    721                 moStandaloneDenied moContent "${moContent[@]}"
    722                 moTrimWhitespace moTag "${moTag:1}"
    723                 moFullTagName moTag "$moCurrent" "$moTag"
    724                 moShow "$moTag" "$moCurrent"
    725                 ;;
    726 
    727             *)
    728                 # Normal environment variable or function call
    729                 moStandaloneDenied moContent "${moContent[@]}"
    730                 moArgs=$moTag
    731                 moTag=${moTag%% *}
    732                 moTag=${moTag%%$'\t'*}
    733                 moArgs=${moArgs:${#moTag}}
    734                 moFullTagName moTag "$moCurrent" "$moTag"
    735 
    736                 # Quote moArgs here, do not quote it later.
    737                 moShow "$moTag" "$moCurrent" "$moArgs"
    738                 ;;
    739         esac
    740 
    741         moIsBeginning=$moNextIsBeginning
    742         moSplit moContent "$moContent" '{{' '}}'
    743     done
    744 
    745     echo -n "${moContent[0]}"
    746 }
    747 
    748 
    749 # Internal: Process a partial.
    750 #
    751 # Indentation should be applied to the entire partial.
    752 #
    753 # This sends back the "is beginning" flag because the newline after a
    754 # standalone partial is consumed. That newline is very important in the middle
    755 # of content. We send back this flag to reset the processing loop's
    756 # `moIsBeginning` variable, so the software thinks we are back at the
    757 # beginning of a file and standalone processing continues to work.
    758 #
    759 # Prefix all variables.
    760 #
    761 # $1 - Name of destination variable. Element [0] is the content, [1] is the
    762 #      true/false flag indicating if we are at the beginning of content.
    763 # $2 - Content before the tag that was not yet written
    764 # $3 - Tag content
    765 # $4 - Content after the tag
    766 # $5 - true/false: is this the beginning of the content?
    767 # $6 - Current context name
    768 #
    769 # Returns nothing.
    770 moPartial() {
    771     # Namespace variables here to prevent conflicts.
    772     local moContent moFilename moIndent moIsBeginning moPartial moStandalone moUnindented
    773 
    774     if moIsStandalone moStandalone "$2" "$4" "$5"; then
    775         moStandalone=( $moStandalone )
    776         echo -n "${2:0:${moStandalone[0]}}"
    777         moIndent=${2:${moStandalone[0]}}
    778         moContent=${4:${moStandalone[1]}}
    779         moIsBeginning=true
    780     else
    781         moIndent=""
    782         echo -n "$2"
    783         moContent=$4
    784         moIsBeginning=$5
    785     fi
    786 
    787     moTrimWhitespace moFilename "${3:1}"
    788 
    789     # Execute in subshell to preserve current cwd and environment
    790     (
    791         # It would be nice to remove `dirname` and use a function instead,
    792         # but that's difficult when you're only given filenames.
    793         cd "$(dirname -- "$moFilename")" || exit 1
    794         moUnindented="$(
    795             moLoadFile moPartial "${moFilename##*/}" || exit 1
    796             moParse "${moPartial}" "$6" true
    797 
    798             # Fix bash handling of subshells and keep trailing whitespace.
    799             # This is removed in moIndentLines.
    800             echo -n "."
    801         )" || exit 1
    802         moIndentLines moPartial "$moIndent" "$moUnindented"
    803         echo -n "$moPartial"
    804     ) || exit 1
    805 
    806     # If this is a standalone tag, the trailing newline after the tag is
    807     # removed and the contents of the partial are added, which typically
    808     # contain a newline. We need to send a signal back to the processing
    809     # loop that the moIsBeginning flag needs to be turned on again.
    810     #
    811     # [0] is the content, [1] is that flag.
    812     local "$1" && moIndirectArray "$1" "$moContent" "$moIsBeginning"
    813 }
    814 
    815 
    816 # Internal: Show an environment variable or the output of a function to
    817 # stdout.
    818 #
    819 # Limit/prefix any variables used.
    820 #
    821 # $1 - Name of environment variable or function
    822 # $2 - Current context
    823 # $3 - Arguments string if $1 is a function
    824 #
    825 # Returns nothing.
    826 moShow() {
    827     # Namespace these variables
    828     local moJoined moNameParts
    829 
    830     if moIsFunction "$1"; then
    831         CONTENT=$(moCallFunction "$1" "" "$3")
    832         moParse "$CONTENT" "$2" false
    833         return 0
    834     fi
    835 
    836     moSplit moNameParts "$1" "."
    837 
    838     if [[ -z "${moNameParts[1]-}" ]]; then
    839         if moIsArray "$1"; then
    840             eval moJoin moJoined "," "\${$1[@]}"
    841             echo -n "$moJoined"
    842         else
    843             # shellcheck disable=SC2031
    844             if moTestVarSet "$1"; then
    845                 echo -n "${!1}"
    846             elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then
    847                 echo "Env variable not set: $1" >&2
    848                 exit 1
    849             fi
    850         fi
    851     else
    852         # Further subindexes are disallowed
    853         eval "echo -n \"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\""
    854     fi
    855 }
    856 
    857 
    858 # Internal: Split a larger string into an array.
    859 #
    860 # $1 - Destination variable
    861 # $2 - String to split
    862 # $3 - Starting delimiter
    863 # $4 - Ending delimiter (optional)
    864 #
    865 # Returns nothing.
    866 moSplit() {
    867     local pos result
    868 
    869     result=( "$2" )
    870     moFindString pos "${result[0]}" "$3"
    871 
    872     if [[ "$pos" -ne -1 ]]; then
    873         # The first delimiter was found
    874         result[1]=${result[0]:$pos + ${#3}}
    875         result[0]=${result[0]:0:$pos}
    876 
    877         if [[ -n "${4-}" ]]; then
    878             moFindString pos "${result[1]}" "$4"
    879 
    880             if [[ "$pos" -ne -1 ]]; then
    881                 # The second delimiter was found
    882                 result[2]="${result[1]:$pos + ${#4}}"
    883                 result[1]="${result[1]:0:$pos}"
    884             fi
    885         fi
    886     fi
    887 
    888     local "$1" && moIndirectArray "$1" "${result[@]}"
    889 }
    890 
    891 
    892 # Internal: Handle the content for a standalone tag.  This means removing
    893 # whitespace (not newlines) before a tag and whitespace and a newline after
    894 # a tag.  That is, assuming, that the line is otherwise empty.
    895 #
    896 # $1 - Name of destination "content" variable.
    897 # $2 - Content before the tag that was not yet written
    898 # $3 - Tag content (not used)
    899 # $4 - Content after the tag
    900 # $5 - true/false: is this the beginning of the content?
    901 #
    902 # Returns nothing.
    903 moStandaloneAllowed() {
    904     local bytes
    905 
    906     if moIsStandalone bytes "$2" "$4" "$5"; then
    907         bytes=( $bytes )
    908         echo -n "${2:0:${bytes[0]}}"
    909         local "$1" && moIndirect "$1" "${4:${bytes[1]}}"
    910     else
    911         echo -n "$2"
    912         local "$1" && moIndirect "$1" "$4"
    913     fi
    914 }
    915 
    916 
    917 # Internal: Handle the content for a tag that is never "standalone".  No
    918 # adjustments are made for newlines and whitespace.
    919 #
    920 # $1 - Name of destination "content" variable.
    921 # $2 - Content before the tag that was not yet written
    922 # $3 - Tag content (not used)
    923 # $4 - Content after the tag
    924 #
    925 # Returns nothing.
    926 moStandaloneDenied() {
    927     echo -n "$2"
    928     local "$1" && moIndirect "$1" "$4"
    929 }
    930 
    931 
    932 # Internal: Determines if the named thing is a function or if it is a
    933 # non-empty environment variable.  When MO_FALSE_IS_EMPTY is set to a
    934 # non-empty value, then "false" is also treated is an empty value.
    935 #
    936 # Do not use variables without prefixes here if possible as this needs to
    937 # check if any name exists in the environment
    938 #
    939 # $1                - Name of environment variable or function
    940 # $2                - Current value (our context)
    941 # MO_FALSE_IS_EMPTY - When set to a non-empty value, this will say the
    942 #                     string value "false" is empty.
    943 #
    944 # Returns 0 if the name is not empty, 1 otherwise.  When MO_FALSE_IS_EMPTY
    945 # is set, this returns 1 if the name is "false".
    946 moTest() {
    947     # Test for functions
    948     moIsFunction "$1" && return 0
    949 
    950     if moIsArray "$1"; then
    951         # Arrays must have at least 1 element
    952         eval "[[ \"\${#${1}[@]}\" -gt 0 ]]" && return 0
    953     else
    954         # If MO_FALSE_IS_EMPTY is set, then return 1 if the value of
    955         # the variable is "false".
    956         # shellcheck disable=SC2031
    957         [[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${!1-}" == "false" ]] && return 1
    958 
    959         # Environment variables must not be empty
    960         [[ -n "${!1-}" ]] && return 0
    961     fi
    962 
    963     return 1
    964 }
    965 
    966 # Internal: Determine if a variable is assigned, even if it is assigned an empty
    967 # value.
    968 #
    969 # $1 - Variable name to check.
    970 #
    971 # Returns true (0) if the variable is set, 1 if the variable is unset.
    972 moTestVarSet() {
    973     [[ "${!1-a}" == "${!1-b}" ]]
    974 }
    975 
    976 
    977 # Internal: Trim the leading whitespace only.
    978 #
    979 # $1   - Name of destination variable
    980 # $2   - The string
    981 # $3   - true/false - trim front?
    982 # $4   - true/false - trim end?
    983 # $5-@ - Characters to trim
    984 #
    985 # Returns nothing.
    986 moTrimChars() {
    987     local back current front last target varName
    988 
    989     target=$1
    990     current=$2
    991     front=$3
    992     back=$4
    993     last=""
    994     shift 4 # Remove target, string, trim front flag, trim end flag
    995 
    996     while [[ "$current" != "$last" ]]; do
    997         last=$current
    998 
    999         for varName in "$@"; do
   1000             $front && current="${current/#$varName}"
   1001             $back && current="${current/%$varName}"
   1002         done
   1003     done
   1004 
   1005     local "$target" && moIndirect "$target" "$current"
   1006 }
   1007 
   1008 
   1009 # Internal: Trim leading and trailing whitespace from a string.
   1010 #
   1011 # $1 - Name of variable to store trimmed string
   1012 # $2 - The string
   1013 #
   1014 # Returns nothing.
   1015 moTrimWhitespace() {
   1016     local result
   1017 
   1018     moTrimChars result "$2" true true $'\r' $'\n' $'\t' " "
   1019     local "$1" && moIndirect "$1" "$result"
   1020 }
   1021 
   1022 
   1023 # Internal: Displays the usage for mo.  Pulls this from the file that
   1024 # contained the `mo` function.  Can only work when the right filename
   1025 # comes is the one argument, and that only happens when `mo` is called
   1026 # with `$0` set to this file.
   1027 #
   1028 # $1 - Filename that has the help message
   1029 #
   1030 # Returns nothing.
   1031 moUsage() {
   1032     grep '^#/' "${MO_ORIGINAL_COMMAND}" | cut -c 4-
   1033     echo ""
   1034     set | grep ^MO_VERSION=
   1035 }
   1036 
   1037 
   1038 # Save the original command's path for usage later
   1039 MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}"
   1040 MO_VERSION="2.0.4"
   1041 
   1042 # If sourced, load all functions.
   1043 # If executed, perform the actions as expected.
   1044 if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
   1045     mo "$@"
   1046 fi