├── .editorconfig ├── .gitattributes ├── .gitignore ├── .markdownlint.json ├── .shellcheckrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── build.sh ├── script.sh ├── source.sh └── template.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | # http://EditorConfig.org 3 | 4 | # Don't search any further up the directory tree 5 | root = true 6 | 7 | # Baseline 8 | [*] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | # Markdown 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | # Shell scripts 20 | [*.sh] 21 | max_line_length = 79 22 | keep_padding = true # shfmt: -kp 23 | space_redirects = true # shfmt: -sr 24 | switch_case_indent = true # shfmt: -ci 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Bash script files should always be LF terminated 2 | *.sh text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | ._* 3 | .DS_Store 4 | .LSOverride 5 | # Icon must end with two carriage returns (\r) 6 | Icon 7 | 8 | # Windows 9 | [Dd]esktop.ini 10 | ehthumbs.db 11 | ehthumbs_vista.db 12 | [Tt]humbs.db 13 | 14 | # Backups 15 | *.bak 16 | 17 | # Logs 18 | *.log 19 | log/ 20 | logs/ 21 | 22 | # Temporary 23 | *~ 24 | *.tmp 25 | *.temp 26 | tmp/ 27 | temp/ 28 | 29 | # Vim 30 | .netrwhist 31 | [._]*.s[a-v][a-z] 32 | [._]*.sw[a-p] 33 | [._]*.un~ 34 | [._]s[a-rt-v][a-z] 35 | [._]ss[a-gi-z] 36 | [._]sw[a-p] 37 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "header-style": { "style": "setext_with_atx" }, 4 | "no-trailing-spaces": { "br_spaces": 2 }, 5 | "line-length": false, 6 | "no-trailing-punctuation": { "punctuation": ".,;:!" } 7 | } 8 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # ShellCheck settings 2 | # 3 | # Last reviewed release: v0.10.0 4 | 5 | # List of optional checks to enable 6 | enable=all 7 | 8 | # List of checks to disable 9 | # 10 | # Check ID Symbolic name Type 11 | # SC2250 require-variable-braces optional 12 | disable=SC2250 13 | 14 | # Add the script directory to the search path for source statements 15 | source-path=SCRIPTDIR 16 | 17 | # vim: syntax=conf cc=80 tw=79 ts=4 sw=4 sts=4 et sr 18 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "davidanson.vscode-markdownlint", 4 | "editorconfig.editorconfig", 5 | "foxundermoon.shell-format", 6 | "timonwong.shellcheck", 7 | "yzhang.markdown-all-in-one" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // File paths to language mappings 3 | "files.associations": { 4 | // ShellCheck 5 | ".shellcheckrc": "properties" 6 | }, 7 | 8 | // Important to disable for EditorConfig to apply correctly 9 | // See: https://github.com/editorconfig/editorconfig-vscode/issues/153 10 | "files.trimTrailingWhitespace": false, 11 | 12 | // Markdown table of contents generation 13 | "markdown.extension.toc.levels": "2..2", 14 | "markdown.extension.toc.slugifyMode": "github", 15 | 16 | // Use EditorConfig for shfmt settings 17 | "shellformat.useEditorConfig": true, 18 | 19 | "[shellscript]": { 20 | "editor.formatOnSave": true, 21 | "editor.rulers": [79] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Samuel Leslie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bash-script-template 2 | ==================== 3 | 4 | [![license](https://img.shields.io/github/license/ralish/bash-script-template)](https://choosealicense.com/licenses/mit/) 5 | 6 | A *Bash* scripting template incorporating best practices & several useful functions. 7 | 8 | - [Motivation](#motivation) 9 | - [Files](#files) 10 | - [Usage](#usage) 11 | - [Controversies](#controversies) 12 | - [License](#license) 13 | 14 | Motivation 15 | ---------- 16 | 17 | I write Bash scripts frequently and realised that I often copied a recent script whenever I started writing a new one. This provided me with a basic scaffold to work on and several useful helper functions I'd otherwise likely end up duplicating. 18 | 19 | Rather than continually copying old scripts and flensing the irrelevant code, I'm publishing a more formalised template to ease the process for my own usage and anyone else who may find it helpful. Suggestions for improvements are most welcome. 20 | 21 | Files 22 | ----- 23 | 24 | | File | Description | 25 | | --------------- |------------------------------------------------------------------------------------------------ | 26 | | **template.sh** | A fully self-contained script which combines `source.sh` & `script.sh` | 27 | | **source.sh** | Designed for sourcing into scripts; contains only those functions unlikely to need modification | 28 | | **script.sh** | Sample script which sources in `source.sh` and contains those functions likely to be modified | 29 | | **build.sh** | Generates `template.sh` by combining `source.sh` & `template.sh` (just a helper script) | 30 | 31 | Usage 32 | ----- 33 | 34 | Being a Bash script you're free to *slice-and-dice* the source as you see fit. 35 | 36 | The following steps outline what's typically involved to help you get started: 37 | 38 | 1. Choose between using either: 39 | 1. `template.sh` (fully self-contained) 40 | 2. `script.sh` with `source.sh` (source in most functions) 41 | 2. Depending on your choice open `template.sh` or `script.sh` for editing 42 | 3. Update the `script_usage()` function with additional usage guidance 43 | 4. Update the `parse_params()` function with additional script parameters 44 | 5. Add additional functions to implement the desired functionality 45 | 6. Update the `main()` function to call your additional functions 46 | 47 | ### Adding a `hostname` parameter 48 | 49 | The following contrived example demonstrates how to add a parameter to display the system's hostname. 50 | 51 | Update the `script_usage()` function by inserting the following before the `EOF`: 52 | 53 | ```plain 54 | --hostname Display the system's hostname 55 | ``` 56 | 57 | Update the `parse_params()` function by inserting the following before the default case statement (`*)`): 58 | 59 | ```bash 60 | --hostname) 61 | hostname=true 62 | ;; 63 | ``` 64 | 65 | Update the `main()` function by inserting the following after the existing initialisation statements: 66 | 67 | ```bash 68 | if [[ -n ${hostname-} ]]; then 69 | pretty_print "Hostname is: $(hostname)" 70 | fi 71 | ``` 72 | 73 | Controversies 74 | ------------- 75 | 76 | The Bash scripting community is an opinionated one. This is not a bad thing, but it does mean that some decisions made in this template aren't going to be agreed upon by everyone. A few of the most notable ones are highlighted here with an explanation of the rationale. 77 | 78 | ### errexit (*set -e*) 79 | 80 | Conventional wisdom has for a long time held that at the top of every Bash script should be `set -e` (or the equivalent `set -o errexit`). This modifies the behaviour of Bash to exit immediately when it encounters a non-zero exit code from an executed command if it meets certain criteria. This would seem like an obviously useful behaviour in many cases, however, controversy arises both from the complexity of the grammar which determines if a command is eligible for this behaviour and the fact that there are many circumstances where a non-zero exit code is expected and should not result in termination of the script. An excellent overview of the argument against this option can be found in [BashFAQ/105](https://mywiki.wooledge.org/BashFAQ/105). 81 | 82 | My personal view is that the benefits of `errexit` outweigh its disadvantages. More importantly, a script which is compatible with this option will work just as well if it is disabled, however, the inverse is not true. By being compatible with `errexit` those who find it useful can use this template without modification while those opposed can simply disable it without issue. 83 | 84 | ### nounset (*set -u*) 85 | 86 | By enabling `set -u` (or the equivalent `set -o nounset`) the script will exit if an attempt is made to expand an unset variable. This can be useful both for detecting typos as well as potentially premature usage of variables which were expected to have been set earlier. The controvery here arises in that many Bash scripting coding idioms rely on referencing unset variables, which in the absence of this option are perfectly valid. Further discussion on this option can be found in [BashFAQ/112](https://mywiki.wooledge.org/BashFAQ/112). 87 | 88 | This option is enabled for the same reasons as described above for `errexit`. 89 | 90 | License 91 | ------- 92 | 93 | All content is licensed under the terms of [The MIT License](LICENSE). 94 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Assembles the all-in-one template script by combining source.sh & script.sh 4 | 5 | # Enable xtrace if the DEBUG environment variable is set 6 | if [[ ${DEBUG-} =~ ^1|yes|true$ ]]; then 7 | set -o xtrace # Trace the execution of the script (debug) 8 | fi 9 | 10 | # A better class of script... 11 | set -o errexit # Exit on most errors (see the manual) 12 | set -o errtrace # Make sure any error trap is inherited 13 | set -o nounset # Disallow expansion of unset variables 14 | set -o pipefail # Use last non-zero exit code in a pipeline 15 | 16 | # Main control flow 17 | function main() { 18 | # shellcheck source=source.sh 19 | source "$(dirname "${BASH_SOURCE[0]}")/source.sh" 20 | 21 | trap "script_trap_err" ERR 22 | trap "script_trap_exit" EXIT 23 | 24 | script_init "$@" 25 | build_template 26 | } 27 | 28 | # This is quite brittle, but it does work. I appreciate the irony given it's 29 | # assembling a template meant to consist of good Bash scripting practices. I'll 30 | # make it more durable once I have some spare time. Likely some arcane sed... 31 | function build_template() { 32 | local tmp_file 33 | local shebang header 34 | local source_file script_file 35 | local script_options source_data script_data 36 | 37 | shebang="#!/usr/bin/env bash" 38 | header=" 39 | # A best practices Bash script template with many useful functions. This file 40 | # combines the source.sh & script.sh files into a single script. If you want 41 | # your script to be entirely self-contained then this should be what you want!" 42 | 43 | source_file="$script_dir/source.sh" 44 | script_file="$script_dir/script.sh" 45 | 46 | script_options="$(head -n 26 "$script_file" | tail -n 17)" 47 | source_data="$(tail -n +10 "$source_file" | head -n -1)" 48 | script_data="$(tail -n +27 "$script_file")" 49 | 50 | { 51 | printf '%s\n' "$shebang" 52 | printf '%s\n\n' "$header" 53 | printf '%s\n\n' "$script_options" 54 | printf '%s\n\n' "$source_data" 55 | printf '%s\n' "$script_data" 56 | } > template.sh 57 | 58 | tmp_file="$(mktemp /tmp/template.XXXXXX)" 59 | sed -e '/# shellcheck source=source\.sh/{N;N;d;}' \ 60 | -e 's/BASH_SOURCE\[1\]/BASH_SOURCE[0]/' \ 61 | template.sh > "$tmp_file" 62 | mv "$tmp_file" template.sh 63 | chmod +x template.sh 64 | } 65 | 66 | # Template, assemble! 67 | main 68 | 69 | # vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr 70 | -------------------------------------------------------------------------------- /script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # A best practices Bash script template with many useful functions. This file 4 | # sources in the bulk of the functions from the source.sh file which it expects 5 | # to be in the same directory. Only those functions which are likely to need 6 | # modification are present in this file. This is a great combination if you're 7 | # writing several scripts! By pulling in the common functions you'll minimise 8 | # code duplication, as well as ease any potential updates to shared functions. 9 | 10 | # Enable xtrace if the DEBUG environment variable is set 11 | if [[ ${DEBUG-} =~ ^1|yes|true$ ]]; then 12 | set -o xtrace # Trace the execution of the script (debug) 13 | fi 14 | 15 | # Only enable these shell behaviours if we're not being sourced 16 | # Approach via: https://stackoverflow.com/a/28776166/8787985 17 | if ! (return 0 2> /dev/null); then 18 | # A better class of script... 19 | set -o errexit # Exit on most errors (see the manual) 20 | set -o nounset # Disallow expansion of unset variables 21 | set -o pipefail # Use last non-zero exit code in a pipeline 22 | fi 23 | 24 | # Enable errtrace or the error trap handler will not work as expected 25 | set -o errtrace # Ensure the error trap handler is inherited 26 | 27 | # DESC: Usage help 28 | # ARGS: None 29 | # OUTS: None 30 | # RETS: None 31 | function script_usage() { 32 | cat << EOF 33 | Usage: 34 | -h|--help Displays this help 35 | -v|--verbose Displays verbose output 36 | -nc|--no-colour Disables colour output 37 | -cr|--cron Run silently unless we encounter an error 38 | EOF 39 | } 40 | 41 | # DESC: Parameter parser 42 | # ARGS: $@ (optional): Arguments provided to the script 43 | # OUTS: Variables indicating command-line parameters and options 44 | # RETS: None 45 | function parse_params() { 46 | local param 47 | while [[ $# -gt 0 ]]; do 48 | param="$1" 49 | shift 50 | case $param in 51 | -h | --help) 52 | script_usage 53 | exit 0 54 | ;; 55 | -v | --verbose) 56 | verbose=true 57 | ;; 58 | -nc | --no-colour) 59 | no_colour=true 60 | ;; 61 | -cr | --cron) 62 | cron=true 63 | ;; 64 | *) 65 | script_exit "Invalid parameter was provided: $param" 1 66 | ;; 67 | esac 68 | done 69 | } 70 | 71 | # DESC: Main control flow 72 | # ARGS: $@ (optional): Arguments provided to the script 73 | # OUTS: None 74 | # RETS: None 75 | function main() { 76 | trap script_trap_err ERR 77 | trap script_trap_exit EXIT 78 | 79 | script_init "$@" 80 | parse_params "$@" 81 | cron_init 82 | colour_init 83 | #lock_init system 84 | } 85 | 86 | # shellcheck source=source.sh 87 | source "$(dirname "${BASH_SOURCE[0]}")/source.sh" 88 | 89 | # Invoke main with args if not sourced 90 | # Approach via: https://stackoverflow.com/a/28776166/8787985 91 | if ! (return 0 2> /dev/null); then 92 | main "$@" 93 | fi 94 | 95 | # vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr 96 | -------------------------------------------------------------------------------- /source.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # A best practices Bash script template with many useful functions. This file 4 | # is suitable for sourcing into other scripts and so only contains functions 5 | # which are unlikely to need modification. It omits the following functions: 6 | # - main() 7 | # - parse_params() 8 | # - script_usage() 9 | 10 | # DESC: Handler for unexpected errors 11 | # ARGS: $1 (optional): Exit code (defaults to 1) 12 | # OUTS: None 13 | # RETS: None 14 | function script_trap_err() { 15 | local exit_code=1 16 | 17 | # Disable the error trap handler to prevent potential recursion 18 | trap - ERR 19 | 20 | # Consider any further errors non-fatal to ensure we run to completion 21 | set +o errexit 22 | set +o pipefail 23 | 24 | # Validate any provided exit code 25 | if [[ ${1-} =~ ^[0-9]+$ ]]; then 26 | exit_code="$1" 27 | fi 28 | 29 | # Output debug data if in Cron mode 30 | if [[ -n ${cron-} ]]; then 31 | # Restore original file output descriptors 32 | if [[ -n ${script_output-} ]]; then 33 | exec 1>&3 2>&4 34 | fi 35 | 36 | # Print basic debugging information 37 | printf '%b\n' "$ta_none" 38 | printf '***** Abnormal termination of script *****\n' 39 | printf 'Script Path: %s\n' "$script_path" 40 | printf 'Script Parameters: %s\n' "$script_params" 41 | printf 'Script Exit Code: %s\n' "$exit_code" 42 | 43 | # Print the script log if we have it. It's possible we may not if we 44 | # failed before we even called cron_init(). This can happen if bad 45 | # parameters were passed to the script so we bailed out very early. 46 | if [[ -n ${script_output-} ]]; then 47 | # shellcheck disable=SC2312 48 | printf 'Script Output:\n\n%s' "$(cat "$script_output")" 49 | else 50 | printf 'Script Output: None (failed before log init)\n' 51 | fi 52 | fi 53 | 54 | # Exit with failure status 55 | exit "$exit_code" 56 | } 57 | 58 | # DESC: Handler for exiting the script 59 | # ARGS: None 60 | # OUTS: None 61 | # RETS: None 62 | function script_trap_exit() { 63 | cd "$orig_cwd" 64 | 65 | # Remove Cron mode script log 66 | if [[ -n ${cron-} && -f ${script_output-} ]]; then 67 | rm "$script_output" 68 | fi 69 | 70 | # Remove script execution lock 71 | if [[ -d ${script_lock-} ]]; then 72 | rmdir "$script_lock" 73 | fi 74 | 75 | # Restore terminal colours 76 | printf '%b' "$ta_none" 77 | } 78 | 79 | # DESC: Exit script with the given message 80 | # ARGS: $1 (required): Message to print on exit 81 | # $2 (optional): Exit code (defaults to 0) 82 | # OUTS: None 83 | # RETS: None 84 | # NOTE: The convention used in this script for exit codes is: 85 | # 0: Normal exit 86 | # 1: Abnormal exit due to external error 87 | # 2: Abnormal exit due to script error 88 | function script_exit() { 89 | if [[ $# -eq 1 ]]; then 90 | printf '%s\n' "$1" 91 | exit 0 92 | fi 93 | 94 | if [[ ${2-} =~ ^[0-9]+$ ]]; then 95 | printf '%b\n' "$1" 96 | # If we've been provided a non-zero exit code run the error trap 97 | if [[ $2 -ne 0 ]]; then 98 | script_trap_err "$2" 99 | else 100 | exit 0 101 | fi 102 | fi 103 | 104 | script_exit 'Missing required argument to script_exit()!' 2 105 | } 106 | 107 | # DESC: Generic script initialisation 108 | # ARGS: $@ (optional): Arguments provided to the script 109 | # OUTS: $orig_cwd: The current working directory when the script was run 110 | # $script_path: The full path to the script 111 | # $script_dir: The directory path of the script 112 | # $script_name: The file name of the script 113 | # $script_params: The original parameters provided to the script 114 | # $ta_none: The ANSI control code to reset all text attributes 115 | # RETS: None 116 | # NOTE: $script_path only contains the path that was used to call the script 117 | # and will not resolve any symlinks which may be present in the path. 118 | # You can use a tool like realpath to obtain the "true" path. The same 119 | # caveat applies to both the $script_dir and $script_name variables. 120 | # shellcheck disable=SC2034 121 | function script_init() { 122 | # Useful variables 123 | readonly orig_cwd="$PWD" 124 | readonly script_params="$*" 125 | readonly script_path="${BASH_SOURCE[1]}" 126 | script_dir="$(dirname "$script_path")" 127 | script_name="$(basename "$script_path")" 128 | readonly script_dir script_name 129 | 130 | # Important to always set as we use it in the exit handler 131 | # shellcheck disable=SC2155 132 | readonly ta_none="$(tput sgr0 2> /dev/null || true)" 133 | } 134 | 135 | # DESC: Initialise colour variables 136 | # ARGS: None 137 | # OUTS: Read-only variables with ANSI control codes 138 | # RETS: None 139 | # NOTE: If --no-colour was set the variables will be empty. The output of the 140 | # $ta_none variable after each tput is redundant during normal execution, 141 | # but ensures the terminal output isn't mangled when running with xtrace. 142 | # shellcheck disable=SC2034,SC2155 143 | function colour_init() { 144 | if [[ -z ${no_colour-} ]]; then 145 | # Text attributes 146 | readonly ta_bold="$(tput bold 2> /dev/null || true)" 147 | printf '%b' "$ta_none" 148 | readonly ta_uscore="$(tput smul 2> /dev/null || true)" 149 | printf '%b' "$ta_none" 150 | readonly ta_blink="$(tput blink 2> /dev/null || true)" 151 | printf '%b' "$ta_none" 152 | readonly ta_reverse="$(tput rev 2> /dev/null || true)" 153 | printf '%b' "$ta_none" 154 | readonly ta_conceal="$(tput invis 2> /dev/null || true)" 155 | printf '%b' "$ta_none" 156 | 157 | # Foreground codes 158 | readonly fg_black="$(tput setaf 0 2> /dev/null || true)" 159 | printf '%b' "$ta_none" 160 | readonly fg_blue="$(tput setaf 4 2> /dev/null || true)" 161 | printf '%b' "$ta_none" 162 | readonly fg_cyan="$(tput setaf 6 2> /dev/null || true)" 163 | printf '%b' "$ta_none" 164 | readonly fg_green="$(tput setaf 2 2> /dev/null || true)" 165 | printf '%b' "$ta_none" 166 | readonly fg_magenta="$(tput setaf 5 2> /dev/null || true)" 167 | printf '%b' "$ta_none" 168 | readonly fg_red="$(tput setaf 1 2> /dev/null || true)" 169 | printf '%b' "$ta_none" 170 | readonly fg_white="$(tput setaf 7 2> /dev/null || true)" 171 | printf '%b' "$ta_none" 172 | readonly fg_yellow="$(tput setaf 3 2> /dev/null || true)" 173 | printf '%b' "$ta_none" 174 | 175 | # Background codes 176 | readonly bg_black="$(tput setab 0 2> /dev/null || true)" 177 | printf '%b' "$ta_none" 178 | readonly bg_blue="$(tput setab 4 2> /dev/null || true)" 179 | printf '%b' "$ta_none" 180 | readonly bg_cyan="$(tput setab 6 2> /dev/null || true)" 181 | printf '%b' "$ta_none" 182 | readonly bg_green="$(tput setab 2 2> /dev/null || true)" 183 | printf '%b' "$ta_none" 184 | readonly bg_magenta="$(tput setab 5 2> /dev/null || true)" 185 | printf '%b' "$ta_none" 186 | readonly bg_red="$(tput setab 1 2> /dev/null || true)" 187 | printf '%b' "$ta_none" 188 | readonly bg_white="$(tput setab 7 2> /dev/null || true)" 189 | printf '%b' "$ta_none" 190 | readonly bg_yellow="$(tput setab 3 2> /dev/null || true)" 191 | printf '%b' "$ta_none" 192 | else 193 | # Text attributes 194 | readonly ta_bold='' 195 | readonly ta_uscore='' 196 | readonly ta_blink='' 197 | readonly ta_reverse='' 198 | readonly ta_conceal='' 199 | 200 | # Foreground codes 201 | readonly fg_black='' 202 | readonly fg_blue='' 203 | readonly fg_cyan='' 204 | readonly fg_green='' 205 | readonly fg_magenta='' 206 | readonly fg_red='' 207 | readonly fg_white='' 208 | readonly fg_yellow='' 209 | 210 | # Background codes 211 | readonly bg_black='' 212 | readonly bg_blue='' 213 | readonly bg_cyan='' 214 | readonly bg_green='' 215 | readonly bg_magenta='' 216 | readonly bg_red='' 217 | readonly bg_white='' 218 | readonly bg_yellow='' 219 | fi 220 | } 221 | 222 | # DESC: Initialise Cron mode 223 | # ARGS: None 224 | # OUTS: $script_output: Path to the file stdout & stderr was redirected to 225 | # RETS: None 226 | function cron_init() { 227 | if [[ -n ${cron-} ]]; then 228 | # Redirect all output to a temporary file 229 | script_output="$(mktemp --tmpdir "$script_name".XXXXX)" 230 | readonly script_output 231 | exec 3>&1 4>&2 1> "$script_output" 2>&1 232 | fi 233 | } 234 | 235 | # DESC: Acquire script lock 236 | # ARGS: $1 (optional): Scope of script execution lock (system or user) 237 | # OUTS: $script_lock: Path to the directory indicating we have the script lock 238 | # RETS: None 239 | # NOTE: This lock implementation is extremely simple but should be reliable 240 | # across all platforms. It does *not* support locking a script with 241 | # symlinks or multiple hardlinks as there's no portable way of doing so. 242 | # If the lock was acquired it's automatically released on script exit. 243 | function lock_init() { 244 | local lock_dir 245 | if [[ $1 = 'system' ]]; then 246 | lock_dir="/tmp/$script_name.lock" 247 | elif [[ $1 = 'user' ]]; then 248 | lock_dir="/tmp/$script_name.$UID.lock" 249 | else 250 | script_exit 'Missing or invalid argument to lock_init()!' 2 251 | fi 252 | 253 | if mkdir "$lock_dir" 2> /dev/null; then 254 | readonly script_lock="$lock_dir" 255 | verbose_print "Acquired script lock: $script_lock" 256 | else 257 | script_exit "Unable to acquire script lock: $lock_dir" 1 258 | fi 259 | } 260 | 261 | # DESC: Pretty print the provided string 262 | # ARGS: $1 (required): Message to print (defaults to a green foreground) 263 | # $2 (optional): Colour to print the message with. This can be an ANSI 264 | # escape code or one of the prepopulated colour variables. 265 | # $3 (optional): Set to any value to not append a new line to the message 266 | # OUTS: None 267 | # RETS: None 268 | function pretty_print() { 269 | if [[ $# -lt 1 ]]; then 270 | script_exit 'Missing required argument to pretty_print()!' 2 271 | fi 272 | 273 | if [[ -z ${no_colour-} ]]; then 274 | if [[ -n ${2-} ]]; then 275 | printf '%b' "$2" 276 | else 277 | printf '%b' "$fg_green" 278 | fi 279 | fi 280 | 281 | # Print message & reset text attributes 282 | if [[ -n ${3-} ]]; then 283 | printf '%s%b' "$1" "$ta_none" 284 | else 285 | printf '%s%b\n' "$1" "$ta_none" 286 | fi 287 | } 288 | 289 | # DESC: Only pretty_print() the provided string if verbose mode is enabled 290 | # ARGS: $@ (required): Passed through to pretty_print() function 291 | # OUTS: None 292 | # RETS: None 293 | function verbose_print() { 294 | if [[ -n ${verbose-} ]]; then 295 | pretty_print "$@" 296 | fi 297 | } 298 | 299 | # DESC: Combines two path variables and removes any duplicates 300 | # ARGS: $1 (required): Path(s) to join with the second argument 301 | # $2 (optional): Path(s) to join with the first argument 302 | # OUTS: $build_path: The constructed path 303 | # RETS: None 304 | # NOTE: Heavily inspired by: https://unix.stackexchange.com/a/40973 305 | function build_path() { 306 | if [[ $# -lt 1 ]]; then 307 | script_exit 'Missing required argument to build_path()!' 2 308 | fi 309 | 310 | local new_path path_entry temp_path 311 | 312 | temp_path="$1:" 313 | if [[ -n ${2-} ]]; then 314 | temp_path="$temp_path$2:" 315 | fi 316 | 317 | new_path= 318 | while [[ -n $temp_path ]]; do 319 | path_entry="${temp_path%%:*}" 320 | case "$new_path:" in 321 | *:"$path_entry":*) ;; 322 | *) 323 | new_path="$new_path:$path_entry" 324 | ;; 325 | esac 326 | temp_path="${temp_path#*:}" 327 | done 328 | 329 | # shellcheck disable=SC2034 330 | build_path="${new_path#:}" 331 | } 332 | 333 | # DESC: Check a binary exists in the search path 334 | # ARGS: $1 (required): Name of the binary to test for existence 335 | # $2 (optional): Set to any value to treat failure as a fatal error 336 | # OUTS: None 337 | # RETS: 0 (true) if dependency was found, otherwise 1 (false) if failure is not 338 | # being treated as a fatal error. 339 | function check_binary() { 340 | if [[ $# -lt 1 ]]; then 341 | script_exit 'Missing required argument to check_binary()!' 2 342 | fi 343 | 344 | if ! command -v "$1" > /dev/null 2>&1; then 345 | if [[ -n ${2-} ]]; then 346 | script_exit "Missing dependency: Couldn't locate $1." 1 347 | else 348 | verbose_print "Missing dependency: $1" "${fg_red-}" 349 | return 1 350 | fi 351 | fi 352 | 353 | verbose_print "Found dependency: $1" 354 | return 0 355 | } 356 | 357 | # DESC: Validate we have superuser access as root (via sudo if requested) 358 | # ARGS: $1 (optional): Set to any value to not attempt root access via sudo 359 | # OUTS: None 360 | # RETS: 0 (true) if superuser credentials were acquired, otherwise 1 (false) 361 | function check_superuser() { 362 | local superuser 363 | if [[ $EUID -eq 0 ]]; then 364 | superuser=true 365 | elif [[ -z ${1-} ]]; then 366 | # shellcheck disable=SC2310 367 | if check_binary sudo; then 368 | verbose_print 'Sudo: Updating cached credentials ...' 369 | if ! sudo -v; then 370 | verbose_print "Sudo: Couldn't acquire credentials ..." \ 371 | "${fg_red-}" 372 | else 373 | local test_euid 374 | test_euid="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')" 375 | if [[ $test_euid -eq 0 ]]; then 376 | superuser=true 377 | fi 378 | fi 379 | fi 380 | fi 381 | 382 | if [[ -z ${superuser-} ]]; then 383 | verbose_print 'Unable to acquire superuser credentials.' "${fg_red-}" 384 | return 1 385 | fi 386 | 387 | verbose_print 'Successfully acquired superuser credentials.' 388 | return 0 389 | } 390 | 391 | # DESC: Run the requested command as root (via sudo if requested) 392 | # ARGS: $1 (optional): Set to zero to not attempt execution via sudo 393 | # $@ (required): Passed through for execution as root user 394 | # OUTS: None 395 | # RETS: None 396 | function run_as_root() { 397 | if [[ $# -eq 0 ]]; then 398 | script_exit 'Missing required argument to run_as_root()!' 2 399 | fi 400 | 401 | if [[ ${1-} =~ ^0$ ]]; then 402 | local skip_sudo=true 403 | shift 404 | fi 405 | 406 | if [[ $EUID -eq 0 ]]; then 407 | "$@" 408 | elif [[ -z ${skip_sudo-} ]]; then 409 | sudo -H -- "$@" 410 | else 411 | script_exit "Unable to run requested command as root: $*" 1 412 | fi 413 | } 414 | 415 | # vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr 416 | -------------------------------------------------------------------------------- /template.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # A best practices Bash script template with many useful functions. This file 4 | # combines the source.sh & script.sh files into a single script. If you want 5 | # your script to be entirely self-contained then this should be what you want! 6 | 7 | # Enable xtrace if the DEBUG environment variable is set 8 | if [[ ${DEBUG-} =~ ^1|yes|true$ ]]; then 9 | set -o xtrace # Trace the execution of the script (debug) 10 | fi 11 | 12 | # Only enable these shell behaviours if we're not being sourced 13 | # Approach via: https://stackoverflow.com/a/28776166/8787985 14 | if ! (return 0 2> /dev/null); then 15 | # A better class of script... 16 | set -o errexit # Exit on most errors (see the manual) 17 | set -o nounset # Disallow expansion of unset variables 18 | set -o pipefail # Use last non-zero exit code in a pipeline 19 | fi 20 | 21 | # Enable errtrace or the error trap handler will not work as expected 22 | set -o errtrace # Ensure the error trap handler is inherited 23 | 24 | # DESC: Handler for unexpected errors 25 | # ARGS: $1 (optional): Exit code (defaults to 1) 26 | # OUTS: None 27 | # RETS: None 28 | function script_trap_err() { 29 | local exit_code=1 30 | 31 | # Disable the error trap handler to prevent potential recursion 32 | trap - ERR 33 | 34 | # Consider any further errors non-fatal to ensure we run to completion 35 | set +o errexit 36 | set +o pipefail 37 | 38 | # Validate any provided exit code 39 | if [[ ${1-} =~ ^[0-9]+$ ]]; then 40 | exit_code="$1" 41 | fi 42 | 43 | # Output debug data if in Cron mode 44 | if [[ -n ${cron-} ]]; then 45 | # Restore original file output descriptors 46 | if [[ -n ${script_output-} ]]; then 47 | exec 1>&3 2>&4 48 | fi 49 | 50 | # Print basic debugging information 51 | printf '%b\n' "$ta_none" 52 | printf '***** Abnormal termination of script *****\n' 53 | printf 'Script Path: %s\n' "$script_path" 54 | printf 'Script Parameters: %s\n' "$script_params" 55 | printf 'Script Exit Code: %s\n' "$exit_code" 56 | 57 | # Print the script log if we have it. It's possible we may not if we 58 | # failed before we even called cron_init(). This can happen if bad 59 | # parameters were passed to the script so we bailed out very early. 60 | if [[ -n ${script_output-} ]]; then 61 | # shellcheck disable=SC2312 62 | printf 'Script Output:\n\n%s' "$(cat "$script_output")" 63 | else 64 | printf 'Script Output: None (failed before log init)\n' 65 | fi 66 | fi 67 | 68 | # Exit with failure status 69 | exit "$exit_code" 70 | } 71 | 72 | # DESC: Handler for exiting the script 73 | # ARGS: None 74 | # OUTS: None 75 | # RETS: None 76 | function script_trap_exit() { 77 | cd "$orig_cwd" 78 | 79 | # Remove Cron mode script log 80 | if [[ -n ${cron-} && -f ${script_output-} ]]; then 81 | rm "$script_output" 82 | fi 83 | 84 | # Remove script execution lock 85 | if [[ -d ${script_lock-} ]]; then 86 | rmdir "$script_lock" 87 | fi 88 | 89 | # Restore terminal colours 90 | printf '%b' "$ta_none" 91 | } 92 | 93 | # DESC: Exit script with the given message 94 | # ARGS: $1 (required): Message to print on exit 95 | # $2 (optional): Exit code (defaults to 0) 96 | # OUTS: None 97 | # RETS: None 98 | # NOTE: The convention used in this script for exit codes is: 99 | # 0: Normal exit 100 | # 1: Abnormal exit due to external error 101 | # 2: Abnormal exit due to script error 102 | function script_exit() { 103 | if [[ $# -eq 1 ]]; then 104 | printf '%s\n' "$1" 105 | exit 0 106 | fi 107 | 108 | if [[ ${2-} =~ ^[0-9]+$ ]]; then 109 | printf '%b\n' "$1" 110 | # If we've been provided a non-zero exit code run the error trap 111 | if [[ $2 -ne 0 ]]; then 112 | script_trap_err "$2" 113 | else 114 | exit 0 115 | fi 116 | fi 117 | 118 | script_exit 'Missing required argument to script_exit()!' 2 119 | } 120 | 121 | # DESC: Generic script initialisation 122 | # ARGS: $@ (optional): Arguments provided to the script 123 | # OUTS: $orig_cwd: The current working directory when the script was run 124 | # $script_path: The full path to the script 125 | # $script_dir: The directory path of the script 126 | # $script_name: The file name of the script 127 | # $script_params: The original parameters provided to the script 128 | # $ta_none: The ANSI control code to reset all text attributes 129 | # RETS: None 130 | # NOTE: $script_path only contains the path that was used to call the script 131 | # and will not resolve any symlinks which may be present in the path. 132 | # You can use a tool like realpath to obtain the "true" path. The same 133 | # caveat applies to both the $script_dir and $script_name variables. 134 | # shellcheck disable=SC2034 135 | function script_init() { 136 | # Useful variables 137 | readonly orig_cwd="$PWD" 138 | readonly script_params="$*" 139 | readonly script_path="${BASH_SOURCE[0]}" 140 | script_dir="$(dirname "$script_path")" 141 | script_name="$(basename "$script_path")" 142 | readonly script_dir script_name 143 | 144 | # Important to always set as we use it in the exit handler 145 | # shellcheck disable=SC2155 146 | readonly ta_none="$(tput sgr0 2> /dev/null || true)" 147 | } 148 | 149 | # DESC: Initialise colour variables 150 | # ARGS: None 151 | # OUTS: Read-only variables with ANSI control codes 152 | # RETS: None 153 | # NOTE: If --no-colour was set the variables will be empty. The output of the 154 | # $ta_none variable after each tput is redundant during normal execution, 155 | # but ensures the terminal output isn't mangled when running with xtrace. 156 | # shellcheck disable=SC2034,SC2155 157 | function colour_init() { 158 | if [[ -z ${no_colour-} ]]; then 159 | # Text attributes 160 | readonly ta_bold="$(tput bold 2> /dev/null || true)" 161 | printf '%b' "$ta_none" 162 | readonly ta_uscore="$(tput smul 2> /dev/null || true)" 163 | printf '%b' "$ta_none" 164 | readonly ta_blink="$(tput blink 2> /dev/null || true)" 165 | printf '%b' "$ta_none" 166 | readonly ta_reverse="$(tput rev 2> /dev/null || true)" 167 | printf '%b' "$ta_none" 168 | readonly ta_conceal="$(tput invis 2> /dev/null || true)" 169 | printf '%b' "$ta_none" 170 | 171 | # Foreground codes 172 | readonly fg_black="$(tput setaf 0 2> /dev/null || true)" 173 | printf '%b' "$ta_none" 174 | readonly fg_blue="$(tput setaf 4 2> /dev/null || true)" 175 | printf '%b' "$ta_none" 176 | readonly fg_cyan="$(tput setaf 6 2> /dev/null || true)" 177 | printf '%b' "$ta_none" 178 | readonly fg_green="$(tput setaf 2 2> /dev/null || true)" 179 | printf '%b' "$ta_none" 180 | readonly fg_magenta="$(tput setaf 5 2> /dev/null || true)" 181 | printf '%b' "$ta_none" 182 | readonly fg_red="$(tput setaf 1 2> /dev/null || true)" 183 | printf '%b' "$ta_none" 184 | readonly fg_white="$(tput setaf 7 2> /dev/null || true)" 185 | printf '%b' "$ta_none" 186 | readonly fg_yellow="$(tput setaf 3 2> /dev/null || true)" 187 | printf '%b' "$ta_none" 188 | 189 | # Background codes 190 | readonly bg_black="$(tput setab 0 2> /dev/null || true)" 191 | printf '%b' "$ta_none" 192 | readonly bg_blue="$(tput setab 4 2> /dev/null || true)" 193 | printf '%b' "$ta_none" 194 | readonly bg_cyan="$(tput setab 6 2> /dev/null || true)" 195 | printf '%b' "$ta_none" 196 | readonly bg_green="$(tput setab 2 2> /dev/null || true)" 197 | printf '%b' "$ta_none" 198 | readonly bg_magenta="$(tput setab 5 2> /dev/null || true)" 199 | printf '%b' "$ta_none" 200 | readonly bg_red="$(tput setab 1 2> /dev/null || true)" 201 | printf '%b' "$ta_none" 202 | readonly bg_white="$(tput setab 7 2> /dev/null || true)" 203 | printf '%b' "$ta_none" 204 | readonly bg_yellow="$(tput setab 3 2> /dev/null || true)" 205 | printf '%b' "$ta_none" 206 | else 207 | # Text attributes 208 | readonly ta_bold='' 209 | readonly ta_uscore='' 210 | readonly ta_blink='' 211 | readonly ta_reverse='' 212 | readonly ta_conceal='' 213 | 214 | # Foreground codes 215 | readonly fg_black='' 216 | readonly fg_blue='' 217 | readonly fg_cyan='' 218 | readonly fg_green='' 219 | readonly fg_magenta='' 220 | readonly fg_red='' 221 | readonly fg_white='' 222 | readonly fg_yellow='' 223 | 224 | # Background codes 225 | readonly bg_black='' 226 | readonly bg_blue='' 227 | readonly bg_cyan='' 228 | readonly bg_green='' 229 | readonly bg_magenta='' 230 | readonly bg_red='' 231 | readonly bg_white='' 232 | readonly bg_yellow='' 233 | fi 234 | } 235 | 236 | # DESC: Initialise Cron mode 237 | # ARGS: None 238 | # OUTS: $script_output: Path to the file stdout & stderr was redirected to 239 | # RETS: None 240 | function cron_init() { 241 | if [[ -n ${cron-} ]]; then 242 | # Redirect all output to a temporary file 243 | script_output="$(mktemp --tmpdir "$script_name".XXXXX)" 244 | readonly script_output 245 | exec 3>&1 4>&2 1> "$script_output" 2>&1 246 | fi 247 | } 248 | 249 | # DESC: Acquire script lock 250 | # ARGS: $1 (optional): Scope of script execution lock (system or user) 251 | # OUTS: $script_lock: Path to the directory indicating we have the script lock 252 | # RETS: None 253 | # NOTE: This lock implementation is extremely simple but should be reliable 254 | # across all platforms. It does *not* support locking a script with 255 | # symlinks or multiple hardlinks as there's no portable way of doing so. 256 | # If the lock was acquired it's automatically released on script exit. 257 | function lock_init() { 258 | local lock_dir 259 | if [[ $1 = 'system' ]]; then 260 | lock_dir="/tmp/$script_name.lock" 261 | elif [[ $1 = 'user' ]]; then 262 | lock_dir="/tmp/$script_name.$UID.lock" 263 | else 264 | script_exit 'Missing or invalid argument to lock_init()!' 2 265 | fi 266 | 267 | if mkdir "$lock_dir" 2> /dev/null; then 268 | readonly script_lock="$lock_dir" 269 | verbose_print "Acquired script lock: $script_lock" 270 | else 271 | script_exit "Unable to acquire script lock: $lock_dir" 1 272 | fi 273 | } 274 | 275 | # DESC: Pretty print the provided string 276 | # ARGS: $1 (required): Message to print (defaults to a green foreground) 277 | # $2 (optional): Colour to print the message with. This can be an ANSI 278 | # escape code or one of the prepopulated colour variables. 279 | # $3 (optional): Set to any value to not append a new line to the message 280 | # OUTS: None 281 | # RETS: None 282 | function pretty_print() { 283 | if [[ $# -lt 1 ]]; then 284 | script_exit 'Missing required argument to pretty_print()!' 2 285 | fi 286 | 287 | if [[ -z ${no_colour-} ]]; then 288 | if [[ -n ${2-} ]]; then 289 | printf '%b' "$2" 290 | else 291 | printf '%b' "$fg_green" 292 | fi 293 | fi 294 | 295 | # Print message & reset text attributes 296 | if [[ -n ${3-} ]]; then 297 | printf '%s%b' "$1" "$ta_none" 298 | else 299 | printf '%s%b\n' "$1" "$ta_none" 300 | fi 301 | } 302 | 303 | # DESC: Only pretty_print() the provided string if verbose mode is enabled 304 | # ARGS: $@ (required): Passed through to pretty_print() function 305 | # OUTS: None 306 | # RETS: None 307 | function verbose_print() { 308 | if [[ -n ${verbose-} ]]; then 309 | pretty_print "$@" 310 | fi 311 | } 312 | 313 | # DESC: Combines two path variables and removes any duplicates 314 | # ARGS: $1 (required): Path(s) to join with the second argument 315 | # $2 (optional): Path(s) to join with the first argument 316 | # OUTS: $build_path: The constructed path 317 | # RETS: None 318 | # NOTE: Heavily inspired by: https://unix.stackexchange.com/a/40973 319 | function build_path() { 320 | if [[ $# -lt 1 ]]; then 321 | script_exit 'Missing required argument to build_path()!' 2 322 | fi 323 | 324 | local new_path path_entry temp_path 325 | 326 | temp_path="$1:" 327 | if [[ -n ${2-} ]]; then 328 | temp_path="$temp_path$2:" 329 | fi 330 | 331 | new_path= 332 | while [[ -n $temp_path ]]; do 333 | path_entry="${temp_path%%:*}" 334 | case "$new_path:" in 335 | *:"$path_entry":*) ;; 336 | *) 337 | new_path="$new_path:$path_entry" 338 | ;; 339 | esac 340 | temp_path="${temp_path#*:}" 341 | done 342 | 343 | # shellcheck disable=SC2034 344 | build_path="${new_path#:}" 345 | } 346 | 347 | # DESC: Check a binary exists in the search path 348 | # ARGS: $1 (required): Name of the binary to test for existence 349 | # $2 (optional): Set to any value to treat failure as a fatal error 350 | # OUTS: None 351 | # RETS: 0 (true) if dependency was found, otherwise 1 (false) if failure is not 352 | # being treated as a fatal error. 353 | function check_binary() { 354 | if [[ $# -lt 1 ]]; then 355 | script_exit 'Missing required argument to check_binary()!' 2 356 | fi 357 | 358 | if ! command -v "$1" > /dev/null 2>&1; then 359 | if [[ -n ${2-} ]]; then 360 | script_exit "Missing dependency: Couldn't locate $1." 1 361 | else 362 | verbose_print "Missing dependency: $1" "${fg_red-}" 363 | return 1 364 | fi 365 | fi 366 | 367 | verbose_print "Found dependency: $1" 368 | return 0 369 | } 370 | 371 | # DESC: Validate we have superuser access as root (via sudo if requested) 372 | # ARGS: $1 (optional): Set to any value to not attempt root access via sudo 373 | # OUTS: None 374 | # RETS: 0 (true) if superuser credentials were acquired, otherwise 1 (false) 375 | function check_superuser() { 376 | local superuser 377 | if [[ $EUID -eq 0 ]]; then 378 | superuser=true 379 | elif [[ -z ${1-} ]]; then 380 | # shellcheck disable=SC2310 381 | if check_binary sudo; then 382 | verbose_print 'Sudo: Updating cached credentials ...' 383 | if ! sudo -v; then 384 | verbose_print "Sudo: Couldn't acquire credentials ..." \ 385 | "${fg_red-}" 386 | else 387 | local test_euid 388 | test_euid="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')" 389 | if [[ $test_euid -eq 0 ]]; then 390 | superuser=true 391 | fi 392 | fi 393 | fi 394 | fi 395 | 396 | if [[ -z ${superuser-} ]]; then 397 | verbose_print 'Unable to acquire superuser credentials.' "${fg_red-}" 398 | return 1 399 | fi 400 | 401 | verbose_print 'Successfully acquired superuser credentials.' 402 | return 0 403 | } 404 | 405 | # DESC: Run the requested command as root (via sudo if requested) 406 | # ARGS: $1 (optional): Set to zero to not attempt execution via sudo 407 | # $@ (required): Passed through for execution as root user 408 | # OUTS: None 409 | # RETS: None 410 | function run_as_root() { 411 | if [[ $# -eq 0 ]]; then 412 | script_exit 'Missing required argument to run_as_root()!' 2 413 | fi 414 | 415 | if [[ ${1-} =~ ^0$ ]]; then 416 | local skip_sudo=true 417 | shift 418 | fi 419 | 420 | if [[ $EUID -eq 0 ]]; then 421 | "$@" 422 | elif [[ -z ${skip_sudo-} ]]; then 423 | sudo -H -- "$@" 424 | else 425 | script_exit "Unable to run requested command as root: $*" 1 426 | fi 427 | } 428 | 429 | # DESC: Usage help 430 | # ARGS: None 431 | # OUTS: None 432 | # RETS: None 433 | function script_usage() { 434 | cat << EOF 435 | Usage: 436 | -h|--help Displays this help 437 | -v|--verbose Displays verbose output 438 | -nc|--no-colour Disables colour output 439 | -cr|--cron Run silently unless we encounter an error 440 | EOF 441 | } 442 | 443 | # DESC: Parameter parser 444 | # ARGS: $@ (optional): Arguments provided to the script 445 | # OUTS: Variables indicating command-line parameters and options 446 | # RETS: None 447 | function parse_params() { 448 | local param 449 | while [[ $# -gt 0 ]]; do 450 | param="$1" 451 | shift 452 | case $param in 453 | -h | --help) 454 | script_usage 455 | exit 0 456 | ;; 457 | -v | --verbose) 458 | verbose=true 459 | ;; 460 | -nc | --no-colour) 461 | no_colour=true 462 | ;; 463 | -cr | --cron) 464 | cron=true 465 | ;; 466 | *) 467 | script_exit "Invalid parameter was provided: $param" 1 468 | ;; 469 | esac 470 | done 471 | } 472 | 473 | # DESC: Main control flow 474 | # ARGS: $@ (optional): Arguments provided to the script 475 | # OUTS: None 476 | # RETS: None 477 | function main() { 478 | trap script_trap_err ERR 479 | trap script_trap_exit EXIT 480 | 481 | script_init "$@" 482 | parse_params "$@" 483 | cron_init 484 | colour_init 485 | #lock_init system 486 | } 487 | 488 | # Invoke main with args if not sourced 489 | # Approach via: https://stackoverflow.com/a/28776166/8787985 490 | if ! (return 0 2> /dev/null); then 491 | main "$@" 492 | fi 493 | 494 | # vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr 495 | --------------------------------------------------------------------------------