Last active
July 18, 2019 14:39
-
-
Save joar/022b88d9a62a4dccf0524d87b5cf654b to your computer and use it in GitHub Desktop.
This file contains hidden or 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
#!/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