#!/bin/sh
# Installs lsh, the LogiShell command line, for this user: curl -fsSL https://cli.lsh.dev/install.sh | sh
#
# LSH_VERSION a release to install (0.2.0); default: the latest
# LSH_INSTALL_DIR where the binary goes; default: $HOME/.local/bin
# LSH_DL where releases live; default: https://cli.lsh.dev/cli
#
# No sudo, no edits of shell rc files: when the directory is not on PATH this script says
# which line to add. Layout of the release server: harness/cli/dist/README.md.
set -eu
main() {
base="${LSH_DL:-https://cli.lsh.dev/cli}"
base="${base%/}"
dir="${LSH_INSTALL_DIR:-${HOME:?HOME is not set}/.local/bin}"
version="${LSH_VERSION:-}"
version="${version#v}"
need uname
need mkdir
need chmod
need mv
need rm
pick_fetch
pick_sha
target="$(detect_target)"
name="lsh-$target"
if [ -n "$version" ]; then
url="$base/v$version/$name"
say "installing lsh $version for $target"
sumfile="$(fetch_text "$base/v$version/$name.sha256")" ||
die "no lsh $version for $target at $base/v$version/" "see the releases at https://lsh.dev/dl"
sum="$(printf '%s\n' "$sumfile" | awk 'NR == 1 { print tolower($1) }')"
else
manifest="$(fetch_text "$base/latest.json")" ||
die "could not read $base/latest.json" "check the network, or set LSH_DL to a mirror"
version="$(printf '%s' "$manifest" | json_version)"
[ -n "$version" ] || die "$base/latest.json names no version" "report it at https://lsh.dev/gh"
asset="$(printf '%s' "$manifest" | json_asset "$target")"
[ -n "$asset" ] || die "lsh $version has no build for $target" "see the builds at https://lsh.dev/dl"
url="$(printf '%s' "$asset" | json_field url)"
sum="$(printf '%s' "$asset" | json_field sha256 | tr 'A-F' 'a-f')"
say "installing lsh $version for $target"
fi
case "$sum" in
*[!0-9a-f]* | "") die "no valid sha256 for $name" "report it at https://lsh.dev/gh" ;;
esac
[ "${#sum}" -eq 64 ] || die "no valid sha256 for $name" "report it at https://lsh.dev/gh"
mkdir -p "$dir" || die "cannot create $dir" "pick another directory: LSH_INSTALL_DIR=
"
tmp="$dir/.lsh-install-$$"
trap 'rm -f "$tmp"' EXIT INT TERM
fetch_file "$url" "$tmp" || die "download of $url failed" "check the network and run the installer again"
got="$(sha_of "$tmp")"
if [ "$got" != "$sum" ]; then
die "checksum mismatch for $name: expected $sum, got $got" "nothing was installed; run the installer again"
fi
chmod 755 "$tmp"
mv -f "$tmp" "$dir/lsh" || die "cannot write $dir/lsh" "pick another directory: LSH_INSTALL_DIR="
trap - EXIT INT TERM
ok "lsh $version installed to $dir/lsh"
"$dir/lsh" version 2>/dev/null || true
case ":${PATH:-}:" in
*":$dir:"*) lsh_cmd="lsh" ;;
*)
lsh_cmd="$dir/lsh"
warn "$dir is not on your PATH; add this line to your shell profile (~/.zshrc, ~/.bashrc):"
# shellcheck disable=SC2016 # $PATH is meant literally, for the profile
printf ' export PATH="%s:$PATH"\n' "$dir"
;;
esac
next "next: $lsh_cmd onboard"
}
# platform
detect_target() {
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Darwin)
# A shell under Rosetta reports x86_64 on Apple silicon: ask the kernel instead.
if [ "$arch" = "x86_64" ] && [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = "1" ]; then
arch="arm64"
fi
case "$arch" in
arm64 | aarch64) echo "aarch64-apple-darwin" ;;
x86_64) echo "x86_64-apple-darwin" ;;
*) die "no lsh build for macOS on $arch" "builds: https://lsh.dev/dl" ;;
esac
;;
Linux)
case "$arch" in
x86_64 | amd64) echo "x86_64-unknown-linux-musl" ;;
aarch64 | arm64) echo "aarch64-unknown-linux-musl" ;;
*) die "no lsh build for Linux on $arch" "builds: https://lsh.dev/dl" ;;
esac
;;
MINGW* | MSYS* | CYGWIN*)
die "this is Windows" "in PowerShell run: irm https://cli.lsh.dev/install.ps1 | iex"
;;
*) die "no lsh build for $os" "builds: https://lsh.dev/dl" ;;
esac
}
# network
pick_fetch() {
if command -v curl >/dev/null 2>&1; then
fetcher=curl
elif command -v wget >/dev/null 2>&1; then
fetcher=wget
else
die "neither curl nor wget is installed" "install one of them and run the installer again"
fi
}
fetch_text() {
if [ "$fetcher" = curl ]; then
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 "$1"
else
wget --https-only -qO- "$1"
fi
}
fetch_file() {
if [ "$fetcher" = curl ]; then
if [ -t 2 ]; then
curl -fL --proto '=https' --tlsv1.2 --retry 3 --progress-bar -o "$2" "$1"
else
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -o "$2" "$1"
fi
else
wget --https-only -qO "$2" "$1"
fi
}
pick_sha() {
if command -v sha256sum >/dev/null 2>&1; then
sha_tool=sha256sum
elif command -v shasum >/dev/null 2>&1; then
sha_tool=shasum
elif command -v openssl >/dev/null 2>&1; then
sha_tool=openssl
else
die "no sha256sum, shasum or openssl to check the download" "install coreutils and run the installer again"
fi
}
sha_of() {
case "$sha_tool" in
sha256sum) sha256sum "$1" | awk '{ print $1 }' ;;
shasum) shasum -a 256 "$1" | awk '{ print $1 }' ;;
openssl) openssl dgst -sha256 -r "$1" | awk '{ print $1 }' ;;
esac
}
# latest.json without jq
# The manifest is written by our CI (see README.md): flat string and number fields, no
# escaped quotes. These readers rely on that and nothing more.
json_version() {
tr -d '\n\r' | sed -n 's/^[[:space:]]*{[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' |
sed 's/^v//'
}
# The object of one target: {"url":"...","sha256":"...","size":N}
json_asset() {
tr -d '\n\r' | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p"
}
json_field() {
sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p"
}
# output
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
c_ok="$(printf '\033[1;32m')" c_warn="$(printf '\033[1;33m')" c_err="$(printf '\033[1;31m')"
c_acc="$(printf '\033[1;36m')" c_dim="$(printf '\033[2m')" c_off="$(printf '\033[0m')"
else
c_ok="" c_warn="" c_err="" c_acc="" c_dim="" c_off=""
fi
say() { printf '%s\n' "$*"; }
ok() { printf '%s✓%s %s\n' "$c_ok" "$c_off" "$*"; }
warn() { printf '%s!%s %s\n' "$c_warn" "$c_off" "$*"; }
next() { printf '%s→%s %s\n' "$c_acc" "$c_off" "$*"; }
die() {
printf '%serror:%s %s\n' "$c_err" "$c_off" "$1" >&2
if [ -n "${2:-}" ]; then
printf ' %shint:%s %s\n' "$c_dim" "$c_off" "$2" >&2
fi
exit 1
}
need() {
command -v "$1" >/dev/null 2>&1 || die "$1 is needed and not installed"
}
main "$@"