Last active
July 30, 2025 21:17
-
-
Save M1lan/1454743f94f455a0aeff692c29a71ea0 to your computer and use it in GitHub Desktop.
Revisions
-
M1lan revised this gist
Jul 30, 2025 . 1 changed file with 379 additions and 129 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,77 +1,131 @@ #!/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 # @@ -86,8 +140,23 @@ 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 @@ -100,10 +169,67 @@ _OPTION_VALUE= # 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() # @@ -114,16 +240,15 @@ _TARGET_FILE_OR_ACTION= # # 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 } ## @@ -135,11 +260,15 @@ _debug() { # 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 } ## @@ -151,10 +280,46 @@ _exit_1() { # 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}" } ## @@ -165,7 +330,9 @@ _print_warning() { # # Description: Print the program help information. _print_help() { cat <<EOF ${_SELF} version ${_VERSION} (built ${_BUILD_DATE}) Put your helpful text here. Usage: @@ -189,81 +356,165 @@ EOF # 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() @@ -274,12 +525,11 @@ _example() { # Description: # Entry point. _main() { if ((_PRINT_HELP)); then _print_help else _example "$@" fi } _main "$@" -
M1lan created this gist
Sep 17, 2024 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,285 @@ #!/usr/bin/env bash ## # Note: almost always, we want to inherit the encapsulating shell # environment. If we don't, we can run scripts from within a clean # shell environment like this: #!/usr/bin/env -iS -- bash ## # Shellcheck configuration. # # shellcheck enable=avoid-nullary-conditions # shellcheck enable=add-default-case,check-extra-masked-returns # shellcheck enable=check-set-e-suppressed,require-double-brackets # shellcheck enable=check-unassigned-uppercase,deprecate-which # ##################################################################### # production ready bash # ##################################################################### ## # This is a simple template for production ready Bash scripts. # # It also includes a style-guide. # # In production, 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. # ##################################################################### # style guide # ##################################################################### ## # - Always use `shellcheck` or `shellharden` (or both)! The former # prints information about warnings and errors and how to fix them, # while the latter simply prints the already fixed script. # # - Column limit: 79 column (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. # # - Preferably 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}` # Good: `some_command "${arg1}" "${arg2}" "${arg3}"` # Good: `some_command "$arg1" "$arg2" "$arg3"` # # - Prefer `printf` over `echo`. For more information, see: # http://unix.stackexchange.com/a/65819 # # - Prefer `$_explicit_variable_name` over names like `$var`. # # - Use the `#!/usr/bin/env bash` shebang in order to run the # preferred Bash version rather than hard-coding the executable # path. ##################################################################### # 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' # This script's basename. _SELF="$(basename "${0}")" _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= ##################################################################### # helpers # ##################################################################### ## # _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() { { printf "%s " "$(tput setaf 1 || true)!$(tput sgr0 || true)" "${@}" } 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() { { printf "%s " "$(tput setaf 1 || true)!$(tput sgr0 || true)" "${@}" } 1>&2 } ## # _print_help() # # Usage: # _print_help # # Description: Print the program help information. _print_help() { cat <<EOF 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() { _debug printf "this is a debug message...\\n" ## 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 ## 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 "$@"