2023-04-14 21:27:40 -07:00
|
|
|
#!/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
|
|
|
|
#######################################################################
|
2024-09-15 18:47:22 -07:00
|
|
|
VERSION="v0.8.0"
|
2023-04-14 21:27:40 -07:00
|
|
|
#######################################################################
|
|
|
|
# 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
|
2024-09-15 18:47:22 -07:00
|
|
|
DIRECTIVE_REGEX="^([[:blank:]]*)${d}([a-zA-Z_-]+\??)(.*)\$"
|
2023-04-14 21:27:40 -07:00
|
|
|
|
|
|
|
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'
|
|
|
|
}
|
2024-09-15 18:47:22 -07:00
|
|
|
##
|
|
|
|
# escape_string
|
|
|
|
# Escapes value compatible with posix `printf %b`
|
|
|
|
# usage: escape_string varname
|
|
|
|
#
|
|
|
|
function escape_string {
|
|
|
|
# Escape '\' first since we'll be adding more later
|
|
|
|
local e="${!1//$'\\'/\\}"
|
|
|
|
# Some man pages mention \0NNN but in practice it seems \NNN is also works.
|
|
|
|
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}"
|
|
|
|
}
|
2023-04-14 21:27:40 -07:00
|
|
|
|
|
|
|
##
|
|
|
|
# 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() {
|
2024-09-15 18:47:22 -07:00
|
|
|
local stmt_indent line formats args arg quoted
|
2023-04-14 21:27:40 -07:00
|
|
|
stmt_indent="${1}"
|
|
|
|
line="${2}"
|
2024-09-15 18:47:22 -07:00
|
|
|
formats=""
|
|
|
|
args=()
|
2023-04-14 21:27:40 -07:00
|
|
|
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
|
2024-09-15 18:47:22 -07:00
|
|
|
quoted="${BASH_REMATCH[1]}"
|
|
|
|
escape_string quoted
|
|
|
|
#printf -v quoted "%q" "${BASH_REMATCH[1]}"
|
|
|
|
formats="${formats}%b"
|
|
|
|
args+=("${quoted}")
|
2023-04-14 21:27:40 -07:00
|
|
|
line="${BASH_REMATCH[2]}"
|
|
|
|
elif [[ "${line}" =~ $TAG_QUOTE_REGEX ]]; then
|
|
|
|
# echo "# QUOTE TAG MATCH: $(declare -p BASH_REMATCH)" >&2
|
2024-09-15 18:47:22 -07:00
|
|
|
formats="${formats}%s"
|
|
|
|
args+=("\"${BASH_REMATCH[1]}\"")
|
2023-04-14 21:27:40 -07:00
|
|
|
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
|
2024-09-15 18:47:22 -07:00
|
|
|
formats="${formats}%s"
|
|
|
|
args+=("\"\$(${arg})\"")
|
2023-04-14 21:27:40 -07:00
|
|
|
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
|
2024-09-15 18:47:22 -07:00
|
|
|
formats="${formats}%s"
|
|
|
|
args+=("\"${arg}\"")
|
2023-04-14 21:27:40 -07:00
|
|
|
line="${BASH_REMATCH[5]}"
|
|
|
|
# Assume next character is TEXT - extract and process remainder
|
|
|
|
#
|
|
|
|
elif [[ "${line}" =~ (.)(.*) ]]; then
|
2024-09-15 18:47:22 -07:00
|
|
|
# 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}")
|
2023-04-14 21:27:40 -07:00
|
|
|
line="${BASH_REMATCH[2]}"
|
|
|
|
fi
|
|
|
|
# echo "# LINE @ END: $(declare -p line)" >&2
|
|
|
|
done
|
|
|
|
local stmt
|
2024-09-15 18:47:22 -07:00
|
|
|
if [[ ${#args[@]} -gt 0 ]]; then
|
|
|
|
printf -v stmt "printf \"%s\\\\n\" %s" "${formats}" "${args[*]}"
|
2023-04-14 21:27:40 -07:00
|
|
|
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
|
2024-09-15 18:47:22 -07:00
|
|
|
INCLUDE | INCLUDE\?)
|
|
|
|
local file_not_found_ok=""
|
|
|
|
if [ "${directive}" = "INCLUDE?" ]; then
|
|
|
|
file_not_found_ok="1"
|
|
|
|
fi
|
2023-04-14 21:27:40 -07:00
|
|
|
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}" \
|
2024-09-15 18:47:22 -07:00
|
|
|
${file_not_found_ok:+'--file-not-found-ok'} \
|
2023-04-14 21:27:40 -07:00
|
|
|
"${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
|
|
|
|
;;
|
2024-09-15 18:47:22 -07:00
|
|
|
--file-not-found-ok) # internal flag to support .INCLUDE?
|
|
|
|
FILE_NOT_FOUND_OK=1
|
|
|
|
shift
|
|
|
|
;;
|
2023-04-14 21:27:40 -07:00
|
|
|
-)
|
|
|
|
__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
|
|
|
|
|
2024-09-15 18:47:22 -07:00
|
|
|
FILE_NOT_FOUND_OK=""
|
|
|
|
if [ -n "${BASH_TPL_FILE_NOT_FOUND_OK}" ]; then
|
|
|
|
FILE_NOT_FOUND_OK="1"
|
|
|
|
fi
|
|
|
|
|
2023-04-14 21:27:40 -07:00
|
|
|
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
|
2024-09-15 18:47:22 -07:00
|
|
|
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
|
2023-04-14 21:27:40 -07:00
|
|
|
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
|
2024-01-14 14:57:09 -08:00
|
|
|
|
|
|
|
return 0 # ALL OK
|
2023-04-14 21:27:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
# Only process main logic if not being sourced (ie tested)
|
|
|
|
#
|
2024-01-14 14:57:09 -08:00
|
|
|
(return 0 2> /dev/null) || main "$@"
|