diff --git a/bashrc b/bashrc index be2ccf7..4b995c7 100644 --- a/bashrc +++ b/bashrc @@ -6,6 +6,7 @@ # Bash config is split into modules, loaded in the order listed below: modules=( + funcs bugs path terminal @@ -20,6 +21,7 @@ modules=( rust video local + unset_funcs ) for mod in "${modules[@]}"; do source ~/.config/bash/"${mod}.sh" diff --git a/config/bash/alias.sh b/config/bash/alias.sh index feb29d3..2f3edc7 100644 --- a/config/bash/alias.sh +++ b/config/bash/alias.sh @@ -1,3 +1,6 @@ +alias rm='rm -i' +alias cp='cp -i' +alias mv='mv -i' alias ls='ls --color=auto' alias grep='grep --color=auto' alias bb='rlwrap bb' diff --git a/config/bash/bugs.sh b/config/bash/bugs.sh index 59a2967..50d02f3 100644 --- a/config/bash/bugs.sh +++ b/config/bash/bugs.sh @@ -1,3 +1,6 @@ # Some distros (not most) automatically set USERNAME, a redundant copy of USER. # Since this is non-standard, unset it: unset USERNAME + +# Uncomment the following line if you don't like systemctl's auto-paging feature: +# export SYSTEMD_PAGER= diff --git a/config/bash/funcs.sh b/config/bash/funcs.sh new file mode 100644 index 0000000..fbbb1f5 --- /dev/null +++ b/config/bash/funcs.sh @@ -0,0 +1,368 @@ +stderr(){ echo "$@" >/dev/stderr; } +error(){ stderr "Error: $@"; } +fault(){ test -n "$1" && error $1; stderr "Exiting."; exit 1; } +cancel(){ stderr "Canceled."; exit 2; } +exe() { (set -x; "$@"); } +print_array(){ printf '%s\n' "$@"; } +trim_trailing_whitespace() { sed -e 's/[[:space:]]*$//'; } +trim_leading_whitespace() { sed -e 's/^[[:space:]]*//'; } +trim_whitespace() { trim_leading_whitespace | trim_trailing_whitespace; } +upper_case() { tr '[:lower:]' '[:upper:]'; } +lower_case() { tr '[:upper:]' '[:lower:]'; } +wizard() { ${BIN}/script-wizard "$@"; } +check_var(){ + local __missing=false + local __vars="$@" + for __var in ${__vars}; do + if [[ -z "${!__var}" ]]; then + error "${__var} variable is missing." + __missing=true + fi + done + if [[ ${__missing} == true ]]; then + fault + fi +} + +check_num(){ + local var=$1 + check_var var + if ! [[ ${!var} =~ ^[0-9]+$ ]] ; then + fault "${var} is not a number: '${!var}'" + fi +} + +debug_var() { + local var=$1 + check_var var + stderr "## DEBUG: ${var}=${!var}" +} + +debug_array() { + local -n ary=$1 + echo "## DEBUG: Array '$1' contains:" + for i in "${!ary[@]}"; do + echo "## ${i} = ${ary[$i]}" + done +} + +ask() { + ## Ask the user a question and set the given variable name with their answer + local __prompt="${1}"; local __var="${2}"; local __default="${3}" + read -e -p "${__prompt}"$'\x0a: ' -i "${__default}" ${__var} + export ${__var} +} + +ask_no_blank() { + ## Ask the user a question and set the given variable name with their answer + ## If the answer is blank, repeat the question. + local __prompt="${1}"; local __var="${2}"; local __default="${3}" + while true; do + read -e -p "${__prompt}"$'\x0a: ' -i "${__default}" ${__var} + export ${__var} + [[ -z "${!__var}" ]] || break + done +} + +ask_echo() { + ## Ask the user a question then print the non-blank answer to stdout + ( + prompt=$1; shift + ask_no_blank "$1" ASK_ECHO_VARNAME $@ >/dev/stderr + echo "${ASK_ECHO_VARNAME}" + ) +} + + +require_input() { + ## require_input {PROMPT} {VAR} {DEFAULT} + ## Read variable, set default if blank, error if still blank + test -z ${3} && dflt="" || dflt=" (${3})" + read -e -p "$1$dflt: " $2 + eval $2=${!2:-${3}} + test -v ${!2} && fault "$2 must not be blank." +} + +make_var_name() { + # Make an environment variable out of any string + # Replaces all invalid characters with a single _ + echo "$@" | sed -e 's/ */_/g' -e 's/--*/_/g' -e 's/[^a-zA-Z0-9_]/_/g' -e 's/__*/_/g' -e 's/.*/\U&/' -e 's/__*$//' -e 's/^__*//' +} + +color() { + ## Print text in ANSI color + set -e + if [[ $# -lt 2 ]]; then + fault "Not enough args: expected COLOR and TEXT arguments" + fi + local COLOR_CODE_PREFIX='\033[' + local COLOR_CODE_SUFFIX='m' + local COLOR=$1; shift + local TEXT="$*" + local LIGHT=1 + check_var COLOR TEXT + case "${COLOR}" in + "black") COLOR=30; LIGHT=0;; + "red") COLOR=31; LIGHT=0;; + "green") COLOR=32; LIGHT=0;; + "brown") COLOR=33; LIGHT=0;; + "orange") COLOR=33; LIGHT=0;; + "blue") COLOR=34; LIGHT=0;; + "purple") COLOR=35; LIGHT=0;; + "cyan") COLOR=36; LIGHT=0;; + "light gray") COLOR=37; LIGHT=0;; + "dark gray") COLOR=30; LIGHT=1;; + "light red") COLOR=31; LIGHT=1;; + "light green") COLOR=32; LIGHT=1;; + "yellow") COLOR=33; LIGHT=1;; + "light blue") COLOR=34; LIGHT=1;; + "light purple") COLOR=35; LIGHT=1;; + "light cyan") COLOR=36; LIGHT=1;; + "white") COLOR=37; LIGHT=1;; + *) fault "Unknown color" + esac + echo -en "${COLOR_CODE_PREFIX}${LIGHT};${COLOR}${COLOR_CODE_SUFFIX}${TEXT}${COLOR_CODE_PREFIX}0;0${COLOR_CODE_SUFFIX}" +} + +colorize() { + ## Highlight text patterns in stdin with ANSI color + set -e + if [[ $# -lt 2 ]]; then + fault "Not enough args: expected COLOR and PATTERN arguments" + fi + local COLOR=$1; shift + local PATTERN=$1; shift + check_var COLOR PATTERN + case "${COLOR}" in + "black") COLOR=30;; + "red") COLOR=31;; + "green") COLOR=32;; + "brown") COLOR=33;; + "orange") COLOR=33;; + "blue") COLOR=34;; + "purple") COLOR=35;; + "cyan") COLOR=36;; + "white") COLOR=37;; + *) fault "Unknown color" + esac + PATTERN='^.*'"${PATTERN}"'.*$|' + readarray stdin + echo "${stdin[@]}" | \ + GREP_COLORS="mt=01;${COLOR}" grep --color -E "${PATTERN}" +} + +element_in_array () { + # element_in_array "${profile}" "${ALL_PROFILES[@]}" + local e match="$1"; shift; + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +gen_password() { + set -eo pipefail + LENGTH=${1:-30} + openssl rand -base64 ${LENGTH} | tr '=' '0' | tr '+' '0' | tr '/' '0' | tr '\n' '0' | head -c ${LENGTH} +} + +version_spec() { + ## Check the lock file to see if the apps INSTALLED_VERSION is ok + # version_spec APP_NAME INSTALLED_VERSION + set -eo pipefail + # The name of the app: + local APP=$1; + check_var APP + # The installed version to check against the lock file version, could be blank: + local CHECK_VERSION=$2; + local VERSION_LOCK="${ROOT_DIR}/.tools.lock.json" + if [[ ! -f "$VERSION_LOCK" ]]; then + fault "The version lock spec file is missing: ${VERSION_LOCK}" + fi + # Grab the locked version of APP from the lock file: + local LOCKED_VERSION=$(jq -r ".dependencies.\"${APP}\"" ${ROOT_DIR}/.tools.lock.json) + (test -z "${LOCKED_VERSION}" || test "${LOCKED_VERSION}" == "null") && fault "The app '${APP}' is not listed in ${VERSION_LOCK}" + + # Return the locked version string: + echo ${LOCKED_VERSION} + + # But error if the installed version is different than the locked version: + if [[ -n "${CHECK_VERSION}" ]] && [[ "${VERSION}" != "${CHECK_VERSION}" ]]; then + fault "Installed ${APP} version ${CHECK_VERSION} does not match the locked version: ${LOCKED_VERSION}" + fi +} + +text_centered() { + local columns="$1" + check_var columns + shift + local text="$@" + check_var text + printf "%*s\n" $(( (${#text} + columns) / 2)) "$text" +} + +text_centered_full() { + local columns="$(tput cols)" + text_centered ${columns} "$@" +} + +text_centered_wrap() { + local wrap="$1" + check_var wrap + shift; + local wrap_rev="${wrap}" + wrap_rev=$(text_reverse "${wrap}") + local columns="$1" + check_var columns + shift; + local wrap_length=${#wrap} + local text="$@" + check_var text + centered_text=$(text_centered "${columns}" "${text}") + trailing_whitespace=$(text_repeat $((${#centered_text}-${#text})) " ") + whitespace_offset=${wrap_length} + new_text="${wrap}${centered_text:${#wrap}}${trailing_whitespace:${whitespace_offset}}${wrap}" + if [[ $((wrap_length%2)) -eq 0 ]] && [[ $((${#new_text}%2)) -eq 1 ]]; then + whitespace_offset=$((whitespace_offset-1)) + elif [[ $((wrap_length%2)) -eq 1 ]] && [[ $((${#new_text}%2)) -eq 1 ]]; then + whitespace_offset=$((whitespace_offset-1)) + fi + new_text="${wrap}${centered_text:${#wrap}}${trailing_whitespace:${whitespace_offset}}${wrap_rev}" + echo "${new_text}" +} + +text_repeat() { + local repeat="$1"; + check_var repeat + shift + local text="$@" + check_var text + readarray -t repeated < <(yes "${text}" | head -n ${repeat}) + printf "%s" "${repeated[@]}" + echo +} + +text_reverse() { + local text="$@" + check_var text + for((i=${#text}-1;i>=0;i--)); do rev="$rev${text:$i:1}"; done + echo "${rev}" +} + +text_mirror() { + local text="$@" + check_var text + rev=$(text_reverse "${text}") + echo "${text}${rev}" +} + +text_line() { + # Fill a line of the target width with a repeating pattern + # If width is 0, fill the entire line. + local width="$1"; + local pattern="$2"; + check_var width + shift 2 + if [[ "${width}" == "0" ]]; then + width="$(tput cols)" + fi + local pattern_length="${#pattern}" + text_repeat $((width/pattern_length)) "${pattern}" + if [[ "$#" -gt 0 ]]; then + echo "$(text_centered "$*")" + text_repeat $((width/pattern_length)) "${pattern}" + fi +} + +separator() { + local pattern="$1" + check_var pattern + shift + local width="$1" + check_var width + shift + if [[ "${width}" == "0" ]]; then + width="$(tput cols)" + fi + local text="$@" + echo + local sep=$(text_line ${width} "${pattern}") + local index_half=$((${#sep}/2)) + sep="${sep:0:${index_half}}" + sep=$(text_mirror "${sep}") + local columns="${#sep}" + echo "${sep}" + if [[ -n "${text}" ]]; then + text_centered_wrap "${pattern}" "${columns}" "${text}" + echo "${sep}" + fi + echo +} + + +random_element() { + local arr=("$@") + if [[ "${#@}" -lt 1 ]]; then + fault "Need more args" + fi + echo "${arr[ $RANDOM % ${#arr[@]} ]}" +} + +confirm() { + ## Confirm with the user. + ## Check env for the var YES, if it equals "yes" then bypass this confirm. + ## This version depends on `script-wizard` being installed. + test ${YES:-no} == "yes" && exit 0 + + local default=$1; local prompt=$2; local question=${3:-". Proceed?"} + + check_var default prompt question + + if [[ -f ${BIN}/script-wizard ]]; then + ## Check if script-wizard is installed, and prefer to use that: + local exit_code=0 + if [[ $default == "y" || $default == "true" ]]; then + default="yes" + elif [[ $default == "n" || $default == "false" ]]; then + default="no" + fi + wizard confirm --cancel-code=2 "$prompt$question" "$default" && exit_code=$? || exit_code=$? + if [[ "${exit_code}" == "2" ]]; then + cancel + fi + return ${exit_code} + else + ## Otherwise use a pure bash version: + if [[ $default == "y" || $default == "yes" || $default == "true" ]]; then + dflt="Y/n" + else + dflt="y/N" + fi + + read -e -p "${prompt}${question} (${dflt}): " answer + answer=${answer:-${default}} + + if [[ ${answer,,} == "y" || ${answer,,} == "yes" || ${answer,,} == "true" ]]; then + return 0 + else + return 1 + fi + fi +} + +choose() { + local exit_code=0 + wizard choose --cancel-code=2 "$@" && exit_code=$? || exit_code=$? + if [[ "${exit_code}" == "2" ]]; then + cancel + fi + return ${exit_code} +} + +select_wizard() { + local exit_code=0 + wizard select --cancel-code=2 "$@" && exit_code=$? || exit_code=$? + if [[ "${exit_code}" == "2" ]]; then + cancel + fi + return ${exit_code} +} + diff --git a/config/bash/rust.sh b/config/bash/rust.sh index 93d4335..93fd84e 100644 --- a/config/bash/rust.sh +++ b/config/bash/rust.sh @@ -1,4 +1,14 @@ ## Rustup cargo environment -## On a new machine, you should run rustup-init first. -PATH=${PATH}:${HOME}/.cargo/bin -test -f "$HOME/.cargo/env" && source "$HOME/.cargo/env" + +if [[ -f "$HOME/.cargo/env" ]]; then + unset -f fedora-bootstrap-rust + source "$HOME/.cargo/env" +else + ## On a new machine, you should run bootstrap-rust. + bootstrap-rust() { + sudo dnf install -y rustup + rustup-init -y --no-modify-path + source "$HOME/.cargo/env" + rustup component add rust-analyzer + } +fi diff --git a/config/bash/unset_funcs.sh b/config/bash/unset_funcs.sh new file mode 100644 index 0000000..decf318 --- /dev/null +++ b/config/bash/unset_funcs.sh @@ -0,0 +1,20 @@ +## Unset utility functions loaded from funcs.sh + +unset_funcs() { + # Unset all functions found in FUNCS_SOURCE + FUNCS_SOURCE=$1 + check_var FUNCS_SOURCE + if [[ ! -f ${FUNCS_SOURCE} ]]; then + error "Failed cleanup in ${BASH_SOURCE}" + error "${FUNCS_SOURCE} does not exist." + return 1 + fi + read -ra funcs < <(grep -P "^\w+()" "${FUNCS_SOURCE}" | \ + grep -Po "^\w+" | \ + cut -d"(" -f 1 | tr '\n' ' ') + for func in "${funcs[@]}"; do + unset -f ${func} + done +} +unset_funcs ~/.config/bash/funcs.sh +unset unset_funcs