shell-base/modules/scripting/btpl.lib

1299 lines
33 KiB
Bash

#!/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 <filename>
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-<EOF>}"
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 "$@"