Last active
July 31, 2022 01:13
-
-
Save pcrockett/8e04641f8473081c3a93de744873f787 to your computer and use it in GitHub Desktop.
Bash script 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
#!/usr/bin/env bash | |
# This is free and unencumbered software released into the public domain. | |
# | |
# Anyone is free to copy, modify, publish, use, compile, sell, or | |
# distribute this software, either in source code form or as a compiled | |
# binary, for any purpose, commercial or non-commercial, and by any | |
# means. | |
# | |
# In jurisdictions that recognize copyright laws, the author or authors | |
# of this software dedicate any and all copyright interest in the | |
# software to the public domain. We make this dedication for the benefit | |
# of the public at large and to the detriment of our heirs and | |
# successors. We intend this dedication to be an overt act of | |
# relinquishment in perpetuity of all present and future rights to this | |
# software under copyright law. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
# OTHER DEALINGS IN THE SOFTWARE. | |
# ----------------------------------------------------------------------------- | |
# | |
# Feel free to delete the above legalese from your own scripts. I put it there | |
# just in case anyone else wants to use this. | |
# | |
# ----------------------------------------------------------------------------- | |
# | |
# This script is based on the template here: | |
# | |
# https://gist.github.com/pcrockett/8e04641f8473081c3a93de744873f787 | |
# | |
# It was copy/pasted here into this file and then modified extensively. | |
# | |
# Useful links when writing a script: | |
# | |
# Shellcheck: https://github.com/koalaman/shellcheck | |
# vscode-shellcheck: https://github.com/timonwong/vscode-shellcheck | |
# | |
# I stole many of my ideas here from: | |
# | |
# https://blog.yossarian.net/2020/01/23/Anybody-can-write-good-bash-with-a-little-effort | |
# https://dave.autonoma.ca/blog/2019/05/22/typesetting-markdown-part-1/ | |
# https://github.com/kvz/bash3boilerplate | |
# | |
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ | |
set -Eeuo pipefail | |
[[ "${BASH_VERSINFO[0]}" -lt 4 ]] && echo "Bash >= 4 required" && exit 1 | |
function show_usage() { | |
cat >&2 << EOF | |
Usage: ${SCRIPT_NAME} [OPTION...] | |
-v, --verbose Display verbose messages | |
-h, --help Show this help message then exit | |
EOF | |
} | |
DEPENDENCIES=() # Put space-delimited dependencies here (i.e. borg gpg ping) | |
SCRIPT_DIR=$(dirname "$(readlink -f "${0}")") | |
SCRIPT_NAME=$(basename "${0}") | |
LOG_DEBUG="true" | |
readonly DEPENDENCIES | |
readonly SCRIPT_DIR | |
readonly SCRIPT_NAME | |
readonly LOG_DEBUG | |
# Colors: https://stackoverflow.com/a/33206814 | |
readonly COLOR_OFF="\033[0m" | |
readonly COLOR_GRAY="\033[37;2m" | |
readonly COLOR_YELLOW="\033[1;33m" | |
readonly COLOR_DKRED="\033[31m" | |
readonly COLOR_ITALIC_RED="\033[31;1;3m" | |
readonly COLOR_MAGENTA="\033[35m" | |
readonly COLOR_PANIC="${COLOR_ITALIC_RED}" | |
readonly COLOR_ERROR="${COLOR_DKRED}" | |
readonly COLOR_WARNING="${COLOR_YELLOW}" | |
readonly COLOR_INFO="${COLOR_OFF}" | |
readonly COLOR_VERBOSE="${COLOR_GRAY}" | |
readonly COLOR_DEBUG="${COLOR_MAGENTA}" | |
function should_ignore_color() { | |
# Inspired by: | |
# https://github.com/kvz/bash3boilerplate/blob/9f06b1a8c668592e73f6f9a884776ed1e4a7e0fa/main.sh#L87 | |
if [[ "${NO_COLOR:-}" = "true" ]]; then | |
return 0 | |
elif [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]]; then | |
return 0 | |
elif [[ ! -t 1 ]]; then | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
function color_text() { | |
local color="${1}" | |
local color_reset="${COLOR_OFF}" | |
if should_ignore_color; then | |
color="" | |
color_reset="" | |
fi | |
if [ "${#}" -gt "1" ]; then | |
shift 1 | |
local message="${*}" | |
echo -e -n "${color}" | |
echo -n "${message}" | |
echo -e -n "${color_reset}" | |
else | |
# Read from stdin | |
while IFS= read -r line; | |
do | |
echo -e -n "${color}" | |
echo -n "${line}" | |
echo -e -n "${color_reset}" | |
echo | |
done | |
fi | |
} | |
function panic() { | |
# Prints a message like this: | |
# | |
# Fatal: Error message goes here! | |
# Line 1234, my_script.sh | |
# | |
>&2 color_text "${COLOR_PANIC}" "Fatal: ${*}" | |
>&2 echo | |
>&2 color_text "${COLOR_PANIC}" " Line $(caller)" | |
>&2 echo | |
# Do a "clean" exit with an error code | |
exit 1 | |
} | |
function log_error() { | |
if [ "${#}" -gt "0" ]; then | |
>&2 color_text "${COLOR_ERROR}" "ERROR: ${*}" | |
>&2 echo | |
else | |
# Read from stdin | |
while IFS= read -r line; | |
do | |
>&2 color_text "${COLOR_ERROR}" "ERROR: ${line}" | |
>&2 echo | |
done | |
fi | |
} | |
function log_warning() { | |
if [ "${#}" -gt "0" ]; then | |
color_text "${COLOR_WARNING}" "WARNING: ${*}" | |
echo | |
else | |
# Read from stdin | |
while IFS= read -r line; | |
do | |
color_text "${COLOR_WARNING}" "WARNING: ${line}" | |
echo | |
done | |
fi | |
} | |
function log_info() { | |
if [ "${#}" -gt "0" ]; then | |
color_text "${COLOR_INFO}" "${*}" | |
echo | |
else | |
# Read from stdin | |
color_text "${COLOR_INFO}" | |
fi | |
} | |
function log_verbose() { | |
if [ "${ARG_VERBOSE:-}" = "true" ]; then | |
if [ "${#}" -gt "0" ]; then | |
color_text "${COLOR_VERBOSE}" "verbose: ${*}" | |
echo | |
else | |
# Read from stdin | |
while IFS= read -r line; | |
do | |
color_text "${COLOR_VERBOSE}" "verbose: ${line}" | |
echo | |
done | |
fi | |
elif [ "${#}" -eq 0 ]; then | |
while IFS= read -r line; | |
do | |
true # Swallow input. Verbose flag has not been specified. | |
done | |
fi | |
} | |
function log_debug() { | |
if [ "${LOG_DEBUG}" = "true" ]; then | |
if [ "${#}" -gt "0" ]; then | |
color_text "${COLOR_DEBUG}" "DEBUG: ${*}" | |
echo | |
else | |
# Read from stdin | |
while IFS= read -r line; | |
do | |
color_text "${COLOR_DEBUG}" "DEBUG: ${line}" | |
echo | |
done | |
fi | |
elif [ "${#}" -eq 0 ]; then | |
while IFS= read -r line; | |
do | |
true # Swallow input. Debug output is not turned on right now. | |
done | |
fi | |
} | |
function is_installed() { | |
command -v "${1}" >/dev/null 2>&1 | |
} | |
function parse_commandline() { | |
while [ "${#}" -gt "0" ]; do | |
local consume=1 | |
case "${1}" in | |
-v|--verbose) | |
ARG_VERBOSE="true" | |
;; | |
-h|-\?|--help) | |
ARG_HELP="true" | |
;; | |
*) | |
log_error "Unrecognized argument: ${1}" | |
show_usage | |
exit 1 | |
;; | |
esac | |
shift ${consume} | |
done | |
} | |
parse_commandline "${@}" | |
if [ "${ARG_HELP:-}" = "true" ]; then | |
show_usage | |
exit 0 | |
fi | |
function cleanup_before_exit() { | |
log_verbose "Cleaning up before exit..." | |
# Add cleanup logic that runs every time your script exits | |
log_verbose "Finished cleaning up" | |
} | |
trap cleanup_before_exit EXIT | |
function unexpected_error() { | |
local line_num="${1}" | |
local script_path="${2}" | |
local faulting_command="${3}" | |
color_text "${COLOR_PANIC}" <<EOF | |
Unexpected error at line ${line_num} ${script_path}: | |
Command: "${faulting_command}" | |
EOF | |
} | |
trap 'unexpected_error ${LINENO} ${BASH_SOURCE[0]} ${BASH_COMMAND}' ERR # Single-quotes are important, see https://unix.stackexchange.com/a/39660 | |
test "$(id --user)" -eq 0 || log_warning "This script is not being run as root." | |
for dep in "${DEPENDENCIES[@]}"; do | |
is_installed "${dep}" || panic "Missing dependency \"${dep}\"" | |
done | |
function log_demo() { | |
log_verbose "Beginning log demo..." | |
log_info "This is a demo of how to log messages for the user" | |
log_debug "To hide this message, set the LOG_DEBUG variable at the top of the script to \"false\"" | |
echo "This is what a non-fatal error looks like. You can use pipes as well." | log_error | |
log_info <<EOF | |
You can also use here documents as well. | |
Yippee! | |
EOF | |
log_info "Use the \`panic\` function to show an error and crash the script." | |
log_verbose "End log demo." | |
log_verbose "" | |
} | |
function loop_file_demo() { | |
log_verbose "Beginning loop file demo..." | |
log_verbose "Note that using \`ls\` is bad practice. See https://github.com/koalaman/shellcheck/wiki/SC2012" | |
log_verbose "Here are ALL the contents of your current directory:" | |
log_verbose "" | |
# shellcheck disable=2012 | |
ls -lha | log_verbose | |
log_info "Here's a list of all \".sh\" files in ${SCRIPT_DIR}, sorted by name:" | |
log_verbose "Btw this is also a good demo of for loops and finding / sorting files..." | |
readarray -d '' all_scripts_sorted < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type f -name "*.sh" -print0 | sort --zero-terminated) | |
for script_path in "${all_scripts_sorted[@]}" | |
do | |
log_info " ${script_path}" | |
done | |
log_verbose "End loop file demo." | |
} | |
function get_random_string() { | |
# Inspired by https://stackoverflow.com/a/34329799 | |
od --read-bytes 16 --output-duplicates --address-radix n --format x /dev/urandom \ | |
| tr --delete " " | |
} | |
function loop_through_file_contents() { | |
test "${#}" -eq 1 || panic "Expecting 1 argument: file path" | |
local file_path="${1}" | |
while IFS= read -r line | |
do | |
log_info "Check it out: ${line}" | |
done < "${file_path}" | |
} | |
function main() { | |
log_demo | |
loop_file_demo | |
local random_string | |
random_string=$(get_random_string) | |
log_info "Here's a random string: ${random_string}" | |
loop_through_file_contents "/etc/hosts" | |
} | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment