From a0268cdae7beaa4388d0df4e06ec1398bbaf9901 Mon Sep 17 00:00:00 2001 From: Peter Urda Date: Tue, 11 Aug 2026 19:25:09 -0700 Subject: [PATCH] Version 2.2.0 release Prevent command injection through prompt data Keep Git and GNU Screen values behind one-pass variable references when promptvars is enabled. Preserve direct display when promptvars is disabled. --- .github/workflows/testing.yaml | 17 ++++ CHANGELOG.md | 41 ++++++++ Makefile | 42 ++++++-- README.md | 49 +++++++++- VERSION | 2 +- bash_aliases | 3 - bash_exports | 3 +- bash_functions | 154 +++++++++++++++-------------- bashrc | 66 ++++++++++--- release_check.sh | 170 +++++++++++++++++++++++++++++++++ 10 files changed, 449 insertions(+), 98 deletions(-) create mode 100755 release_check.sh diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index ca137f9..59157f5 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -24,3 +24,20 @@ jobs: - name: ๐Ÿงช urda.bash Testing run: make test + + - name: ๐Ÿ’จ urda.bash Smoke Test + run: make smoke + + smoke-macos: + runs-on: macos-latest + + steps: + # https://github.com/marketplace/actions/checkout + - name: โฌ‡๏ธ Checkout + uses: actions/checkout@v5 + + - name: ๐Ÿ” Report `bash` Version + run: /bin/bash --version + + - name: ๐Ÿ’จ urda.bash Smoke Test (bash 3.2) + run: make smoke SMOKE_BASH=/bin/bash diff --git a/CHANGELOG.md b/CHANGELOG.md index a119972..4c65046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # CHANGELOG +## 2.2.0 + +This is an enhancement, housekeeping, and bugfix release. + +- `bashrc` + - Added extension drop-in loading: `*.sh` files in `~/.config/urda.bash/extensions.d/` are sourced in sorted order after core files and before `bash_secrets`. + - Added `URDABASH_LOADED_EXTENSIONS` count of loaded extensions. + - Moved `_prepend_path_once` in from `bash_functions` so PATH setup no longer depends on another file loading. + - Guarded the startup version check so a partial install fails quietly. + - Fixed the git and screen prompt lines to treat branch and session names as data, closing a command injection path through prompt expansion. + - Fixed XDG variables to respect values already set by the environment. + - Fixed a stray `pc` variable leaking into every shell. + - Fixed the `pnpm` PATH entry to use `${PNPM_HOME}/bin`, where pnpm v11 stores global binaries. +- `bash_aliases` + - Removed `get_uuid` alias (now a function, see `bash_functions`). +- `bash_exports` + - Fixed `GPG_TTY` to declare and export separately. +- `bash_functions` + - Added `get_uuid` function with fallbacks: `uuidgen`, the Linux kernel, then `python3`. Output is lowercase on all platforms. + - Added input validation to `bak`. + - Added `URDABASH_LOADED_EXTENSIONS` to `_urdabash_info`. + - Fixed `psg` to treat multiple words as a single search pattern. + - Fixed `unarc` to return an error on unknown archive types and print errors to stderr. + - Moved `_prepend_path_once` to `bashrc`. + - Reordered `_urdabash_update` before `_urdabash_version_check` to restore alphabetical order. +- `Makefile` + - Added `release-check` target for documentation parity checks. + - Added `smoke` target that boots bashrc in a clean shell against a temp HOME, including extension loader checks; `SMOKE_BASH` selects the shell under test. + - Added `install.sh` and `release_check.sh` to shellcheck coverage. + - Removed the SC2155 shellcheck exclusion. + - Switched variable references to braced style. +- `README.md` + - Documented the Extensions drop-in directory, including a trust warning. + - Documented the `release-check` and smoke targets. + - Updated `get_uuid` documentation (alias to function). +- `release_check.sh` + - New script verifying aliases and functions agree across the code, the help screen, and the README, and that all lists stay alphabetized. +- `.github/workflows/testing.yaml` + - Added the smoke test to the Ubuntu job. + - Added a macOS job running the smoke test under `/bin/bash` 3.2. + ## 2.1.3 This is a bugfix release. diff --git a/Makefile b/Makefile index e855a67..b6ddd19 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,9 @@ DIFF := $(firstword $(shell which colordiff diff)) DIFF_ARGS := -u MANIFEST_IGNORE := bash_history|bash_secrets SHELLCHECK := $(shell which shellcheck) -SHELLCHECK_ARGS := --shell=bash -e SC1090 -e SC1091 -e SC2009 -e SC2155 -o require-variable-braces -SHELLCHECK_FILES := $(addprefix ./,$(shell cat MANIFEST)) +SMOKE_BASH := bash +SHELLCHECK_ARGS := --shell=bash -e SC1090 -e SC1091 -e SC2009 -o require-variable-braces +SHELLCHECK_FILES := $(addprefix ./,$(shell cat MANIFEST)) ./install.sh ./release_check.sh ######################################################################################################################## # Commands @@ -22,7 +23,7 @@ SHELLCHECK_FILES := $(addprefix ./,$(shell cat MANIFEST)) # `make help` needs to be first so it is ran when a bare `make` is called. .PHONY: help help: # Show this help screen - @grep -E '^[a-zA-Z_-]+:.*# .*$$' $(MAKEFILE_LIST) |\ + @grep -E '^[a-zA-Z_-]+:.*# .*$$' ${MAKEFILE_LIST} |\ sort -k1,1 |\ awk 'BEGIN {FS = ":.*?# "}; {printf "\033[1m%-30s\033[0m %s\n", $$1, $$2}' @@ -38,27 +39,54 @@ copy: # Copy the local bash configs into your home directory (DESTRUCTIVE). diffs: # Run a diff against your local shell files against this repo's shell files. @while IFS= read -r file; do \ echo "diff ~/.$${file} ./$${file}"; \ - $(DIFF) $(DIFF_ARGS) "$${HOME}/.$${file}" "./$${file}" || :; \ + ${DIFF} ${DIFF_ARGS} "$${HOME}/.$${file}" "./$${file}" || :; \ done < MANIFEST .PHONY: manifest-check manifest-check: # Validate MANIFEST matches managed bash files. - @repo_files=$$(ls -1 bash* | grep -Ev '$(MANIFEST_IGNORE)' | sort); \ + @repo_files=$$(ls -1 bash* | grep -Ev '${MANIFEST_IGNORE}' | sort); \ manifest_files=$$(sort MANIFEST); \ if [ "$${repo_files}" = "$${manifest_files}" ]; then \ echo "MANIFEST is up to date"; \ exit 0; \ else \ echo "MANIFEST mismatch:"; \ - $(DIFF) $(DIFF_ARGS) --label REPO --label MANIFEST <(echo "$${repo_files}") <(echo "$${manifest_files}"); \ + ${DIFF} ${DIFF_ARGS} --label REPO --label MANIFEST <(echo "$${repo_files}") <(echo "$${manifest_files}"); \ exit 1; \ fi +.PHONY: release-check +release-check: # Run release checklist parity checks (docs vs code). +release-check: version-check +release-check: manifest-check + ./release_check.sh + +.PHONY: smoke +smoke: # Boot bashrc in a clean interactive shell against a temp HOME. + @tmp_home=$$(mktemp -d); \ + while IFS= read -r file; do \ + cp "$${file}" "$${tmp_home}/.$${file}"; \ + done < MANIFEST; \ + mkdir -p "$${tmp_home}/.config/urda.bash/extensions.d"; \ + echo 'SMOKE_ORDER="$${SMOKE_ORDER}10-"' > "$${tmp_home}/.config/urda.bash/extensions.d/10-a.sh"; \ + echo 'SMOKE_ORDER="$${SMOKE_ORDER}20"' > "$${tmp_home}/.config/urda.bash/extensions.d/20-b.sh"; \ + echo 'SMOKE_ORDER="BROKEN"' > "$${tmp_home}/.config/urda.bash/extensions.d/notes.txt"; \ + env -i HOME="$${tmp_home}" PATH="$${PATH}" TERM=dumb \ + ${SMOKE_BASH} -i -c '_urdabash_info && _urdabash_help >/dev/null \ + && [ "$${URDABASH_LOADED_EXTENSIONS}" = "2" ] \ + && [ "$${SMOKE_ORDER}" = "10-20" ]' \ + >/dev/null 2>"$${tmp_home}/stderr.log"; \ + status=$$?; \ + grep -vE "job control|ioctl" "$${tmp_home}/stderr.log" >&2 || :; \ + rm -rf "$${tmp_home}"; \ + if [ $${status} -eq 0 ]; then echo "smoke OK"; else echo "smoke FAILED"; fi; \ + exit $${status} + .PHONY: test test: # Test and check shell scripts for issues. test: version-check test: manifest-check - $(SHELLCHECK) $(SHELLCHECK_ARGS) $(SHELLCHECK_FILES) + ${SHELLCHECK} ${SHELLCHECK_ARGS} ${SHELLCHECK_FILES} .PHONY: test-update test-update: # Test _urdabash_update against localhost:8000 (run 'serve' first). diff --git a/README.md b/README.md index 6245c48..c3597c5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This is a collection of my bash prompt settings, aliases, exports, and other rel - [`pnpm`](https://pnpm.io/) support. - Weekly `VERSION` check against GitHub remote (non-blocking). - Also supports on-demand version checking with `_urdabash_version_check now` +- Extension drop-in directory (`~/.config/urda.bash/extensions.d/`). Should work with `bash 3.2` or higher. @@ -33,7 +34,6 @@ Should work with `bash 3.2` or higher. - `dadjoke` - Random dad joke from `icanhazdadjoke.com`. - `diff` - Unified diff format, with color via `colordiff` when available. - `epoch` - Print current unix timestamp (seconds). -- `get_uuid` - Generate a random UUID. - `headers` - Fetch HTTP response headers only. - `ll` - Long listing format (`ls -hlF`). - `moon` - Current moon phase via `wttr.in`. @@ -54,6 +54,8 @@ Should work with `bash 3.2` or higher. - Back up a file with a `.bak` extension. - `coinflip` - Flip a coin. +- `get_uuid` + - Generate a random lowercase UUID (`uuidgen`, Linux kernel, or `python3`, whichever is present). - `mkcd` - Create a directory and `cd` into it in one step. - `psg` @@ -111,6 +113,27 @@ Version check state is stored at `${XDG_STATE_HOME}/urda.bash/` (`~/.local/state - `last_check` - Timestamp file used to determine when the next fetch is due. - `remote_version` - Cached remote version string from the last successful fetch. +### Extensions + +Drop `*.sh` files into `${XDG_CONFIG_HOME}/urda.bash/extensions.d/` +(`~/.config/urda.bash/extensions.d/`) and they are sourced automatically at +shell startup, in sorted order, after the core urda.bash files and before +`bash_secrets` and `bash_local`. Use `NN-` prefixes to control load order: + +```text +10-bw-session.sh +20-work-tools.sh +``` + +The directory is optional and never touched by the updater. `_urdabash_info` +reports how many extensions loaded via `URDABASH_LOADED_EXTENSIONS`. + +#### A Word on Trust + +Sourcing an extension is running shell code in every interactive shell, with +everything your user can do. Only place files you trust in this directory, +and inspect anything you did not write yourself before your next shell starts. + ### Local Customizations If `~/.bash_local` exists, it is sourced automatically after all other files. Use this file for machine-specific aliases, functions, or overrides that should survive upgrades. @@ -141,6 +164,20 @@ This will also run a `make version-check`. make test ``` +### Smoke test + +Boot the managed files in a clean interactive shell against a temporary `HOME`: + +```bash +make smoke +``` + +Set `SMOKE_BASH` to select the shell under test: + +```bash +make smoke SMOKE_BASH=/bin/bash +``` + ### Copying files to your `${HOME}` **WARNING!** This is a **DESTRUCTIVE** operation and copies `bash` files from the project into your `${HOME}`. @@ -152,3 +189,13 @@ make copy ### Project version check Just run `make version-check`. + +### Release checks + +Before cutting a release, run the documentation parity checks: + +```bash +make release-check +``` + +This verifies that every alias and function agrees across the code, the `_urdabash_help` screen, and this README, that each list is alphabetized, and runs the version and MANIFEST checks. diff --git a/VERSION b/VERSION index ac2cdeb..ccbccc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.3 +2.2.0 diff --git a/bash_aliases b/bash_aliases index 57bafe6..8dd99bf 100644 --- a/bash_aliases +++ b/bash_aliases @@ -24,9 +24,6 @@ fi # current unix epoch in seconds alias epoch='date +%s' -# get a UUID on demand -alias get_uuid='python3 -c "import uuid;print(uuid.uuid4())"' - # fetch HTTP response headers only alias headers='curl -sI --max-time 5' diff --git a/bash_exports b/bash_exports index d243ac7..f5c15e6 100644 --- a/bash_exports +++ b/bash_exports @@ -9,7 +9,8 @@ export GIT_PS1_SHOWSTASHSTATE=true export GIT_PS1_SHOWUNTRACKEDFILES=true # GPG -export GPG_TTY="$(tty)" +GPG_TTY="$(tty)" +export GPG_TTY # History Settings export HISTCONTROL=ignoreboth diff --git a/bash_functions b/bash_functions index 1936547..df79f0c 100644 --- a/bash_functions +++ b/bash_functions @@ -9,6 +9,10 @@ bak() { # Back up a file with a .bak extension. # Usage: bak + if [ ! -f "${1}" ]; then + echo "'${1}' is not a valid file" >&2 + return 1 + fi cp -v "${1}" "${1}.bak" } @@ -18,6 +22,22 @@ coinflip() { (( RANDOM % 2 )) && echo "heads" || echo "tails" } +get_uuid() { + # Generate a random lowercase UUID. + # Tries uuidgen, then the Linux kernel, then python3. + # Usage: get_uuid + if command -v uuidgen >/dev/null 2>&1; then + uuidgen | tr '[:upper:]' '[:lower:]' + elif [ -r /proc/sys/kernel/random/uuid ]; then + cat /proc/sys/kernel/random/uuid + elif command -v python3 >/dev/null 2>&1; then + python3 -c "import uuid;print(uuid.uuid4())" + else + echo "get_uuid: no uuid source available" >&2 + return 1 + fi +} + mkcd() { # Create a directory and cd into it in one step. # Usage: mkcd @@ -28,7 +48,7 @@ psg() { # Search running processes by name. # Filters out the grep process itself to avoid false matches. # Usage: psg - ps aux | grep -v grep | grep -i "${@}" + ps aux | grep -v grep | grep -i "${*}" } roll() { @@ -53,7 +73,7 @@ unarc() { # Supports: tar.bz2, tar.gz, tar.lzma, tar.xz, tar, 7z, bz2, gz, lzma, rar, xz, Z, zip # Usage: unarc if [ ! -f "${1}" ]; then - echo "'${1}' is not a valid file" + echo "'${1}' is not a valid file" >&2 return 1 fi case "${1}" in @@ -70,7 +90,7 @@ unarc() { *.xz) unxz -v "${1}" ;; # -v=verbose, decompress xz (not a tar) *.Z) uncompress -v "${1}" ;; # -v=verbose, legacy Unix compress format *.zip) unzip "${1}" ;; # verbose by default - *) echo "'${1}' cannot be extracted via unarc()" ;; + *) echo "'${1}' cannot be extracted via unarc()" >&2; return 1 ;; esac } @@ -100,19 +120,6 @@ fi # Internal Functions ################################################################################ -_prepend_path_once() { - # Prepend a directory to PATH if it exists and is not already present. - # Usage: _prepend_path_once - local dir=${1} - [[ -d ${dir} ]] || return - case ":${PATH}:" in - # Already present - *":${dir}:"*) ;; - # Prepend once - *) PATH="${dir}:${PATH}" ;; - esac -} - _urdabash_help() { # Print a quick reference of all urda.bash aliases and functions. echo "urda.bash ${URDABASH_VERSION} - Quick Reference" @@ -124,7 +131,6 @@ _urdabash_help() { echo " dadjoke ......... Random dad joke from icanhazdadjoke.com" echo " diff ............ Unified diff format, with color via colordiff when available" echo " epoch ........... Print current unix timestamp (seconds)" - echo " get_uuid ........ Generate a random UUID" echo " headers ......... Fetch HTTP response headers only" echo " ll .............. Long listing format (ls -hlF)" echo " moon ............ Current moon phase via wttr.in" @@ -142,6 +148,7 @@ _urdabash_help() { echo "Functions:" echo " bak ............. Back up a file with a .bak extension" echo " coinflip ........ Flip a coin" + echo " get_uuid ........ Generate a random lowercase UUID" echo " mkcd ............ Create a directory and cd into it" echo " psg ............. Search running processes by name" echo " roll ............ Roll a die (d6 default, or specify sides)" @@ -160,12 +167,14 @@ _urdabash_info() { # Print urda.bash configuration and loaded module status. local onepassword_status="0" local direnv_status="0" + local extensions_status="0" local fnm_status="0" local homebrew_status="0" local local_status="0" local pnpm_status="0" [[ -n ${URDABASH_LOADED_1PASSWORD+x} ]] && onepassword_status="${URDABASH_LOADED_1PASSWORD}" [[ -n ${URDABASH_LOADED_DIRENV+x} ]] && direnv_status="${URDABASH_LOADED_DIRENV}" + [[ -n ${URDABASH_LOADED_EXTENSIONS+x} ]] && extensions_status="${URDABASH_LOADED_EXTENSIONS}" [[ -n ${URDABASH_LOADED_FNM+x} ]] && fnm_status="${URDABASH_LOADED_FNM}" [[ -n ${URDABASH_LOADED_HOMEBREW+x} ]] && homebrew_status="${URDABASH_LOADED_HOMEBREW}" [[ -n ${URDABASH_LOADED_LOCAL+x} ]] && local_status="${URDABASH_LOADED_LOCAL}" @@ -177,67 +186,13 @@ _urdabash_info() { echo "URDABASH_OS ................. ${URDABASH_OS}" echo "URDABASH_LOADED_1PASSWORD ... ${onepassword_status}" echo "URDABASH_LOADED_DIRENV ...... ${direnv_status}" + echo "URDABASH_LOADED_EXTENSIONS .. ${extensions_status}" echo "URDABASH_LOADED_FNM ......... ${fnm_status}" echo "URDABASH_LOADED_HOMEBREW .... ${homebrew_status}" echo "URDABASH_LOADED_LOCAL ....... ${local_status}" echo "URDABASH_LOADED_PNPM ........ ${pnpm_status}" } -_urdabash_version_check() { - # Check for a newer urda.bash release on GitHub. - # Reads a cached remote version for instant comparison, then schedules - # a background fetch if the cache is stale (older than 7 days). - # Usage: _urdabash_version_check [now] - # now - skip the interval check and force a background fetch - local state_dir="${XDG_STATE_HOME}/urda.bash" - local cached="${state_dir}/remote_version" - local force_check_now=${1:-} # pass "now" to skip interval and force fetch - local interval=604800 # 7 days - local last_check_timestamp="${state_dir}/last_check" - - # Check cached remote version (instant, no network) - if [ -f "${cached}" ]; then - local remote - remote=$(<"${cached}") - remote=${remote//[[:space:]]/} - if [ -n "${remote}" ] && [ "${remote}" != "${URDABASH_VERSION}" ]; then - local newest - newest=$(printf '%s\n%s' "${remote}" "${URDABASH_VERSION}" | sort -V | tail -1) - if [ "${newest}" = "${remote}" ]; then - printf "An urda.bash update is available:\nLocal version: '%s'\nRemote version: '%s'\nRun '_urdabash_update' to update.\n" "${URDABASH_VERSION}" "${remote}" >&2 - fi - fi - fi - - # Determine if a background fetch is needed - local now - local last=0 - now=$(date +%s) - if [ -f "${last_check_timestamp}" ]; then - last=$(stat -c %Y "${last_check_timestamp}" 2>/dev/null || stat -f %m "${last_check_timestamp}" 2>/dev/null || echo 0) - fi - if [[ ${force_check_now} != now ]] && (( now - last < interval )); then - return - fi - - # Background fetch: update timestamp and fetch remote version - # Suppress job control notifications ([1] PID / [1]+ Done) - { - ( - mkdir -p "${state_dir}" || return - touch "${last_check_timestamp}" || return - - command -v curl >/dev/null 2>&1 || return - local fetched - fetched=$(curl -fs -m 5 "${URDABASH_VERSION_URL}" 2>/dev/null) || return - - fetched=${fetched//[[:space:]]/} - [ -n "${fetched}" ] && printf '%s' "${fetched}" > "${cached}" - ) & - disown - } 2>/dev/null -} - _urdabash_update() { # Self-update urda.bash by fetching the latest files from GitHub. # Downloads VERSION and MANIFEST from remote, stages all managed files @@ -338,3 +293,58 @@ _urdabash_update() { echo "urda.bash updated: ${URDABASH_VERSION} -> ${remote_version}" echo "Open a new shell to use the new version." } + +_urdabash_version_check() { + # Check for a newer urda.bash release on GitHub. + # Reads a cached remote version for instant comparison, then schedules + # a background fetch if the cache is stale (older than 7 days). + # Usage: _urdabash_version_check [now] + # now - skip the interval check and force a background fetch + local state_dir="${XDG_STATE_HOME}/urda.bash" + local cached="${state_dir}/remote_version" + local force_check_now=${1:-} # pass "now" to skip interval and force fetch + local interval=604800 # 7 days + local last_check_timestamp="${state_dir}/last_check" + + # Check cached remote version (instant, no network) + if [ -f "${cached}" ]; then + local remote + remote=$(<"${cached}") + remote=${remote//[[:space:]]/} + if [ -n "${remote}" ] && [ "${remote}" != "${URDABASH_VERSION}" ]; then + local newest + newest=$(printf '%s\n%s' "${remote}" "${URDABASH_VERSION}" | sort -V | tail -1) + if [ "${newest}" = "${remote}" ]; then + printf "An urda.bash update is available:\nLocal version: '%s'\nRemote version: '%s'\nRun '_urdabash_update' to update.\n" "${URDABASH_VERSION}" "${remote}" >&2 + fi + fi + fi + + # Determine if a background fetch is needed + local now + local last=0 + now=$(date +%s) + if [ -f "${last_check_timestamp}" ]; then + last=$(stat -c %Y "${last_check_timestamp}" 2>/dev/null || stat -f %m "${last_check_timestamp}" 2>/dev/null || echo 0) + fi + if [[ ${force_check_now} != now ]] && (( now - last < interval )); then + return + fi + + # Background fetch: update timestamp and fetch remote version + # Suppress job control notifications ([1] PID / [1]+ Done) + { + ( + mkdir -p "${state_dir}" || return + touch "${last_check_timestamp}" || return + + command -v curl >/dev/null 2>&1 || return + local fetched + fetched=$(curl -fs -m 5 "${URDABASH_VERSION_URL}" 2>/dev/null) || return + + fetched=${fetched//[[:space:]]/} + [ -n "${fetched}" ] && printf '%s' "${fetched}" > "${cached}" + ) & + disown + } 2>/dev/null +} diff --git a/bashrc b/bashrc index e0fbe6a..725e847 100644 --- a/bashrc +++ b/bashrc @@ -20,7 +20,7 @@ bind 'set mark-symlinked-directories on' 2>/dev/null ################################################################################ if [[ -z ${URDABASH_VERSION+x} ]]; then - readonly URDABASH_VERSION="2.1.3" + readonly URDABASH_VERSION="2.2.0" export URDABASH_VERSION fi @@ -30,14 +30,28 @@ if [[ -z ${URDABASH_VERSION_URL+x} ]]; then fi if [[ -z ${URDABASH_OS+x} ]]; then - readonly URDABASH_OS="$(uname -s)" + URDABASH_OS="$(uname -s)" + readonly URDABASH_OS export URDABASH_OS fi -export XDG_CONFIG_HOME="${HOME}/.config" -export XDG_CACHE_HOME="${HOME}/.cache" -export XDG_DATA_HOME="${HOME}/.local/share" -export XDG_STATE_HOME="${HOME}/.local/state" +export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME}/.config}" +export XDG_CACHE_HOME="${XDG_CACHE_HOME:-${HOME}/.cache}" +export XDG_DATA_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}" +export XDG_STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}" + +_prepend_path_once() { + # Prepend a directory to PATH if it exists and is not already present. + # Usage: _prepend_path_once + local dir=${1} + [[ -d ${dir} ]] || return + case ":${PATH}:" in + # Already present + *":${dir}:"*) ;; + # Prepend once + *) PATH="${dir}:${PATH}" ;; + esac +} _source_if_exists() { # Source a file if it exists and is readable, skip otherwise. @@ -70,6 +84,17 @@ _source_if_exists "${HOME}/.bash_aliases" # Load common function definitions _source_if_exists "${HOME}/.bash_functions" +# Load extensions from the drop-in directory (sorted, *.sh only) +# shellcheck disable=SC2034 +URDABASH_LOADED_EXTENSIONS=0 +for _ext in "${XDG_CONFIG_HOME}/urda.bash/extensions.d"/*.sh; do + [[ -r "${_ext}" ]] || continue + # shellcheck source=/dev/null + source "${_ext}" + (( URDABASH_LOADED_EXTENSIONS++ )) +done +unset _ext + # Load any secrets _source_if_exists "${HOME}/.bash_secrets" @@ -132,7 +157,7 @@ if [[ -z ${URDABASH_LOADED_PNPM+x} ]]; then if command -v pnpm >/dev/null 2>&1; then readonly URDABASH_LOADED_PNPM=1 export PNPM_HOME="${XDG_DATA_HOME}/pnpm" - _prepend_path_once "${PNPM_HOME}" + _prepend_path_once "${PNPM_HOME}/bin" else readonly URDABASH_LOADED_PNPM=0 fi @@ -142,7 +167,7 @@ fi # Update Check ################################################################################ -_urdabash_version_check +type -t _urdabash_version_check >/dev/null && _urdabash_version_check ################################################################################ # Prompt Functions @@ -167,6 +192,9 @@ _ps1_header_line() { _ps1_git_line() { # Render the git prompt line: โ• โ•[git : branch_name] # Walks up the directory tree to detect .git before forking for __git_ps1. + # This value must remain global because PS1 expands it after this function returns. + _URDABASH_PS1_GIT_TEXT="" + local _dir="${PWD}" while [[ "${_dir}" != "/" ]]; do [[ -e "${_dir}/.git" ]] && break @@ -176,13 +204,18 @@ _ps1_git_line() { [[ "${_dir}" != "/" ]] || return 0 type __git_ps1 >/dev/null 2>&1 || return 0 - local branch - branch=$(__git_ps1 "%s") || branch="" - [[ -n "${branch}" ]] || return 0 + _URDABASH_PS1_GIT_TEXT=$(__git_ps1 "%s") || _URDABASH_PS1_GIT_TEXT="" + [[ -n "${_URDABASH_PS1_GIT_TEXT}" ]] || return 0 + local git_text="${_URDABASH_PS1_GIT_TEXT}" + if shopt -q promptvars; then + # Defer expansion so branch contents are not parsed as PS1 source. + # shellcheck disable=SC2016 + git_text='${_URDABASH_PS1_GIT_TEXT}' + fi local outline=${1} green=${2} blue=${3} reset=${4} local _line printf -v _line '%s%s%s[%sgit%s %s: %s%s%s]%s\\n' \ - "${outline}" $'\xE2\x95\xa0' $'\xE2\x95\x90' "${green}" "${reset}" "${outline}" "${blue}" "${branch}" "${outline}" "${reset}" + "${outline}" $'\xE2\x95\xa0' $'\xE2\x95\x90' "${green}" "${reset}" "${outline}" "${blue}" "${git_text}" "${outline}" "${reset}" _PS1_BUF+=${_line} } @@ -190,10 +223,16 @@ _ps1_screen_line() { # Render the screen prompt line: โ• โ•[screen : session_name] # Only shown when inside a GNU screen session (STY is set). [[ -n "${STY}" ]] || return 0 + local screen_text="${STY}" + if shopt -q promptvars; then + # Defer expansion so session contents are not parsed as PS1 source. + # shellcheck disable=SC2016 + screen_text='${STY}' + fi local outline=${1} green=${2} blue=${3} reset=${4} local _line printf -v _line '%s%s%s[%sscreen%s %s: %s%s%s]%s\\n' \ - "${outline}" $'\xE2\x95\xa0' $'\xE2\x95\x90' "${green}" "${reset}" "${outline}" "${blue}" "${STY}" "${outline}" "${reset}" + "${outline}" $'\xE2\x95\xa0' $'\xE2\x95\x90' "${green}" "${reset}" "${outline}" "${blue}" "${screen_text}" "${outline}" "${reset}" _PS1_BUF+=${_line} } @@ -248,6 +287,7 @@ case ";${pc};" in PROMPT_COMMAND="history -a;${pc:+${pc};}_set_ps1;" ;; esac +unset pc ################################################################################ # Priority PATH Overrides diff --git a/release_check.sh b/release_check.sh new file mode 100755 index 0000000..ce93115 --- /dev/null +++ b/release_check.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# +# release_check.sh - Documentation parity checks for urda.bash releases. +# +# Compares three sources of truth that must agree before a release: +# 1. The code: bash_aliases, bash_functions, bash_osx, bashrc +# 2. The help screen: _urdabash_help output +# 3. The docs: README.md +# +# Checks name parity between the sources and alphabetical order within +# each list. Exits non-zero if any check fails. +# +# Usage: ./release_check.sh (from the repo root, or via `make release-check`) + +set -euo pipefail + +cd "$(dirname "${0}")" + +FAILURES=0 + +pass() { + # Report a passing check. + # Usage: pass