Last active
July 30, 2025 21:17
-
-
Save M1lan/1454743f94f455a0aeff692c29a71ea0 to your computer and use it in GitHub Desktop.
Bash template
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 | |
| ## | |
| # Note: almost always, we /want/ to inherit the encapsulating shell | |
| # environment. If we don't want or "trust" that environment, we can | |
| # run scripts from within a clean shell environment like this: | |
| # #!/usr/bin/env -iS -- bash | |
| # | |
| # If anyone knows about an actual use case for this, pls let me know. | |
| ## | |
| # Shellcheck configuration example for your .shellcheckrc: | |
| # | |
| # | |
| # enable=add-default-case | |
| # enable=avoid-nullary-conditions | |
| # enable=check-extra-masked-returns | |
| # enable=check-set-e-suppressed | |
| # enable=check-unassigned-uppercase | |
| # enable=deprecate-which | |
| # enable=quote-safe-variables | |
| # enable=require-double-brackets | |
| # enable=require-variable-braces | |
| # external-sources=true | |
| ## | |
| # In production scenarios, we want to know about all failure | |
| # conditions and ensure they are handled correctly. Bash is not the | |
| # best language for that and we need to know the subtle differences | |
| # between exit codes and what is printed to stdout/stderr and what | |
| # means sucess or failure in the specific case we execute scripted | |
| # commands or external commands. Using `set -euo pipefail` doesn't | |
| # guarantee anything. The idea is more that instead of keep going at | |
| # all costs, we want to fail early. | |
| # | |
| # If in doubt, use Python or Go. | |
| # If multiple kilo-SLOC script, use Python or Go. | |
| # | |
| # Instead of a library of scripts, write one cli with Go cobra/kong. | |
| # | |
| # If you want it to be a "TUI" kind of interactive terminal tool, | |
| # consider Rust. Or at least Go. | |
| # | |
| # "Interactive" is irrelevant for production. apply YAGNI. What's | |
| # usually important for production is to fail early and give a good | |
| # error message, the return code is usually less important. We | |
| # normally treat scripts as if they are normal programs that just | |
| # suceed or fail, with Bash we have the luxury to do more but we | |
| # rarely want/need it because that assumes we have knowledge about the | |
| # system that we run on (which again, sysadmin experience is a whole | |
| # different story from learning a scripting language also used in | |
| # sysadmin applying it in a pure cloud environment. We can also just | |
| # "keep going" by appending || true which is usually bad style unless | |
| # we don't care about how a subshell exits. | |
| # | |
| # In any case, we need to not only know the scripting language, but | |
| # also understand what will happen if the script runs on a certain | |
| # system, and for that we need to know that system! | |
| # | |
| # Make sure to check version of Bash and understand the scripting | |
| # language capacities (aka bashisms) of that version. Make extra sure | |
| # to check the versions of the command line tools you use inside of | |
| # our scripts. | |
| # | |
| # Make sure to check if you want/need POSIX sh in which case you | |
| # cannot use many of Bash's convenient language features. | |
| # | |
| # You don't need strict POSIX compliance for any modern scenarios, and | |
| # Linux itself is not strictly POSIX compliant. | |
| # | |
| # "sh" is actually still bash on most systems; only very rarely will | |
| # you still encounter a bourne shell or a korn shell. | |
| # | |
| # zsh won't be available anywhere else but on your MacBook bash will | |
| # always be available (both can be installed/updated). | |
| # | |
| # Bash (and Python, Go) is consent for scripting on Unix/Linux, while | |
| # many people prefer zsh or fish for interactive use. | |
| # | |
| # Your script can be portable by installing bash, or it can be | |
| # portable by being "posixly correct" which is highly irrelevant | |
| # unless you are working on older mainframes,- that certainly won't | |
| # ever happen in the cloud. | |
| # | |
| # Always use `shellcheck`. | |
| # | |
| # Column limit: 79 columns (similar to PEP-8). | |
| # | |
| # Semi-reliable exit codes: If anything in the script exits with a | |
| # non-zero exit code, we want the whole script to stop immediately | |
| # and also return with the same code. Never use `<command> || true` | |
| # unless we really don't care about <command>. C.f. BASHFAQ 105. | |
| # Use leading underscores on internal variable and function names | |
| # in order to avoid name collisions. For unintentionally global | |
| # variables defined without `local`, such as those defined outside of | |
| # a function or automatically through a `for` loop, prefix with | |
| # double underscores. | |
| # You don't have to /always/ use braces when referencing variables, | |
| # `"Hi, my name is: ${NAME}."` instead of `"Hi, my name is | |
| # $NAME."`. While quotes are required for variable references, braces | |
| # are only required in some cases. | |
| # Bad: `some_command $arg1 $arg2 $arg3` | |
| # Bad: `some_command ${arg1} ${arg2} ${arg3}` | |
| # Bad: `echo "somestring $somevar somestring$someothervar" | |
| # Good: `some_command "${arg1}" "${arg2}" "${arg3}"` | |
| # Good: `some_command "$arg1" "$arg2" "$arg3"` | |
| # Good: `echo "somestring${somevar} somestring${someothervar}" | |
| # | |
| # All 6 Good and Bad are valid, but the Good ones are readable. | |
| # Everything else synax results in sytax errors. | |
| # | |
| # - Prefer `printf` over `echo`. For more information, see: | |
| # http://unix.stackexchange.com/a/65819 | |
| # | |
| # - Prefer `$_explicit_variable_name` over names like `$var`. | |
| # | |
| # - Underscores as function name prefix for internal functions | |
| # | |
| # - Use the `#!/usr/bin/env bash` shebang in order to run the | |
| # preferred Bash version rather than hard-coding the executable | |
| # path. | |
| # | |
| # For simple option parsing, getopts can be sufficient: | |
| # while getopts "hdx:o:" opt; do ... done | |
| # For complex cases with long options, use the manual parsing shown below. | |
| ##################################################################### | |
| # header # | |
| ##################################################################### | |
| # exit immediately on err; useful for CI/CD. | |
| set -e | |
| # exit if referencing undefined var. | |
| set -u | |
| # return exit code of failed command in pipe, not last. | |
| set -o pipefail | |
| # filenames might have spaces | |
| IFS=$'\n\t' | |
| # Version information | |
| readonly _VERSION="1.0.0" | |
| # Declare and assign separately to avoid masking return values | |
| _BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| readonly _BUILD_DATE | |
| # This script's basename. | |
| _SELF="$(basename "${0}")" | |
| readonly _SELF | |
| # Save original directory and script directory | |
| readonly _ORIGINAL_DIR="${PWD}" | |
| _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| readonly _SCRIPT_DIR | |
| # Temporary directory for this script | |
| _TEMP_DIR="" | |
| _PRINT_HELP=0 | |
| _USE_DEBUG=0 | |
| __DEBUG_COUNTER=0 | |
| _OPTION_X=0 | |
| _OPTION_VALUE= | |
| # everything that comes after '--' is not an option flag, but usually | |
| # the filename we run the script on or an action command. | |
| _TARGET_FILE_OR_ACTION= | |
| # Log levels for structured logging | |
| readonly _LOG_LEVELS=(ERROR WARN INFO DEBUG) | |
| _LOG_LEVEL="${LOG_LEVEL:-INFO}" | |
| ##################################################################### | |
| # helpers # | |
| ##################################################################### | |
| ## | |
| # _cleanup() | |
| # | |
| # Description: Cleanup function called on exit | |
| _cleanup() { | |
| local exit_code=$? | |
| # Remove temp files if created | |
| if [[ -n "${_TEMP_DIR}" ]] && [[ -d "${_TEMP_DIR}" ]]; then | |
| rm -rf "${_TEMP_DIR}" | |
| fi | |
| exit "${exit_code}" | |
| } | |
| # Set traps for cleanup and interruption | |
| trap _cleanup EXIT | |
| trap '_exit_1 printf "Script interrupted\\n"' INT TERM | |
| ## | |
| # _log() | |
| # | |
| # Usage: | |
| # _log LEVEL "message" | |
| # | |
| # Description: Structured logging with levels | |
| _log() { | |
| local level="${1}" | |
| shift | |
| local level_num | |
| case "${level}" in | |
| ERROR) level_num=0 ;; | |
| WARN) level_num=1 ;; | |
| INFO) level_num=2 ;; | |
| DEBUG) level_num=3 ;; | |
| *) return 1 ;; | |
| esac | |
| # Compare with current log level | |
| local current_level_num | |
| case "${_LOG_LEVEL}" in | |
| ERROR) current_level_num=0 ;; | |
| WARN) current_level_num=1 ;; | |
| INFO) current_level_num=2 ;; | |
| DEBUG) current_level_num=3 ;; | |
| *) current_level_num=2 ;; # Default to INFO if unknown | |
| esac | |
| if [[ ${level_num} -le ${current_level_num} ]]; then | |
| local timestamp | |
| timestamp="$(date -Iseconds)" || timestamp="[timestamp error]" | |
| printf "[%s] %s: %s\\n" "${timestamp}" "${level}" "$*" >&2 | |
| fi | |
| } | |
| ## | |
| # _debug() | |
| # | |
| # Usage: | |
| # _debug <command> <options> | |
| # | |
| # Description: Print debug info to stderr. | |
| # | |
| # Example: _debug printf "[DEBUG] Variable: %s\\n" "$0" | |
| _debug() { | |
| if ((${_USE_DEBUG:-0})); then | |
| __DEBUG_COUNTER=$((__DEBUG_COUNTER + 1)) | |
| { | |
| printf "――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n" | |
| printf "🐛 %s " "${__DEBUG_COUNTER} >>" | |
| "${@}" | |
| printf "――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n" | |
| } 1>&2 | |
| fi | |
| } | |
| ## | |
| # _exit_1() | |
| # | |
| # Usage: | |
| # _exit_1 <command> | |
| # | |
| # Description: Exit with status 1 after sending a message to stdout | |
| # and stderr. | |
| _exit_1() { | |
| { | |
| if [[ -t 2 ]]; then # Check if stderr is a terminal | |
| printf "%s " "$(tput setaf 1 || true)!$(tput sgr0 || true)" | |
| else | |
| printf "! " | |
| fi | |
| "${@}" | |
| } 1>&2 | |
| exit 1 | |
| } | |
| ## | |
| # _warn() | |
| # | |
| # Usage: | |
| # _warn <command> | |
| # | |
| # Description: Print warning about a non-critical error to stdout and | |
| # stderr, but don't exit. | |
| _print_warning() { | |
| { | |
| if [[ -t 2 ]]; then # Check if stderr is a terminal | |
| printf "%s " "$(tput setaf 1 || true)!$(tput sgr0 || true)" | |
| else | |
| printf "! " | |
| fi | |
| "${@}" | |
| } 1>&2 | |
| } | |
| ## | |
| # _require_commands() | |
| # | |
| # Usage: | |
| # _require_commands command1 command2 ... | |
| # | |
| # Description: Verify required commands exist | |
| _require_commands() { | |
| local missing=() | |
| for cmd in "$@"; do | |
| if ! command -v "${cmd}" >/dev/null 2>&1; then | |
| missing+=("${cmd}") | |
| fi | |
| done | |
| if [[ ${#missing[@]} -gt 0 ]]; then | |
| _exit_1 printf "Missing required commands: %s\\n" "${missing[*]}" | |
| fi | |
| } | |
| ## | |
| # _create_temp_dir() | |
| # | |
| # Usage: | |
| # _create_temp_dir | |
| # | |
| # Description: Create a temporary directory for this script | |
| _create_temp_dir() { | |
| _TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/${_SELF}.XXXXXX") | |
| readonly _TEMP_DIR | |
| _debug printf "Created temp directory: %s\\n" "${_TEMP_DIR}" | |
| } | |
| ## | |
| # _print_help() | |
| # | |
| # Usage: | |
| # _print_help | |
| # | |
| # Description: Print the program help information. | |
| _print_help() { | |
| cat <<EOF | |
| ${_SELF} version ${_VERSION} (built ${_BUILD_DATE}) | |
| Put your helpful text here. | |
| Usage: | |
| ${_SELF} [--options] [<arguments>] [--] [target, action, or file] | |
| ${_SELF} -h | --help | |
| Options: | |
| -h, --help Display this help information. | |
| -d, --debug Debug mode. | |
| -x, --option-x A simple option 'x'. | |
| -o, --option-with-value Some option, followed by a required value. | |
| EOF | |
| } | |
| ## | |
| # __get_option_value() | |
| # | |
| # Usage: | |
| # __get_option_value <option> <value> | |
| # | |
| # Description: For the given option, return the value or exit 1 if | |
| # value is blank or appears to be another option. | |
| __get_option_value() { | |
| local __arg="${1:-}" | |
| local __val="${2:-}" | |
| if [[ -n "${__val:-}" ]] && [[ ! "${__val:-}" =~ ^- ]]; then | |
| printf "%s\\n" "${__val}" | |
| else | |
| _exit_1 printf "%s requires a valid argument.\\n" "${__arg}" | |
| fi | |
| } | |
| # parse options | |
| while ((${#})); do | |
| __arg="${1:-}" | |
| __val="${2:-}" | |
| case "${__arg}" in | |
| -h | --help) | |
| _PRINT_HELP=1 | |
| ;; | |
| -d | --debug) | |
| _USE_DEBUG=1 | |
| ;; | |
| -x | --option-x) | |
| _OPTION_X=1 | |
| ;; | |
| -o | --option-with-value) | |
| _OPTION_VALUE="$(__get_option_value "${__arg}" "${__val:-}")" | |
| shift | |
| ;; | |
| --) | |
| _TARGET_FILE_OR_ACTION="$( | |
| IFS=$' ' | |
| printf "%s" "${*:2}" | |
| )" | |
| break | |
| ;; | |
| -*) | |
| _exit_1 printf "Unexpected option: %s\\n" "${__arg}" | |
| ;; | |
| *) | |
| _TARGET_FILE_OR_ACTION="$( | |
| IFS=$' ' | |
| printf "%s" "${*:1}" | |
| )" | |
| break | |
| ;; | |
| esac | |
| shift | |
| done | |
| ##################################################################### | |
| # script starts here # | |
| ##################################################################### | |
| ## | |
| # _example_function_returns() | |
| # | |
| # Description: Examples of proper function return values | |
| _example_function_returns() { | |
| # Return success/failure | |
| _check_file_exists() { | |
| if [[ -f "${1}" ]]; then | |
| return 0 # success | |
| else | |
| return 1 # failure | |
| fi | |
| } | |
| # Return values via stdout | |
| _get_computed_value() { | |
| local result="computed value" | |
| printf "%s" "${result}" | |
| } | |
| # Usage examples | |
| # Note: When used in 'if', set -e is disabled for the function | |
| local file_check_result=0 | |
| # Temporarily disable to capture exit code | |
| set +e | |
| _check_file_exists "/etc/passwd" | |
| file_check_result=$? | |
| set -e | |
| if [[ ${file_check_result} -eq 0 ]]; then | |
| _debug printf "File exists\\n" | |
| fi | |
| local value | |
| value=$(_get_computed_value) | |
| _debug printf "Got value: %s\\n" "${value}" | |
| } | |
| ## | |
| # _example_array_handling() | |
| # | |
| # Description: Examples of proper array usage | |
| _example_array_handling() { | |
| # Good array practices | |
| local -a my_array=() | |
| my_array+=("element1") | |
| my_array+=("element with spaces") | |
| my_array+=("third element") | |
| # Iterate safely | |
| for element in "${my_array[@]}"; do | |
| _debug printf "Element: %s\\n" "${element}" | |
| done | |
| # Pass arrays to functions (bash 4.3+) | |
| _process_array() { | |
| local -n arr=$1 # nameref | |
| for item in "${arr[@]}"; do | |
| _debug printf "Processing: %s\\n" "${item}" | |
| done | |
| } | |
| _process_array my_array | |
| } | |
| _example() { | |
| _debug printf "this is a debug message...\\n" | |
| # Log examples | |
| _log INFO "Script starting" | |
| _log DEBUG "Debug information" | |
| # Check for required commands (example) | |
| # _require_commands jq curl git | |
| ## handle options | |
| if ((_OPTION_X)); then | |
| printf "Received -x. Doing nothing.\\n" | |
| _debug printf "the --option-x flag is set.\\n" | |
| else | |
| _debug printf "the --option-x flag is NOT set.\\n" | |
| fi | |
| if [[ -n "${_OPTION_VALUE}" ]]; then | |
| printf "Option value: %s\\n" "${_OPTION_VALUE}" | |
| fi | |
| if [[ -n "${_TARGET_FILE_OR_ACTION}" ]]; then | |
| printf "Operating on file / selected action: %s\\n" "${_TARGET_FILE_OR_ACTION}" | |
| fi | |
| # Demonstrate array handling | |
| _example_array_handling | |
| # Demonstrate function returns | |
| _example_function_returns | |
| # Create temp directory if needed | |
| # _create_temp_dir | |
| # echo "temp file" > "${_TEMP_DIR}/example.txt" | |
| ## do stuff here! | |
| printf "Bash is here: %s\\n" "$(command -v bash || true)" | |
| printf "Script ran successfully.\\n" | |
| } | |
| # _main() | |
| # | |
| # Usage: | |
| # _main [<options>] [<arguments>] | |
| # | |
| # Description: | |
| # Entry point. | |
| _main() { | |
| if ((_PRINT_HELP)); then | |
| _print_help | |
| else | |
| _example "$@" | |
| fi | |
| } | |
| _main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment