#!/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.9.0"
#######################################################################
# 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: '%')
  --tag-fmt-delims 'x x'
        set tag format-specifier delimiters (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_TAG_FMT_DELIMS
    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:]])$'
TAG_FMT_DELIM_REGEX='^([^[:blank:]]) ([^[: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='%'

	TAG_FMT_START_DELIM='|'
	TAG_FMT_STOP_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_tag_fmt_delims
# $1 = delims
# $2 = src (for error msg)
#
function parse_tag_fmt_delims() {
	if [[ "${1}" =~ $TAG_FMT_DELIM_REGEX ]]; then
		TAG_FMT_START_DELIM="${BASH_REMATCH[1]}"
		TAG_FMT_STOP_DELIM="${BASH_REMATCH[2]}"
	else
		echo "Error: Invalid or missing tag fmt delimiter values for ${2-tag fmt delims}: '${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 d1_ d2 d2_ d3 d3_ d4 d4_ f1 f2 f2_

	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}"
	d1_="${d1}"
	escape_regex d1

	d2="${TAG_START_DELIM2}"
	d2_="${d2}"
	escape_regex d2

	d3="${TAG_STOP_DELIM1}"
	d3_="${d3}"
	escape_regex d3

	d4="${TAG_STOP_DELIM2}"
	d4_="${d4}"
	escape_regex d4

	ds="${TAG_STMT_DELIM}"
	escape_regex ds

	f1="${TAG_FMT_START_DELIM}"
	escape_regex f1

	f2="${TAG_FMT_STOP_DELIM}"
	f2_="${f2}"
	escape_regex f2

	local fmt_regex std_regex quote_regex stmt_regex

	fmt_regex="${f1}([^${f2_}]*)${f2}"

	TAG_TEXT_REGEX="^([^${d1_}]+|${d1}$|${d1}[^${d1_}${d2_}]+)(.*)"

	std_regex="((([^${d3_}])|(${d3}[^${d4_}]))*)"

	# No [[:space:]] checks after fmt_regex as we'll let std_regex pick them up then trim them
	TAG_STD_REGEX="^${d1}${d2}[[:space:]]*(${fmt_regex})?${std_regex}${d3}${d4}(.*)"
	TAG_STD_MATCH_IDX_FMT=2
	TAG_STD_MATCH_IDX_TXT=3
	TAG_STD_MATCH_IDX_END=7

	quote_regex="\"((([^\"])|(\"[^${d3_}])|(\"${d3}[^${d4_}]))*)\""

	TAG_QUOTE_REGEX="^${d1}${d2}[[:space:]]*(${fmt_regex})?[[:space:]]*${quote_regex}[[:space:]]*${d3}${d4}(.*)"
	TAG_QUOTE_MATCH_IDX_FMT=2
	TAG_QUOTE_MATCH_IDX_TXT=3
	TAG_QUOTE_MATCH_IDX_END=8

	stmt_regex="${ds}((([^${d3_}])|(${d3}[^${d4_}]))*)"

	# No trailing [[:space:]] check as we'll let std_regex pick them up then trim them
	TAG_STATEMENT_REGEX="^${d1}${d2}[[:space:]]*(${fmt_regex})?[[:space:]]*${stmt_regex}${d3}${d4}(.*)"
	TAG_STATEMENT_MATCH_IDX_FMT=2
	TAG_STATEMENT_MATCH_IDX_TXT=3
	TAG_STATEMENT_MATCH_IDX_END=7

	# 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_string
# Escapes value compatible with posix `printf %b`
# usage: escape_string varname
#
function escape_string {
	local e="${!1}"
	# Escape '\' first since we'll be adding more later
	# Octal support = \0[0-9]{0,3}
	e="${e//\\/\\\\}"
	e="${e//\'/\\0047}"
	e="${e//$'\a'/\\a}"
	e="${e//$'\b'/\\b}"
	e="${e//$'\f'/\\f}"
	e="${e//$'\n'/\\n}"
	e="${e//$'\r'/\\r}"
	e="${e//$'\t'/\\t}"
	e="${e//$'\v'/\\v}"
	printf -v "${1}" "'%s'" "${e}"
}

##
# escape_regex
# usage: escape_regex varname
#
function escape_regex() {
	local e="${!1}"
	# Escape '\' first since we'll be adding more after
	e="${e//\\/\\\\}"
	e="${e//'$'/\\$}"
	e="${e//'*'/\\*}"
	e="${e//'+'/\\+}"
	e="${e//'.'/\\.}"
	e="${e//'?'/\\?}"
	e="${e//'^'/\\^}"
	e="${e//'|'/\\|}"
	e="${e//'('/\\(}"
	e="${e//')'/\\)}"
	e="${e//'['/\\[}"
	e="${e//']'/\\]}"
	e="${e//'{'/\\{}"
	local RBRACE='}'
	e="${e//'}'/\\${RBRACE}}"
	printf -v "${1}" "%s" "${e}"
}

##
# 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 formats args fmt arg quoted
	stmt_indent="${1}"
	line="${2}"
	formats=""
	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
			quoted="${BASH_REMATCH[1]}"
			escape_string quoted
			#printf -v quoted "%q" "${BASH_REMATCH[1]}"
			formats="${formats}%b"
			args+=("${quoted}")
			line="${BASH_REMATCH[2]}"
		elif [[ "${line}" =~ $TAG_QUOTE_REGEX ]]; then
			# echo "# QUOTE TAG MATCH: $(declare -p BASH_REMATCH)" >&2
			fmt="${BASH_REMATCH[${TAG_QUOTE_MATCH_IDX_FMT}]}"
			trim fmt
			arg="${BASH_REMATCH[${TAG_QUOTE_MATCH_IDX_TXT}]}"
			formats="${formats}${fmt:-%s}"
			args+=("\"${arg}\"")
			line="${BASH_REMATCH[${TAG_QUOTE_MATCH_IDX_END}]}"
		elif [[ "${line}" =~ $TAG_STATEMENT_REGEX ]]; then
			# echo "# STMT TAG MATCH: $(declare -p BASH_REMATCH)" >&2
			fmt="${BASH_REMATCH[${TAG_STATEMENT_MATCH_IDX_FMT}]}"
			trim fmt
			arg="${BASH_REMATCH[${TAG_STATEMENT_MATCH_IDX_TXT}]}"
			trim arg
			formats="${formats}${fmt:-%s}"
			args+=("\"\$(${arg})\"")
			line="${BASH_REMATCH[${TAG_STATEMENT_MATCH_IDX_END}]}"
		# Check standard regex last as it's a super-set of quote and stmt regex
		# But check fmt regex first
		elif [[ "${line}" =~ $TAG_STD_REGEX ]]; then
			# echo "# STD TAG MATCH: $(declare -p BASH_REMATCH)" >&2
			fmt="${BASH_REMATCH[${TAG_STD_MATCH_IDX_FMT}]}"
			trim fmt
			arg="${BASH_REMATCH[${TAG_STD_MATCH_IDX_TXT}]}"
			trim arg
			formats="${formats}${fmt:-%s}"
			args+=("\"${arg}\"")
			line="${BASH_REMATCH[${TAG_STD_MATCH_IDX_END}]}"
		# Assume next character is TEXT - extract and process remainder
		#
		elif [[ "${line}" =~ (.)(.*) ]]; then
			# echo "# DEFAULT: Assuming first char is TEXT: $(declare -p line)" >&2
			quoted="${BASH_REMATCH[1]}"
			escape_string quoted
			#printf -v quoted "%q" "${BASH_REMATCH[1]}"
			formats="${formats}%b"
			args+=("${quoted}")
			line="${BASH_REMATCH[2]}"
		fi
		# echo "# LINE @ END: $(declare -p line)" >&2
	done
	local stmt
	if [[ ${#args[@]} -gt 0 ]]; then
		printf -v stmt "printf \"%s\\\\n\" %s" "${formats}" "${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_TAG_FMT_REGEX='[Tt][Aa][Gg][_-]?[Ff][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 | INCLUDE\?)
			local file_not_found_ok=""
			if [ "${directive}" = "INCLUDE?" ]; then
				file_not_found_ok="1"
			fi
			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}" \
				--tag-fmt-delims "${TAG_FMT_START_DELIM} ${TAG_FMT_STOP_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}" \
				${file_not_found_ok:+'--file-not-found-ok'} \
				"${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
			# TAG-FMT
			#
			if [[ "${3}" =~ $DELIM_DIR_TAG_FMT_REGEX ]]; then
				parse_tag_fmt_delims "${BASH_REMATCH[1]}" 'DELIMS TAG-FMT 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_TAG_FMT_DELIMS}" ]; then
		parse_tag_fmt_delims "${BASH_TPL_TAG_FMT_DELIMS}" "BASH_TPL_TAG_FMT_DELIMS"
	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
				;;
			--tag-fmt-delims)
				parse_tag_fmt_delims "$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
				;;
			--file-not-found-ok) # internal flag to support .INCLUDE?
				FILE_NOT_FOUND_OK=1
				shift
				;;
			-)
				__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

	FILE_NOT_FOUND_OK=""
	if [ -n "${BASH_TPL_FILE_NOT_FOUND_OK}" ]; then
		FILE_NOT_FOUND_OK="1"
	fi

	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
				if [ -z "${FILE_NOT_FOUND_OK}" ]; then
					echo "File not found: '${1}'" >&2
					exit 1
				else
					# Fail silently, no message, no error code
					exit 0
				fi
			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 "$@"
