#!/usr/bin/env bash 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 # local red local green local blue local input input="${1}" red="${2}" green="${3}" blue="${4}" printf "\e[0;38;2;%s;%s;%sm%s\e[m\n" "${red}" "${green}" "${blue}" "${input}" } 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 # 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")}" EXIT_ON_ERROR=0 REPEAT=1 WRITE_TEMPLATE_MANUALLY_SET=1 WRITE_TEMPLATE_TO="" declare -A template_handlers template_handlers=( ["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 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}" } _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" } 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 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() { local file file="${1}" 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 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}" } write_to_handler() { 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 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 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}") # 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)")" # 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 "=" -f 2-)")" 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 } 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 } parse_template_line() { local line line="${1}" 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 } 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}" } templater() { local read_file read_file="${1}" [[ ! -f "${read_file}" ]] && log "error" "$(important "${read_file}") does not exist!" && return 1 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 } arg_required() { echo_rgb "${1}" 255 183 0 } arg_optional() { echo_rgb "${1}" 117 255 255 } arg_description() { echo_rgb "${@}" 220 190 255 } usage() { # Print out usage instructions for the local script # # Arguments: # None # # Usage: # usage # # POSIX Compliant: # Yes # cat < | $(arg_required "-t") $(arg_description "A template file to parse and apply rules for Example: --template-file example.tmpl") $(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\` 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 Example: --output-to myfile.out") $(arg_optional "--no-exit") | $(arg_optional "-n") $(arg_description "Do not exit on any errors, continue executing Example: --no-exit") EOF } 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 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! Check \`-h\` for help." && exit 1 templater "${template_file}" } main "$@"