install.sh RawHistoryFolder
1#!/usr/bin/env bash
2#
3# zut installer.
4#
5# Usage:
6# curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash
7# curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash -s -- v0.1.0 ~/bin
8#
9# Positional arguments:
10# $1 version — release tag (e.g. v0.1.0). Defaults to "latest".
11# $2 prefix — install directory. Defaults to the first writable
12# directory in: /usr/local/bin, $HOME/.local/bin,
13# $HOME/bin. Created if missing. Add it to your PATH
14# if it isn't already.
15#
16# Environment overrides:
17# ZUT_VERSION same as $1
18# ZUT_PREFIX same as $2
19# GITHUB_TOKEN personal access token — required while the repo is
20# private, ignored once it goes public. Must have at
21# least `contents:read` scope on the zut repository.
22#
23# The script detects your OS and architecture, downloads the matching
24# archive from the GitHub release, verifies the sha256 against the
25# release's checksums.txt, extracts the binary, and moves it into the
26# prefix directory. No sudo unless you explicitly pick a prefix that
27# needs it.
28
29set -euo pipefail
30
31OWNER="bnema"
32REPO="zut"
33BINARY="zut"
34
35VERSION="${1:-${ZUT_VERSION:-latest}}"
36PREFIX="${2:-${ZUT_PREFIX:-}}"
37
38msg() { printf "\033[1m==>\033[0m %s\n" "$*"; }
39warn() { printf "\033[33mwarn:\033[0m %s\n" "$*" >&2; }
40die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
41
42command -v curl >/dev/null 2>&1 || die "curl is required"
43command -v tar >/dev/null 2>&1 || die "tar is required"
44
45# CURL_AUTH is prepended to every curl invocation so private-repo
46# downloads work while $GITHUB_TOKEN is set. Empty array when the repo
47# is public.
48#
49# Note the "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" pattern at every call
50# site: bash 3.2 (the default /bin/bash on macOS) treats an
51# unquoted empty-array expansion as an unbound variable under
52# `set -u` and aborts with "CURL_AUTH[@]: unbound variable". The
53# guard expands to nothing when the array is empty and to the
54# array's contents when it isn't. Bash 4+ doesn't need this, but
55# the installer's primary audience is `curl | bash` on macOS.
56CURL_AUTH=()
57if [ -n "${GITHUB_TOKEN:-}" ]; then
58 CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN")
59fi
60
61# ---- detect OS + arch ----
62
63uname_s=$(uname -s)
64uname_m=$(uname -m)
65
66case "$uname_s" in
67 Linux) OS=linux ;;
68 Darwin) OS=darwin ;;
69 MINGW*|MSYS*|CYGWIN*)
70 die "windows detected — use install.ps1 from powershell instead"
71 ;;
72 *) die "unsupported os: $uname_s" ;;
73esac
74
75case "$uname_m" in
76 x86_64|amd64) ARCH=amd64 ;;
77 arm64|aarch64) ARCH=arm64 ;;
78 *) die "unsupported arch: $uname_m" ;;
79esac
80
81# ---- resolve version ----
82
83if [ "$VERSION" = "latest" ]; then
84 # Private-repo friendly: hit the api, grab tag_name. Falls back to
85 # following the /releases/latest redirect on public repos.
86 if [ ${#CURL_AUTH[@]} -gt 0 ]; then
87 VERSION=$(curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" \
88 "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \
89 | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1)
90 else
91 VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
92 "https://github.com/${OWNER}/${REPO}/releases/latest" \
93 | sed -E 's|.*/tag/([^/]+).*|\1|')
94 fi
95 [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)"
96fi
97
98case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
99VER_NUM="${VERSION#v}"
100
101# ---- pick an install prefix ----
102
103pick_prefix() {
104 local candidates=()
105 [ -n "$PREFIX" ] && { echo "$PREFIX"; return; }
106 candidates+=("/usr/local/bin")
107 [ -n "${HOME:-}" ] && candidates+=("$HOME/.local/bin" "$HOME/bin")
108 for d in "${candidates[@]}"; do
109 if [ -d "$d" ] && [ -w "$d" ]; then
110 echo "$d"
111 return
112 fi
113 done
114 # Nothing writable yet — create ~/.local/bin and use that.
115 if [ -n "${HOME:-}" ]; then
116 mkdir -p "$HOME/.local/bin"
117 echo "$HOME/.local/bin"
118 return
119 fi
120 die "no writable install prefix found; pass one as the second argument"
121}
122
123PREFIX=$(pick_prefix)
124mkdir -p "$PREFIX"
125
126# ---- download + verify + extract ----
127
128ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"
129BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}"
130ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"
131CHECKSUMS_URL="${BASE_URL}/checksums.txt"
132
133TMP=$(mktemp -d)
134trap 'rm -rf "$TMP"' EXIT
135
136msg "downloading ${ARCHIVE}"
137curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
138 || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)"
139
140msg "verifying checksum"
141curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
142 || die "download failed: $CHECKSUMS_URL"
143
144expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
145[ -n "$expected" ] || die "no checksum for $ARCHIVE in checksums.txt"
146
147if command -v sha256sum >/dev/null 2>&1; then
148 actual=$(sha256sum "$TMP/$ARCHIVE" | awk '{print $1}')
149elif command -v shasum >/dev/null 2>&1; then
150 actual=$(shasum -a 256 "$TMP/$ARCHIVE" | awk '{print $1}')
151else
152 die "no sha256 tool found (sha256sum or shasum)"
153fi
154
155[ "$expected" = "$actual" ] \
156 || die "checksum mismatch: expected $expected, got $actual"
157
158msg "extracting"
159tar -xzf "$TMP/$ARCHIVE" -C "$TMP"
160
161[ -f "$TMP/$BINARY" ] || die "archive did not contain a '$BINARY' binary"
162
163msg "installing to $PREFIX/$BINARY"
164install -m 0755 "$TMP/$BINARY" "$PREFIX/$BINARY" 2>/dev/null \
165 || { cp "$TMP/$BINARY" "$PREFIX/$BINARY" && chmod 0755 "$PREFIX/$BINARY"; }
166
167# ---- PATH hint ----
168
169case ":$PATH:" in
170 *":$PREFIX:"*) ;;
171 *)
172 warn "$PREFIX is not on your PATH"
173 warn "add this to your shell rc file:"
174 warn " export PATH=\"$PREFIX:\$PATH\""
175 ;;
176esac
177
178msg "installed $("$PREFIX/$BINARY" --version 2>/dev/null || echo zut)"
179msg "run: zut (interactive tui)"
180msg "run: zut --help (all flags and subcommands)"