Skip to content

Instantly share code, notes, and snippets.

@joar
Last active July 18, 2019 14:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joar/022b88d9a62a4dccf0524d87b5cf654b to your computer and use it in GitHub Desktop.
Save joar/022b88d9a62a4dccf0524d87b5cf654b to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# This section should be copied to any script that uses it.
# BEGIN UTILS
# Utility functions ⚠ Do not edit ⚠ Automatically inserted by scripts/utils.sh
# ==============================================================================
# Does not capture stdout, prints helpful info to stderr
function show_call_passthrough {
# passthrough mode
info "$(blue "run") $(quote "$@") ... "
"${@}"
if test "$?" -gt 0; then
info "$(paint B "... " N "$(quote "$@") -> " R "FAILED")"
return 1
else
info "$(paint B "... " N "$(quote "$@") -> " G "OK")"
fi
}
# Does not capture stdout, only prints command if it failed.
function show_call_candid {
# candid
"${@}"
if test "$?" -gt 0; then
info "$(quote "$@") -> $(red "FAILED")"
return 1
fi
}
# Execute a command and print helpful things about the executed command and its
# arguments.
function show_call {
case "$CHECK_CALL_MODE" in
candid)
show_call_candid "$@"
;;
passthrough)
show_call_passthrough "$@"
;;
""|quiet)
# default mode
local output
info -n "$(blue "run") $(quote "$@") ... "
if ! output="$("$@" 2>&1)"; then
info "$(red "FAILED")"
info "Error: $output"
return 1
else
info "$(green "OK")"
fi
;;
*)
info "Invalid CHECK_CALL_MODE: ${CHECK_CALL_MODE}"
exit 3
;;
esac
}
# Works like show_call, but exits the entire script if the command fails.
function check_call {
if ! show_call "$@" ; then
info "$(red "Command failed. Aborting script.")"
exit 1
fi
}
# Works like show_call_passthrough, but exits the entire script if the command fails.
function check_call_passthrough {
if ! show_call_passthrough "$@"; then
info "$(red "Command failed. Aborting script.")"
exit 1
fi
}
# Usage: paint R "this is red " B "this is blue " N "this is normal"
function paint {
local color
local text
while test "$#" -gt 0; do
color="$1"
text="$2"
shift
shift
case "$color" in
R)
red "$text"
;;
B)
blue "$text"
;;
G)
green "$text"
;;
Y)
yellow "$text"
;;
N)
printf "%s" "$text"
;;
*)
echo "Invalid color: $color";
exit 1
;;
esac
done
}
function quote {
declare -a quoted_items
quoted_items=()
for item in "$@"; do
local token
local quoted
token="$(printf '"%s"' "$item")"
quoted="$(sed -E 's/^"([a-zA-Z0-9:._*/-]+)"$/\1/g' <<<"$token")"
quoted_items=("${quoted_items[@]}" "$quoted")
done
echo "${quoted_items[@]}"
}
function info { echo "$@" >&2; } # like echo, but prints to stderr
function red { printf '\x1b[31m%s\x1b[0m' "$@"; }
function green { printf '\x1b[32m%s\x1b[0m' "$@"; }
function yellow { printf '\x1b[33m%s\x1b[0m' "$@"; }
function blue { printf '\x1b[34m%s\x1b[0m' "$@"; }
# Usage: join_by TEXT [ ITEMS... ]
# Example: join_by ", " "foo" "bar" # -> "foo, bar"
function join_by { local d="$1"; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; }
# Usage: repeat NUM CHARACTER
# Example: repeat 3 "-" # => ---
function repeat { for ((i=0; i<"${1:?}"; i++)); do printf '%s' "${2:?}"; done; }
# END UTILS
################################################################################
# Anything below this point exists only for this "meta-utils" script
################################################################################
# Functions
# ==============================================================================
UTILS_SCRIPT_FILE="$0"
function replace_utils_section {
local src
local dst
src="${1:?}"
dst="${2:?}"
sed -E -e '/^# BEGIN UTILS/,/^# END UTILS/{r '<(sed -n -E '/^# BEGIN UTILS/,/^# END UTILS/p' < "$UTILS_SCRIPT_FILE") -e 'd}' "$src" > "$dst"
}
# Testing utilities
# ==============================================================================
function run_test {
local code_to_test
local script_seed
local test_script
local utils_section
code_to_test="${1:?}"
# Create a "seed.sh" script file with "code to test" in it and an empty utils
# section
test_dir="$(mktemp -t -d utils-self-test.XXXXX)"
# We need to hide the utils section here to avoid it being snapped up from
# the script we're in an the moment.
utils_section="# BEGIN UTILS"$'\n'"# END UTILS"
tee "$test_dir/seed.sh" > /dev/null <<SEED
${utils_section}
${code_to_test} \\
> ${test_dir}/stdout \\
2> ${test_dir}/stderr
# Will not execute if check_call does "exit 1"
echo \$? > ${test_dir}/retval
SEED
# Fill in the utils section and store it as "script.sh"
replace_utils_section "$test_dir/seed.sh" "$test_dir/script.sh"
info $'\n'"$(paint B "#### " G "Running test ${code_to_test@Q}")"
bash "$test_dir/script.sh"
# Assign effects to global state variable
TEST_RESULT=(
[exitcode]="$?"
[stderr]="$(cat "$test_dir/stderr")"
[stdout]="$(cat "$test_dir/stdout")"
[retval]="$(test -f "$test_dir/retval" && cat "$test_dir/retval")"
)
# Check $test_dir is set & sane, then remove $test_dir
grep "utils-self-test." <<<"$test_dir" > /dev/null && rm -r "$test_dir"
info $'\n'"$(paint G "Observed effects: ")"
for key in "${!TEST_RESULT[@]}"; do
info "$(paint B "$key " N " = ${TEST_RESULT[$key]@Q}")"
done
# We write the "Checks:" here in the expectation that, following this, checks
# will be performed through "expect_result".
info $'\n'"$(paint G "Checks: ")"
}
function expect_result {
local key
local expected
local actual
key="${1:?}"
expected="${2?}"
actual="${TEST_RESULT[$key]}"
if test "$actual" = "$expected"; then
info "$(paint G "expect_result " B "$key" N " = " N "${expected@Q}" G " ✅")"
else
info "$(paint R "expect_result " B "$key" N " = " N "${expected@Q}" R " ❌")"
info "expected: ${expected@Q}"
info " got: ${actual@Q}"
exit 53
fi
}
# Test entrypoint
# ==============================================================================
function utils_self_test {
run_test "show_call_passthrough echo \"example stdout\""
expect_result stdout "example stdout"
run_test "check_call false"
expect_result retval ""
expect_result stdout ""
expect_result stderr $'\E[34mrun\E[0m false ... \E[31mFAILED\E[0m\nError: \n\E[31mCommand failed. Aborting script.\E[0m'
expect_result exitcode "1"
run_test "show_call_passthrough true"
expect_result retval "0"
expect_result stdout ""
expect_result exitcode "0"
run_test "show_call_passthrough false"
expect_result retval "1"
expect_result stdout ""
expect_result exitcode "0"
run_test "CHECK_CALL_MODE=passthrough show_call false"
expect_result retval "1"
expect_result stdout ""
expect_result exitcode "0"
run_test "CHECK_CALL_MODE=passthrough check_call echo hello"
expect_result retval "0"
expect_result stdout "hello"
expect_result exitcode "0"
run_test "CHECK_CALL_MODE=candid check_call echo hello"
expect_result retval "0"
expect_result stdout "hello"
expect_result stderr ""
run_test "CHECK_CALL_MODE=candid check_call bash -c 'echo hello; exit 1'"
expect_result retval ""
expect_result exitcode "1"
expect_result stdout "hello"
expect_result stderr $'bash -c "echo hello; exit 1" -> \E[31mFAILED\E[0m\n\E[31mCommand failed. Aborting script.\E[0m'
}
################################################################################
# Entrypoints
################################################################################
USAGE="$(cat <<USAGE
Usage: $0 COMMAND
Where COMMAND is one of:
update [ FILES... ]
Updates the UTILS sections (# BEGIN UTILS ... # END UTILS) in FILES with the
contents of this script's ($0) UTILS section.
self-test
Run self-tests, see "utils_self_test" in $0
USAGE
)"
# Run self-tests if UTILS_SELF_TEST is set to "yes"
# ==============================================================================
case "$1" in
"self-test")
declare -A TEST_RESULT
export -f replace_utils_section
utils_self_test
exit 0
;;
update)
# Run the update utility
# ==============================================================================
# Updates the contents of another script with this file's "UTILS" section
shift # Drops COMMAND
declare -a FILES
FILES=("$@")
if [[ "${#FILES[@]}" -eq 0 ]]; then
info "$(red "No arguments passed.")"
info "$USAGE"
exit 24
fi
TEMP_FILE="$(mktemp --suffix "$(basename "$FILE")")"
# It's inappropriate to litter
function clean {
if test -f "$TEMP_FILE"; then
rm "$TEMP_FILE"
fi
}
trap clean EXIT
for FILE in "${FILES[@]}"; do
replace_utils_section "$FILE" "$TEMP_FILE"
# Set the same permissions for the temporary file as FILE
chmod --reference="$FILE" "$TEMP_FILE"
if ! git diff --no-index "$FILE" "$TEMP_FILE"; then
if test -t 0; then
# Prompt if stdin is a tty
read -r -p "Update utils in $FILE? [y/n]: " prompt_response
else
# Assume yes if stdin isn't a tty
prompt_response="y"
fi
if ! [[ "$prompt_response" =~ ^y$ ]]; then
info "$(paint R "Not updating " N "$FILE")"
else
info "$(paint G "Updated utils in " N "$FILE")"
mv "$TEMP_FILE" "$FILE"
fi
else
info "$(paint Y "Utils in " N "$FILE" Y " are already up to date")"
# Update the modification time of the target file as a signal to
# Makefile rules that the file is up-to-date
touch "$FILE"
fi
done
exit 0
;;
gist)
GIST_URL="https://gist.github.com/joar/022b88d9a62a4dccf0524d87b5cf654b"
# Updates the gist in GIST_URL with the latest version of this file.
# ... maybe I should write a script that updates itself from the GIST_URL as
# well, but that might be too much.
info "$(paint Y "Updating gist " N "$GIST_URL")"
gist -u "$GIST_URL" "$0"
exit 0
;;
*)
if test -n "$1"; then
info "$(paint R "Invalid command: $1")"
fi
info "$USAGE"
exit 1
;;
esac
# A lot of magic things happen in this file. I'm still figuring out if it's
# worth the hassle, or if I should compile this script as a disk image
# just boot straight from it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment