Last active
April 24, 2017 06:20
-
-
Save adrolter/51931cbed06c27e29192f1fa5045696e to your computer and use it in GitHub Desktop.
Gist-based Bash script app template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# bash-app-template-v1.sh | |
# description: Gist-based Bash script app template | |
# author: Adrian Günter <adrian|gntr/me> | |
# grepping: | |
# - sections: ^### (?<section>.+) ### | |
# - actions: ^# ACTION: (?<action>.+) | |
# - a function's definition: func_name\(\) | |
# sections: SHELLCFG,DEPENDENCIES,CONSTANTS,FUNCTIONS,ACTIONS,ROUTINE | |
# actions: self-install | |
# notes: | |
# - Find spaces at the end of code lines: /^\s*[^\s#].*\s+$/ | |
# (dis)conventions: | |
# - An alternate delimiter is needed when using regex constants containing URLs (as well as any | |
# other path patterns containing slashes), as they are not escaped. Pipes "|" are therefore | |
# used as perl regex delimiters ("m|<pattern>|") throughout this script | |
# - Use "$PNL" ("$P '\n'") in place of "echo" | |
# - Use "$P 'whatever\n'" in place of "echo whatever" | |
# - Seriously, please use printf ($PRINTF|$P). Don't define "ECHO=/usr/bin/echo". Just. use. printf. | |
# - Separate var declarations from command substitution assignments | |
# * This avoids masking the return value of the cmd substitution (unless intentionally | |
# ignored), as the return value of "local", "export", "declare", etc. will be | |
# substituted for it | |
# ``` | |
# local foo; foo="$(bar)" NOT local foo="$(bar)" | |
# foo="$(bar)"; declare -r foo NOT declare -r foo="$(bar)" OR local -r foo="$(bar)" | |
# ``` | |
# - Perl-based "grep -q" replacement w/ token matching on space-separated K/V pairs | |
# * This doesn't seem to be much faster than grep -q with a regex; is there any use other than | |
# in removing grep entirely from the dependencies? Grep seems to be only used twice in the | |
# script as of 20170416, so this might be worth real consideration | |
# ``` | |
# $PERL -ae'$F[0]eq"COLUMN_A_VALUE"&&($F[1]eq"COLUMN_B_VALUE"&&exit 0||exit 1)' | |
# ``` | |
# - Print the second column of a line if the first matches | |
# * If used in a pipe you will need to "set +o pipefail" temporarily using a subshell | |
# or set +o followed by set -o (we exit early if a match is found, causing a SIGPIPE) | |
# * TODO: gracefully close the handles if possible instead of causing SIGPIPE | |
# ``` | |
# $PERL -ae'$F[0]eq"COLUMN_A_VALUE"&&print $F[1] and exit' | |
# ``` | |
# | |
# indent-hint | |
declare -r GIST_AUTHOR='adrianguenter' | |
declare -r GIST_AUTHOR_EMAIL='adrian|gntr/me' | |
declare -r GIST_AUTHOR_NAME='Adrian Günter' | |
declare -r GIST_COMMIT_HASH= | |
declare -ri GIST_COMMIT_HASH_UNIQLEN=40 | |
declare -r GIST_COMMIT_TIME= | |
declare -r GIST_FILE_NAME='bash-app-template-v1.sh' | |
declare -r GIST_ID='51931cbed06c27e29192f1fa5045696e' | |
declare -ri GIST_VERSION= | |
### SHELLCFG ### | |
set -o errexit -o nounset -o noclobber -o pipefail | |
umask 0027 | |
# Path searching not allowed – all external command paths must be absolute | |
# shellcheck disable=SC2123 | |
PATH='' | |
### DEPENDENCIES ### | |
# pacman -S bash binutils coreutils grep perl time util-linux | |
declare -r BASENAME='/usr/bin/basename' | |
declare -r CAT='/usr/bin/cat' | |
declare -r DATE='/usr/bin/date' | |
declare -r DIFF='/usr/bin/diff' # Optional, used as backup diff tool | |
declare -r DIRNAME='/usr/bin/dirname' | |
declare -r GETOPT='/usr/bin/getopt' # Must be modern GNU Getopt! | |
declare -r GIT='/usr/bin/git' # Optional, used as default diff tool | |
declare -r GREP='/usr/bin/grep' | |
declare -r INSTALL='/usr/bin/install' | |
declare -r LESS='/usr/bin/less' # Optional, default pager (used by diff tools) | |
declare -r MKFIFO='/usr/bin/mkfifo' | |
declare -r MKTEMP='/usr/bin/mktemp' | |
declare -r PERL='/usr/bin/perl' | |
declare -r PRINTF='/usr/bin/printf' | |
declare -r RM='/usr/bin/rm' | |
declare -r SYNC='/usr/bin/sync' | |
declare -r TIME='/usr/bin/time' | |
# Aliases | |
declare -r P="$PRINTF" | |
declare -r PNL="$P \n" | |
# Builtins | |
declare -r READ='builtin read -r' | |
# _printferr: printf to STDERR | |
# USAGE: _printferr <format> [arg]... | |
_printferr(){ >&2 $P "$@"; }; declare -fr _printferr | |
# _octdump: Print octal-escape representation of a string | |
# USAGE: _octdump <string> | |
# shellcheck disable=SC2016 | |
_octdump(){ $PERL -e'length$ARGV[0]>0&&printf"\\%0*v3o","\\",$ARGV[0];'; }; declare -fr _octdump | |
# _octescape: Octal-escape specified characters in a string then print it | |
# USAGE: _octescape <char-list> <string> | |
# EXAMPLE: _octescape '.[(' 'test[(file]).name' > "test\133\050file])\056name" | |
# shellcheck disable=SC2016,SC2026 | |
_octescape(){ $PERL -e'length$ARGV[0]>0||exit 2;'\ | |
-e'my$l=sprintf"\\%0*v3o","\\",$ARGV[0];$_=$ARGV[1];s/([$l]+)/sprintf"\\%0*v3o","\\",$1/ge;print;'\ | |
"$1" "$2"; }; declare -fr _octescape | |
# _E: General-purpose auto-exiting error macro | |
# USAGE: [NOEXIT=] _E <format> [arg]... | |
# EXAMPLES: | |
# - _E 'something %s happened!' awful | |
# - NOEXIT='' _E 'something %s happened!' recoverable | |
_E(){ _printferr '%s: %s\n' "$APP_NAME" "$($P "$@")"; [ "${NOEXIT+X}" ] || exit 1; }; declare -fr _E | |
# _T: Measure the amount of wall time taken by a command (retrieve by calling _T_RESULT) | |
# USAGE: _T <cmd> [arg]... | |
# NOTE: Double-quote the subcommand for subshells, e.g. _T sh -c '"sleep 1;sleep 2"' | |
# shellcheck disable=2034 | |
declare -g _T_RESULT | |
_T(){ declare o; o="$($MKTEMP -u)"; $MKFIFO "$o"; $TIME -f'%e' -o>($CAT>"$o") "$@"; | |
$READ _T_RESULT <"$o"; $RM "$o"; }; declare -fr _T | |
# _V: Return 0 only if verbosity is greater than or equal to $1. If $1 is less than or equal to 0, | |
# return 0 only if verbosity is equal to 0 (supports checking for --quiet mode with "_V 0" | |
# instead of "! _V 1") | |
# shellcheck disable=SC2015 | |
_V(){ [ "$1" -le 0 ] && { [ "${OPT[V]}" -eq 0 ]; return $?; } || [ "${OPT[V]}" -ge "$1" ]; | |
}; declare -fr _V | |
# _confirm: Return 0 only if Y is entered | |
# USAGE: _confirm <question> [default<Y|N>] | |
# EXAMPLE: _confirm 'Continue?' N || exit | |
# Adapted from https://gist.github.com/davejamesmiller/1965569 | |
# shellcheck disable=SC2015 | |
_confirm(){ [ -t 0 ] || _E 'not interactive'; declare p='y/n' d='' r='' | |
if [ "${2:-}" = Y ]; then p='Y/n';d=Y; elif [ "${2:-}" = N ]; then p='y/N';d=N; fi | |
while :; do $P '%s [%s] ' "$1" "$p"; $READ r; r="${r^^}"; | |
[ -z "$r" ] && r=$d; [ "$r" = Y ] && return 0 || { [ "$r" = N ] && return 1; }; done | |
};declare -fr _confirm | |
# _checkdep: Error if any of the listed paths don't exist | |
# USAGE: _checkdep <path>... | |
# EXAMPLE: _checkdep $GREP $LESS | |
_checkdep(){ for d in "$@"; do [ -e "$d" ] || _E\ | |
'not available due to missing dependency "%s"' "$d"; done }; declare -fr _checkdep | |
# _str_lazyeq: Return 0 only if string $1 matches the beginning, or the entirety, of string $2 | |
# USAGE: _str_lazyeq <1> <2> | |
# EXAMPLES: | |
# - _str_lazyeq foo foobar # Return value is 0 | |
# - _str_lazyeq foobar foobar # Return value is 0 | |
# - _str_lazyeq foobared foobar # Return value is NOT 0 | |
# - _str_lazyeq bar foobar # Return value is NOT 0 | |
_str_lazyeq(){ [ -n "$1" ] && [ "$1" = "${2:0:${#1}}" ]; }; declare -fr _str_lazyeq | |
### CONSTANTS ### | |
# We can't use readlink to resolve fd 1 because in a subshell it gets a pipe | |
TTY= ; [ -t 1 ] && TTY=/dev/fd/1; declare -r TTY | |
declare -ra ACTIONS=('self-install') | |
declare -r APP_BASENAME="${0##*/}" | |
declare -r RE_ESCAPE_CHARS='.^$*+?()[{\|' | |
# TODO: Fix me - poor substitute for GitHub URL escaping (used for github/gist#file-name URL building which has '-sh' hard coded) | |
declare -r GIST_FILE_FORENAME="${GIST_FILE_NAME%%.*}" | |
# shellcheck disable=SC2034,SC2155 | |
declare -r GIST_FILE_NAME_RE="$(_octescape "${RE_ESCAPE_CHARS}" "${GIST_FILE_NAME}")" | |
declare -r GIST_HTTPURL=https://gist.github.com/${GIST_ID} # This will redirect to $GIST_AUTHOR but is shorter | |
declare -ri GIST_HASH_LEN=40 # Safe to use unquoted | |
# shellcheck disable=SC2034 | |
declare -r GIST_HASH_RE="[0-9a-f]{${GIST_HASH_LEN}}" | |
# shellcheck disable=SC2034 | |
declare -r NL=' | |
'; | |
### FUNCTIONS ### | |
# __usage: Print contextualized help based on the current value of $ACTION and, | |
# optionally, the values of flags and options within the $OPT array | |
__usage() { | |
[ $# -ge 1 ] && NOEXIT=1 _E "${@}" | |
$P '\n' | |
case ${ACTION} in | |
'self_install') _printferr "$($CAT <<'EOF' | |
usage: %s [-v, --verbose]... self-install [--force] [--commit=<commit-ish> | --version=<revision>] | |
%s [[-q, --quiet] self-install --force] [--commit=<commit-ish> | --version=<revision>] | |
EOF | |
)" "${APP_BASENAME}" "${APP_BASENAME}";; | |
*) _printferr "$($CAT <<'EOF' | |
usage: %s [-q, --quiet] [-v, --verbose]... <action> [option]... [argument]... | |
%s --version | |
actions: | |
self-install | |
EOF | |
)" "${APP_BASENAME}" "${APP_BASENAME}" ;; | |
esac | |
$P '\n' | |
exit 1 | |
}; declare -fr __usage | |
# __version: Print version and author info | |
__version() { | |
declare fmt_commit_time file_viewurl | |
fmt_commit_time="${GIST_COMMIT_TIME:+$($DATE --date=${GIST_COMMIT_TIME} -R)}" | |
file_viewurl="${GIST_COMMIT_HASH:+${GIST_HTTPURL}/${GIST_COMMIT_HASH}#file-${GIST_FILE_FORENAME}-sh}" | |
$P '%s version %d (%.*s)\nCommitted %s\n%s\n\nWritten by %s <%s> GitHub: @%s\n' \ | |
"${APP_BASENAME}" ${GIST_VERSION} ${GIST_COMMIT_HASH_UNIQLEN:--1} "${GIST_COMMIT_HASH:-unknown}" \ | |
"${fmt_commit_time:-at an unknown point in time}" \ | |
"${file_viewurl:-"${GIST_HTTPURL}#file-${GIST_FILE_FORENAME}-sh"}" \ | |
"${GIST_AUTHOR_NAME}" "${GIST_AUTHOR_EMAIL}" "${GIST_AUTHOR}" | |
exit 0 | |
} | |
# __main: Parse the provided arguments and call the requested action | |
__main() { | |
# Parse options | |
OPT[V]=1 | |
declare parg_data | |
parg_data=$($GETOPT -o'+qv' -l'quiet,verbose,version' -n"${APP_NAME}" -- "$@") || __usage | |
eval set -- "${parg_data}"; unset parg_data | |
while :; do | |
case "${1}" in | |
'-q'|'--quiet') OPT[V]=0; shift;; # quiet resets verbosity to 0 (defaults to 1) | |
'-v'|'--verbose') OPT[V]=$((OPT[V]+1)); shift;; | |
'--version') __version; shift;; | |
'--') shift; break;; | |
*) break;; | |
esac | |
done | |
# All actions require root privileges | |
# TODO: Make this optional | |
[ ${EUID} -eq 0 ] || { _printferr '%s must be run as root\n' "${APP_NAME}"; exit 1; } | |
# We don't declare these variables readonly at this point because its cumbersome | |
# from a function. Future mutability might well be beneficial anyway | |
_V 1 && V1OUT="${TTY:-/dev/fd/1}" | |
_V 2 && V2OUT="${TTY:-/dev/fd/1}" | |
_V 3 && V3OUT="${TTY:-/dev/fd/1}" | |
# Assert that first argument must not be empty | |
[ "${1+X}" ] || __usage 'missing action' | |
declare ACTION='' | |
for a in "${ACTIONS[@]}"; do | |
! _str_lazyeq "${1}" "${a}" && continue | |
declare -r ACTION=${a//-/_} | |
break | |
done | |
# Assert that first argument must map to a recognized action | |
[ -n "${ACTION}" ] || __usage 'unknown action "%s"' "${1}" | |
shift; eval '__act_"${ACTION}" "$@"' | |
} | |
### ACTIONS ### | |
# EXAMPLE | |
# === | |
# Placeholders: | |
# - <name>: "my_action" – [a-z_] identifier of the action. Used for the | |
# function name, et al. | |
# - <arg>: "my-action" – The argument string which triggers the action | |
# - <pfx>: "MA" – A unique, <= 3 char, [A-Z] identifier used for OPT | |
# elements | |
# | |
# Template: | |
## ACTION: <arg> | |
## __act_<name>() { | |
## APP_NAME+=' <arg>' | |
## declare parg_data | |
## parg_data=$($GETOPT -o '' -l '' -n"${APP_NAME}" -- "$@") || __usage | |
## eval set -- "${parg_data}"; unset parg_data | |
## while :; do | |
## case "${1}" in | |
## '-f'|'--flag') OPT[<pfx>_FLAG]=1; shift;; | |
## '-o'|'--option') OPT[<pfx>_OPTION]="${2}"; shift 2;; | |
## '--') shift; break;; | |
## *) break;; | |
## esac | |
## done | |
## # Handle positional arguments | |
## declare -ar parg=("$@") | |
## # ... | |
## } | |
# ACTION: self-install | |
# TODO: Add optional shellcheck dependency to fail out of install if parsing or other errors are found | |
__act_self_install() { | |
APP_NAME+=' self-install' | |
OPT[SI_FORCE]=0 | |
_checkdep $DIFF $LESS $GIT | |
# Options: Backup, install location, group, permissions, ? | |
declare parg_data | |
parg_data=$($GETOPT -o 'l:' -l 'location:,commit:,version:,force' -n"${APP_NAME}" -- "$@") || __usage | |
eval set -- "${parg_data}"; unset parg_data | |
while :; do | |
case "${1}" in | |
'-l'|'--location') OPT[SI_LOCATION]="${2}"; shift 2;; | |
'--commit') OPT[SI_COMMIT]="${2}"; shift 2;; | |
'--version') OPT[SI_VERSION]="${2}"; shift 2;; | |
'--force') OPT[SI_FORCE]=1; shift;; | |
'--') shift; break;; | |
*) break;; | |
esac | |
done | |
# Handle positional arguments | |
# shellcheck disable=SC2034 | |
declare -ar parg=("$@") | |
# --quiet requires --force, and therefore also implies --force (OPT[SI_FORCE] check unnecessary when using _V 0) | |
_V 0 && [ "${OPT[SI_FORCE]}" -ne 1 ] && __usage \ | |
'-q|--quiet requires --force' | |
# Use --quiet to bypass the TTY check (dangerous!) | |
_V 1 && [ -z "${TTY}" ] && _E 'the process must have a controlling TTY for this action' | |
# Declare locals | |
declare commitish gittemp install_dir install_name install_path \ | |
{,p_}commit_hash {,p_}commit_hash_short {,p_}commit_time | |
declare -i {,p_}commit_hash_uniqlen {,p_}version | |
# Build install_path, using --location if provided | |
install_dir=/usr/local/bin # Default | |
install_name="${APP_BASENAME}" # Default | |
install_path="${install_dir}/${install_name}" # Default | |
if [ "${OPT[SI_LOCATION]+X}" ]; then | |
[ -n "${OPT[SI_LOCATION]}" ] || __usage $'argument to \'--location\' requires a value' | |
install_path="${OPT[SI_LOCATION]}" | |
# If given a directory, we use it and append the default value of $install_name to built path | |
if [ -d "${install_path}" ]; then | |
install_dir="${install_path}" | |
install_path+="/${install_name}" | |
fi | |
fi | |
declare -r install_path | |
# If the path exists, we perform file type and content checks. If it doesn't exist, we | |
# make sure its directory exists | |
if [ -e "${install_path}" ]; then | |
[ -f "${install_path}" ] || _E \ | |
'location "%s" exists, but is not a directory or file' "${install_path}" | |
install_dir="$($DIRNAME "${install_path}")" | |
install_name="$($BASENAME "${install_path}")" | |
declare line name value | |
declare -i valid_gist_id=0 valid_gist_file_name=0 | |
while $READ line; do | |
IFS=$'\2' $READ name value <<<"${line}" | |
# shellcheck disable=SC2034 | |
case "${name}" in | |
'GIST_ID') if [ -n "${value}" ] && [ "${value}" = "${GIST_ID}" ]; | |
then valid_gist_id=1; else break; fi;; | |
'GIST_FILE_NAME') if [ -n "${value}" ] && [ "${value}" = "${GIST_FILE_NAME}" ]; | |
then valid_gist_file_name=1; else break; fi;; | |
'GIST_COMMIT_HASH') p_commit_hash="${value}";; | |
'GIST_COMMIT_HASH_UNIQLEN') p_commit_hash_uniqlen="${value}";; | |
'GIST_COMMIT_TIME') p_commit_time="${value}";; | |
'GIST_VERSION') p_version="${value}";; | |
esac | |
done < <($PERL -ne"$($CAT <<'EOF' | |
if ($.==1 && not m%^#!(/usr)?/bin/bash$%) { exit $.; } # Exit if shebang test fails | |
elsif (/^\s*\#/) { ; } # Noop on comment lines | |
elsif (/^\s*declare\s+-ri?\s+(GIST_[A-Z_]+)=(?:'([^']*)'|([^'\s]?[^\s]*))/) { | |
printf "%s\2%s\n", $1, (length $2 > 0 ? $2 : $3); } # Print pairs separated by 0x02 | |
else { exit $.; } # Exit on first non-comment line that doesn't match the regex | |
EOF | |
)" "${install_path}") | |
if [ ${valid_gist_id} -ne 1 ] || [ ${valid_gist_file_name} -ne 1 ]; then | |
_E 'file at "%s" failed validation, refusing to overwrite' "${install_path}" | |
fi | |
unset line name value valid_gist_id valid_gist_file_name | |
else | |
install_dir="$($DIRNAME "${install_path}")" | |
install_name="$($BASENAME "${install_path}")" | |
[ -d "${install_dir}" ] || _E 'all directories in location "%s" must exist' "${install_path}" | |
fi | |
# Set the default commit-ish ref string, or validate and use --commit option if exists | |
commitish=FETCH_HEAD # Default | |
if [ "${OPT[SI_COMMIT]+X}" ]; then | |
[ "${OPT[SI_VERSION]+X}" ] \ | |
&& __usage 'options --commit and --version are mutually exclusive' | |
$GREP -qPe'^[0-9a-f]{5,'${GIST_HASH_LEN}'}$' <<<"${OPT[SI_COMMIT]}"\ | |
|| __usage 'commit must be 5-%d characters of [0-9a-f]' ${GIST_HASH_LEN} | |
commitish="${OPT[SI_COMMIT]}" | |
fi | |
# Sanitize and validate --version option if exists | |
# Very important as $version gets implicit $(()) wrapped around assignments! | |
if [ "${OPT[SI_VERSION]+X}" ]; then | |
version="${OPT[SI_VERSION]//[!0-9]/}" | |
[ ${version} -lt 1 ] && __usage \ | |
'version must be a number greater than zero' | |
fi | |
# Fetch the repository | |
gittemp="$($MKTEMP -d)" | |
>"$V2OUT" $GIT -C "${gittemp}" init | |
>"$V2OUT" $GIT -C "${gittemp}" remote add origin "${GIST_HTTPURL}.git" | |
&>"$V2OUT" $GIT -C "${gittemp}" fetch --all | |
# Do a version lookup if set, otherwise verify and resolve commitish | |
if [ ${version+X} ]; then | |
commit_hash="$($GIT -C "${gittemp}" rev-list --reverse FETCH_HEAD\ | |
| $PERL -ne'$.=='${version}'&&print and exit')" | |
[ -n "${commit_hash}" ] || _E 'failed to find version "%d"' "${version}" | |
else | |
commit_hash="$($GIT -C "${gittemp}" rev-parse --verify --quiet "${commitish}^{commit}")" | |
[ -n "${commit_hash}" ] || _E \ | |
'failed to find a unique commit matching "%s"' "${commitish}" | |
version="$($GIT -C "${gittemp}" rev-list --count "${commit_hash}")" | |
fi | |
# Get shortest possible SHA of current revision | |
commit_hash_short="$($GIT -C "${gittemp}" rev-parse --short=5 "${commit_hash}")" | |
commit_hash_uniqlen=${#commit_hash_short} | |
# - Should we use author (%a?) instead of committer (%c?) dates? | |
# - For collecting more info this should be a read call with proc sub and more fmt specs in the format string | |
commit_time="$($GIT -C "${gittemp}" log -1 --format="%cI" "${commit_hash}")" | |
# Print a message with the correct verbage "Installing/upgrading/downgrading ..." | |
if [ ! -e "${install_path}" ] || [ ! "${p_version+X}" ] || [ "${p_version}" -le 0 ]; then | |
_V 1 && $P 'Installing %s (%s), committed %s\n' \ | |
"${version}" "${commit_hash_short}" "$($DATE --date="${commit_time}" -R)" | |
elif [ "${p_version}" -eq "${version}" ]; then | |
# Abort if not --force | |
if [ "${OPT[SI_FORCE]}" -ne 1 ]; then | |
_V 1 && NOEXIT=1 _E 'version %s (%s) is already installed' \ | |
"${version}" "${commit_hash_short}" | |
exit 2 | |
fi | |
_V 1 && $P 'Reinstalling %s (%s), committed %s\n' \ | |
"${version}" "${commit_hash_short}" "$($DATE --date="${commit_time}" -R)" | |
else | |
declare verb= | |
[ "${p_version}" -lt "${version}" ] && verb=Upgrading || verb=Downgrading | |
_V 1 && $P '%s %s from %s (%.*s) to %s (%s), committed %s\n' \ | |
${verb} "${install_path}" "${p_version}" "${p_commit_hash_uniqlen:--1}" \ | |
"${p_commit_hash:-unknown}" "${version}" "${commit_hash_short}" \ | |
"$($DATE --date="${commit_time}" -R)" | |
# Confirm downgrade if not --quiet | |
if _V 1 && [ "${p_version}" -gt "${version}" ] && ($PNL; ! _confirm \ | |
'Downgrading can break self-install and other features. Are you sure?' N) | |
then | |
_printferr 'Aborting.\n' | |
exit 5 | |
fi | |
unset verb | |
fi | |
# Checkout the revision at $commit_hash | |
>"$V2OUT" $GIT -C "${gittemp}" reset --hard "${commit_hash}" | |
# Write version info into the new script for self-install and version printing | |
$PERL -i -pe"$($CAT <<EOF | |
BEGIN { my \$changes = 0; }; next if \$changes == 4; | |
if ($. == 1 && not m%^\#!(/usr)?/bin/bash$%) { exit 255; } # Exit if shebang test fails | |
elsif (/^\s*\#/) { ; } # Noop on comment lines | |
# Fail on first non-comment or GIST_VAR declaration if changes hasn't reached its target value | |
elsif (not /^\s*declare\s+-ri?\s+GIST_[A-Z_]+=/) { exit 1; } | |
elsif (s/^\s*declare\s+-r\s+GIST_COMMIT_HASH=\K.*/'${commit_hash}'/) { \$changes++; } | |
elsif (s/^\s*declare\s+-ri\s+GIST_COMMIT_HASH_UNIQLEN=\K.*/${commit_hash_uniqlen}/) { \$changes++; } | |
elsif (s/^\s*declare\s+-r\s+GIST_COMMIT_TIME=\K.*/'${commit_time}'/) { \$changes++; } | |
elsif (s/^\s*declare\s+-ri\s+GIST_VERSION=\K.*/${version}/) { \$changes++; } | |
# Implicit "else { ; }" – only ignored GIST_VAR declarations fall through to here | |
EOF | |
)" "${gittemp}/${GIST_FILE_NAME}" || _E 'failed writing version info' | |
# Present a diff of the changes between the previous and next versions | |
if [ -e "${install_path}" ]; then | |
# -c core.filemode=false doesn't seem to work with --no-index, so we have to do this | |
declare difftemp; difftemp="$($MKTEMP)" | |
$INSTALL -m0640 "${install_path}" "${difftemp}" | |
if _V 1 && ! $GIT diff --no-index --quiet -- "${difftemp}" "${gittemp}/${GIST_FILE_NAME}" \ | |
&& ($PNL; _confirm 'View a unified diff of the previous and next version (RECOMMENDED)?' Y) | |
then | |
PATH="${LESS%/*}" $GIT diff --no-index -- "${difftemp}" "${gittemp}/${GIST_FILE_NAME}"||: | |
fi | |
$RM "${difftemp}" | |
unset difftemp | |
fi | |
# Provide an option to abort | |
if _V 1 && ($PNL; ! _confirm "$($P \ | |
'Install version %d to "%s"?' "${version}" "${install_path}")" N) | |
then | |
_printferr 'Aborting.\n' | |
exit 5 | |
fi | |
# Install the script and set permissions | |
$INSTALL -m0750 -gwheel "${gittemp}/${GIST_FILE_NAME}" "${install_path}" | |
# TODO: We really need a trap to clean up this directory as there are multiple failure conditions leading to early exits above | |
# A way to add paths to the cleanup routine right after creation would be helpful (push onto a global array?) | |
$RM -rf "${gittemp}" | |
$SYNC "${install_path}" | |
} | |
### RUN ### | |
declare -A OPT=() | |
ACTION='' | |
APP_NAME="${APP_BASENAME}" | |
# shellcheck disable=SC2034 | |
V1OUT=/dev/null | |
V2OUT=/dev/null | |
# shellcheck disable=SC2034 | |
V3OUT=/dev/null | |
__main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment