Last active
August 29, 2015 14:19
-
-
Save nfarrar/e3e5b1c0c79f432fbf36 to your computer and use it in GitHub Desktop.
WIP: Configuration mangement ... in bash.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# Author: Nathan Farrar <[email protected]> | |
# 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