Skip to content

Instantly share code, notes, and snippets.

@nfarrar
Last active August 29, 2015 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nfarrar/e3e5b1c0c79f432fbf36 to your computer and use it in GitHub Desktop.
Save nfarrar/e3e5b1c0c79f432fbf36 to your computer and use it in GitHub Desktop.
WIP: Configuration mangement ... in bash.
#!/usr/bin/env bash
# Author: Nathan Farrar <nfarrar@crunk.io>
# Website: http://dotfiles.crunk.io/
#
#/ Usage: source _lib.bash
#/ bash _lib.bash
#/
#/ This script is required by all the other scripts in bootstrap & modules
#/ directories. It provides a set of reusable 'library' functions for building
#/ small, cross-platform scripts (bundled as modules) that install & configure
#/ system resources.
# Do not re-source this library.
[[ ${_BOOTSTRAP_LIB_LOADED:-false} == true ]] && return 0
_BOOTSTRAP_LIB_LOADED=true
# --- [ CONFIGURATION ] ---------------------------------------------------- #
BOOTSTRAP_ROOT_PATH=${BOOTSTRAP_ROOT_PATH:-$HOME/.crib/bootstrap}
BOOTSTRAP_CFG_PATH=${BOOTSTRAP_CFG_PATH:-$BOOTSTRAP_ROOT_PATH/_cfg.bash}
BOOTSTRAP_LIB_PATH=${BOOTSTRAP_LIB_PATH:-$BOOTSTRAP_ROOT_PATH/_lib.bash}
BOOTSTRAP_MODULES_PATH=${BOOTSTRAP_MODULES_PATH:-$BOOTSTRAP_ROOT_PATH/modules}
USER_CFG_DIR=${USER_CFG_DIR:-$HOME/.config}
USER_BIN_DIR=${USER_BIN_DIR:-$HOME/.local/bin}
USER_SRC_DIR=${USER_SRC_DIR:-$HOME/.local/src}
TMPDIR=${TMPDIR:-/tmp}
LOGFILE=${LOGFILE:-$TMPDIR/crib.log}
DBGMSGS=${DBGMSGS:-true}
LOGMSGS=${LOGMSGS:-true}
# To override the default settings, define them in a local configuration
# file and export the path to your configation file in your shell.
if [[ -f $BOOTSTRAP_CFG_PATH ]]; then
source $BOOTSTRAP_CFG_PATH ]]
fi
# --- [ ERROR & SIGNAL HANDLING ] ----------------------------------------- #
# Standard exit status codes.
_EX_SUCCESS=0
_EX_FAILURE=1
_EX_BUILTIN_MISUSE=2
_EX_USAGE=64
_EX_DATAERR=65
_EX_NOINPUT=66
_EX_NOUSER=67
_EX_NOHOST=68
_EX_UNAVAILABLE=69
_EX_SOFTWARE=70
_EX_OSERR=71
_EX_OSFILE=72
_EX_CANTCREAT=73
_EX_IOERR=74
_EX_TEMPFAIL=75
_EX_PROTOCOL=76
_EX_NOPERM=77
_EX_CONFIG=78
# Global flag used by exit & error handlers to prevent duplicate tracebacks
# from being displayed.
_DISPLAYED_TRACEBACK=false
# Display an error message and exit. This is called when things go wrong,
# so it intentionally depends on nothing else.
function errexit() {
local _errmsg=${1:-An unexpected error occurred. Terminating.}
local _errstatus=${2:-1}
printf "\x1b[31;1m%s\x1b[0m\n" "${_errmsg}" 1>&2
_traceback 1
_DISPLAYED_TRACEBACK=true
exit ${_errstatus}
}
# Private function, called by exit and error signal handlers. Displays a
# python-style stacktrace.
function _traceback() {
# Hide the traceback() call.
local -i _start_frame=$(( ${1:-0} + 1 ))
local -i _end_frame=${#BASH_SOURCE[@]}
local -i i=0
local -i j=0
printf "\x1b[31m" 1>&2
echo "Traceback (last called is first):" 1>&2
for ((i=${_start_frame}; i < ${_end_frame}; i++)); do
j=$(( $i - 1 ))
local function="${FUNCNAME[$i]}"
local file="${BASH_SOURCE[$i]}"
local line="${BASH_LINENO[$j]}"
echo " ${function}() in ${file}:${line}" 1>&2
done
printf "\x1b[0m" 1>&2
}
# Private function called on exit signal. If exit status is non-zero, the
# _traceback function is called to display a python-style stacktrace.
function _exit_trap() {
local _exit_code="$?"
local _stack_frame=1
if [[ $_exit_code != 0 && "${_DISPLAYED_TRACEBACK}" != true ]]; then
_traceback ${_stack_frame}
fi
}
# Private function called on error signal. Calls the _traceback function to
# display a python-style stacktrace.
function _err_trap() {
local _exit_code="$?"
local _stack_frame=1
local _err_cmd="${BASH_COMMAND:-unknown}"
local _err_msg="The command ${_err_cmd} exited with exit code ${_exit_code}." 1>&2
_traceback ${_stack_frame}
_DISPLAYED_TRACEBACK=true
printf "\x1b[31m%s\x1b[0m\n" "${_err_msg}" 1>&2
}
# Set the EXIT and ERR signal handling functions.
trap _exit_trap EXIT
trap _err_trap ERR
# --- [ MISCELLANEOUS ] ---------------------------------------------------- #
# "Print" function. Wraps printf and redirects "user" output to to stderr.
function _print() {
printf "${*:-}\n" 1>&2
}
# "Return function". Dumps $1 to stdout.
function _return() {
printf "${*:-}" 1>&1
}
# Escape special characters in a string.
function _get_escaped_string() {
local _unescaped="$*"
_return "$(echo "${somevar}" | sed -e 's/[^][a-zA-Z0-9/.:?,;(){}<>=*+-]/\\&/g' )"
}
function _get_timestamp() {
_return "$(date +%FT%T%Z)"
}
function _pushd() {
_pushd $1 > /dev/null
}
function _popd() {
_popd $1 > /dev/null
}
# --- [ MESSAGES ] --------------------------------------------------------- #
# Text formatting bindings.
_NORMAL=$(tput sgr0)
_BOLD=$(tput bold)
_UNDERLINE=$(tput smul)
_RED=$(tput setaf 1)
_GREEN=$(tput setaf 2)
_YELLOW=$(tput setaf 3)
_BLUE=$(tput setaf 4)
_MAGENTA=$(tput setaf 5)
_CYAN=$(tput setaf 6)
_WHITE=$(tput setaf 7)
# Set messaging defaults.
_LOGSTART=false
# Enable debug messages.
function enable_debugging() {
DBGMSGS=true
}
# Enable debug messages.
function disable_debugging() {
DBGMSGS=false
}
# Toggle debug messages.
function toggle_debugging() {
if [[ ${DBGMSGS:-false} == false ]]; then
DBGMSGS=true
else
DBGMSGS=false
fi
}
# Enable message logging.
function enable_logging() {
LOGMSGS=true
}
# Enable message logging.
function disable_logging() {
LOGMSGS=false
}
# Toggle debug messages.
function toggle_logging() {
if [[ ${DBGMSGS:-false} == false ]]; then
LOGMSGS=true
else
LOGMSGS=false
fi
}
# Write messages to the log file if logging is enabled.
function _log_msg() {
local _level=${1:-UNDEFINED}
local _msg=${2:-Undefined message}
if [[ ${LOGMSGS} == true ]]; then
# Automatically write a log separator when the first log line is written.
if [[ ${_LOGSTART} == false ]]; then
_log_separator
_LOGSTART=true
fi
printf "$(_get_timestamp) %-12s%s\n" "${_level}" "${_msg}" &>> "${LOGFILE}"
fi
}
# Write a separator line to the log file.
function _log_separator() {
printf "%0.1s" "-"{1..80} &>> "${LOGFILE}"
printf "\n" &>> "${LOGFILE}"
}
# Display information messages (these are always displayed).
function info() {
local _msg="$*"
_log_msg "INFO" "${_msg}"
echo -e "${_GREEN}${_msg}${_NORMAL}" 1>&2
}
# Display error messages (these are always displayed).
function error() {
local _msg="$*"
_log_msg "ERROR" "${_msg}"
echo -e "${_RED}${_msg}${_NORMAL}" 1>&2
}
# Display warning messages (these are always displayed).
function warning() {
local _msg="$*"
_log_msg "WARNING" "${_msg}"
echo -e "${_MAGENTA}${_msg}${_NORMAL}" 1>&2
}
# Display debug messages (these are only displayed if DEBUG is true).
function debug() {
local _msg="$*"
_log_msg "DEBUG" "${_msg}"
if [[ ${DBGMSGS:-false} == true ]]; then
local _msg="$*"
echo -e "${_CYAN}${_msg}${_NORMAL}" 1>&2
fi
}
# --- [ USER INPUT ] ------------------------------------------------------- #
# - http://stackoverflow.com/questions/1989439/shell-function-to-prompt-for-and-return-input
# - https://github.com/vbarbarosh/w132_bash_input_readchar/blob/master/bin/bash_input_readchar
function _read_secret() {
read -s -p
}
function prompt_bool() {
_return true
}
function prompt_input() {
_return true
}
function prompt_secret() {
_return true
}
# Prompt the user for a true/false response using the read function. Depending
# on the user's response, true or false will be written to stdout. All
# feedback and formatting are written to stderr. This catches invalid input
# and continues to reprompt user for y/n until a valid answer has been
# entered. This is considered a private function and should not be called
# directly (the internals may change). Use the prompt_bool function.
function _read_bool() {
local _valid_response=false
while [[ ${_valid_response} != true ]]; do
printf "${_YELLOW}${1} (y/n): ${_NORMAL}" 1>&2
read -n 1 -r -p ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
_valid_response=true
_return true
_print "\n"
elif [[ $REPLY =~ ^[Nn]$ ]]; then
_valid_response=true
_return false
_print "\n"
else
_print " ${_RED}... invalid response, enter y or n.${_NORMAL}\n"
fi
done
}
#
function _read_input() {
local _valid_response=false
while [[ ${_valid_response} != true ]]; do
_print "${_YELLOW}${1} (y/n): ${_NORMAL}"
read -n 1 -r -p ""
done
}
# --- [ SYSTEM INFORMATION ] ----------------------------------------------- #
# Returns the operating system name.
function _get_os_name() {
if [[ ${OSTYPE:-false} == false ]]; then
errexit "\$OSTYPE is not set. Unsupported shell or operating system." "${_EX_OSERR}"
elif [[ $OSTYPE == cygwin ]]; then
_return "cygwin"
elif [[ $OSTYPE == darwin* ]]; then
_return "macosx"
elif [[ $OSTYPE == linux-gnu ]] && [[ -f /etc/os-release ]]; then
source /etc/os-release
_return "$ID"
else
errexit "$OSTYPE is not a supported platform." "${_EX_OSERR}"
fi
}
# Returns the operating system version.
function _get_os_version() {
if [[ ${OSTYPE:-false} == false ]]; then
errexit "\$OSTYPE is not set. Unsupported shell or operating system." "${_EX_OSERR}"
elif [[ $OSTYPE == cygwin ]]; then
_return $(uname -r)
elif [[ $OSTYPE == darwin* ]]; then
_return "$(sw_vers -productVersion)"
elif [[ $OSTYPE == linux-gnu ]] && [[ -f /etc/os-release ]]; then
source /etc/os-release
_return "$VERSION_ID"
else
errexit "$OSTYPE is not a supported platform." "${_EX_OSERR}"
fi
}
# Returns the operating system architecture.
function _get_os_architecture() {
if [[ ${OSTYPE:-false} == false ]]; then
errexit "OSTYPE is not set. Unsupported shell or operating system." "${_EX_OSERR}"
elif [[ $OSTYPE == cygwin ]]; then
_return "$($(uname -m) | cut -f1 -d '(')"
elif [[ $OSTYPE == darwin* ]]; then
_return "$(uname -m)"
elif [[ $OSTYPE == linux-gnu ]] && [[ -f /etc/os-release ]]; then
_return "$(arch)"
else
errexit "$OSTYPE is not a supported platform." "${_EX_OSERR}"
fi
}
# Returns true if executing on cygwin.
function _is_cygwin() {
if [[ $(_get_os_name) == cygwin ]]; then
_return true
else
_return false
fi
}
# "Return" true if executing on OSX.
function _is_macosx() {
if [[ $(_get_os_name) == macosx ]]; then
_return true
else
_return false
fi
}
# "Return" true if executing on Ubuntu.
function _is_ubuntu() {
if [[ $(_get_os_name) == ubuntu ]]; then
_return true
else
_return false
fi
}
# --- [ MODULES ] ---------------------------------------------------------- #
# Returns the name of the last module that was loaded.
function _get_module_name() {
local _scriptpath _mpath
for (( i=${#BASH_SOURCE[@]}-1 ; i>=0 ; i-- )) ; do
if [[ ${BASH_SOURCE[i]} != *_lib.bash ]]; then
pushd $(dirname ${BASH_SOURCE[i]}) > /dev/null
_return "$(basename $(pwd))"
popd > /dev/null
break
fi
done
}
# Returns the path to the specified module.
function _get_module_path() {
[[ $# -ne 1 ]] && errexit "Invalid number of arguments." ${_EX_USAGE}
local _module_name=$1
_return ${BOOTSTRAP_MODULES_PATH}/${_module_name}
}
# Return a space-deliminted string containing the names of the available
# modules.
function _get_module_list() {
local _dirname
declare -a _modules=()
for _dirname in $(find $BOOTSTRAP_MODULES_PATH/* -type d); do
_modules+=($(basename $_dirname))
done
_return ${_modules[@]}
}
# Execute a module's platform-specific initialization routine.
function module_init() {
[[ $# -ne 1 ]] && errexit "Invalid number of arguments. Module name is required." ${_EX_USAGE}
local _module_name=$1
local _module_path=$(_get_module_path ${_module_name})
local _init_function="init_$(_get_os_name)"
info "Initializing module '${_module_name}'."
source ${_module_path}/init.bash
$_init_function
}
# Execute a module's platform-specific deinitialization routine.
function module_deinit() {
[[ $# -ne 1 ]] && errexit "Invalid number of arguments. Module name is required." ${_EX_USAGE}
local _module_name=$1
local _module_path=$(_get_module_path ${_module_name})
local _deinit_function="deinit_$(_get_os_name)"
info "Deinitializing module '${_module_name}'."
source ${_module_path}/deinit.bash
$_deinit_function
}
# --- [ MODULE HELPERS ] --------------------------------------------------- #
function install_package_aptcyg() {
[[ $# -ne 1 ]] && errexit "Invalid number of arguments. Package name is required." ${_EX_USAGE}
cyg-apt install "$1"
}
function install_package_brew() {
[[ $# -ne 1 ]] && errexit "Invalid number of arguments. Package name is required." ${_EX_USAGE}
brew install "$1"
}
function install_package_apt() {
[[ $# -ne 1 ]] && errexit "Invalid number of arguments. Package name is required." ${_EX_USAGE}
if ! dpkg -s silversearch-ag; then
info "Installing package $1."
sudo apt-get -y install "$1"
else
info "Package $1 is already installed."
fi
}
function _git_clone() {
[[ $# -ne 2 ]] && errexit "Invalid number of arguments. URL & path are required." ${_EX_USAGE}
local _giturl=$1
local _srcpath=$2
if type git >/dev/null 2>&1; then
if [[ ! -d ${_srcpath} ]]; then
git clone "${_giturl}" "${_srcpath}"
else
warning "Directory already present at ${_srcpath}."
fi
else
errexit "Git command is not available. Terminating."
fi
}
# --- [ TESTS ] ------------------------------------------------------------ #
# Only execute if _lib.bash is executed directly (not being sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
# Enable strict execution.
set -euo pipefail
debug "Executing _lib.bash tests."
# Test message functions.
info "This is an info message (always displayed)."
error "This is an error message (always displayed)."
warning "This is an warning message (always displayed)."
debug "This is a debug message (only displayed if DBGMSGS==true)."
# Display current settings.
info "DBGMSGS: $DBGMSGS"
info "LOGMSGS: $LOGMSGS"
info "BOOTSTRAP_ROOT_PATH: $BOOTSTRAP_ROOT_PATH"
info "BOOTSTRAP_MODULES_PATH: $BOOTSTRAP_MODULES_PATH"
info "USER_CFG_DIR: $USER_CFG_DIR"
info "USER_BIN_DIR: $USER_BIN_DIR"
info "USER_SRC_DIR: $USER_SRC_DIR"
info "TMPDIR: $TMPDIR"
info "LOGFILE: $LOGFILE"
# # Test _read_bool with variable assignment.
# choice=$(_read_bool "Can we correctly assign a response to a variable?")
# info "User response was $choice."
# # Test _read_bool in if statement.
# if [[ $(_read_bool "Can we test the response in an if statement?") == true ]]; then
# info "User entered true."
# else
# info "User entered false."
# fi
# Test the operating system information functions.
info "OS Name: $(_get_os_name)."
info "OS Version: $(_get_os_version)."
info "OS Architecture: $(_get_os_architecture)."
# Test output from is_os functions.
info "Current platform is cygwin: $(_is_cygwin)."
info "Current platform is cygwin: $(_is_macosx)."
info "Current platform is cygwin: $(_is_ubuntu)."
# Test output from is_os functions in if statements.
if [[ $(_is_cygwin) == true ]]; then
info "Current platform is cygwin."
elif [[ $(_is_macosx) == true ]]; then
info "Current platform is macosx."
elif [[ $(_is_ubuntu) == true ]]; then
info "Current platform is ubuntu."
fi
# Test module loading.
declare -a modules=($(_get_module_list))
info "Available modules: ${modules[@]}"
# set -x
module_init "example"
module_deinit "example"
# set +x
# Text the errexit function. This will terminate the execution of this script with a non-success exit status.
errexit "This is a call to errexit with a status of 64." "${_EX_USAGE}"
info "This message occurs after the call to errexit and should not be displayed."
fi
# Notify user this file has been loaded if debugging is enabled.
debug "Finished loading _lib.bash."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment