Improve installer updates and dependency checks and add command aliases
Release / release (push) Successful in 7m57s
Release / release (push) Successful in 7m57s
This commit is contained in:
1 parent
12cc64fdd9
commit
4b28d441b0
13 files changed
+973
-148
No files matched your search
+221
-75
@@ -1,94 +1,240 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
# Bash 3 (macOS) treats empty arrays as unset under nounset.
|
||||
: "${HOME:?HOME must be set}"
|
||||
|
||||
REPO="Sebi/ssh-netbox-wrapper"
|
||||
BASE_URL="https://git.zb-server.de"
|
||||
BINARY="netssh"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
need() { command -v "$1" >/dev/null 2>&1 || die "'$1' is required but not installed"; }
|
||||
# Check the complete toolchain before any network request or filesystem change.
|
||||
MISSING=()
|
||||
for program in curl uname sed awk mktemp rm dirname readlink mkdir cat install mv cp chmod cmp; do
|
||||
if ! command -v "$program" >/dev/null 2>&1; then MISSING+=("$program"); fi
|
||||
done
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
SHA256=(sha256sum)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
SHA256=(shasum -a 256)
|
||||
else
|
||||
MISSING+=("sha256sum or shasum (SHA-256 verification)")
|
||||
fi
|
||||
if [[ ${#MISSING[@]} -gt 0 ]]; then
|
||||
printf 'error: required programs are missing:\n' >&2
|
||||
printf ' - %s\n' "${MISSING[@]}" >&2
|
||||
die "install these dependencies using your package manager, then run the installer again"
|
||||
fi
|
||||
|
||||
# --- helpers -----------------------------------------------------------------
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
|
||||
die() { red "error: $*" >&2; exit 1; }
|
||||
|
||||
need() {
|
||||
command -v "$1" &>/dev/null || die "'$1' is required but not installed"
|
||||
}
|
||||
|
||||
# --- detect OS / arch --------------------------------------------------------
|
||||
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Linux) echo linux ;;
|
||||
Darwin) echo darwin ;;
|
||||
*) die "unsupported OS: $(uname -s)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) echo amd64 ;;
|
||||
aarch64|arm64) echo arm64 ;;
|
||||
*) die "unsupported architecture: $(uname -m)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- main --------------------------------------------------------------------
|
||||
|
||||
need curl
|
||||
|
||||
OS=$(detect_os)
|
||||
ARCH=$(detect_arch)
|
||||
|
||||
bold "netssh installer"
|
||||
info "platform : $OS/$ARCH"
|
||||
# Re-running the one-liner updates the existing installation in its current path.
|
||||
if [[ -z "${INSTALL_DIR:-}" ]]; then
|
||||
EXISTING=$(command -v "$BINARY" || true)
|
||||
if [[ "$EXISTING" == /* && -f "$EXISTING" ]]; then
|
||||
INSTALL_DIR=$(dirname "$EXISTING")
|
||||
else
|
||||
INSTALL_DIR=/usr/local/bin
|
||||
fi
|
||||
fi
|
||||
[[ "$INSTALL_DIR" == /* ]] || INSTALL_DIR="$PWD/$INSTALL_DIR"
|
||||
# Check sudo only when the destination needs elevated permissions, also before
|
||||
# downloading. For a new directory, inspect its nearest existing parent.
|
||||
PRIVILEGE=()
|
||||
INSTALL_PARENT=$INSTALL_DIR
|
||||
while [[ ! -e "$INSTALL_PARENT" ]]; do INSTALL_PARENT=$(dirname "$INSTALL_PARENT"); done
|
||||
[[ -d "$INSTALL_PARENT" ]] || die "installation path is not a directory: $INSTALL_PARENT"
|
||||
if [[ ! -w "$INSTALL_PARENT" || ! -x "$INSTALL_PARENT" ]]; then
|
||||
need sudo
|
||||
PRIVILEGE=(sudo)
|
||||
fi
|
||||
case "$(uname -s)" in
|
||||
Linux) PLATFORM_OS=linux ;;
|
||||
Darwin) PLATFORM_OS=darwin ;;
|
||||
*) die "unsupported OS: $(uname -s)" ;;
|
||||
esac
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) PLATFORM_ARCH=amd64 ;;
|
||||
aarch64|arm64) PLATFORM_ARCH=arm64 ;;
|
||||
*) die "unsupported architecture: $(uname -m)" ;;
|
||||
esac
|
||||
printf 'netssh install / update\n'
|
||||
info "platform : $PLATFORM_OS/$PLATFORM_ARCH"
|
||||
info "target : $INSTALL_DIR/$BINARY"
|
||||
|
||||
# Fetch the latest release tag from the Gitea API.
|
||||
API_URL="$BASE_URL/api/v1/repos/$REPO/releases/latest"
|
||||
TAG=$(curl -sf "$API_URL" | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])") \
|
||||
|| die "could not fetch latest release from $API_URL"
|
||||
|
||||
ASSET="${BINARY}_${OS}_${ARCH}"
|
||||
DOWNLOAD_URL="$BASE_URL/$REPO/releases/download/$TAG/$ASSET"
|
||||
CHECKSUM_URL="$BASE_URL/$REPO/releases/download/$TAG/checksums.txt"
|
||||
|
||||
RELEASE=$(curl -fsSL "$API_URL") || die "could not fetch latest release from $API_URL"
|
||||
TAG=$(printf '%s\n' "$RELEASE" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.+-][a-zA-Z0-9.+-]+)?$ ]] || die "invalid or missing release tag"
|
||||
ASSET="${BINARY}_${PLATFORM_OS}_${PLATFORM_ARCH}"
|
||||
DOWNLOAD_URL="$BASE_URL/$REPO/releases/download/$TAG"
|
||||
info "version : $TAG"
|
||||
echo
|
||||
|
||||
# Download binary and checksums into a temp directory.
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
STAGED=""
|
||||
cleanup() {
|
||||
rm -rf "$TMP"
|
||||
if [[ -n "$STAGED" ]]; then "${PRIVILEGE[@]}" rm -f "$STAGED"; fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
info "downloading $ASSET..."
|
||||
curl -fL --progress-bar -o "$TMP/$ASSET" "$DOWNLOAD_URL"
|
||||
curl -fL --progress-bar -o "$TMP/$ASSET" "$DOWNLOAD_URL/$ASSET"
|
||||
curl -fsSL -o "$TMP/checksums.txt" "$DOWNLOAD_URL/checksums.txt" \
|
||||
|| die "could not fetch checksums; installation aborted"
|
||||
EXPECTED=$(awk -v asset="$ASSET" '$2 == asset || $2 == "*" asset {print tolower($1)}' "$TMP/checksums.txt")
|
||||
[[ "$EXPECTED" =~ ^[a-f0-9]{64}$ ]] || die "missing, duplicate, or invalid checksum; installation aborted"
|
||||
ACTUAL=$("${SHA256[@]}" "$TMP/$ASSET")
|
||||
ACTUAL=${ACTUAL%% *}
|
||||
[[ "$ACTUAL" == "$EXPECTED" ]] || die "checksum mismatch; installation aborted"
|
||||
info "checksum OK"
|
||||
|
||||
info "verifying checksum..."
|
||||
curl -sf -o "$TMP/checksums.txt" "$CHECKSUM_URL" \
|
||||
|| { info "warning: could not fetch checksums, skipping verification"; }
|
||||
# Only standalone commands and our managed blocks are removed. Unrelated content
|
||||
# and commands which merely mention netssh are retained.
|
||||
cat > "$TMP/clean-profile.awk" <<'AWK'
|
||||
function legacy(s) {
|
||||
if (s ~ /^([^ \t]*\/)?netssh[ \t]+shell-init[ \t]*(#.*)?$/) return 1
|
||||
if (s ~ /^(source|\.)[ \t]+["']?[^;|&]*netssh\/aliases\.sh["']?([ \t]+2>\/dev\/null)?[ \t]*(#.*)?$/) return 1
|
||||
if (s ~ /^eval[ \t]+["']?\$\(([^ \t]*\/)?netssh[ \t]+alias([ \t]+--shell[ \t]+(bash|zsh))?\)["']?[ \t]*(#.*)?$/) return 1
|
||||
if (s ~ /^([^ \t]*\/)?netssh[ \t]+alias([ \t]+--shell[ \t]+fish)?[ \t]*\|[ \t]*source[ \t]*(#.*)?$/) return 1
|
||||
if (s ~ /^source[ \t]+<\(([^ \t]*\/)?netssh[ \t]+alias\)[ \t]*(#.*)?$/) return 1
|
||||
if (s ~ /^alias[ \t]+[A-Za-z0-9_.-]+(=|[ \t]+)["']netssh([ \t]+[^"']+)?["'][ \t]*(#.*)?$/) return 1
|
||||
return 0
|
||||
}
|
||||
{
|
||||
s = $0
|
||||
sub(/^[ \t]+/, "", s); sub(/[ \t\r]+$/, "", s)
|
||||
if (s == "# >>> netssh >>>") { if (inside) bad = 1; inside = 1; next }
|
||||
if (s == "# <<< netssh <<<") { if (!inside) bad = 1; inside = 0; next }
|
||||
if (!inside && !legacy(s)) print $0
|
||||
}
|
||||
END { if (inside || bad) exit 1 }
|
||||
AWK
|
||||
|
||||
if [[ -f "$TMP/checksums.txt" ]]; then
|
||||
# checksums.txt was built with `sha256sum` in the dist/ dir, so entries look
|
||||
# like "abc123 netssh_linux_amd64". We need to cd into the temp dir first.
|
||||
(cd "$TMP" && grep "$ASSET" checksums.txt | sha256sum --check --status) \
|
||||
|| die "checksum mismatch — aborting installation"
|
||||
info "checksum OK"
|
||||
fi
|
||||
# POSIX-style single quoting also works for these paths in fish, except embedded
|
||||
# quotes, which fish escapes with a backslash instead of concatenated quotes.
|
||||
quote_sh() { printf "'%s'" "${1//\'/\'\\\'\'}"; }
|
||||
quote_fish() {
|
||||
local value=${1//\\/\\\\}
|
||||
value=${value//\'/\\\'}
|
||||
printf "'%s'" "$value"
|
||||
}
|
||||
resolve_profile() {
|
||||
local path=$1 target steps=0
|
||||
while [[ -L "$path" ]]; do
|
||||
steps=$((steps + 1))
|
||||
[[ "$steps" -le 40 ]] || die "symlink loop in $1"
|
||||
target=$(readlink "$path")
|
||||
if [[ "$target" == /* ]]; then path=$target; else path="$(dirname "$path")/$target"; fi
|
||||
done
|
||||
printf '%s\n' "$path"
|
||||
}
|
||||
|
||||
chmod +x "$TMP/$ASSET"
|
||||
|
||||
# Install — try without sudo first, fall back if needed.
|
||||
if [[ -w "$INSTALL_DIR" ]]; then
|
||||
mv "$TMP/$ASSET" "$INSTALL_DIR/$BINARY"
|
||||
PROFILE_PATHS=()
|
||||
PROFILE_TARGETS=()
|
||||
PROFILE_DATA=()
|
||||
SHELLS=()
|
||||
CURRENT_SHELL=${SHELL:-}
|
||||
CURRENT_SHELL=${CURRENT_SHELL##*/}
|
||||
CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
if [[ "$PLATFORM_OS" == darwin ]]; then
|
||||
CACHE_DIR=${XDG_CACHE_HOME:-$HOME/Library/Caches}
|
||||
else
|
||||
info "sudo required to write to $INSTALL_DIR"
|
||||
sudo mv "$TMP/$ASSET" "$INSTALL_DIR/$BINARY"
|
||||
CACHE_DIR=${XDG_CACHE_HOME:-$HOME/.cache}
|
||||
fi
|
||||
ZSH_DIR=${ZDOTDIR:-$HOME}
|
||||
plan_profile() {
|
||||
local path=$1 kind=$2 target data previous count
|
||||
if [[ ! -e "$path" && ! -L "$path" ]] && { [[ -z "$kind" ]] || [[ "$kind" != "$CURRENT_SHELL" ]]; }; then return; fi
|
||||
target=$(resolve_profile "$path")
|
||||
# A dotfile manager may point two profile names at the same file.
|
||||
for previous in "${PROFILE_TARGETS[@]}"; do [[ "$previous" != "$target" ]] || return 0; done
|
||||
data="$TMP/profile-${#PROFILE_PATHS[@]}"
|
||||
if [[ -e "$target" ]]; then
|
||||
awk -f "$TMP/clean-profile.awk" "$target" > "$data" \
|
||||
|| die "unclosed or nested netssh block in $path; profiles were not changed"
|
||||
else
|
||||
: > "$data"
|
||||
fi
|
||||
if [[ -n "$kind" ]]; then
|
||||
count=0
|
||||
for previous in "${SHELLS[@]}"; do [[ "$previous" != "$kind" ]] || count=1; done
|
||||
if [[ "$count" == 0 ]]; then SHELLS+=("$kind"); fi
|
||||
{
|
||||
printf '# >>> netssh >>>\n'
|
||||
if [[ "$kind" == fish ]]; then
|
||||
printf 'set -gx PATH %s $PATH\n' "$(quote_fish "$INSTALL_DIR")"
|
||||
printf '%s shell-init\n' "$(quote_fish "$INSTALL_DIR/$BINARY")"
|
||||
else
|
||||
printf 'export PATH=%s:"$PATH"\n' "$(quote_sh "$INSTALL_DIR")"
|
||||
printf '%s shell-init\n' "$(quote_sh "$INSTALL_DIR/$BINARY")"
|
||||
printf '[ ! -f %s ] || . %s\n' "$(quote_sh "$CACHE_DIR/netssh/aliases.sh")" "$(quote_sh "$CACHE_DIR/netssh/aliases.sh")"
|
||||
fi
|
||||
printf '# <<< netssh <<<\n'
|
||||
} >> "$data"
|
||||
fi
|
||||
PROFILE_PATHS+=("$path"); PROFILE_TARGETS+=("$target"); PROFILE_DATA+=("$data")
|
||||
}
|
||||
# Preflight every profile before installing the binary or modifying any profile.
|
||||
plan_profile "$HOME/.bashrc" bash
|
||||
plan_profile "$HOME/.bash_profile" ""
|
||||
plan_profile "$HOME/.bash_login" ""
|
||||
plan_profile "$HOME/.profile" ""
|
||||
plan_profile "$ZSH_DIR/.zshrc" zsh
|
||||
plan_profile "$ZSH_DIR/.zprofile" ""
|
||||
plan_profile "$ZSH_DIR/.zshenv" ""
|
||||
if [[ "$ZSH_DIR" != "$HOME" ]]; then
|
||||
plan_profile "$HOME/.zshrc" ""
|
||||
plan_profile "$HOME/.zprofile" ""
|
||||
plan_profile "$HOME/.zshenv" ""
|
||||
fi
|
||||
plan_profile "$CONFIG_DIR/fish/config.fish" fish
|
||||
|
||||
echo
|
||||
green "✓ netssh $TAG installed to $INSTALL_DIR/$BINARY"
|
||||
info "run 'netssh --help' to get started"
|
||||
if [[ ${#PRIVILEGE[@]} -gt 0 ]]; then
|
||||
info "sudo required to write to $INSTALL_DIR"
|
||||
fi
|
||||
"${PRIVILEGE[@]}" mkdir -p "$INSTALL_DIR"
|
||||
# Stage on the destination filesystem to replace a running binary atomically.
|
||||
STAGED=$("${PRIVILEGE[@]}" mktemp "$INSTALL_DIR/.netssh.XXXXXX")
|
||||
"${PRIVILEGE[@]}" install -m 755 "$TMP/$ASSET" "$STAGED"
|
||||
"${PRIVILEGE[@]}" mv -f "$STAGED" "$INSTALL_DIR/$BINARY"
|
||||
STAGED=""
|
||||
|
||||
# Regenerate aliases, including the configured command alias, as the calling user.
|
||||
# An empty cache clears obsolete generated aliases. Never run the wizard here.
|
||||
write_aliases() {
|
||||
local kind=$1 path=$2 staged
|
||||
"$INSTALL_DIR/$BINARY" alias --shell "$kind" > "$TMP/aliases-$kind" \
|
||||
|| die "could not regenerate $kind aliases; profiles were not changed"
|
||||
mkdir -p "$(dirname "$path")"
|
||||
staged=$(mktemp "$(dirname "$path")/.netssh.XXXXXX")
|
||||
cat "$TMP/aliases-$kind" > "$staged"
|
||||
chmod 600 "$staged"
|
||||
mv -f "$staged" "$path"
|
||||
}
|
||||
write_aliases bash "$CACHE_DIR/netssh/aliases.sh"
|
||||
for kind in "${SHELLS[@]}"; do
|
||||
if [[ "$kind" == fish ]]; then write_aliases fish "$CONFIG_DIR/fish/conf.d/netssh.fish"; fi
|
||||
done
|
||||
|
||||
for ((index=0; index<${#PROFILE_PATHS[@]}; index++)); do
|
||||
target=${PROFILE_TARGETS[$index]}
|
||||
data=${PROFILE_DATA[$index]}
|
||||
if [[ -e "$target" ]] && cmp -s "$target" "$data"; then continue; fi
|
||||
mkdir -p "$(dirname "$target")"
|
||||
if [[ -e "$target" ]]; then
|
||||
backup=$(mktemp "$target.netssh-backup.XXXXXX")
|
||||
cp -p "$target" "$backup"
|
||||
info "backup : $backup"
|
||||
fi
|
||||
staged=$(mktemp "$(dirname "$target")/.netssh.XXXXXX")
|
||||
if [[ -e "$target" ]]; then cp -p "$target" "$staged"; else chmod 644 "$staged"; fi
|
||||
cat "$data" > "$staged"
|
||||
mv -f "$staged" "$target"
|
||||
info "updated : ${PROFILE_PATHS[$index]}"
|
||||
done
|
||||
for kind in "${SHELLS[@]}"; do
|
||||
if ! "$INSTALL_DIR/$BINARY" completion install --shell "$kind"; then
|
||||
info "warning: could not update $kind completion"
|
||||
fi
|
||||
done
|
||||
printf '\nnetssh %s installed / updated at %s/%s\n' "$TAG" "$INSTALL_DIR" "$BINARY"
|
||||
info "Open a new terminal to load the current hooks and aliases."
|
||||
Reference in new issue
Block a user