#!/usr/bin/env bash case $- in *i*) ;; *) return ;; esac __RBASH_SCRIPT="${BASH_SOURCE[0]}" RBASH_HOME="${RBASH_HOME:-$HOME/.rbash}" RBASH_REMOTE="${RBASH_REMOTE:-https://sgit.k8s.astropenguin.net}" RBASH_REPO="${RBASH_REPO:-penguin/utils}" RBASH_REF="${RBASH_REF:-master}" RBASH_PATH="${RBASH_PATH:-bash}" RBASH_CACHE_DIR="$RBASH_HOME/cache" RBASH_CONFIG="$RBASH_HOME/config" RBASH_ENV="$RBASH_HOME/env" RBASH_MERGED="$RBASH_CACHE_DIR/merged" RBASH_BIN="$RBASH_HOME/bin" RBASH_SOURCES_SUM="508ba717f5f31796f03fd176ecb50432a2c3d236896cef066f2cc535369bbca9" # ---- rbash: bash/core/00_env.bash ---- # Host prompt placeholders can be replaced by rbash-upgrade/install. RHOSTNAME="${RHOSTNAME:-}" RDOMAIN="${RDOMAIN:-}" RCOLOR="${RCOLOR:-}" if [ "$RHOSTNAME" = "" ]; then RHOSTNAME="$(whoami)" RDOMAIN="@HOST_NAME" RCOLOR=$(( RANDOM * 6 / 32767 + 1 )) fi RBASH_SIGNING_FPR="7C1E948D841254C569EDFBDED5CF5FB1CFAD27C3" _RBASH_LOADED= # ---- rbash: bash/core/05_defaults.bash ---- __rbash_init_config() { if [ -f "$RBASH_CONFIG" ]; then return 0 fi cat > "$RBASH_CONFIG" <<'EOF' rbash_load "sources/10_aliases" rbash_load "sources/12_shortcuts" rbash_load "sources/40_go-command" rbash_load "sources/41_pivot-command" EOF chmod 600 "$RBASH_CONFIG" } # ---- rbash: bash/core/10_shell.bash ---- # Source global definitions. if [ -f /etc/bashrc ]; then # shellcheck source=/etc/bashrc source /etc/bashrc fi HISTCONTROL=ignoreboth HISTSIZE=1000 HISTFILESIZE=2000 shopt -s histappend shopt -s checkwinsize if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then debian_chroot="$(cat /etc/debian_chroot)" fi if [ -f "$HOME/.bash_aliases" ]; then # shellcheck source=/dev/null source "$HOME/.bash_aliases" fi if ! shopt -oq posix; then if [ -f /usr/share/bash-completion/bash_completion ]; then # shellcheck source=/usr/share/bash-completion/bash_completion source /usr/share/bash-completion/bash_completion elif [ -f /etc/bash_completion ]; then # shellcheck source=/etc/bash_completion source /etc/bash_completion fi fi __func_head() { echo "Usage: ${FUNCNAME[1]} $*" } __func_help() { echo " ${FUNCNAME[1]} $*" } # ---- rbash: bash/core/20_io.bash ---- rbash_url() { local path="$1" printf '%s/%s/raw/%s/%s/%s\n' \ "$RBASH_REMOTE" "$RBASH_REPO" "$RBASH_REF" "$RBASH_PATH" "$path" } rbash_download() { local url="$1" if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" || { local rc=$? echo "rbash: download failed: $url" >&2 echo "rbash: curl exit code: $rc" >&2 return "$rc" } elif command -v wget >/dev/null 2>&1; then wget -qO- "$url" || { local rc=$? echo "rbash: download failed: $url" >&2 echo "rbash: wget exit code: $rc" >&2 return "$rc" } else echo "rbash: curl or wget is required" >&2 return 127 fi } function rbash_download_sig { local _URL _LOC _SIG tmp _SIG="$1" _URL="$2" _LOC="$3" tmp=$( mktemp ) rbash_download "$_URL" > "$tmp" sha256sum "$tmp" | grep -q "$_SIG" if [ $? -eq 0 ]; then mv "$tmp" "$_LOC" else echo "Download $_LOC: Signature mismatch" >&2 return 1 fi } rbash_cache() { local force_redownload=0 local sig url name key file OPTIND=1 while getopts ":r" opt; do case "$opt" in r) force_redownload=1 ;; \?) echo "usage: rbash_cache [-r] SIG URL [NAME]" >&2 return 2 ;; esac done shift $((OPTIND - 1)) if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then echo "usage: rbash_cache [-r] SIG URL [NAME]" >&2 return 2 fi sig="$1" url="$2" name="${3:-}" if [ -z "${RBASH_CACHE_DIR:-}" ]; then echo "rbash: RBASH_CACHE_DIR is not set" >&2 return 1 fi mkdir -p "$RBASH_CACHE_DIR" || { echo "rbash: failed to create cache dir: $RBASH_CACHE_DIR" >&2 return 1 } if [ -n "$name" ]; then file="$RBASH_CACHE_DIR/$name" else if command -v md5sum >/dev/null 2>&1; then key="$(printf '%s' "$url" | md5sum | cut -d' ' -f1)" else key="$(printf '%s' "$url" | md5 | cut -d' ' -f1)" fi file="$RBASH_CACHE_DIR/$key" fi if [ "$force_redownload" -eq 1 ]; then rm -f "$file" fi if [ ! -f "$file" ]; then rbash_download_sig "$sig" "$url" "$file" || { local rc=$? rm -f "$file" echo "rbash: download failed: $url" >&2 return "$rc" } chmod 700 "$file" fi printf '%s\n' "$file" } _require_jq() { if ! command -v jq >/dev/null 2>&1; then echo "This command requires jq." >&2 return 1 fi } # ---- rbash: bash/core/30_loader.bash ---- rbash-reload() { _RBASH_LOADED= _RBASH_MERGE_START=1 if [ "$1" = "-f" ]; then rm -rf "$RBASH_CACHE_DIR" fi mkdir -p "$RBASH_CACHE_DIR" rm -f "$RBASH_MERGED" : > "$RBASH_MERGED" || { echo "rbash: failed to write merged cache: $RBASH_MERGED" >&2 _RBASH_MERGE_START= return 1 } # shellcheck source=/dev/null source "$RBASH_CONFIG" _RBASH_MERGE_START= _RBASH_LOADED=1 } __rbash_autoload() { if [ -n "${_RBASH_LOADED:-}" ]; then return 0 fi if [ -f "$RBASH_MERGED" ]; then # shellcheck source=/dev/null source "$RBASH_MERGED" && { _RBASH_LOADED=1 return 0 } echo "rbash: merged cache failed, rebuilding" >&2 rm -f "$RBASH_MERGED" fi rbash-reload } rbash_load() { local rel="$1" local url file # Normal startup should not individually fetch sources. # It should source RBASH_MERGED via __rbash_autoload. if [ -z "${_RBASH_MERGE_START:-}" ]; then __rbash_autoload return 0 fi echo "Load: $rel" url="$(rbash_url "$rel")" key="$(printf '%s' "$rel" | sed "s|^sources/||g" | grep -o '^[0-9]\+' || true)" sig="${_RBASH_SOURCES_SUM["s$key"]}" file="$(rbash_cache "$sig" "$url")" || { echo " ..download failed" >&2 return 1 } # shellcheck source=/dev/null if source "$file"; then { printf 'echo "Load: %s"\n' "$rel" cat "$file" printf '\n' } >> "$RBASH_MERGED" else echo " ..failed" >&2 printf 'echo "Skipped: %s. Use rbash-reload to try again."\n' "$rel" >> "$RBASH_MERGED" fi chmod 700 "$RBASH_MERGED" } rbash-run() { local rel="$1" local file shift || true echo "Getting: $rel" file="$(rbash_cache "$(rbash_url "$rel")")" || return 1 "$file" "$@" } # ---- rbash: bash/core/40_sources.bash ---- rbash_sources_list() { printf '%s/%s/raw/master/bash/dist/sources.list\n' \ "$RBASH_REMOTE" "$RBASH_REPO" } declare -gA _RBASH_DICT declare -gA _RBASH_SOURCES_SUM rbash_sources_load() { local hash path item key sources_list _RBASH_DICT=() _RBASH_SOURCES_SUM=() sources_list=$(rbash_cache "$RBASH_SOURCES_SUM" "$(rbash_sources_list)" "sources.list") while read -r hash path; do [ -n "$hash" ] || continue [ -n "$path" ] || continue item="${path#sources/}" key="$(printf '%s' "$item" | grep -o '^[0-9]\+' || true)" [ -n "$key" ] || continue _RBASH_DICT["s$key"]="$item" _RBASH_SOURCES_SUM["s$key"]="$hash" done < "$sources_list" } rbash-list() { local key item enabled_marker rbash_sources_load || return 1 echo "Available sources:" while IFS= read -r key; do item="${_RBASH_DICT[$key]}" enabled_marker=" " if grep -q -F "rbash_load \"sources/$item\"" "$RBASH_CONFIG"; then enabled_marker="*" fi printf ' %s %s\n' "$enabled_marker" "$item" done < <(printf '%s\n' "${!_RBASH_DICT[@]}" | sort -V) } r2ensource() { if [ "$1" = "-f" ]; then sources_list=$(rbash_cache -r "$RBASH_SOURCES_SUM" "$(rbash_sources_list)" "sources.list") fi rbash-list || return 1 local ids id selected tmp local -a enable=() read -r -p "Enter the script id XX, delimited by space: " ids for id in $ids; do selected="${_RBASH_DICT["s$id"]}" if [ -z "$selected" ]; then echo "No such id: $id" >&2 return 1 fi if grep -q -F "rbash_load \"sources/$selected\"" "$RBASH_CONFIG"; then echo "$selected is already enabled." continue fi enable+=("$selected") done tmp="$(mktemp)" cp "$RBASH_CONFIG" "$tmp" for selected in "${enable[@]}"; do echo "rbash_load \"sources/$selected\"" >> "$tmp" echo "Enabling $selected" done sort -u "$tmp" > "$RBASH_CONFIG" rm -f "$tmp" rm -rf "$RBASH_CACHE_DIR" } r2dissource() { local ids id tmp pattern echo "Currently enabled sources:" grep -E '^rbash_load "sources/' "$RBASH_CONFIG" | sed 's/^rbash_load "sources\///; s/"$//' | sed 's/^/ /' read -r -p "Enter the script id XX, delimited by space: " ids tmp="$(mktemp)" cp "$RBASH_CONFIG" "$tmp" for id in $ids; do pattern="rbash_load \"sources/${id}_" if ! grep -q -F "$pattern" "$tmp"; then echo "No such id: $id" >&2 rm -f "$tmp" return 1 fi echo "Disabling $id" grep -v -F "$pattern" "$tmp" > "$tmp.next" mv "$tmp.next" "$tmp" done sort -u "$tmp" > "$RBASH_CONFIG" rm -f "$tmp" rm -rf "$RBASH_CACHE_DIR" } # ---- rbash: bash/core/50_upgrade.bash ---- rbash_verify_upgrade() { local new_rbashrc="$1" local sig_file="$2" local key_file="$3" local gnupg_home key_fpr rc command -v gpg >/dev/null 2>&1 || { echo "gpg is required for rbash upgrade verification" return 1 } [ -n "${RBASH_SIGNING_FPR:-}" ] || { echo "RBASH_SIGNING_FPR is not set" echo "Refusing to upgrade without a pinned signing key fingerprint" return 1 } [ -f "$new_rbashrc" ] || { echo "missing rbashrc: $new_rbashrc" return 1 } [ -f "$sig_file" ] || { echo "missing signature: $sig_file" return 1 } [ -f "$key_file" ] || { echo "missing public key: $key_file" return 1 } gnupg_home="$(mktemp -d)" chmod 700 "$gnupg_home" GNUPGHOME="$gnupg_home" gpg --quiet --batch --import "$key_file" >/dev/null 2>&1 || { rc=$? rm -rf "$gnupg_home" echo "failed to import rbash signing key" return "$rc" } key_fpr="$( GNUPGHOME="$gnupg_home" gpg --with-colons --fingerprint 2>/dev/null | awk -F: '$1 == "fpr" { print $10; exit }' )" if [ "$key_fpr" != "$RBASH_SIGNING_FPR" ]; then rm -rf "$gnupg_home" echo "rbash signing key fingerprint mismatch" echo "expected: $RBASH_SIGNING_FPR" echo "got: ${key_fpr:-}" return 1 fi GNUPGHOME="$gnupg_home" gpg \ --quiet \ --batch \ --verify "$sig_file" "$new_rbashrc" >/dev/null 2>&1 rc=$? rm -rf "$gnupg_home" if [ "$rc" -ne 0 ]; then echo "rbash signature verification failed" return "$rc" fi return 0 } rbash-upgrade() { local target backup backup_suffix local tmp_dir tmp_rbashrc tmp_sig tmp_key local base_url rbashrc_url sig_url key_url local rc target="${__RBASH_SCRIPT:-$HOME/.bashrc}" base_url="$(rbash_url "dist")" rbashrc_url="$base_url/rbashrc" sig_url="$base_url/rbashrc.asc" key_url="$base_url/rbash-signing-key.asc" command -v gpg >/dev/null 2>&1 || { echo "gpg is required" return 1 } tmp_dir="$(mktemp -d)" tmp_rbashrc="$tmp_dir/rbashrc" tmp_sig="$tmp_dir/rbashrc.asc" tmp_key="$tmp_dir/rbash-signing-key.asc" __rbash_upgrade_cleanup() { rm -rf "$tmp_dir" } trap __rbash_upgrade_cleanup RETURN echo "Downloading rbashrc..." rbash_download "$rbashrc_url" > "$tmp_rbashrc" || return 1 echo "Downloading rbashrc signature..." rbash_download "$sig_url" > "$tmp_sig" || return 1 echo "Downloading rbash signing public key..." rbash_download "$key_url" > "$tmp_key" || return 1 echo "Verifying rbashrc signature..." rbash_verify_upgrade "$tmp_rbashrc" "$tmp_sig" "$tmp_key" || { echo "rbash upgrade aborted" return 1 } backup_suffix="$(date +%Y%m%d%H%M%S)" backup="$target.bak.$backup_suffix" echo "Updating $target" echo "Backup: $backup" if [ -f "$target" ]; then cp "$target" "$backup" || { echo "failed to create backup: $backup" return 1 } fi cp "$tmp_rbashrc" "$target" || { rc=$? echo "failed to install new rbashrc" if [ -f "$backup" ]; then echo "restoring backup" cp "$backup" "$target" || true fi return "$rc" } chmod 600 "$target" 2>/dev/null || true echo "rbash upgrade complete" } # ---- rbash: bash/core/80_public_api.bash ---- rbash-host() { local host="${1:-}" local domain="${2:-}" local color="${3:-}" if [ -z "$host" ] || [ -z "$domain" ]; then echo "Usage: rbash-host HOST DOMAIN [COLOR]" >&2 echo "Example: rbash-host penguin @macstudio 6" >&2 return 1 fi mkdir -p "$RBASH_HOME" { printf 'RHOSTNAME=%q\n' "$host" printf 'RDOMAIN=%q\n' "$domain" if [ -n "$color" ]; then printf 'RCOLOR=%q\n' "$color" fi } > "$RBASH_ENV" echo "Wrote $RBASH_ENV" # shellcheck source=/dev/null source "$RBASH_ENV" __rbash_setup_prompt } # ---- rbash: bash/core/90_prompt.bash ---- __rbash_setup_prompt() { local shelld="\$" case "$OSTYPE" in cygwin*) if net session >/dev/null 2>&1; then shelld="#" fi ;; esac export PS1="This is ${RHOSTNAME}\e[1;3${RCOLOR}m${RDOMAIN}\e[0m: \w\n$shelld " export EDITOR="${EDITOR:-vim}" export PATH="$RBASH_BIN:$PATH" } # ---- rbash: bash/core/99_main.bash ---- __rbash_main() { mkdir -p "$RBASH_HOME" "$RBASH_CACHE_DIR" "$RBASH_BIN" __rbash_init_config if [ -f "$RBASH_ENV" ]; then # shellcheck source=/dev/null source "$RBASH_ENV" fi rbash_sources_load __rbash_autoload __rbash_setup_prompt } __rbash_main unset -f __rbash_main