Last active
March 19, 2026 13:44
-
-
Save milo-minderbinder/6737217136ccfad263aab36106741e57 to your computer and use it in GitHub Desktop.
bash-scripts
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 | |
| set -o errexit -o errtrace -o noclobber -o nounset -o pipefail | |
| trap 'e=$?; if [ "$e" -ne "0" ]; then printf "LINE %s: exit %s <- %s%s\\n" "$BASH_LINENO" "$e" "${BASH_COMMAND}" "$(printf " <- %s" "${FUNCNAME[@]:-main}")" 1>&2; fi' EXIT | |
| PROGNAME="${0##*/}" | |
| _log_msg() { | |
| local level | |
| local ansi_escapes | |
| local prefix | |
| if [ "$#" -eq "0" ]; then | |
| _log_msg error "${FUNCNAME[0]} requires at least one argument" | |
| return 1 | |
| elif [ "$#" -eq "1" ]; then | |
| printf '%s\n' "$1" 1>&2 | |
| return 0 | |
| fi | |
| case "$(tr '[:lower:]' '[:upper:]' <<< "$1")" in | |
| DEBUG) | |
| if [ -z "${VERBOSITY:-}" ] || [ "${#VERBOSITY}" -lt "3" ]; then | |
| return 0 | |
| fi | |
| level='DEBUG' | |
| ansi_escapes="$(tput setaf 2)" | |
| ;; | |
| INFO) | |
| if [ -z "${VERBOSITY:-}" ] || [ "${#VERBOSITY}" -lt "1" ]; then | |
| return 0 | |
| fi | |
| level='INFO' | |
| ansi_escapes="$(tput setaf 2)" | |
| ;; | |
| WARN*) | |
| level='WARN' | |
| ansi_escapes="$(tput setaf 3)" | |
| ;; | |
| ERROR) | |
| level='ERROR' | |
| ansi_escapes="$(tput setaf 0)$(tput setab 1)" | |
| ;; | |
| *) | |
| level="$(tr '[:lower:]' '[:upper:]' <<<"$1")" | |
| ansi_escapes='' | |
| if [ "$#" -gt "2" ]; then | |
| ansi_escapes="$2" | |
| shift | |
| fi | |
| ;; | |
| esac | |
| shift | |
| prefix="${level:-}" | |
| if [ -n "${VERBOSITY:-}" ] && [ "${#VERBOSITY}" -ge "3" ]; then | |
| prefix="$(date -Iseconds)${prefix:+ $prefix} ${PROGNAME}" | |
| fi | |
| if [ -z "$level" ]; then | |
| for msg in "$@"; do | |
| printf '%s%s%s%s\n' "${ansi_escapes:-}" "${prefix:+$prefix: }" "$msg" "${ansi_escapes:+$(tput sgr0)}" 1>&2 | |
| done | |
| else | |
| printf '%s%s%s: ' "${ansi_escapes:-}" "$prefix" "${ansi_escapes:+$(tput sgr0)}" 1>&2 | |
| # indent all message lines by the length of the length of the | |
| # log message prefix | |
| printf '%s\n' "$@" | \ | |
| sed "$(printf '2,$s/^/%*s/' "$((${#prefix} + 2))" '')" 1>&2 | |
| fi | |
| } | |
| log_debug() { | |
| _log_msg debug "$@" | |
| } | |
| log_info() { | |
| _log_msg info "$@" | |
| } | |
| log_warn() { | |
| _log_msg warn "$@" | |
| } | |
| log_error() { | |
| _log_msg error "$@" | |
| } | |
| log_verbose() { | |
| if [ -n "${VERBOSITY:-}" ] && [ "${#VERBOSITY}" -ge "2" ]; then | |
| log_info "$@" | |
| fi | |
| } | |
| get_context() { | |
| local line | |
| local subroutine | |
| local filename | |
| line="$1" | |
| subroutine='call' | |
| if [ "$#" -eq "2" ]; then | |
| filename="$2" | |
| elif [ "$#" -eq "3" ]; then | |
| subroutine="$2" | |
| filename="$3" | |
| else | |
| log_error 'incorrect number of arguments!' | |
| exit 1 | |
| fi | |
| printf '%s%s on line %d of %s:%s\n' "$(tput setaf 1)" "$subroutine" "$line" "$filename" "$(tput sgr0)" | |
| awk 'NR>L-4 && NR<L+4 { printf "%-5d%3s%s\n",NR,(NR==L?">>>":""),$0 }' L="$line" "$filename" | |
| } | |
| log_stack_trace() { | |
| local last_exit=$? | |
| local depth | |
| local call_info | |
| if [ "$last_exit" -ne "0" ]; then | |
| declare -i depth="${1:-$((${#FUNCNAME[@]} - 2))}" | |
| while [ "$depth" -ge "0" ] && call_info=($(caller "$depth" 2>/dev/null)); do | |
| log_error "$(get_context "${call_info[0]}" "${call_info[1]}" "${call_info[*]:2}")" | |
| (( depth -= 1 )) | |
| done | |
| call_info=($(caller 0)) | |
| log_error "$(printf '%s(%d): %s -> exit %d\n' "${call_info[*]:2}" "${call_info[0]}" "${call_info[1]}" "$last_exit")" | |
| fi | |
| } | |
| append_trap () { | |
| local trap_cmd | |
| local trap_sig | |
| local old_trap_cmd | |
| trap_cmd="$1" | |
| trap_sig="$2" | |
| old_trap_cmd="$(trap -p "$trap_sig" | sed -E -e "s/^[^'\"]*['\"]//" -e "s/['\"][[:space:]]*${trap_sig}\$//")" | |
| if [[ -n "$old_trap_cmd" ]]; then | |
| trap_cmd="$old_trap_cmd; $trap_cmd" | |
| fi | |
| trap "$trap_cmd" "$trap_sig" | |
| } | |
| trap 'log_stack_trace' EXIT | |
| get_script_dir() { | |
| ## resolve the directory of the given script | |
| # example: | |
| # SCRIPTDIR="$(get_script_dir "${BASH_SOURCE[0]}")" | |
| SOURCE="${1}" | |
| #SOURCE="${BASH_SOURCE[0]}" | |
| while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink | |
| SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |
| SOURCE="$(readlink "$SOURCE")" | |
| [[ $SOURCE != /* ]] && SOURCE="$SCRIPTDIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located | |
| done | |
| SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |
| printf '%s\n' "${SCRIPTDIR}" | |
| } | |
| contains_value() { | |
| local value | |
| value="$1" | |
| shift | |
| for arg in "$@"; do | |
| if [ "$value" == "$arg" ]; then | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| ltrim_ws() { | |
| local input | |
| local smallest_lws | |
| input="$(cat "${1:--}")" | |
| smallest_lws=$(printf '%s\n' "$input" | sed -e '/^[[:space:]]*$/d' -e 's/[^[:space:]].*$//' | sort | head -n 1 | wc -m) | |
| printf '%s\n' "$input" | cut -c ${smallest_lws}- | |
| } | |
| get_color() { | |
| local color | |
| case "$1" in | |
| grey*|dim|gra*) color=8;; | |
| light-r*) color=9;; | |
| light-g*) color=10;; | |
| light-y*) color=11;; | |
| light-blu*) color=12;; | |
| light-m*|light-p*) color=13;; | |
| light-c*) color=14;; | |
| light-w*|bright*) color=15;; | |
| bla*) color=0;; | |
| r*) color=1;; | |
| g*) color=2;; | |
| y*) color=3;; | |
| blu*) color=4;; | |
| m*|p*) color=5;; | |
| c*) color=6;; | |
| w*) color=7;; | |
| *) | |
| 1>&2 printf '%s[ERROR]%s: unknown color name: %s\n' "$(tput setaf 1)" "$(tput sgr0)" "$1" | |
| return 1 | |
| ;; | |
| esac | |
| printf '%d\n' "$color" | |
| } | |
| hl() { | |
| local fg | |
| local bg | |
| local text | |
| if [ "$#" -lt "2" ] || [ "$#" -gt "3" ]; then | |
| cat <<EOF | ltrim_ws 1>&2 | |
| $(tput setaf 1)[ERROR]$(tput sgr0): incorrect # of arguments | |
| USAGE | |
| ${FUNCNAME[0]:-main} FG_COLOR [BG_COLOR] TEXT | |
| EXAMPLE | |
| ${FUNCNAME[0]:-main} red 'this text is red' | |
| EOF | |
| return 1 | |
| fi | |
| fg="$(tput setaf "$(get_color "$1")")" | |
| if [ "$#" -eq "3" ]; then | |
| bg="$(tput setab "$(get_color "$2")")" | |
| shift | |
| else | |
| bg="" | |
| fi | |
| text="$2" | |
| printf '%s%s%s\n' "${fg}${bg:-}" "$text" "$(tput sgr0)" | |
| } | |
| xml_format() { | |
| local indent | |
| indent='\t' | |
| if [ "$#" -ge "2" ]; then | |
| if [ "$1" == "-i" ] || [ "$1" == "--indent" ]; then | |
| indent="$2" | |
| shift 2 | |
| else | |
| printf '%sERROR:%s invalid %s opt (only -i/--indent supported): %s\n' "$(tput setaf 1)" "$(tput sgr0)" "${FUNCNAME[0]}" "$1" 1>&2 | |
| return 2 | |
| fi | |
| fi | |
| cat "${1:--}" | sed -E 's/^[[:space:]]+//' | tr -d '\n' | XMLLINT_INDENT="$(printf "$indent")" xmllint --format - ; | |
| } | |
| add_to_path() { | |
| local append | |
| local path_entries | |
| append=0 | |
| if [ "$1" == '-a' ] || [ "$1" == '--append' ]; then | |
| shift | |
| append=1 | |
| fi | |
| for path_entry in "$@"; do | |
| if ! tr ':' '\n' <<<"$PATH" | grep --fixed-strings --line-regexp --quiet "${path_entry}"; then | |
| if [ ! -d "${path_entry}" ]; then | |
| printf '%sWARNING:%s Adding non-directory path to PATH: %s\n' "$(tput setaf 1)" "$(tput sgr0)" "${path_entry}" 1>&2 | |
| else | |
| printf '%sINFO:%s Adding to PATH: %s\n' "$(tput setaf 2)" "$(tput sgr0)" "${path_entry}" 1>&2 | |
| fi | |
| if [ "${append}" -eq "0" ]; then | |
| export PATH="${path_entry}:${PATH}" | |
| else | |
| export PATH="${PATH}:${path_entry}" | |
| fi | |
| fi | |
| done | |
| } | |
| update_path() { | |
| local formula_name | |
| if [[ $# -eq 1 ]]; then | |
| formula_name="$1" | |
| else | |
| formula_name="all" | |
| fi | |
| if [ "$(uname)" == "Darwin" ]; then | |
| if [ "${formula_name}" == "all" ] || [ "${formula_name}" == "grep" ]; then | |
| add_to_path "$(brew --prefix 'grep')/libexec/gnubin" | |
| fi | |
| if [ "${formula_name}" == "all" ] || [ "${formula_name}" == "gnu-getopt" ]; then | |
| add_to_path "$(brew --prefix 'gnu-getopt')/bin" | |
| fi | |
| if [ "${formula_name}" == "all" ] || [ "${formula_name}" == "coreutils" ]; then | |
| add_to_path "$(brew --prefix 'coreutils')/libexec/gnubin" | |
| fi | |
| fi | |
| log_verbose "updated PATH: ${PATH}" | |
| printf '%s\n' "${PATH}" | |
| } | |
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 | |
| verbose="${verbose:-n}" | |
| SOURCE="${BASH_SOURCE[0]}" | |
| while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink | |
| SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |
| SOURCE="$(readlink "$SOURCE")" | |
| [[ $SOURCE != /* ]] && SOURCE="$SCRIPTDIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located | |
| done | |
| SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |
| source "${SCRIPTDIR}/common.bash" | |
| export PATH="$(update_path 'gnu-getopt')" | |
| if command -v apt-get 1> /dev/null && ! command -v xmllint 1> /dev/null; then | |
| log_warn 'installing missing dependencies' | |
| DEBIAN_FRONTEND=noninteractive apt-get -q -y update && apt-get -q -y install \ | |
| libxml2-utils | |
| fi | |
| find_xml_attr_vals() { | |
| OPTIONS=e:a: | |
| LONGOPTS=element-xpath:,attr-name: | |
| ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "${0##*/}" -- "$@") | |
| if [[ ${PIPESTATUS[0]} -ne 0 ]]; then | |
| exit 2 | |
| fi | |
| eval set -- "$PARSED" | |
| local element_xpath | |
| local attr_name | |
| local input | |
| local xml_data | |
| while true; do | |
| case "$1" in | |
| -e|--element-xpath) | |
| element_xpath="$2" | |
| shift 2 | |
| ;; | |
| -a|--attr-name) | |
| attr_name="$2" | |
| shift 2 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| *) | |
| echo "Programming error" | |
| exit 2 | |
| ;; | |
| esac | |
| done | |
| if [ "${element_xpath:-}" == "" ]; then | |
| log_error "find_xml_attr_vals --element-xpath option is requred" | |
| exit 123 | |
| fi | |
| if [ "${attr_name:-}" == "" ]; then | |
| log_error "find_xml_attr_vals --attr-name option is requred" | |
| exit 123 | |
| fi | |
| if [[ $# -ne 1 ]]; then | |
| log_error "find_xml_attr_vals accepts one positional argument for the xml input, but got $#" | |
| exit 123 | |
| fi | |
| [ -f "$1" ] && input="$1" || input="-" | |
| xml_data="$(cat "${input}" | sed 's/xmlns[[:space:]]*=/ignore=/')" | |
| local num_matched | |
| num_matched="$(printf '%s' "${xml_data}" | \ | |
| xmllint --xpath "count(${element_xpath}/@${attr_name})" - )" | |
| log_verbose "found ${num_matched} attrs matching '${element_xpath}/@${attr_name}'" | |
| local attr_val | |
| for i in $(seq 1 "${num_matched}"); do | |
| attr_val="$(printf '%s' "${xml_data}" | \ | |
| xmllint --xpath "${element_xpath}[${i}]/@${attr_name}" - | \ | |
| sed -E 's/.*"(.*)".*/\1/')" | |
| #log_verbose " ${element_xpath}[${i}]/@${attr_name} = ${attr_val}" | |
| printf '%s\n' "${attr_val}" | |
| done | |
| } | |
| get_versions() { | |
| local versions_url | |
| local versions_xml | |
| local versions | |
| versions_url="$1" | |
| log_verbose "fetching latest versions from ${versions_url}" | |
| versions_xml="$(curl -s "${versions_url}" | sed -n '2,$p')" | |
| versions=() | |
| while IFS= read -r; do | |
| versions+=("${REPLY}") | |
| done < <(printf '%s' "${versions_xml}" | \ | |
| find_xml_attr_vals --element-xpath "//a" --attr-name "href" - | \ | |
| grep -Ev '\-|\.\.' | \ | |
| sed 's/\/$//' | \ | |
| sort -rV) | |
| log_verbose "${#versions[@]} parsed versions:$(printf '\n\t%s' "${versions[@]}")" | |
| printf '%s\n' "${versions[@]}" | |
| } | |
| get_latest_version() { | |
| local versions_url | |
| local major_version | |
| local versions | |
| local latest_version | |
| versions_url="$1" | |
| if [[ $# -eq 2 ]]; then | |
| major_version="$2" | |
| fi | |
| versions="$(get_versions "${versions_url}" | grep -E "^${major_version:-[0-9]+}\." | sort -rV)" || \ | |
| (log_error "no versions ${major_version:+matching ${major_version}.X.X }found"; exit 2) | |
| latest_version="$(printf '%s' "${versions}" | head -n 1)" | |
| log_verbose "latest version: ${latest_version}" | |
| printf '%s' "${latest_version}" | |
| } | |
| get_latest_version "$@" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment