uci-docker-build/lib/bash-tpl
David Kebler efe8d0fa2d refactor: generate Dockerfile from a template
refactor src/ to be just a basic build
add examples
write up help.md and usage subcommand
add test folder for dev testing
improve install script
many numerous improved to build script,
 moved portions of build script to functions in helpers.lib
2023-04-14 21:27:40 -07:00

1296 lines
No EOL
33 KiB
Bash
Executable file

#!/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.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: '%')
--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
}
# Only process main logic if not being sourced (ie tested)
#
(return 0 2> /dev/null) || main "$@"