#!/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 "$@"