From 1bc1e3d3ed7db58dd217f1f783faea54677f5443 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Fri, 15 Sep 2023 17:01:12 -0500 Subject: [PATCH] refactor: improve templater with new functionality --- Misc/Templater.bash | 674 ++++++++++++++++++++++++++------------------ Misc/Templater.md | 27 +- 2 files changed, 425 insertions(+), 276 deletions(-) diff --git a/Misc/Templater.bash b/Misc/Templater.bash index 4b5b0e0..7088aa6 100755 --- a/Misc/Templater.bash +++ b/Misc/Templater.bash @@ -1,24 +1,132 @@ #!/usr/bin/env bash -# "Import" our needed functions from our library -LIBRARY_PATH="/usr/local/lib/Custom-Scripts" -# shellcheck source=/usr/local/lib/ -if ! source "${LIBRARY_PATH}/Logging-RGB.bash"; then - echo "Unable to source Logging-RGB.bash at ${LIBRARY_PATH}" - exit 1 -fi -# shellcheck source=/usr/local/lib/ -if ! source "${LIBRARY_PATH}/Trim.bash"; then - log "error" "Unable to source Trim.bash at ${LIBRARY_PATH}" - exit 1 -fi +echo_rgb() { + # Echo a colored string to the terminal based on rgb values + # + # Positional Arguments: + # + # message + # - The message to be printed to stdout + # red + # - The red value from 0 to 255 + # green + # - The green value from 0 to 255 + # blue + # - The blue value from 0 to 255 + # + # Usage: + # echo_rgb "Yep" 10 8 30 + # + # POSIX Compliant: + # N/A + # -# Tmp file used because most distros have this dir exist in RAM, use PID of our process to write to -TEMP_FILE="/tmp/$$" + local red + local green + local blue + local input + + input="${1}" + red="${2}" + green="${3}" + blue="${4}" + + if [ -t 1 ]; then + printf "\e[0;38;2;%s;%s;%sm%s\e[m\n" "${red}" "${green}" "${blue}" "${input}" + else + printf "%s\n" "${input}" + fi +} + +log() { + # Print a message and send it to stdout or stderr depending upon log level, also configurable with debug etc. + # + # Arguments: + # level + # - The log level, defined within a case check in this function + # message + # - The info message + # line_number + # - The line number of the calling function (${LINNO}) + # + # Usage: + # log "info" "Could not find that directory" + # + # POSIX Compliant: + # Yes + # + + # Set debug status depending if a global debug variable has been set to either 1 or 0 + local debug + if [ ${DEBUG} ]; then + debug=${DEBUG} + else + debug=0 + fi + + local FORMAT + FORMAT="[$(echo_rgb "$(date +%Y-%m-%dT%H:%M:%S)" 180 140 255)]" + + # Convert the level to uppercase + local level + level=$(echo "${1}" | tr '[:lower:]' '[:upper:]') + + local message + message="${2}" + + case "${level}" in + INFO) + # Output all info log levels to stdout + printf "${FORMAT}[$(echo_rgb "INFO" 0 140 255)] %s\n" "${message}" >&1 + return 0 + ;; + WARN | WARNING) + # Output all warning log levels to stdout + printf "${FORMAT}[$(echo_rgb "WARNING" 255 255 0)] %s\n" "${message}" >&1 + return 0 + ;; + DEBUG) + # Output all debug log levels to stdout + if [ "${DEBUG}" ]; then + printf "${FORMAT}[$(echo_rgb "DEBUG" 0 160 110)] %s\n" "${message}" >&1 + fi + return 0 + ;; + ERROR) + # Output all error log levels to stderr + printf "${FORMAT}[$(echo_rgb "ERROR" 255 0 0)] %s\n" "${message}" >&2 + return 0 + ;; + # Further log levels can be added by extending this switch statement with more comparisons + + *) # Default case, no matches + # Returns non-zero code as an improper log option was passed, this helps with using `set -e` + printf "${FORMAT}[ERROR] %s\n" "Invalid log level passed, received level \"${level}\" with message \"${message}\"" >&2 + return 1 + ;; + esac +} + +trim() { + local input="${1}" + + while [[ "${input}" == " "* ]]; do + input="${input## }" + done + + while [[ "${input}" == *" " ]]; do + input="${input%% }" + done + + echo "${input}" +} + +# Get a temp working file so we don't pollute directories +TEMP_FILE="$(mktemp)" # Set the Contents directory to the env variable, script set, or to their .contents file in the user home CONTENTS_DIRECTORY="${CONTENTS_DIRECTORY:=\ - $(mkdir -p "${HOME}/.contents" && echo "${HOME}/.contents")}" + $(mkdir -p "${HOME}/.contents" && echo "${HOME}/.contents")}" EXIT_ON_ERROR=0 @@ -29,342 +137,358 @@ WRITE_TEMPLATE_TO="" declare -A template_handlers template_handlers=( - ["FILE"]="file_handler" - ["WRITE-TO"]="write_to_handler" - ["TEMPLATE"]="template_handler" + ["FILE"]="file_handler" + ["WRITE-TO"]="write_to_handler" + ["TEMPLATE"]="template_handler" + ["VAR"]="var_handler" + ["EVAL"]="eval_handler" ) write_content() { - local content - local leading_spaces - content="${1}" - leading_spaces="${2}" + local content + local leading_spaces + content="${1}" + leading_spaces="${2}" - local string_builder - string_builder="" + local string_builder + string_builder="" - for ((i=1; i<=leading_spaces; i++)); do - string_builder="${string_builder} " - done - string_builder="${string_builder}${content}" - _write_to_buffer "${string_builder}" + for ((i = 1; i <= leading_spaces; i++)); do + string_builder="${string_builder} " + done + string_builder="${string_builder}${content}" + _write_to_buffer "${string_builder}" } _write_to_buffer() { - log "debug" "Received content to write to buffer: -${1}" - printf "%s\n" "${1}" >> "${TEMP_FILE}" - log "debug" "Finished writing content to buffer" + log "debug" "Received content to write to buffer: + ${1}" + printf "%s\n" "${1}" >>"${TEMP_FILE}" + log "debug" "Finished writing content to buffer" +} + +var_handler() { + local variable="${1}" + local leading_spaces="${2}" + while IFS="\n" read -r line; do + write_content "${line}" "${leading_spaces}" + done <<<"${variable}" +} + +eval_handler() { + local eval_statement="${1}" + local leading_spaces="${2}" + while IFS="\n" read -r line; do + write_content "${line}" "${leading_spaces}" + done <<<"$(eval "${eval_statement}")" } template_handler() { - local template_file - template_file="${1}" + local template_file + template_file="${1}" - local leading_spaces - leading_spaces="${2}" - # Check that the given file is not an absolute path - if [[ ! "${template_file:0:1}" == "/" ]]; then - template_file="${CONTENTS_DIRECTORY}/${template_file}" - fi - if [[ ! -f "${template_file}" ]]; then - log "error" "The given template file \"${template_file}\" does not exist" - return 1 - fi + local leading_spaces + leading_spaces="${2}" + # Check that the given file is not an absolute path + if [[ ! "${template_file:0:1}" == "/" ]]; then + template_file="${CONTENTS_DIRECTORY}/${template_file}" + fi + if [[ ! -f "${template_file}" ]]; then + log "error" "The given template file \"${template_file}\" does not exist" + return 1 + fi - file_handler "${template_file}" "${leading_spaces}" - REPEAT=0 + file_handler "${template_file}" "${leading_spaces}" + REPEAT=0 } file_handler() { - local file - file="${1}" + local file + file="${1}" - local leading_spaces - leading_spaces="${2}" + local leading_spaces + leading_spaces="${2}" - # Check that the given file is not an absolute path - if [[ ! "${file:0:1}" == "/" ]] && [[ ! -f "${file}" ]]; then - file="${CONTENTS_DIRECTORY}/${file}" - fi + # Check that the given file is not an absolute path + if [[ ! "${file:0:1}" == "/" ]] && [[ ! -f "${file}" ]]; then + file="${CONTENTS_DIRECTORY}/${file}" + fi + if [[ ! -f "${file}" ]]; then + log "error" "The given file \"${file}\" does not exist" + return 1 + fi - if [[ ! -f "${file}" ]]; then - log "error" "The given file \"${file}\" does not exist" - return 1 - fi - - while IFS="\n" read -r line; do - write_content "${line}" "${leading_spaces}" - done < "${file}" + while IFS="\n" read -r line; do + write_content "${line}" "${leading_spaces}" + done <"${file}" } - write_to_handler() { - local write_path - write_path=${1} + local write_path + write_path=${1} - log "debug" "Received write path \"${write_path}\"" - if [[ ! "${write_path:0:1}" == "/" ]]; then - log "error" "Given write path was invalid, received \"${write_path}\", the path must be an absolute path" - return 1 - fi + log "debug" "Received write path \"${write_path}\"" + if [[ ! "${write_path:0:1}" == "/" ]]; then + log "error" "Given write path was invalid, received \"${write_path}\", the path must be an absolute path" + return 1 + fi - if [[ -d "${write_path}" ]]; then - log "error" "Given write path was invalid, received \"${write_path}\", the path must be a path to an output file (doesn't have to exist), not a directory" - return 1 - fi - # This means that it was NOT manually set, 0 is true in shell - if (("${WRITE_TEMPLATE_MANUALLY_SET}" == 1)); then - WRITE_TEMPLATE_TO=${write_path} - fi + if [[ -d "${write_path}" ]]; then + log "error" "Given write path was invalid, received \"${write_path}\", the path must be a path to an output file (doesn't have to exist), not a directory" + return 1 + fi + # This means that it was NOT manually set, 0 is true in shell + if (("${WRITE_TEMPLATE_MANUALLY_SET}" == 1)); then + WRITE_TEMPLATE_TO=${write_path} + fi } parse_template_extracted() { - local tmpl_extracted - tmpl_extracted="${1}" + local tmpl_extracted + tmpl_extracted="${1}" - local full_line - full_line="${2}" + local full_line + full_line="${2}" - local num_leading_spaces - num_leading_spaces=$(awk -F"[ ]" '{for(i=1;i<=NF && ($i=="");i++);print ""i-1""}' <<< "${full_line}") + local num_leading_spaces + num_leading_spaces=$(awk -F"[ ]" '{for(i=1;i<=NF && ($i=="");i++);print ""i-1""}' <<<"${full_line}") - # Basic checks to ensure template is correct, first not empty, second that an '=' was passed to give type specifier - # third is to ensure only one equals sign is passed - if [[ -z "${tmpl_extracted}" ]]; then - log "error" "Template passed was empty... ignoring" - return 1 - elif [[ ! "${tmpl_extracted}" = *"="* ]]; then - log "error" "Template line passed was of an invalid format, missing = specifier, assumed type is missing as well" - return 1 - elif [ "$(echo "${tmpl_extracted}" | grep -o "=" | wc -l)" -gt 1 ]; then - log "error" "Template line passed had too many \"=\", there should only be 1" - return 1 - fi + # Basic checks to ensure template is correct, first not empty, second that an '=' was passed to give type specifier + # third is to ensure only one equals sign is passed + if [[ -z "${tmpl_extracted}" ]]; then + log "error" "Template passed was empty... ignoring" + return 1 + elif [[ ! "${tmpl_extracted}" = *"="* ]]; then + log "error" "Template line passed was of an invalid format, missing = specifier, assumed type is missing as well" + return 1 + elif [ "$(echo "${tmpl_extracted}" | grep -o "=" | wc -l)" -gt 1 ]; then + log "error" "Template line passed had too many \"=\", there should only be 1" + return 1 + fi - local var_type - var_type="$(trim "$(echo "${tmpl_extracted}" | cut -d "=" -f1)")" + local var_type + var_type="$(trim "$(echo "${tmpl_extracted}" | cut -d "=" -f1)")" - # Shellcheck incorrectly reads the below line - # shellcheck disable=SC2116 - var_type="$(echo "${var_type^^}")" + # Shellcheck incorrectly reads the below line + # shellcheck disable=SC2116 + var_type="$(echo "${var_type^^}")" - local var_arg - var_arg="$(trim "$(echo "${tmpl_extracted}" | cut -d "=" -f2)")" + local var_arg + var_arg="$(trim "$(echo "${tmpl_extracted}" | cut -d "=" -f2)")" - local handler_to_use - handler_to_use="${template_handlers[${var_type}]}" + local handler_to_use + handler_to_use="${template_handlers[${var_type}]}" - if [[ -z "${handler_to_use}" ]]; then - log "error" "Invalid handler found, no such handler \"${var_type}\" exists as a handler" - return 1 - else - log "debug" "Using handler \"${handler_to_use}\"" - if ! eval "${handler_to_use}" "${var_arg}" "${num_leading_spaces}"; then - log "error" "Arguments passed for \"${var_type}\" were invalid, check the argument" - return 1 - fi - log "debug" "Finished using handler \"${handler_to_use}\"" - fi + if [[ -z "${handler_to_use}" ]]; then + log "error" "Invalid handler found, no such handler \"${var_type}\" exists as a handler" + return 1 + else + log "debug" "Using handler \"${handler_to_use}\"" + if ! eval "${handler_to_use}" "${var_arg}" "${num_leading_spaces}"; then + log "error" "Arguments passed for \"${var_type}\" were invalid, check the argument" + return 1 + fi + log "debug" "Finished using handler \"${handler_to_use}\"" + fi } output_finished_template() { - log "debug" "Outputting finished template" - mkdir -p "$(dirname "${WRITE_TEMPLATE_TO}")" - # We want globbing here - # shellcheck disable=SC2086 - if [[ -z "${WRITE_TEMPLATE_TO}" ]]; then - log "error" "Was never given a path to write the template to, check your template file or explicitly specify an output location" - exit 1 - else - mv "${TEMP_FILE}" ${WRITE_TEMPLATE_TO} - fi + log "debug" "Outputting finished template" + mkdir -p "$(dirname "${WRITE_TEMPLATE_TO}")" + # We want globbing here + # shellcheck disable=SC2086 + if [[ -z "${WRITE_TEMPLATE_TO}" ]]; then + log "error" "Was never given a path to write the template to, check your template file or explicitly specify an output location" + exit 1 + else + mv "${TEMP_FILE}" ${WRITE_TEMPLATE_TO} + fi } parse_template_line() { - local line - line="${1}" + local line + line="${1}" - local tmpl_extracted - if [[ "${line}" = *\{#%*%#\}* ]]; then - tmpl_extracted="${line#*\{#%}" - tmpl_extracted="${tmpl_extracted%\%#\}*}" - tmpl_extracted="$(trim "${tmpl_extracted}")" + local tmpl_extracted + if [[ "${line}" = *\{#%*%#\}* ]]; then + tmpl_extracted="${line#*\{#%}" + tmpl_extracted="${tmpl_extracted%\%#\}*}" + tmpl_extracted="$(trim "${tmpl_extracted}")" - log "debug" "Extracted a template: ${tmpl_extracted}" - # Check for non zero status codes and parse the extracted template - if ! parse_template_extracted "${tmpl_extracted}" "${line}"; then - return 1 - fi - log "debug" "Finished extracting template: ${tmpl_extracted}" - else - write_content "${line}" "0" - fi + log "debug" "Extracted a template: ${tmpl_extracted}" + # Check for non zero status codes and parse the extracted template + if ! parse_template_extracted "${tmpl_extracted}" "${line}"; then + return 1 + fi + log "debug" "Finished extracting template: ${tmpl_extracted}" + else + write_content "${line}" "0" + fi } read_template() { - local read_file - local line - local line_num - line_num=1 - read_file="${1}" - log "info" "Parsing template file \"${read_file}\"" - while IFS="\n" read -r line; do - # Check for non zero status code - if ! parse_template_line "${line}"; then - log "error" "Invalid template passed from \"${read_file}\", check line ${line_num}" - if (( "${EXIT_ON_ERROR}" == 0 )); then - exit 1 - fi - fi - line_num=$((line_num + 1)) - done <"${read_file}" + local read_file + local line + local line_num + line_num=1 + read_file="${1}" + log "info" "Parsing template file \"${read_file}\"" + while IFS="\n" read -r line; do + # Check for non zero status code + if ! parse_template_line "${line}"; then + log "error" "Invalid template passed from \"${read_file}\", check line ${line_num}" + if (("${EXIT_ON_ERROR}" == 0)); then + exit 1 + fi + fi + line_num=$((line_num + 1)) + done <"${read_file}" } templater() { - local read_file - read_file="${1}" - [[ ! -f "${read_file}" ]] && - log "error" "$(important "${read_file}") does not exist!" && - return 1 + local read_file + read_file="${1}" + [[ ! -f "${read_file}" ]] && + log "error" "$(important "${read_file}") does not exist!" && + return 1 - read_template "${read_file}" + read_template "${read_file}" - if (( "${REPEAT}" == 0 )); then - REPEAT=1 - log "debug" "Repeat set to 0, repeating on the finished file located at ${TEMP_FILE}" - mv "${TEMP_FILE}" "${TEMP_FILE}-repeat" - main "-t" "${TEMP_FILE}-repeat" - else - log "info" "Finished parsing template file \"${read_file}\"" - log "info" "Writing finished template..." - output_finished_template - log "info" "Finished writing template for \"${read_file}\" to \"${WRITE_TEMPLATE_TO}\"" - fi + if (("${REPEAT}" == 0)); then + REPEAT=1 + log "debug" "Repeat set to 0, repeating on the finished file located at ${TEMP_FILE}" + mv "${TEMP_FILE}" "${TEMP_FILE}-repeat" + main "-t" "${TEMP_FILE}-repeat" + else + log "info" "Finished parsing template file \"${read_file}\"" + log "info" "Writing finished template..." + output_finished_template + log "info" "Finished writing template for \"${read_file}\" to \"${WRITE_TEMPLATE_TO}\"" + fi } arg_required() { - echo_rgb "${1}" 255 183 0 + echo_rgb "${1}" 255 183 0 } -arg_optional(){ - echo_rgb "${1}" 117 255 255 +arg_optional() { + echo_rgb "${1}" 117 255 255 } -arg_description(){ - echo_rgb "${@}" 220 190 255 +arg_description() { + echo_rgb "${@}" 220 190 255 } usage() { - # Print out usage instructions for the local script - # - # Arguments: - # None - # - # Usage: - # usage - # - # POSIX Compliant: - # Yes - # - printf "Usage: %s\n" \ - "$(basename ${0}) [OPTIONS] - $(arg_required "REQUIRED") - $(arg_optional "OPTIONAL") + # Print out usage instructions for the local script + # + # Arguments: + # None + # + # Usage: + # usage + # + # POSIX Compliant: + # Yes + # + printf "Usage: %s\n" \ + "$(basename ${0}) [OPTIONS] + $(arg_required "REQUIRED") + $(arg_optional "OPTIONAL") - $(arg_required "--template-file") | $(arg_required "-t") - $(arg_description "A template file to parse and apply rules for + $(arg_required "--template-file") | $(arg_required "-t") + $(arg_description "A template file to parse and apply rules for - Example: - --template-file example.tmpl") + Example: + --template-file example.tmpl") - $(arg_optional "--contents-dir") | $(arg_optional "-c") - $(arg_description "The files that contain contents read by the templates. + $(arg_optional "--contents-dir") | $(arg_optional "-c") + $(arg_description "The files that contain contents read by the templates. - By default this is set to ~/.contents/ if unset, to set via variable you can either use this argument or pass - export CONTENTS_DIRECTORY=\`your contents directory\` + By default this is set to ~/.contents/ if unset, to set via variable you can either use this argument or pass + export CONTENTS_DIRECTORY=\`your contents directory\` - Example: - --contents-dir ~/Desktop/contents") + Example: + --contents-dir ~/Desktop/contents") - $(arg_optional "--output-to") | $(arg_optional "-o") - $(arg_description "The file to output the applied template to, this can be set within a template, but can be overriden with this option + $(arg_optional "--output-to") | $(arg_optional "-o") + $(arg_description "The file to output the applied template to, this can be set within a template, but can be overriden with this option - Example: - --output-to myfile.out") + Example: + --output-to myfile.out") - $(arg_optional "--no-exit") | $(arg_optional "-n") - $(arg_description "Do not exit on any errors, continue executing + $(arg_optional "--no-exit") | $(arg_optional "-n") + $(arg_description "Do not exit on any errors, continue executing - Example: - --no-exit")" + Example: + --no-exit")" } main() { - # Parse input arguments - # - # Arguments: - # Consult the `usage` function - # - # Usage: - # parse_args "$@" - # - All arguments should be ingested by parse_args first for variable setting - # - # POSIX Compliant: - # Yes - # - local template_file + # Parse input arguments + # + # Arguments: + # Consult the `usage` function + # + # Usage: + # parse_args "$@" + # - All arguments should be ingested by parse_args first for variable setting + # + # POSIX Compliant: + # Yes + # + local template_file - while :; do - case ${1} in - -h | -\? | --help) - usage # Display a usage synopsis. - exit - ;; - --) # End of all options. - break - ;; - -t | --template-file) - shift - template_file="${1}" - [[ ! -f "${template_file}" ]] && - log "error" "The given template file \"${template_file}\" does not exist" && - exit 1 - log "info" "Set template file to \"${template_file}\"" - ;; - -c | --contents-dir) - shift - CONTENTS_DIRECTORY="${1}" - [[ ! -d "${CONTENTS_DIRECTORY}" ]] && - log "error" "\"${CONTENTS_DIRECTORY}\" is an invalid path, contents directory must be an absolute path" && - exit 1 - log "info" "Set contents directory to \"${CONTENTS_DIRECTORY}\"" - ;; - -o | --output-to) - shift - WRITE_TEMPLATE_MANUALLY_SET=0 - WRITE_TEMPLATE_TO="${1}" - ;; - -n | --no-exit) - EXIT_ON_ERROR=1 - log "info" "No longer exiting on errors." - ;; - -?*) - printf 'Unknown option: %s\n' "$1" >&2 - usage - exit 1 - ;; - *) # Default case: No more options, so break out of the loop. - break ;; - esac - shift - done - [[ -z "${template_file}" ]] && - log "error" "No template file provided, exiting" && - exit 1 - templater "${template_file}" + while :; do + case ${1} in + -h | -\? | --help) + usage # Display a usage synopsis. + exit + ;; + --) # End of all options. + break + ;; + -t | --template-file) + shift + template_file="${1}" + [[ ! -f "${template_file}" ]] && + log "error" "The given template file \"${template_file}\" does not exist" && + exit 1 + log "info" "Set template file to \"${template_file}\"" + ;; + -c | --contents-dir) + shift + CONTENTS_DIRECTORY="${1}" + [[ ! -d "${CONTENTS_DIRECTORY}" ]] && + log "error" "\"${CONTENTS_DIRECTORY}\" is an invalid path, contents directory must be an absolute path" && + exit 1 + log "info" "Set contents directory to \"${CONTENTS_DIRECTORY}\"" + ;; + -o | --output-to) + shift + WRITE_TEMPLATE_MANUALLY_SET=0 + WRITE_TEMPLATE_TO="${1}" + ;; + -n | --no-exit) + EXIT_ON_ERROR=1 + log "info" "No longer exiting on errors." + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + usage + exit 1 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + [[ -z "${template_file}" ]] && + log "error" "No template file provided, exiting" && + exit 1 + templater "${template_file}" } main "$@" diff --git a/Misc/Templater.md b/Misc/Templater.md index 84eb6f9..56f8b60 100644 --- a/Misc/Templater.md +++ b/Misc/Templater.md @@ -17,7 +17,32 @@ For example - This is used to tell the current process where to write to, only one should exist in a template and sub-templates if multiple templates are chained together - TEMPLATE - - This is used to parse the contents of another template and include the contents that are parsed. + - This is used to parse the contents of another template and include the contents that are parsed. For example, say we have a primary template `main.tmpl` that uses the sub-template `sub.tmpl` then when the parsing process begins it will also parse the contents of `sub.tmpl` and append them to the contents parsed by `main.tmpl` +- VAR + - This is used when you want to expand a variable from your environment, say "${HOME}" +- EVAL + - This is used when you want to evaluate a statement in bash, be careful as any errors here can really nuke things and + cause security issues. + + +## Example + +Given a template file with content like so: + +``` +{#% VAR=$HOME %#} +{#% EVAL="ls -alh" %#} +``` + +The output would be something like +``` +/home/username +total 52K +drwxr-xr-x 1 sam sam 196 Sep 15 17:03 . +drwxr-xr-x 1 sam sam 94 Sep 15 16:42 .. +-rwxr-xr-x 1 sam sam 13K Sep 15 17:01 Templater.bash +-rw-r--r-- 1 sam sam 1.1K Sep 15 17:03 Templater.md +```