Add support for options in install script

This adds support for serveral new command line options (taken from the usage):
    "-b, --bin-dir" "Override the bin installation directory [default: ${_bin_dir}]" \
    "-m, --man-dir" "Override the man installation directory [default: ${_man_dir}]" \
    "-a, --arch" "Override the architecture identified by the installer [default: ${_arch}]" \
    "-h, --help" "Display this help message"
This also (coincidentally) allows for $BIN_DIR, $MAN_DIR, and $ARCH to be set to create default settings for these.
These variables are *always* overwritten by the command line arguments.

This diff is unusually large, as my formatter ([shfmt](https://github.com/mvdan/sh)) ran, reformatting the majority of the script.

This also adds a few utilities to the script (`log`/`abort`) and modifies `err` to simply output to stderr.
This allows for more fine grain control over script flow and output, without relying on `echo >&2`.
This commit is contained in:
aarondill 2023-07-05 12:48:58 -05:00
parent 72a49ec9c9
commit f1a54433a2
1 changed files with 443 additions and 312 deletions

View File

@ -1,37 +1,117 @@
#!/bin/sh #!/bin/sh
# shellcheck shell=dash # shellcheck shell=dash
# shellcheck disable=SC3043
# The official zoxide installer. # The official zoxide installer.
# #
# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local` # It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local`
# extension. Note: Most shells limit `local` to 1 var per line, contra bash. # extension. Note: Most shells limit `local` to 1 var per line, contra bash.
usage() {
printf "%s\n" \
"install.sh [option]" \
"" \
"Fetch and install the latest version of zoxide, if zoxide is already" \
"installed it will be updated to the latest version."
printf "\n%s\n" "Options"
printf "\t%s\n\t\t%s\n\n" \
"-b, --bin-dir" "Override the bin installation directory [default: ${_bin_dir}]" \
"-m, --man-dir" "Override the man installation directory [default: ${_man_dir}]" \
"-a, --arch" "Override the architecture identified by the installer [default: ${_arch}]" \
"-h, --help" "Display this help message"
}
# outputs sudo command to use to stdout, exits script if no command can be found
elevate_priv() {
if has sudo; then
if sudo true; then
printf 'sudo'
return 0
else
err "Sudo failed. Checking doas support."
fi
fi
if has doas; then
if doas true; then
printf "doas"
return 0
else
err "doas failed."
fi
fi
# returns before this if a command is found.
err 'Could not find the command "sudo" or "doas", needed to get permissions for install.'
err "If you are on Windows, please run your shell as an administrator, then"
err "rerun this script. Otherwise, please run this script as root, or install"
err "sudo or doas."
exit 1
}
main() { main() {
if [ "${KSH_VERSION-}" = 'Version JM 93t+ 2010-03-05' ]; then if [ "${KSH_VERSION-}" = 'Version JM 93t+ 2010-03-05' ]; then
# The version of ksh93 that ships with many illumos systems does not # The version of ksh93 that ships with many illumos systems does not
# support the "local" extension. Print a message rather than fail in # support the "local" extension. Print a message rather than fail in
# subtle ways later on: # subtle ways later on:
err 'the installer does not work with this ksh93 version; please try bash' abort 'the installer does not work with this ksh93 version; please try bash'
fi fi
set -u set -u
# Detect and print host target triple. # Detect and print host target triple.
if [ -n "${ARCH:-}" ]; then
# if the user specifed, trust them - don't error on unrecognized hardware.
local _arch="${ARCH}"
else
ensure get_architecture ensure get_architecture
local _arch="${RETVAL}" local _arch="${RETVAL}"
fi
assert_nz "${_arch}" "arch" assert_nz "${_arch}" "arch"
echo "Detected architecture: ${_arch}" log "Detected architecture: ${_arch}"
local _bin_dir="${HOME}/.local/bin"
local _bin_name
local _man_dir="${HOME}/.local/share/man"
parse_args "$@" # sets global variables (BIN_DIR, MAN_DIR, ARCH)
_bin_dir=${BIN_DIR:-$_bin_dir}
_man_dir=${MAN_DIR:-$_man_dir}
_arch=${ARCH:-$_arch}
case "${_arch}" in
*windows*) _bin_name="zoxide.exe" ;;
*) _bin_name="zoxide" ;;
esac
if ! [ -d "${_bin_dir}" ]; then
abort "Please ensure the destination exists! (${_bin_dir})"
fi
if ! [ -d "${_man_dir}" ]; then
abort "Please ensure the manual destination exists! (${_man_dir})"
fi
local sudo=""
if test_writeable "${_bin_dir}"; then
log "Installing zoxide, please wait…"
else
log "Escalated permissions are required to install to ${_bin_dir}"
sudo=$(elevate_priv)
log "Installing zoxide as root, please wait…"
fi
# Create and enter a temporary directory. # Create and enter a temporary directory.
local _tmp_dir local _tmp_dir
_tmp_dir="$(mktemp -d)" || err "mktemp: could not create temporary directory" _tmp_dir="$(mktemp -d)" || abort "mktemp: could not create temporary directory"
cd "${_tmp_dir}" || err "cd: failed to enter directory: ${_tmp_dir}" cd -- "${_tmp_dir}" || abort "cd: failed to enter directory: ${_tmp_dir}"
# Download and extract zoxide. # Download and extract zoxide.
ensure download_zoxide "${_arch}" ensure download_zoxide "${_arch}"
local _package="${RETVAL}" local _package="${RETVAL}"
assert_nz "${_package}" "package" assert_nz "${_package}" "package"
echo "Downloaded package: ${_package}" log "Downloaded package: ${_package}"
case "${_package}" in case "${_package}" in
*.tar.gz) *.tar.gz)
need_cmd tar need_cmd tar
@ -42,76 +122,79 @@ main() {
ensure unzip -oq "${_package}" ensure unzip -oq "${_package}"
;; ;;
*) *)
err "unsupported package format: ${_package}" abort "unsupported package format: ${_package}"
;; ;;
esac esac
# Install binary. # Install binary.
local _bin_dir="${HOME}/.local/bin" # shellcheck disable=SC2086 # The lack of quoting is intentional. This may not be the best way to do it, but it's hard to properly do in POSIX
local _bin_name {
case "${_arch}" in ensure ${sudo} cp "${_bin_name}" "${_bin_dir}"
*windows*) _bin_name="zoxide.exe" ;; ensure ${sudo} chmod +x "${_bin_dir}/${_bin_name}"
*) _bin_name="zoxide" ;; }
esac log "Installed zoxide to ${_bin_dir}"
ensure mkdir -p "${_bin_dir}"
ensure cp "${_bin_name}" "${_bin_dir}"
ensure chmod +x "${_bin_dir}/${_bin_name}"
echo "Installed zoxide to ${_bin_dir}"
# Install manpages. # Install manpages.
local _man_dir="${HOME}/.local/share/man" # shellcheck disable=SC2086 # The lack of quoting is intentional.
ensure mkdir -p "${_man_dir}/man1" ensure ${sudo} cp "man/man1/"* "${_man_dir}/man1/"
ensure cp "man/man1/"* "${_man_dir}/man1/" log "Installed manpages to ${_man_dir}"
echo "Installed manpages to ${_man_dir}"
# Print success message and check $PATH. # Print success message and check $PATH.
echo "" log ""
echo "zoxide is installed!" log "zoxide is installed!"
if ! echo ":${PATH}:" | grep -Fq ":${_bin_dir}:"; then case ":${PATH}:" in
echo "NOTE: ${_bin_dir} is not on your \$PATH. zoxide will not work unless it is added to \$PATH." *":${_bin_dir}:"*) true ;; # noop
fi *) log "NOTE: ${_bin_dir} is not on your \$PATH. zoxide will not work unless it is added to \$PATH." ;;
esac
} }
download_zoxide() { download_zoxide() {
local _arch="$1" local _arch="$1"
if check_cmd curl; then if has_cmd curl; then
_dld=curl _dld=curl
elif check_cmd wget; then elif has_cmd wget; then
_dld=wget _dld=wget
elif has_cmd fetch; then
_dld="fetch"
# cmd="fetch --quiet --output=$file $url"
else else
need_cmd 'curl or wget' abort 'curl or wget are required to download zoxide'
fi fi
need_cmd grep need_cmd grep
local _releases_url="https://api.github.com/repos/ajeetdsouza/zoxide/releases/latest" local _releases_url="https://api.github.com/repos/ajeetdsouza/zoxide/releases/latest"
local _releases local _releases
case "${_dld}" in case "${_dld}" in
curl) _releases="$(curl -sL "${_releases_url}")" || curl) _releases="$(curl -sL "${_releases_url}")" ||
err "curl: failed to download ${_releases_url}" ;; abort "curl: failed to download ${_releases_url}" ;;
wget) _releases="$(wget -qO- "${_releases_url}")" || wget) _releases="$(wget -qO- "${_releases_url}")" ||
err "wget: failed to download ${_releases_url}" ;; abort "wget: failed to download ${_releases_url}" ;;
*) err "unsupported downloader: ${_dld}" ;; fetch) _releases="$(fetch --quiet "${_releases_url}")" ||
abort "fetch: failed to download ${_releases_url}" ;;
*) abort "unsupported downloader: ${_dld}" ;;
esac esac
(echo "${_releases}" | grep -q 'API rate limit exceeded') && (printf "%s" "${_releases}" | grep -q 'API rate limit exceeded') &&
err "you have exceeded GitHub's API rate limit. Please try again later, or use a different installation method: https://github.com/ajeetdsouza/zoxide/#installation" abort "you have exceeded GitHub's API rate limit. Please try again later, or use a different installation method: https://github.com/ajeetdsouza/zoxide/#installation"
local _package_url local _package_url
_package_url="$(echo "${_releases}" | grep "browser_download_url" | cut -d '"' -f 4 | grep "${_arch}")" || _package_url="$(printf "%s" "${_releases}" | grep "browser_download_url" | cut -d '"' -f 4 | grep "${_arch}")" ||
err "zoxide has not yet been packaged for your architecture (${_arch}), please file an issue: https://github.com/ajeetdsouza/zoxide/issues" abort "zoxide has not yet been packaged for your architecture (${_arch}), please file an issue: https://github.com/ajeetdsouza/zoxide/issues"
local _ext local _ext
case "${_package_url}" in case "${_package_url}" in
*.tar.gz) _ext="tar.gz" ;; *.tar.gz) _ext="tar.gz" ;;
*.zip) _ext="zip" ;; *.zip) _ext="zip" ;;
*) err "unsupported package format: ${_package_url}" ;; *) abort "unsupported package format: ${_package_url}" ;;
esac esac
local _package="zoxide.${_ext}" local _package="zoxide.${_ext}"
case "${_dld}" in case "${_dld}" in
curl) _releases="$(curl -sLo "${_package}" "${_package_url}")" || err "curl: failed to download ${_package_url}" ;; curl) _releases="$(curl -sLo "${_package}" "${_package_url}")" || abort "curl: failed to download ${_package_url}" ;;
wget) _releases="$(wget -qO "${_package}" "${_package_url}")" || err "wget: failed to download ${_package_url}" ;; wget) _releases="$(wget -qO "${_package}" "${_package_url}")" || abort "wget: failed to download ${_package_url}" ;;
*) err "unsupported downloader: ${_dld}" ;; fetch) _releases="$(fetch --quiet --output="${_package}" "${_package_url}")" || abort "fetch: failed to download ${_package_url}" ;;
*) abort "unsupported downloader: ${_dld}" ;;
esac esac
RETVAL="${_package}" RETVAL="${_package}"
@ -187,7 +270,7 @@ get_architecture() {
_ostype=pc-windows-msvc _ostype=pc-windows-msvc
;; ;;
*) *)
err "unrecognized OS type: ${_ostype}" abort "unrecognized OS type: ${_ostype}"
;; ;;
esac esac
@ -249,7 +332,7 @@ get_architecture() {
_cputype=riscv64gc _cputype=riscv64gc
;; ;;
*) *)
err "unknown CPU type: ${_cputype}" abort "unknown CPU type: ${_cputype}"
;; ;;
esac esac
@ -258,10 +341,9 @@ get_architecture() {
case ${_cputype} in case ${_cputype} in
x86_64) x86_64)
# 32-bit executable for amd64 = x32 # 32-bit executable for amd64 = x32
if is_host_amd64_elf; then { if is_host_amd64_elf; then
echo "x32 userland is unsupported" 1>&2 abort "x32 userland is unsupported"
exit 1 else
}; else
_cputype=i686 _cputype=i686
fi fi
;; ;;
@ -280,7 +362,7 @@ get_architecture() {
fi fi
;; ;;
riscv64gc) riscv64gc)
err "riscv64 with 32-bit userland unsupported" abort "riscv64 with 32-bit userland unsupported"
;; ;;
*) ;; *) ;;
esac esac
@ -311,18 +393,18 @@ get_bitness() {
local _current_exe_head local _current_exe_head
_current_exe_head=$(head -c 5 /proc/self/exe) _current_exe_head=$(head -c 5 /proc/self/exe)
if [ "${_current_exe_head}" = "$(printf '\177ELF\001')" ]; then if [ "${_current_exe_head}" = "$(printf '\177ELF\001')" ]; then
echo 32 printf 32
elif [ "${_current_exe_head}" = "$(printf '\177ELF\002')" ]; then elif [ "${_current_exe_head}" = "$(printf '\177ELF\002')" ]; then
echo 64 printf 64
else else
err "unknown platform bitness" abort "unknown platform bitness"
fi fi
} }
get_endianness() { get_endianness() {
local cputype=$1 local cputype="$1"
local suffix_eb=$2 local suffix_eb="$2"
local suffix_el=$3 local suffix_el="$3"
# detect endianness without od/hexdump, like get_bitness() does. # detect endianness without od/hexdump, like get_bitness() does.
need_cmd head need_cmd head
@ -331,11 +413,11 @@ get_endianness() {
local _current_exe_endianness local _current_exe_endianness
_current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)"
if [ "${_current_exe_endianness}" = "$(printf '\001')" ]; then if [ "${_current_exe_endianness}" = "$(printf '\001')" ]; then
echo "${cputype}${suffix_el}" printf "%s" "${cputype}${suffix_el}"
elif [ "${_current_exe_endianness}" = "$(printf '\002')" ]; then elif [ "${_current_exe_endianness}" = "$(printf '\002')" ]; then
echo "${cputype}${suffix_eb}" printf "%s" "${cputype}${suffix_eb}"
else else
err "unknown platform endianness" abort "unknown platform endianness"
fi fi
} }
@ -350,38 +432,87 @@ is_host_amd64_elf() {
[ "${_current_exe_machine}" = "$(printf '\076')" ] [ "${_current_exe_machine}" = "$(printf '\076')" ]
} }
# Test if a location is writeable by trying to write to it. Windows does not let
# you test writeability other than by writing: https://stackoverflow.com/q/1999988
test_writeable() {
path="${1:-}/test.txt"
if touch "${path}" 2>/dev/null; then
rm "${path}"
return 0
else
return 1
fi
}
check_proc() { check_proc() {
# Check for /proc by looking for the /proc/self/exe link. # Check for /proc by looking for the /proc/self/exe link.
# This is only run on Linux. # This is only run on Linux.
if ! test -L /proc/self/exe; then if ! test -L /proc/self/exe; then
err "unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc." abort "unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc."
fi fi
} }
need_cmd() { need_cmd() {
if ! check_cmd "$1"; then if ! has_cmd "$1"; then
err "need '$1' (command not found)" abort "need '$1' (command not found)"
fi fi
} }
check_cmd() { has_cmd() { command -v "$1" >/dev/null 2>&1; }
command -v "$1" >/dev/null 2>&1
# parse the arguments passed and set the environment variables accordingly
parse_args() {
# parse argv variables
while [ "$#" -gt 0 ]; do
case "$1" in
-b | --bin-dir)
BIN_DIR="$2"
shift 2
;;
-m | --man-dir)
MAN_DIR="$2"
shift 2
;;
-a | --arch)
ARCH="$2"
shift 2
;;
-h | --help)
usage
exit 0
;;
-b=* | --bin-dir=*)
BIN_DIR="${1#*=}"
shift 1
;;
-m=* | --man-dir=*)
MAN_DIR="${1#*=}"
shift 1
;;
-a=* | --arch=*)
ARCH="${1#*=}"
shift 1
;;
*)
err "Unknown option: $1"
usage
exit 1
;;
esac
done
} }
# Run a command that should never fail. If the command fails execution # Run a command that should never fail. If the command fails execution
# will immediately terminate with an error showing the failing # will immediately terminate with an error showing the failing
# command. # command.
ensure() { ensure() { if ! "$@"; then abort "command failed: $*"; fi; }
if ! "$@"; then err "command failed: $*"; fi assert_nz() { if [ -z "$1" ]; then abort "found empty string: $2"; fi; }
} log() { printf '%s\n' "$1"; }
err() { printf 'Error: %s\n' "$1" >&2; }
assert_nz() { abort() {
if [ -z "$1" ]; then err "found empty string: $2"; fi err "$1"
} exit "${2:-1}"
err() {
echo "Error: $1" >&2
exit 1
} }
# This is put in braces to ensure that the script does not run until it is # This is put in braces to ensure that the script does not run until it is