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