diff --git a/modules/scripting/bash-tpl.lib b/modules/scripting/bash-tpl.lib new file mode 100644 index 0000000..4e9bf11 --- /dev/null +++ b/modules/scripting/bash-tpl.lib @@ -0,0 +1,1298 @@ +#!/usr/bin/env bash +####################################################################### +# SPDX-License-Identifier: MIT +# Copyright (c) 2021 TekWizely & co-authors +# +# Use of this source code is governed by the MIT license. +# See the accompanying LICENSE file, if present, or visit: +# https://opensource.org/licenses/MIT +####################################################################### +VERSION="v0.7.1" +####################################################################### +# Bash-TPL: A Smart, Lightweight shell script templating engine +# +# Lets you mark up textual files with shell commands and variable +# replacements, while minimally impacting your original file layout. +# +# Templates are compiled into shell scripts that you can invoke +# (along with variables, arguments, etc.) to generate complete and +# well-formatted output text files. +# +# Smart +# +# Encourages you to use extra indentation to write clean, well- +# formatted templates, and smartly removes the indentations from the +# generated template scripts. +# +# This results in both templates that are easily readable and +# maintainable, and generated text files that look as good as if they +# were written by hand. +# +# NOTE: Consistent Formatting +# +# The key to success with Bash-TPL indentation fix-up logic is +# Consistent Formatting; using consistent indentation throughout your +# template will yield best results. +# +# Learn More: +# https://github.com/TekWizely/bash-tpl +####################################################################### + +function usage() { + cat << USAGE +Bash-TPL is a smart, lightweight shell script templating engine + +usage: bash-tpl [flags] [--] file + bash-tpl [flags] - + cat file | bash-tpl [flags] + +options: + -h, --help + show help screen + --version + show version + -o, --output-file + write to specified file (default: stdout) + -- treat remaining options as positional arguments + - read from stdin + +customize delimiters: + --tag-delims 'xx xx' + set tag delimiters (default: '<% %>') + --tag-stmt-delim 'x' + set tag statement delimiter (default: '%') + --stmt-delim 'x+' + set statement delimiter (default: '%') + --stmt-block-delims 'x+ x+' + set statement block delimiters + defaults to statement delimiter if not explicitly set + --txt-delim + --text-delim 'x+[ ]?' (single trailing space allowed) + set text delimiter (default: '% ') + --dir-delim + --directive-delim 'x+' + set directive delimiter (default: '.') + --cmt-delim + --comment-delim 'x+' + set template comment delimiter + defaults to directive delimiter + '#' if not explicitly set + --reset-delims + reset all delimiters to defaults + delim options provided after this option are honored + +supported environment variables: + BASH_TPL_TAG_DELIMS + BASH_TPL_TAG_STMT_DELIM + BASH_TPL_STMT_DELIM + BASH_TPL_STMT_BLOCK_DELIMS + BASH_TPL_TEXT_DELIM + BASH_TPL_DIR_DELIM + BASH_TPL_CMT_DELIM + +example: + $ echo 'Hello <% \$NAME %>' > test.tpl + $ NAME="Chuck Norris" source <( bash-tpl test.tpl ) + + Hello Chuck Norris + +learn more: https://github.com/TekWizely/bash-tpl + +USAGE +} + +####################################################################### +# Delim Functions +####################################################################### + +TAG_DELIM_REGEX='^([^[:blank:]])([^[:blank:]]) ([^[:blank:]])([^[:blank:]])$' +TAG_STMT_DELIM_REGEX='^([^[:blank:]])$' +STMT_DELIM_REGEX='^([^[:blank:]]+)$' +STMT_BLOCK_DELIM_REGEX='^([^[:blank:]]+) ([^[:blank:]]+)$' +STMT_BLOCK_TEXT_REGEX='^([^[:blank:]]+\ ?)$' # Optional trailing ' ' + +## +# reset_delims +# +function reset_delims() { + TAG_START_DELIM1='<' + TAG_START_DELIM2='%' + TAG_STOP_DELIM1='%' + TAG_STOP_DELIM2='>' + + TAG_STMT_DELIM='%' + + STMT_DELIM='%' + + TEXT_DELIM_UNDEFINED=1 + TEXT_DELIM='' + + STMT_BLOCK_DELIM_UNDEFINED=1 + STMT_BLOCK_START_DELIM='' + STMT_BLOCK_STOP_DELIM='' + + DIRECTIVE_DELIM='.' + + COMMENT_DELIM_UNDEFINED=1 + COMMENT_DELIM='' +} + +## +# parse_tag_delims +# $1 = delims +# $2 = src (for error msg) +# +function parse_tag_delims() { + if [[ "${1}" =~ $TAG_DELIM_REGEX ]]; then + TAG_START_DELIM1="${BASH_REMATCH[1]}" + TAG_START_DELIM2="${BASH_REMATCH[2]}" + TAG_STOP_DELIM1="${BASH_REMATCH[3]}" + TAG_STOP_DELIM2="${BASH_REMATCH[4]}" + else + echo "Error: Invalid or missing tag delimiter values for ${2-tag delims}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_tag_stmt_delims +# $1 = delim +# $2 = src (for error msg) +# +function parse_tag_stmt_delim() { + if [[ "${1}" =~ $TAG_STMT_DELIM_REGEX ]]; then + TAG_STMT_DELIM="${BASH_REMATCH[1]}" + else + echo "Error: Invalid or missing tag stmt delimiter value for ${2-tag stmt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_stmt_delim +# $1 = delim +# $2 = src (for error msg) +# +function parse_stmt_delim() { + if [[ "${1}" =~ $STMT_DELIM_REGEX ]]; then + STMT_DELIM="${BASH_REMATCH[1]}" + else + echo "Error: Invalid or missing stmt delimiter value for ${2:-stmt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_stmt_block_delims +# $1 = delims +# $2 = src (for error msg) +# +function parse_stmt_block_delims() { + if [[ "${1}" =~ $STMT_BLOCK_DELIM_REGEX ]]; then + STMT_BLOCK_START_DELIM="${BASH_REMATCH[1]}" + STMT_BLOCK_STOP_DELIM="${BASH_REMATCH[2]}" + STMT_BLOCK_DELIM_UNDEFINED='' + else + echo "Error: Invalid or missing stmt-block delimiter values for ${2:-stmt-block delims}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_text_delim - Uses STMT delim regex +# $1 = delim +# $2 = src (for error msg) +# +function parse_text_delim() { + if [[ "${1}" =~ $STMT_BLOCK_TEXT_REGEX ]]; then + TEXT_DELIM="${1}" + TEXT_DELIM_UNDEFINED='' + else + echo "Error: Invalid or missing text delimiter value for ${2:-txt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_directive_delim - Uses STMT delim regex +# $1 = delim +# $2 = src (for error msg) +# +function parse_directive_delim() { + if [[ "${1}" =~ $STMT_DELIM_REGEX ]]; then + DIRECTIVE_DELIM="${1}" + else + echo "Error: Invalid or missing directive delimiter value for ${2:-dir delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# parse_comment_delim - Uses STMT delim regex +# $1 = delim +# $2 = src (for error msg) +# +function parse_comment_delim() { + if [[ "${1}" =~ $STMT_DELIM_REGEX ]]; then + COMMENT_DELIM="${1}" + COMMENT_DELIM_UNDEFINED='' + else + echo "Error: Invalid or missing comment delimiter value for ${2:-cmt delim}: '${1}'" >&2 + exit 1 + fi +} + +## +# reset_template_regexes +# +function reset_template_regexes() { + # Fixup STMT_BLOCK delims - Default to STMT_DELIM if not set + # + if [[ -n "${STMT_BLOCK_DELIM_UNDEFINED}" ]]; then + STMT_BLOCK_START_DELIM="${STMT_DELIM}" + STMT_BLOCK_STOP_DELIM="${STMT_DELIM}" + fi + + # Fixup TEXT delim - Default to STMT_DELIM followed by ' ' if not set + # + if [[ -n "${TEXT_DELIM_UNDEFINED}" ]]; then + TEXT_DELIM="${STMT_DELIM} " # Note trailing space (' ') + fi + + # Fixup COMMENT delim - Default to STMT_DELIM followed by '#' if not set + # + if [[ -n "${COMMENT_DELIM_UNDEFINED}" ]]; then + COMMENT_DELIM="${STMT_DELIM}#" + fi + + # + # Create regexes + # + + local d ds d1 d2 d3 d4 + + d="${DIRECTIVE_DELIM}" + escape_regex d + DIRECTIVE_REGEX="^([[:blank:]]*)${d}([a-zA-Z_-]+)(.*)\$" + + d="${COMMENT_DELIM}" + escape_regex d + COMMENT_REGEX="^([[:blank:]]*)${d}" + + d="${STMT_DELIM}" + escape_regex d + STATEMENT_REGEX="^([[:blank:]]*)${d}[[:blank:]]+(.+)\$" + + d="${STMT_BLOCK_START_DELIM}" + escape_regex d + STATEMENT_BLOCK_START_REGEX="^([[:blank:]]*)${d}[[:blank:]]*\$" + + d="${STMT_BLOCK_STOP_DELIM}" + escape_regex d + STATEMENT_BLOCK_STOP_REGEX="^([[:blank:]]*)${d}[[:blank:]]*\$" + + d="${TEXT_DELIM}" + escape_regex d + STATEMENT_BLOCK_TEXT_REGEX="^([[:blank:]]*)${d}([[:blank:]]*[^[:blank:]](.*))\$" + + TEXT_REGEX='^([[:blank:]]*)([^[:blank:]](.*))?$' + + d1="${TAG_START_DELIM1}" + escape_regex d1 + + d2="${TAG_START_DELIM2}" + escape_regex d2 + + d3="${TAG_STOP_DELIM1}" + escape_regex d3 + + d4="${TAG_STOP_DELIM2}" + escape_regex d4 + + ds="${TAG_STMT_DELIM}" + escape_regex ds + + TAG_TEXT_REGEX="^([^${d1}]+|${d1}$|${d1}[^${d1}${d2}]+)(.*)" + + TAG_STD_REGEX="^${d1}${d2}((([^${d3}])|(${d3}[^${d4}]))*)${d3}${d4}(.*)" + + TAG_QUOTE_REGEX="^${d1}${d2}\"((([^\"])|(\"[^${d3}])|(\"${d3}[^${d4}]))*)\"${d3}${d4}(.*)" + + TAG_STATEMENT_REGEX="^${d1}${d2}${ds}((([^${d3}])|(${d3}[^${d4}]))*)${d3}${d4}(.*)" + + # printf "# ---> delim regexes:" + # printf "# STATEMENT_BLOCK_START_REGEX: '%s'\n" "${STATEMENT_BLOCK_START_REGEX}" + # printf "# STATEMENT_BLOCK_STOP_REGEX: '%s'\n" "${STATEMENT_BLOCK_STOP_REGEX}" + # printf "# COMMENT_REGEX: '%s'\n" "${COMMENT_REGEX}" + # printf "# DIRECTIVE_REGEX: '%s'\n" "${DIRECTIVE_REGEX}" + # printf "# STATEMENT_REGEX: '%s'\n" "${STATEMENT_REGEX}" + # printf "# TAG_TEXT_REGEX: '%s'\n" "${TAG_TEXT_REGEX}" + # printf "# TAG_STD_REGEX: '%s'\n" "${TAG_STD_REGEX}" + # printf "# TAG_QUOTE_REGEX: '%s'\n" "${TAG_QUOTE_REGEX}" + # printf "# TAG_STATEMENT_REGEX: '%s'\n" "${TAG_STATEMENT_REGEX}" +} + +####################################################################### +# Misc Functions +####################################################################### + +## +# trim +# usage: trim varname +# NOTE: Expects value to NOT contain '\n' +# +function trim() { + read -r "$1" <<< "${!1}"$'\n' +} + +## +# escape_regex +# usage: escape_regex varname +# +function escape_regex() { + local result + # shellcheck disable=SC2001 # Too complex for ${variable//search/replace} + # shellcheck disable=SC2016 # Not using expansion, prefer single quotes + # shellcheck disable=SC2034 # ref is used + result=$(sed 's/[][\.|$(){}?+*^]/\\&/g' <<< "${!1}") + printf -v "${1}" "%s" "${result}" +} + +## +# normalize_directive +# usage: normalize_directive varname +# +function normalize_directive() { + local result + # shellcheck disable=SC2034 # ref is used + result=$(tr 'a-z_' 'A-Z-' <<< "${!1}") + printf -v "${1}" "%s" "${result}" +} + +####################################################################### +# STATES +####################################################################### + +STATES=() # empty => DEFAULT +STATE="DEFAULT" # [ DEFAULT, MAYBE_TXT_BLOCK, TXT_BLOCK, START_STMT_BLOCK, STMT_BLOCK ] + +## +# push_state +# +function push_state() { + STATES+=("${STATE}") + STATE="${1}" +} + +## +# pop_state +# +function pop_state() { + if [[ ${#STATES[@]} -gt 0 ]]; then + STATE="${STATES[${#STATES[@]} - 1]}" + unset "STATES[${#STATES[@]}-1]" + else + STATE="DEFAULT" + fi +} + +####################################################################### +# TEXT_INDENTS +####################################################################### + +TEXT_INDENTS=() # empty => "" +TEXT_INDENT="" + +## +# push_text_indent +# +function push_text_indent() { + TEXT_INDENTS+=("${TEXT_INDENT}") + TEXT_INDENT="${1}" +} + +## +# pop_text_indent +# +function pop_text_indent() { + if [[ ${#TEXT_INDENTS[@]} -gt 0 ]]; then + TEXT_INDENT="${TEXT_INDENTS[${#TEXT_INDENTS[@]} - 1]}" + unset "TEXT_INDENTS[${#TEXT_INDENTS[@]} - 1]" + else + TEXT_INDENT="" + fi +} + +####################################################################### +# STATEMENT_INDENTS +####################################################################### + +STATEMENT_INDENTS=() # empty => "" +STATEMENT_INDENT="" + +## +# push_statement_indent +# +function push_statement_indent() { + STATEMENT_INDENTS+=("${STATEMENT_INDENT}") + STATEMENT_INDENT="${1}" +} + +## +# pop_statement_indent +# +function pop_statement_indent() { + if [[ ${#STATEMENT_INDENTS[@]} -gt 0 ]]; then + STATEMENT_INDENT="${STATEMENT_INDENTS[${#STATEMENT_INDENTS[@]} - 1]}" + unset "STATEMENT_INDENTS[${#STATEMENT_INDENTS[@]} - 1]" + else + STATEMENT_INDENT="" + fi +} + +####################################################################### +# BLOCK_INDENTS +####################################################################### + +BLOCK_INDENTS=() # empty => "" +BLOCK_INDENT="" + +## +# push_block_indent +# +function push_block_indent() { + BLOCK_INDENTS+=("${BLOCK_INDENT}") + BLOCK_INDENT="${1}" +} + +## +# pop_block_indent +# +function pop_block_indent() { + if [[ ${#BLOCK_INDENTS[@]} -gt 0 ]]; then + BLOCK_INDENT="${BLOCK_INDENTS[${#BLOCK_INDENTS[@]} - 1]}" + unset "BLOCK_INDENTS[${#BLOCK_INDENTS[@]} - 1]" + else + BLOCK_INDENT="" + fi +} + +####################################################################### +# Print Functions +####################################################################### + +## +# print_statement +# $1 = leading indentation +# $2 = statement +# +function print_statement() { + local indent="${1}" + if [[ "${indent}" == "${BLOCK_INDENT}"* ]]; then + indent="${indent/#$BLOCK_INDENT/}" + fi + printf "%s\n" "${BASE_STMT_INDENT}${STATEMENT_INDENT}${indent}${2}" +} + +## +# print_text - generates a printf statement for template text +# $1 = leading text indentation +# $2 = text +# $3 = leading stmt indentation [OPTIONAL] +# +function print_text() { + local indent="${1}" + if [[ "${indent}" == "${BLOCK_INDENT}"* ]]; then + indent="${indent/#$BLOCK_INDENT/}" + fi + process_tags "${3-${BLOCK_INDENT}}" "${BASE_TEXT_INDENT}${TEXT_INDENT}${indent}${2}" +} + +####################################################################### +# Process Functions +####################################################################### + +## +# process_tags +# $1 = statement indentation +# $2 = full line of text to process +# +function process_tags() { + local stmt_indent line args arg quoted + stmt_indent="${1}" + line="${2}" + args="" + while [ -n "${line}" ]; do + # echo "# LINE @ START: $(declare -p line)" >&2 + if [[ "${line}" =~ $TAG_TEXT_REGEX ]]; then + # echo "# TEXT TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + printf -v quoted "%q" "${BASH_REMATCH[1]}" + args="${args}${quoted}" + line="${BASH_REMATCH[2]}" + elif [[ "${line}" =~ $TAG_QUOTE_REGEX ]]; then + # echo "# QUOTE TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + args="${args}\"${BASH_REMATCH[1]}\"" + line="${BASH_REMATCH[6]}" + elif [[ "${line}" =~ $TAG_STATEMENT_REGEX ]]; then + # echo "# STMT TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + arg="${BASH_REMATCH[1]}" + trim arg + args="${args}\"\$(${arg})\"" + line="${BASH_REMATCH[5]}" + # Check standard regex last as it's a super-set of quote and stmt regex + # + elif [[ "${line}" =~ $TAG_STD_REGEX ]]; then + # echo "# STD TAG MATCH: $(declare -p BASH_REMATCH)" >&2 + arg="${BASH_REMATCH[1]}" + trim arg + args="${args}\"${arg}\"" + line="${BASH_REMATCH[5]}" + # Assume next character is TEXT - extract and process remainder + # + elif [[ "${line}" =~ (.)(.*) ]]; then + # echo "# DEFAULT: Assuming first char is TEXT: $(declare -p line)" + printf -v quoted "%q" "${BASH_REMATCH[1]}" + args="${args}${quoted}" + line="${BASH_REMATCH[2]}" + fi + # echo "# LINE @ END: $(declare -p line)" >&2 + done + local stmt + if [ -n "${args}" ]; then + printf -v stmt "printf \"%%s\\\\n\" %s" "${args}" + else + printf -v stmt "printf \"\\\\n\"" + fi + print_statement "${stmt_indent}" "${stmt}" +} + +DELIM_DIR_TAG_REGEX='[Tt][Aa][Gg]\s*=\s*"([^"]*)"' +DELIM_DIR_TAG_STMT_REGEX='[Tt][Aa][Gg][_-]?[Ss][Tt][Mm][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_STMT_REGEX='[Ss][Tt][Mm][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_STMT_BLOCK_REGEX='[Ss][Tt][Mm][Tt][_-]?[Bb][Ll][Oo][Cc][Kk]\s*=\s*"([^"]*)"' +DELIM_DIR_TXT_REGEX='[Tt][Xx][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_TEXT_REGEX='[Tt][Ee][Xx][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_DIR_REGEX='[Dd][Ii][Rr]\s*=\s*"([^"]*)"' +DELIM_DIR_DIRECTIVE_REGEX='[Dd][Ii][Rr][Ee][Cc][Tt][Ii][Vv][Ee]\s*=\s*"([^"]*)"' +DELIM_DIR_CMT_REGEX='[Cc][Mm][Tt]\s*=\s*"([^"]*)"' +DELIM_DIR_COMMENT_REGEX='[Cc][Oo][Mm][Mm][Ee][Nn][Tt]\s*=\s*"([^"]*)"' + +## +# process_directive +# $1 = leading_indent +# $2 = directive +# $3 = directive arg(s) +# +function process_directive() { + local directive + directive="${2}" + normalize_directive directive + case "${directive}" in + INCLUDE) + local indent="${1}" + if [[ "${indent}" == "${BLOCK_INDENT}"* ]]; then + indent="${indent/#$BLOCK_INDENT/}" + fi + local args args_arr + args="${3}" + trim args + declare -a args_arr="(${args})" + # shellcheck disable=SC2128 # We choose BASH_SOURCE vs BASH_SOURCE[0] for compatability + "${BASH_SOURCE}" \ + --text-indent "${BASE_TEXT_INDENT}${TEXT_INDENT}${indent}" \ + --stmt-indent "${BASE_STMT_INDENT}${STATEMENT_INDENT}${indent}" \ + --block-indent "${BASE_BLOCK_INDENT}${BLOCK_INDENT}" \ + --tag-delims "${TAG_START_DELIM1}${TAG_START_DELIM2} ${TAG_STOP_DELIM1}${TAG_STOP_DELIM2}" \ + --tag-stmt-delim "${TAG_STMT_DELIM}" \ + --stmt-delim "${STMT_DELIM}" \ + --stmt-block-delims "${STMT_BLOCK_START_DELIM} ${STMT_BLOCK_STOP_DELIM}" \ + --txt-delim "${TEXT_DELIM}" \ + --dir-delim "${DIRECTIVE_DELIM}" \ + --cmt-delim "${COMMENT_DELIM}" \ + "${args_arr[@]}" + ;; + DELIMS) + # TAG + # + if [[ "${3}" =~ $DELIM_DIR_TAG_REGEX ]]; then + parse_tag_delims "${BASH_REMATCH[1]}" 'DELIMS TAG directive' + fi + # TAG-STMT + # + if [[ "${3}" =~ $DELIM_DIR_TAG_STMT_REGEX ]]; then + parse_tag_stmt_delim "${BASH_REMATCH[1]}" 'DELIMS TAG-STMT directive' + fi + # STMT + # + if [[ "${3}" =~ $DELIM_DIR_STMT_REGEX ]]; then + parse_stmt_delim "${BASH_REMATCH[1]}" 'DELIMS STMT directive' + fi + # STMT-BLOCK + # + if [[ "${3}" =~ $DELIM_DIR_STMT_BLOCK_REGEX ]]; then + parse_stmt_block_delims "${BASH_REMATCH[1]}" '"DELIMS STMT-BLOCK directive' + fi + # TEXT + # + if [[ "${3}" =~ $DELIM_DIR_TXT_REGEX || "${3}" =~ $DELIM_DIR_TEXT_REGEX ]]; then + parse_text_delim "${BASH_REMATCH[1]}" 'DELIMS TEXT directive' + fi + # DIRECTIVE + # + if [[ "${3}" =~ $DELIM_DIR_DIR_REGEX || "${3}" =~ $DELIM_DIR_DIRECTIVE_REGEX ]]; then + parse_directive_delim "${BASH_REMATCH[1]}" 'DELIMS DIR directive' + fi + # COMMENT + # + if [[ "${3}" =~ $DELIM_DIR_CMT_REGEX || "${3}" =~ $DELIM_DIR_COMMENT_REGEX ]]; then + parse_comment_delim "${BASH_REMATCH[1]}" 'DELIMS CMT directive' + fi + # Apply changes + # + reset_template_regexes + ;; + RESET-DELIMS) + reset_delims + reset_template_regexes + ;; + *) # unsupported directive + echo "Error: Unknown directive: '${directive}' - Skipping" >&2 + ;; + esac +} + +function debug_array() { + printf "[" + local need_comma="" + while [[ ${#@} -gt 0 ]]; do + if [ -n "${need_comma:-}" ]; then + printf ", " + fi + if [ -n "${1:-}" ]; then + printf "'%q'" "${1}" + else + printf "''" + fi + need_comma=1 + shift + done + printf "]" +} +## +# debug_state logs the state of the global variables. +# To use this set the BASH_TPL_DEBUG variable to a non-empty value +# when invoking the script. +# TODO Track source input file+line numbers +# $1 = template line | EOF +# +function debug_state() { + printf "#<< ---------------\n" + printf "#LINE TEXT : '%s'\n" "${1-}" + printf "#STATE : %s\n" "${STATE:-}" + printf "#STATES : %s\n" "$(debug_array "${STATES[@]}")" + printf "#TEXT_INDENT : %q\n" "${TEXT_INDENT:-}" + printf "#TEXT_INDENTS : %s\n" "$(debug_array "${TEXT_INDENTS[@]}")" + printf "#STATEMENT_INDENT : %q\n" "${STATEMENT_INDENT:-}" + printf "#STATEMENT_INDENTS: %s\n" "$(debug_array "${STATEMENT_INDENTS[@]}")" + printf "#BLOCK_INDENT : %q\n" "${BLOCK_INDENT:-}" + printf "#BLOCK_INDENTS : %s\n" "$(debug_array "${BLOCK_INDENTS[@]}")" + printf "#TEXT_BLOCK_LINES_INDENT : %q\n" "${TEXT_BLOCK_LINES_INDENT}" + printf "#TEXT_BLOCK_LINES_INDENT_SET : %q\n" "${TEXT_BLOCK_LINES_INDENT_SET}" + printf "#STATEMENT_BLOCK_LINES_INDENT : %q\n" "${STATEMENT_BLOCK_LINES_INDENT}" + printf "#STATEMENT_BLOCK_LINES_INDENT_STATE : %q\n" "${STATEMENT_BLOCK_LINES_INDENT_STATE}" + printf "#>> ---------------\n" +} + +function process_line() { + [ -n "${BASH_TPL_DEBUG:-}" ] && debug_state ${@+"$@"} + state_"${STATE}" ${@+"$@"} +} + +function process_stdin() { + local line + while IFS="" read -r line || [ -n "${line}" ]; do + process_line "${line}" + done + # EOF - Notify states + # Call with no args + # + while [[ "${STATE}" != "DEFAULT" ]]; do + process_line + done + process_line # DEFAULT +} + +####################################################################### +# State Handler Functions +####################################################################### + +## +# state_DEFAULT +# Not inside any blocks +# Assumes *_INDENT and STATE arrays are empty +# +function state_DEFAULT() { + [[ ${#@} -gt 0 ]] || return # Exit early on EOF + # Line is a statement + # + if [[ "${1}" =~ $STATEMENT_REGEX ]]; then + print_statement "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + push_statement_indent "${BASH_REMATCH[1]}" + push_state "MAYBE_TXT_BLOCK" + # Line is a statement block start + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_START_REGEX ]]; then + push_statement_indent "${BASH_REMATCH[1]}" + push_state "START_STMT_BLOCK" + # Line is a directive + # + elif [[ "${1}" =~ $DIRECTIVE_REGEX ]]; then + process_directive "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" + # Line is a comment + # + elif [[ "${1}" =~ $COMMENT_REGEX ]]; then + : # Comments do not generate output + # Line is text + # NOTE : Check LAST because regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + print_text "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + fi +} + +TEXT_BLOCK_LINES=() +TEXT_BLOCK_LINES_INDENT="" +TEXT_BLOCK_LINES_INDENT_SET="" + +## +# state_MAYBE_TXT_BLOCK +# Previous line was a statement +# We might be starting a text block +# NOTE: Assumes +# push_state "MAYBE_TXT_BLOCK" +# push_statement_indent +# +function state_MAYBE_TXT_BLOCK() { + # If there's a line to process (i.e. not EOF) + # + if [[ ${#@} -gt 0 ]]; then + # Current line is empty + # Considered text block content, + # but doesn't contribute to indentation tracking + # + if [[ "${1}" == "" ]]; then + TEXT_BLOCK_LINES+=("${1}") # Save line + return + # Current line is a block-end statement, + # i.e. it's a statement at the same indentation as the start statement + # + elif [[ "${1}" =~ $STATEMENT_REGEX && "${BASH_REMATCH[1]}" == "${STATEMENT_INDENT}" ]]; then + # We've saved a FULL text block ! + # Use computed indentation + # + push_text_indent "${TEXT_INDENT}${STATEMENT_INDENT/#$BLOCK_INDENT/}" # Additive + push_statement_indent "${TEXT_BLOCK_LINES_INDENT}" + push_block_indent "${TEXT_BLOCK_LINES_INDENT}" + local state_marker=${#STATES[@]} # Save for cleanup + push_state "TXT_BLOCK" + # Text blocks can be nested, so save lines and cleanup *before* processing + # + local lines=("${TEXT_BLOCK_LINES[@]}") + TEXT_BLOCK_LINES=() + TEXT_BLOCK_LINES_INDENT="" + TEXT_BLOCK_LINES_INDENT_SET="" + # Process saved lines now in new state + # + local line + for line in "${lines[@]}"; do + process_line "${line}" + done + # Clean up our TXT_BLOCK state and any other danglers + # + while [[ ${#STATES[@]} -gt $state_marker ]]; do + process_line # EOF + done + # Clean up our MAYBE_TXT_BLOCK state + # + pop_statement_indent + pop_state + # Process close block in parent context + # + process_line "${1}" + return + # Capture line indentation for tracking + # TEXT_REGEX is perfect for this, so just re-use it + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + TEXT_BLOCK_LINES+=("${1}") # Save line + # If current line is indented + # + if [[ "${BASH_REMATCH[1]}" != "${STATEMENT_INDENT}" && "${BASH_REMATCH[1]}" == "${STATEMENT_INDENT}"* ]]; then + # If first time through + # + if [[ "${TEXT_BLOCK_LINES_INDENT_SET}" == "" ]]; then + # Track current indentation + # + TEXT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + TEXT_BLOCK_LINES_INDENT_SET="1" + return + # If current line is indented SAME OR LESS than tracked + # + elif [[ "${TEXT_BLOCK_LINES_INDENT}" == "${BASH_REMATCH[1]}"* ]]; then + # Update tracked indentation (may set to same value) + # + TEXT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + return + # If current line is indented MORE than tracked + # + elif [[ "${BASH_REMATCH[1]}" == "${TEXT_BLOCK_LINES_INDENT}"* ]]; then + # No change + # + return + # Neither line is a subset of the other + # + else + : # Here for completeness + fi + # Current line is NOT indented + # + else + : # Here for completeness + fi + fi + # EOF + # + else + : # Fall through + fi + # If we haven't returned by now, then we're not in a text block + # Discard saved state and process saved lines + # + pop_statement_indent + pop_state + # Text blocks can be nested, so save lines and cleanup *before* processing + # + local lines=("${TEXT_BLOCK_LINES[@]}") + # Clean up + # + TEXT_BLOCK_LINES=() + TEXT_BLOCK_LINES_INDENT="" + TEXT_BLOCK_LINES_INDENT_SET="" + # Process saved lines now in parent context + # TODO "push" these back onto primary line-processing stream? + # + local line + for line in "${lines[@]}"; do + process_line "${line}" + done +} + +## +# state_TXT_BLOCK +# NOTE: Assumes +# Called within MAYBE_TXT_BLOCK with a complete block to process +# Will NOT be called with TXT Block Close +# Every line has a minimum indentation of BLOCK_INDENT +# push_state TXT_BLOCK +# push_text_indent +# push_statement_indent +# push_block_indent +# +function state_TXT_BLOCK() { + # EOF + # + if [[ ${#@} -eq 0 ]]; then + # End of text block + # Discard saved state + # + pop_text_indent + pop_statement_indent + pop_block_indent + pop_state + # Current line is empty + # + elif [[ "${1}" == "" ]]; then + process_tags "${BLOCK_INDENT}" "" + # Current line is a statement + # + elif [[ "${1}" =~ $STATEMENT_REGEX ]]; then + print_statement "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + push_statement_indent "${BASH_REMATCH[1]}" + push_state "MAYBE_TXT_BLOCK" + # Current line is a statement Start Block + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_START_REGEX ]]; then + push_statement_indent "${BASH_REMATCH[1]}" + push_state "START_STMT_BLOCK" + # Current line is a directive + # + elif [[ "${1}" =~ $DIRECTIVE_REGEX ]]; then + process_directive "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" + # Line is a comment + # + elif [[ "${1}" =~ $COMMENT_REGEX ]]; then + : # Comments do not generate output + # Line is text + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + print_text "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + fi +} + +STATEMENT_BLOCK_LINES=() +STATEMENT_BLOCK_LINES_INDENT="" +STATEMENT_BLOCK_LINES_INDENT_STATE="" + +## +# state_START_STMT_BLOCK +# NOTE: Assumes +# push_state "START_STMT_BLOCK" +# push_statement_indent +# +function state_START_STMT_BLOCK() { + # If there's a line to process (i.e. not EOF) + # + if [[ ${#@} -gt 0 ]]; then + # Current line is empty + # Considered statement block content, + # but doesn't contribute to indentation tracking + # + if [[ "${1}" == "" ]]; then + STATEMENT_BLOCK_LINES+=("${1}") # Save line + return + # Current line is a statement block end + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_STOP_REGEX ]]; then + # If indentation does not match block-open, then error + # TODO Track line numbers for better reporting + # + if [[ "${BASH_REMATCH[1]}" != "${STATEMENT_INDENT}" ]]; then + echo "Error: stmt-block close indentation does not match open" >&2 + exit 1 + fi + # We've saved a FULL statement block ! + # Is it fully indented? + # + if [[ "${STATEMENT_BLOCK_LINES_INDENT_STATE}" == "1" ]]; then + # Use computed indentation + # + push_text_indent "${TEXT_INDENT}${STATEMENT_INDENT/#$BLOCK_INDENT/}" # Additive + push_block_indent "${STATEMENT_BLOCK_LINES_INDENT}" + else + # If not consistently indented, default to no indent + # TODO Print warning? + # + push_text_indent "" + pop_statement_indent + push_statement_indent "" + push_block_indent "" + fi + # Process the saved lines + # NOTE: Statement block end (+ cleanup) will be processed by STMT_BLOCK handler + # + pop_state + push_state "STMT_BLOCK" + # Process saved lines now in new state + # Statement blocks do not nest, so we use global and cleanup *after* + # + local line + for line in "${STATEMENT_BLOCK_LINES[@]}"; do + process_line "${line}" + done + # Clean up our STMT_BLOCK state + # + process_line # EOF + # Clean up + # + STATEMENT_BLOCK_LINES=() + STATEMENT_BLOCK_LINES_INDENT="" + STATEMENT_BLOCK_LINES_INDENT_STATE="" + # Capture line indentation for tracking + # TEXT_REGEX is perfect for this, so just re-use it + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + STATEMENT_BLOCK_LINES+=("${1}") # Save line + # If current line is indented (or even) + # + if [[ "${BASH_REMATCH[1]}" == "${STATEMENT_INDENT}"* ]]; then + # If first time through + # + if [[ "${STATEMENT_BLOCK_LINES_INDENT_STATE}" == "" ]]; then + # Track current indentation + # + STATEMENT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + STATEMENT_BLOCK_LINES_INDENT_STATE="1" + # If still working with fully indented block + # + elif [[ "${STATEMENT_BLOCK_LINES_INDENT_STATE}" == "1" ]]; then + # If current line is indented SAME OR LESS than tracked + # + if [[ "${STATEMENT_BLOCK_LINES_INDENT}" == "${BASH_REMATCH[1]}"* ]]; then + # Update tracked indentation (may set to same value) + # + STATEMENT_BLOCK_LINES_INDENT="${BASH_REMATCH[1]}" + # If current line is indented MORE than tracked + # + elif [[ "${BASH_REMATCH[1]}" == "${STATEMENT_BLOCK_LINES_INDENT}"* ]]; then + # No change + # + : + # Neither line is a subset of the other + # + else + STATEMENT_BLOCK_LINES_INDENT_STATE="2" + fi + fi + # Current line is NOT indented (or even) + # + else + STATEMENT_BLOCK_LINES_INDENT_STATE="3" + fi + fi + # EOF + # + else + # EOF before close block reached is an error + # TODO Track line numbers for better reporting + # + echo "Error: Missing stmt-block close ('${STMT_BLOCK_STOP_DELIM}')" >&2 + exit 1 + fi +} + +## +# state_STMT_BLOCK +# NOTE: Assumes +# Called within START_STMT_BLOCK with a complete block to process +# Will NOT be called with STMT Block Close +# push_state STMT_BLOCK +# push_text_indent +# push_statement_indent +# push_block_indent +# +function state_STMT_BLOCK() { + # EOF + # + if [[ ${#@} -eq 0 ]]; then + # End of statement block + # Discard saved state + # + pop_text_indent + pop_statement_indent + pop_block_indent + pop_state + # Current line is empty + # + elif [[ "${1}" == "" ]]; then + # TODO Do we need a flag to print BASE_STMT_INDENT when included? + # + printf "\n" + # Line is text + # + elif [[ "${1}" =~ $STATEMENT_BLOCK_TEXT_REGEX ]]; then + print_text "${BLOCK_INDENT}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" + # Line is assumed to be a statement + # TEXT_REGEX is perfect for this, so just re-use it + # NOTE: Regex always matches + # + elif [[ "${1}" =~ $TEXT_REGEX ]]; then + print_statement "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + fi +} + +####################################################################### +# Main +####################################################################### + +function version() { + printf "%s\n" "${VERSION}" +} + +## +# parse_env_delims - Set Delims from Environment Vars +# +function parse_env_delims() { + if [ -n "${BASH_TPL_TAG_DELIMS}" ]; then + parse_tag_delims "${BASH_TPL_TAG_DELIMS}" "BASH_TPL_TAG_DELIMS" + fi + + if [ -n "${BASH_TPL_TAG_STMT_DELIM}" ]; then + parse_tag_stmt_delim "${BASH_TPL_TAG_STMT_DELIM}" "BASH_TPL_TAG_STMT_DELIM" + fi + + if [ -n "${BASH_TPL_STMT_DELIM}" ]; then + parse_stmt_delim "${BASH_TPL_STMT_DELIM}" "BASH_TPL_STMT_DELIM" + fi + + if [ -n "${BASH_TPL_STMT_BLOCK_DELIMS}" ]; then + parse_stmt_block_delims "${BASH_TPL_STMT_BLOCK_DELIMS}" "BASH_TPL_STMT_BLOCK_DELIMS" + fi + + if [ -n "${BASH_TPL_TEXT_DELIM}" ]; then + parse_text_delim "${BASH_TPL_TEXT_DELIM}" "BASH_TPL_TEXT_DELIM" + fi + + if [ -n "${BASH_TPL_DIR_DELIM}" ]; then + parse_directive_delim "${BASH_TPL_DIR_DELIM}" "BASH_TPL_DIR_DELIM" + fi + + if [ -n "${BASH_TPL_CMT_DELIM}" ]; then + parse_comment_delim "${BASH_TPL_CMT_DELIM}" "BASH_TPL_CMT_DELIM" + fi +} + +## +# parse_args +# $@ args to parse +# +# Stores positional args in global array __ARGS[@] +# +function parse_args() { + __ARGS=() # Global + while (($#)); do + case "$1" in + -h | --help) + usage + exit 0 + ;; + --version) + version + exit 0 + ;; + -o | --output-file) + if [ -n "${2}" ]; then + OUTPUT_FILE="${2}" + else + echo "Error: Invalid or missing value for --output-file: '${2}'" >&2 + exit 1 + fi + shift 2 + ;; + --reset-delims) + # Reset delims immediately - Any delim flags after this will be honored + # + reset_delims + shift + ;; + --tag-delims) + parse_tag_delims "$2" "$1" + shift 2 + ;; + --tag-stmt-delim) + parse_tag_stmt_delim "$2" "$1" + shift 2 + ;; + --stmt-delim) + parse_stmt_delim "$2" "$1" + shift 2 + ;; + --stmt-block-delims) + parse_stmt_block_delims "$2" "$1" + shift 2 + ;; + --txt-delim | --text-delim) + parse_text_delim "$2" "$1" + shift 2 + ;; + --dir-delim | --directive-delim) + parse_directive_delim "$2" "$1" + shift 2 + ;; + --cmt-delim | --comment-delim) + parse_comment_delim "$2" "$1" + shift 2 + ;; + --text-indent) + BASE_TEXT_INDENT="${2}" + shift 2 + ;; + --stmt-indent) + BASE_STMT_INDENT="${2}" + shift 2 + ;; + --block-indent) + BASE_BLOCK_INDENT="${2}" + shift 2 + ;; + -) + __ARGS+=("$1") + shift + ;; + --) + shift + while (($#)); do + __ARGS+=("$1") + shift + done + ;; + --* | -*) # unsupported flags + echo "Error: unknown flag: '$1'; use -h for help" >&2 + exit 1 + ;; + *) # preserve positional arguments + __ARGS+=("$1") + shift + ;; + esac + done +} + +function main() { + + OUTPUT_FILE="" + + BASE_TEXT_INDENT="" + BASE_STMT_INDENT="" + BASE_BLOCK_INDENT="" + + reset_delims + parse_env_delims + + parse_args "$@" + set -- "${__ARGS[@]}" + unset __ARGS + + # No file argument + # + if [[ -z "${1}" ]]; then + # Nothing waiting on stdin + # + if [[ -t 0 ]]; then + usage + exit 1 + fi + else + # File argument is explicitly stdin + # + if [[ "${1}" == '-' ]]; then + shift + else + # File argument points to non-existing/readable file + # + if [[ ! -r "${1}" ]]; then + echo "File not found: '${1}'" >&2 + exit 1 + fi + # File argument is good, re-route it to stdin + # + exec < "${1}" + shift + fi + fi + + reset_template_regexes + + if [[ -n "${OUTPUT_FILE}" ]]; then + exec > "${OUTPUT_FILE}" + fi + + process_stdin + + return 0 # ALL OK +} + +# Only process main logic if not being sourced (ie tested) +# +(return 0 2> /dev/null) || main "$@" diff --git a/modules/scripting/bash_tpl.inst b/modules/scripting/bash_tpl.inst new file mode 100644 index 0000000..792155e --- /dev/null +++ b/modules/scripting/bash_tpl.inst @@ -0,0 +1 @@ +lastversion --assets --output /shell/base/modules/scripting/bash-tpl.lib download bash-tpl \ No newline at end of file