#!/bin/bash # get - install scripts from toolio.sh into a directory on $PATH # # Usage: # get [script ...] # get --all # get -h # # Downloads scripts from https://toolio.sh and installs them to $INSTALL_DIR # (default: ~/.local/bin), making them executable. With no arguments, lists # the available scripts grouped by category _get() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" # When piped via `curl ... | bash`, BASH_SOURCE[0] is empty or "bash", # so basename returns "bash". Fall back to the canonical name case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="get" ;; esac _show_help() { local s; [ -t 1 ] && s="$(tput smul 2>/dev/null || echo '')" local r; [ -t 1 ] && r="$(tput rmul 2>/dev/null || echo '')" echo "NAME" echo " $SCRIPT_NAME - install scripts from toolio.sh into a directory on \$PATH" echo "SYNOPSIS" echo " $SCRIPT_NAME [${s}script${r} ${s}...${r}]" echo " $SCRIPT_NAME --all" echo " $SCRIPT_NAME -h" echo "DESCRIPTION" echo " Downloads scripts from https://toolio.sh and installs them to" echo " \$INSTALL_DIR (default: ~/.local/bin), making them executable." echo " With no arguments, lists the available scripts grouped by category." echo "" echo " Re-running is the upgrade path: unchanged files are left alone," echo " changed files are replaced atomically." echo "OPTIONS" echo " --all Install every script in the catalog" echo " -h, --help Show this help message" echo "ENVIRONMENT" echo " INSTALL_DIR Override the install directory (default: ~/.local/bin)" echo "EXAMPLES" echo " # List available scripts" echo " curl -fsS https://toolio.sh/$SCRIPT_NAME | bash" echo "" echo " # Install specific scripts" echo " curl -fsS https://toolio.sh/$SCRIPT_NAME | bash -s -- tsd pin-dns" echo "" echo " # Install everything" echo " curl -fsS https://toolio.sh/$SCRIPT_NAME | bash -s -- --all" echo "" echo " # Custom install directory" echo " curl -fsS https://toolio.sh/$SCRIPT_NAME | INSTALL_DIR=~/bin bash -s -- tsd" echo "EXIT STATUS" echo " 0 All requested scripts installed/up-to-date (or list printed)" echo " 1 Some scripts failed (network, write failure)" echo " 2 Argument error (unknown script, bad flag)" echo " 3 Missing dependency (curl)" echo "DEPENDENCIES" echo " curl" } _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } _warn() { echo "[WRN][$SCRIPT_NAME] $*" >&2; } _info() { echo "[INF][$SCRIPT_NAME] $*" >&2; } _fetch_catalog() { local url="${_GET_TEST_INDEX_URL:-https://toolio.sh/INDEX.md}" local body if ! body="$(curl -fsS "$url" 2>&1)"; then _error "Failed to fetch INDEX.md: $body" return 1 fi printf '%s\n' "$body" | awk ' # Skip top-level title and the Jump-to line /^# / || /^\*\*Jump to:/ { next } # Capture category headings; ignore "## Notes" /^## / { sub(/^## /, "", $0) if ($0 == "Notes") { cat = ""; next } cat = $0 next } # Catalog rows: start with "| `NAME`" or "| [`NAME`]" (NAME is any non-backtick) /^\| `[^`]+`/ || /^\| \[`[^`]+`/ { line = $0 # Extract NAME from the first backtick pair i = index(line, "`") if (i == 0) next rest = substr(line, i + 1) j = index(rest, "`") if (j == 0) next name = substr(rest, 1, j - 1) # Description is between the 2nd and last "|" n = 0 second_pipe = 0 for (k = 1; k <= length(line); k++) { if (substr(line, k, 1) == "|") { n++ if (n == 2) { second_pipe = k; break } } } if (second_pipe == 0) next last_pipe = 0 for (k = length(line); k >= 1; k--) { if (substr(line, k, 1) == "|") { last_pipe = k; break } } if (last_pipe <= second_pipe) next desc = substr(line, second_pipe + 1, last_pipe - second_pipe - 1) # Trim leading/trailing whitespace from desc sub(/^[ \t]+/, "", desc) sub(/[ \t]+$/, "", desc) if (cat != "" && name != "") { printf "%s\t%s\t%s\n", cat, name, desc } } ' } _format_listing() { local catalog="$1" local max_width max_width="$(printf '%s\n' "$catalog" | awk -F'\t' '{ if (length($2) > m) m = length($2) } END { print m+0 }')" printf '%s\n' "$catalog" | awk -F'\t' -v w="$max_width" ' BEGIN { cur = "" } { if ($1 != cur) { if (cur != "") print "" print $1 cur = $1 } printf " %-*s %s\n", w, $2, $3 } ' } _install_one() { local name="$1" local dir="$2" local url="${_GET_TEST_SCRIPT_URL_PREFIX:-https://toolio.sh}/$name" local tmp tmp="$(mktemp)" || { echo "failed: $name (mktemp)"; return 1; } if ! curl -fsS "$url" -o "$tmp"; then echo "failed: $name (fetch)" rm -f "$tmp" return 1 fi if [ ! -s "$tmp" ] || ! /bin/bash -n "$tmp" 2>/dev/null; then echo "failed: $name (invalid content)" rm -f "$tmp" return 1 fi local target="$dir/$name" if [ -e "$target" ] && cmp -s "$tmp" "$target"; then echo "up to date: $name" rm -f "$tmp" return 2 # 2 == up-to-date sentinel (caller maps to counter) fi local was_present=0 [ -e "$target" ] && was_present=1 if ! mv "$tmp" "$target"; then echo "failed: $name (mv)" rm -f "$tmp" return 1 fi if ! chmod +x "$target"; then echo "failed: $name (chmod)" rm -f "$target" return 1 fi if [ "$was_present" -eq 1 ]; then echo "updated: $name" return 3 # 3 == updated sentinel else echo "installed: $name" return 0 # 0 == installed fi } # Clean up inner functions when returning if ! command -v curl >/dev/null 2>&1; then _error "curl is required" return 3 fi local all_mode=0 local requested=() while [ $# -gt 0 ]; do case "$1" in -h|--help) _show_help return 0 ;; --all) all_mode=1 ;; -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; *) requested+=("$1") ;; esac shift done if [ "$all_mode" -eq 1 ] && [ "${#requested[@]}" -gt 0 ]; then _error "--all cannot be combined with script names. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi # Fetch catalog once local catalog if ! catalog="$(_fetch_catalog)"; then return 1 fi if [ -z "$catalog" ]; then _error "Catalog is empty" return 1 fi # --all: populate requested from the full catalog if [ "$all_mode" -eq 1 ]; then local name while IFS= read -r name; do [ -n "$name" ] && requested+=("$name") done <<< "$(printf '%s\n' "$catalog" | awk -F'\t' '{print $2}')" fi # Validate requested names against catalog (skipped for --all; Task 7) if [ "$all_mode" -eq 0 ] && [ "${#requested[@]}" -gt 0 ]; then local names names="$(printf '%s\n' "$catalog" | awk -F'\t' '{print $2}')" local unknown=() local r local match local name for r in "${requested[@]}"; do match=0 while IFS= read -r name; do if [ "$name" = "$r" ]; then match=1 break fi done <<< "$names" if [ "$match" -eq 0 ]; then unknown+=("$r") fi done if [ "${#unknown[@]}" -gt 0 ]; then _error "Unknown script(s): ${unknown[*]}. Run \`$SCRIPT_NAME\` with no args to see the catalog" return 2 fi fi if [ "${#requested[@]}" -gt 0 ]; then local install_dir="${INSTALL_DIR:-$HOME/.local/bin}" if [ ! -d "$install_dir" ]; then if ! mkdir -p "$install_dir"; then _error "Failed to create install dir: $install_dir" return 1 fi _info "Created $install_dir" fi # Resolve to absolute path so the PATH warning's export line isn't CWD-dependent install_dir="$(cd "$install_dir" && pwd)" local n_installed=0 local n_updated=0 local n_up_to_date=0 local n_failed=0 local name for name in "${requested[@]}"; do _install_one "$name" "$install_dir" local rc=$? case $rc in 0) n_installed=$((n_installed + 1)) ;; 2) n_up_to_date=$((n_up_to_date + 1)) ;; 3) n_updated=$((n_updated + 1)) ;; *) n_failed=$((n_failed + 1)) ;; esac done local total=${#requested[@]} printf '%d requested: %d installed, %d updated, %d up to date, %d failed\n' \ "$total" "$n_installed" "$n_updated" "$n_up_to_date" "$n_failed" # Warn if install_dir is not on $PATH # Normalize (strip trailing slash) for PATH membership check local norm_install="${install_dir%/}" local norm_path=":${PATH}:" norm_path="${norm_path//\/:/:}" case "$norm_path" in *":$norm_install:"*) : ;; *) _warn "$install_dir is not on your \$PATH. To fix, add this to your shell rc:" echo "export PATH=\"$install_dir:\$PATH\"" >&2 ;; esac if [ "$n_failed" -gt 0 ]; then return 1 fi return 0 fi # Listing mode: no args, no --all if [ "$all_mode" -eq 0 ] && [ "${#requested[@]}" -eq 0 ]; then _format_listing "$catalog" return 0 fi return 0 ) _get "$@" __get_rc=$? unset -f _get if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __get_rc; return $__get_rc" fi eval "unset __get_rc; exit $__get_rc"