Skip to content

Instantly share code, notes, and snippets.

@pcrockett
Last active July 31, 2022 01:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pcrockett/8e04641f8473081c3a93de744873f787 to your computer and use it in GitHub Desktop.
Save pcrockett/8e04641f8473081c3a93de744873f787 to your computer and use it in GitHub Desktop.
Bash script template
#!/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