fsh/f.sh

456 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# # DESCRIPTION #
# functions & utilities for Debian-ish Linux shell.
#
# # LEGAL #
# This is a dangerous toy, read carefully before using in your shop.
# Test it in a copy of development, with your use case, first.
#
# And for Zod's unwholly snake,
### VERIFY YOUR BACKUPS BEFORE USING ###
# ...please.
#
# shellharden & version minimum boilerplate:
if test "$BASH" = "" || "$BASH" -uc "a=();true \"\${a[@]}\"" 2>/dev/null; then
# Bash 4.4, Zsh
set -Eeuox pipefail
else
echo "this script helper requires bash v4.4+, your version is insufficient"
bash --version
exit 13
fi
# ???
shopt -s nullglob globstar
# This allows aliases to be used inside the program.
shopt -s expand_aliases
#
# # METHODOLOGY #
# Write for the user, optimize for the program, develop for automagic deployment.
#
# To my future self,
# This code is an effort,
# dedicated to you,
# and to debugging the crufty parts one function at a time
#
# like real programmers do.
#
# # @TODO - generate automatically #
#
# @todo call, fork, disown, end?
# Variable Factory
## AUTOGENERATED "VFACTORY" SECTION, EDIT IN `compile.sh` ###
#
# If directly sourced, initialize variables locally and setup alias expansion.
if [ "${0}" == "${BASH_SOURCE}" ]; then
# This script uses three associative arrays (see #requirements section)
# to maintain isolation between conceptual paradigms.
# While combining these would be possible, it renders the resulting code
# less approachable, at least from this author's perspective.
#
# As with any object-oriented-adjacent programming, be mindful of
# the cascading effects of refactoring core code.
declare -A envConf commandRegistry activeParameters
fi
# Script version
envConf['version']='0.9.2' # <-- @todo - autopopulate this
# environment investigation preliminaries, configuration is the next block.
### AUTOGENERATED "ENVIRONMENT" SECTION, EDIT IN `compile.sh` ###
#
envConf['scriptName']="$(basename ${0})"
envConf['description']="Functions core program, enables alias usage, etc."
envConf['basePath']="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))/"
envConf['corePath']="${envConf['basePath']}core/"
envConf['dateTimeFormat']='%Y.%m.%d_%H.%M'
envConf['secondsTimeFormat']="${envConf['dateTimeFormat']}.%S"
envConf['preciseTimeFormat']="${envConf['secondsTimeFormat']}.%N"
### END AUTOGEN "ENVIRONMENT" SECTION, "CONFIGURATION" IS NEXT ###
clear
# --- this is the section you're probably looking for ---#
# User-editable configuration variables
#
# Check for config usage in entire repository before removing anything.
# That's just good programming, yeah?
# Set the command character.
envConf['commandChar']='-'
# Commands help, short and long
commandRegistry['help']="command/usage info."
commandRegistry['version']="print contents of '\${envConf['version']}'"
commandRegistry['logLevel']="(0-9) error/debug/status text filtering level."
commandRegistry['file']="Pass in a file reference."
commandRegistry['persist']="Confirms write/install/execution, enables persistence."
# external capabilities
commandRegistry['depends']='Installs dependencies from <file/>.'
# this is the dangerous one, use carefully.
commandRegistry['compile']='Verifies, prepares, and rep-packages this script.'
# commandRegistry['']=''
# alias file, super important
envConf['aliases']="aliases.src"
# default of 2, 0 for quiet, 9 being 'all'
# Set verbosity to default if uninitialized
if [ -z "${envConf['logLevel']+set}" ];then
envConf['logLevel']=3
fi
### END AUTOGEN "ENVIRONMENT" SECTION, "POST-CONFIGURATION" IS NEXT ###
# --- configuration is just above here --- #
# environment setup post-config
### AUTOGENERATED "POST-CONFIGURATION" SECTION, EDIT IN `compile.sh` ###
#
#
envConf['aliases']="${envConf['corePath']}${envConf['aliases']}"
# make sure the command aliases file path leads to something useable...
if [ ! -f "${envConf[aliases]}" ]; then
echo "Debugging time..."
exit 13
fi
# Load the command aliases!
source "${envConf['aliases']}"
# Temporary directory location
envConf['tmpDir']="$(realpath ${envConf['corePath']}../tmp)/"
# Logging
envConf['logFile']="${envConf['tmpDir']}"
envConf['logFile']+="${envConf['scriptName']}."
envConf['logFile']+=$(date +"${envConf['dateTimeFormat']}")
envConf['logFile']+=".log"
# Ensure temporary directory exists
if [ ! -d "${envConf['tmpDir']}" ]; then
mkdir -p "${envConf['tmpDir']}"
fi
# Process ID file location
envConf['pidFile']="${envConf['tmpDir']}${envConf['scriptName']}.pid"
# touch all the things
touch "${envConf['logFile']}"
touch "${envConf['pidFile']}"
### END AUTOGEN "ENVIRONMENT" SECTION, "POST-CONFIGURATION" IS NEXT ###
### Functions ###
function logThis {
# the passed-in message, sanitize slightly
local message="${1/$(whoami)/'$(whoami)'}" # @todo probably need to sanitize the message somehow.
if [ -z "${message}" ];then
message="\t"
fi
# activeParameterseter 2 is optional...
local logging_level
# ...but we check if they're set, and if not, attempt to set from globals
if [ ! -z "${2+set}" ] ; then
# Set the log level (ideally) from the passed-in value...
logging_level="${2}"
elif [ ! -z "${envConf['logLevel']+set}" ] ; then # config is set, passin isn't.
# ...next from the script default...
logging_level="${envConf['logLevel']}"
else
# finally, if none of the default are set, manually set it to quietest
logging_level=9
# @todo eliminate this magic number
fi
local dateTimeNow="$(date +"${envConf['secondsTimeFormat']}")"
if [ "${logging_level}" -le "${envConf['logLevel']}" ] ; then
echo -e "${dateTimeNow}: ${message}" | tee -a "${envConf['logFile']}"
else
echo -e "${dateTimeNow}: ${message}" >> "${envConf['logFile']}"
fi
}
# Outputs a horizontal line to the logfile
# Syntax is logBreak <optional verbosity[0-9]/>
function logBreak {
local logging_level=2
if [ ! -z "${1+set}" ];then
logging_level="${1}"
fi
logThis "#######################################################################" "${logging_level}"
}
# Outputs a horizontal line to the logfile
# Syntax is logBreak <optional verbosity[0-9]/>
function logLine {
local logging_level=2
if [ ! -z "${1+set}" ];then
logging_level="${1}"
fi
logThis "----------------------------------------------------------------------" "${logging_level}"
}
function version {
logThis "\t'${envConf['scriptName']}' version is ${envConf['version']}" 1
}
# Display script usage information and flags.
function help {
logThis "" 1
logThis "Description:"
logThis "${envConf['description']}" 1
logThis "" 1
version
logThis "" 1
logThis "Commands available in '${envConf['scriptName']}':" 1
# iterate through command descriptions and display to the user
local cmdString
for cmd in "${!commandRegistry[@]}"; do
cmdString="\t${envConf['commandChar']}${envConf['commandChar']}${cmd}"
cmdString+="|${envConf['commandChar']}${cmd:0:1}"
cmdString+="\t\t"
cmdString+=${commandRegistry["${cmd}"]}
logThis "${cmdString}" 1
done
}
# @todo - actually implement this
function quiet {
envConf['logLevel']=0
export envConf
}
# sanitize and process short parameters
# short params are a single command character followed by one or more character(s)
# intended as shorthand and ease-of-prototyping flags
#
# function accepts a single input, but this is not enforced or used well
function processShortParams {
# skip if nothing was passed to the function
if [ ! -z "${@}" ]; then
# Local variable for manipulating/using input
local inputParamString="${@}"
#force this variable to re-initialize
activeParameters[0]=''
for key in ${!commandRegistry[@]}; do
if [ "${inputParamString:1:2}" == "${key:0:1}" ];then
# load directly from the registered commands list, both for reasons of
# single authoritative command source (eliminates broken chains) and
# to avoid the possibility of passed-in string shenanigan.
#
# use the current length of the param array to set the index for the
# new entry.
activeParameters["${#activeParameters[@]}"]="${key}"
logThis "...command '"${key}"' acknowledged." 1
fi
done
fi
}
function processLongParams {
# fail early if there's nothing passed to the function
if [ "${@}" ]; then
# Local variable for manipulating/using input
local inputParamString="${@}"
activeParameters[0]=''
# Remove the command sequence from the string
if [ "${envConf['commandChar']}${envConf['commandChar']}" == "${inputParamString:0:2}" ]; then
activeParameters[${#activeParameters[@]}]="${inputParamString:2:${#inputParamString}}"
fi
fi
}
# Waste management is pretty important.
function cleanup {
# signal that the exit trap was successful, for debugging
# use 'exit 13'
if [ "${?}" == 13 ];then
echo "failmuffins"
fi
# Ensure that child processes have finished before designating time of exit.
wait
# Make sure that the temp directory exists
if [ -d "${envConf['tmpDir']}" ]; then
# Check for process ID files.
if [ -f "${envConf['pidFile']}" ]; then
# If the PID file starts with 'LOGGING:', do an end-of-logging printout
if [ "$(cat ${envConf['pidFile']}|cut -d':' -f2)" == "LOGGING" ]; then
logThis "" 1
logThis "" 1
logThis "" 1
logThis "\t---" 1
logThis "\tEND" 1
logThis "\t---" 1
logBreak
fi
rm "${envConf['pidFile']}"
fi
fi
# Let the user know when the script ended.
echo
echo
echo
echo -e "\t\t\t--- cleanup ---"
echo
echo "Completed at:"
when
exit 0
}
function compile {
cd "${envConf['basePath']}"
logThis "" 0
logThis "begin live compile..." 1
./core/compile.sh "${envConf['aliases']}" && compile
logThis "...recompiling." 1 && exit 13
}
### END 'functions' ###
# Print out the date and time for the user
when;echo
# Inform the user of their environment
logBreak;logThis "\t\tExecuting via '$(which f)'" 1
# Timestamp descriptor, must match time/date format config & logThis
echo -e "YYYY.MM.DD_HH.MM.SS\t\t(chronostamp format via 'date +\"${envConf['secondsTimeFormat']}\"')"
# Visual break. set logging status in PID file
logBreak && echo "$$:LOGGING:${envConf['logFile']}" > "${envConf['pidFile']}"
# Inform the user of file locations
logThis "logging to: '${envConf['logFile']}'" 1
logThis "process ID file at: '${envConf['pidFile']}'" 1
logBreak
logThis "" 1
# On exit, do cleanup
#trap "cleanup ${@}" EXIT # <-- attempt to pass arguments to the exit function.
trap cleanup EXIT
# Pass input to the command parser if command character is found
# if no parameters have been passed, display the help
logThis "Processing parameters..." 1
logThis "" 1
for key in ${!commandRegistry[@]}; do
logThis "--> parameter '${key}' registered." 3
done
logThis "" 1
logThis "" 1
logThis "Processed parameter execution..."
# checking command line inputs, display usage help if none.
if [ -z "${@+set}" ]; then
logThis "...none found, displaying usage information." 1
logThis "" 1
help
else
# @todo - convert to use getopts, see http://wiki.bash-hackers.org/howto/getopts_tutorial
for flag in "${@}"; do
case "${flag}" in
# parse any short flags (single command char)
${envConf['commandChar']}[a-zA-Z0-9_]*)
logThis "...short form parameter '${flag}' found..." 1
processShortParams "${flag}"
;;
# parse long params (double command char)
${envConf['commandChar']}${envConf['commandChar']}[a-zA-Z0-9_]*)
logThis "...long form parameter '${flag}' found..." 1
processLongParams "${flag}"
;;
*)
logThis "command line input '${flag}' not registered." 1
echo
;;
esac
done
fi
logThis "" 1
logThis "Input processing complete."
if [ ! -z "${activeParameters[@]+set}" ]; then
logThis "" 1
logThis "" 1
logBreak
logThis "\t Begin parameter processing:"
logBreak
logThis "" 1
logThis "found '$(echo ${activeParameters[@]})', will attempt to process in order." 1
logThis "" 1
for parameter in ${activeParameters[@]}; do
logThis "Directly executing '${parameter}' function..."
logLine
eval "${parameter}"
logThis "" 1
logLine
logThis "done with direct '${parameter}' execution." 1
logThis "" 1
logThis "Core execution attempt..."
if [ -f "core/${parameter}.sh" ]; then
eval "core/${parameter}.sh" && logThis "...success."
else
logThis "...no core file found."
fi
logThis "" 1
logThis "...done with '${parameter}' flag execution." 1
logBreak
logThis "" 1
logThis "" 1
done
logThis "" 1
logBreak
fi