├── LICENSE ├── Makefile ├── README.md └── script.sh /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Oskar Schöldström 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILES ?= $(wildcard *.sh) 2 | 3 | all: lint 4 | 5 | lint: 6 | @$(foreach file, $(FILES), $(shell bash -n $(file))) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bash boilerplate 2 | 3 | ## Features 4 | 5 | * Interactive mode 6 | * Quiet mode 7 | * CLI options parser supporting `-n --name --name=Oxy --name Oxy` 8 | * Also supports bundling of flags. ie. `-vf` instead of `-v -f` 9 | * Helper functions for printing messages. 10 | * Automatically remove color escape codes if the script is piped. 11 | 12 | ## Functions 13 | 14 | ### Print functions 15 | 16 | * `die()` Output message to stderr and exit with error code 1. 17 | * `out()` Output message. 18 | * `err()` Output message to stderr but continue running. 19 | * `success()` Output message as a string. Both `success` and `err` will output message with a colorized symbol, as long as the script isn't piped. 20 | * `log()` Will only output message if user has activated verbose flag. 21 | * `notify()` Delegate the message to either `err` or `success` depending on the last return code. *Remember this function needs to be called once a return code is available.* Eg. 22 | 23 | ```bash 24 | foobar; notify "foobar copied files" 25 | 26 | notify "foobar copied files" $(foobar) 27 | ``` 28 | 29 | ### Misc helpers 30 | 31 | * `escape()` Escape slashes in a string 32 | * `confirm()` Prompt the user to answer Yes or No. *This will automatically return true if --force is used.* Eg. 33 | 34 | ```bash 35 | if ! confirm "Delete file"; then 36 | continue; 37 | fi 38 | ``` 39 | 40 | ### Interactive mode 41 | 42 | With this script comes a wtfmagic interactive mode which prompts the user to enter variables through stdin instead of the command line. 43 | 44 | 1. This works by first defining which variables should be prompted for in the `$interactive_opts` variable. 45 | 46 | 2. Making sure `usage` outputs valid information, where an options longname (eg. --password) has the same name as the variable in `interactive_opts`. 47 | 48 | 3. Once the script has parsed all variables supplied through the command line, it will iterate through the `interactive_opts` array and parse the usage file for the description (also supports multiline). 49 | 50 | 4. Now the user will be prompted and can enter the value through stdin. Note, if the variable is named password, interactive mode will automatically hide the input from prying eyes. 51 | 52 | Once a script has many CLI options it becomes annoying to remember them all and this is when interactive mode shines. You can support both the scriptable CLI as well as a user friendly alternative for that one time per year when you actually need the script. 53 | 54 | ## Acknowledgment 55 | 56 | * Daniel Mills, [options.bash](https://github.com/e36freak/tools/blob/master/options.bash) 57 | -------------------------------------------------------------------------------- /script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # A lot of this is based on options.bash by Daniel Mills. 4 | # @see https://github.com/e36freak/tools/blob/master/options.bash 5 | 6 | # Preamble {{{ 7 | 8 | # Exit immediately on error 9 | set -e 10 | 11 | # Detect whether output is piped or not. 12 | [[ -t 1 ]] && piped=0 || piped=1 13 | 14 | # Defaults 15 | force=0 16 | quiet=0 17 | verbose=0 18 | interactive=0 19 | args=() 20 | 21 | # }}} 22 | # Helpers {{{ 23 | 24 | out() { 25 | ((quiet)) && return 26 | 27 | local message="$@" 28 | if ((piped)); then 29 | message=$(echo $message | sed ' 30 | s/\\[0-9]\{3\}\[[0-9]\(;[0-9]\{2\}\)\?m//g; 31 | s/✖/Error:/g; 32 | s/✔/Success:/g; 33 | ') 34 | fi 35 | printf '%b\n' "$message"; 36 | } 37 | die() { out "$@"; exit 1; } >&2 38 | err() { out " \033[1;31m✖\033[0m $@"; } >&2 39 | success() { out " \033[1;32m✔\033[0m $@"; } 40 | 41 | # Verbose logging 42 | log() { (($verbose)) && out "$@"; } 43 | 44 | # Notify on function success 45 | notify() { [[ $? == 0 ]] && success "$@" || err "$@"; } 46 | 47 | # Escape a string 48 | escape() { echo $@ | sed 's/\//\\\//g'; } 49 | 50 | # Unless force is used, confirm with user 51 | confirm() { 52 | (($force)) && return 0; 53 | 54 | read -p "$1 [y/N] " -n 1; 55 | [[ $REPLY =~ ^[Yy]$ ]]; 56 | } 57 | 58 | # }}} 59 | # Script logic -- TOUCH THIS {{{ 60 | 61 | version="v0.1" 62 | 63 | # A list of all variables to prompt in interactive mode. These variables HAVE 64 | # to be named exactly as the longname option definition in usage(). 65 | interactive_opts=(username password) 66 | 67 | # Print usage 68 | usage() { 69 | echo -n "$(basename $0) [OPTION]... [FILE]... 70 | 71 | Description of this script. 72 | 73 | Options: 74 | -u, --username Username for script 75 | -p, --password Input user password, it's recommended to insert 76 | this through the interactive option 77 | -f, --force Skip all user interaction 78 | -i, --interactive Prompt for values 79 | -q, --quiet Quiet (no output) 80 | -v, --verbose Output more 81 | -h, --help Display this help and exit 82 | --version Output version information and exit 83 | " 84 | } 85 | 86 | # Set a trap for cleaning up in case of errors or when script exits. 87 | rollback() { 88 | die 89 | } 90 | 91 | # Put your script here 92 | main() { 93 | echo -n 94 | } 95 | 96 | # }}} 97 | # Boilerplate {{{ 98 | 99 | # Prompt the user to interactively enter desired variable values. 100 | prompt_options() { 101 | local desc= 102 | local val= 103 | for val in ${interactive_opts[@]}; do 104 | 105 | # Skip values which already are defined 106 | [[ $(eval echo "\$$val") ]] && continue 107 | 108 | # Parse the usage description for spefic option longname. 109 | desc=$(usage | awk -v val=$val ' 110 | BEGIN { 111 | # Separate rows at option definitions and begin line right before 112 | # longname. 113 | RS="\n +-([a-zA-Z0-9], )|-"; 114 | ORS=" "; 115 | } 116 | NR > 3 { 117 | # Check if the option longname equals the value requested and passed 118 | # into awk. 119 | if ($1 == val) { 120 | # Print all remaining fields, ie. the description. 121 | for (i=2; i <= NF; i++) print $i 122 | } 123 | } 124 | ') 125 | [[ ! "$desc" ]] && continue 126 | 127 | echo -n "$desc: " 128 | 129 | # In case this is a password field, hide the user input 130 | if [[ $val == "password" ]]; then 131 | stty -echo; read password; stty echo 132 | echo 133 | # Otherwise just read the input 134 | else 135 | eval "read $val" 136 | fi 137 | done 138 | } 139 | 140 | # Iterate over options breaking -ab into -a -b when needed and --foo=bar into 141 | # --foo bar 142 | optstring=h 143 | unset options 144 | while (($#)); do 145 | case $1 in 146 | # If option is of type -ab 147 | -[!-]?*) 148 | # Loop over each character starting with the second 149 | for ((i=1; i < ${#1}; i++)); do 150 | c=${1:i:1} 151 | 152 | # Add current char to options 153 | options+=("-$c") 154 | 155 | # If option takes a required argument, and it's not the last char make 156 | # the rest of the string its argument 157 | if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then 158 | options+=("${1:i+1}") 159 | break 160 | fi 161 | done 162 | ;; 163 | # If option is of type --foo=bar 164 | --?*=*) options+=("${1%%=*}" "${1#*=}") ;; 165 | # add --endopts for -- 166 | --) options+=(--endopts) ;; 167 | # Otherwise, nothing special 168 | *) options+=("$1") ;; 169 | esac 170 | shift 171 | done 172 | set -- "${options[@]}" 173 | unset options 174 | 175 | # Set our rollback function for unexpected exits. 176 | trap rollback INT TERM EXIT 177 | 178 | # A non-destructive exit for when the script exits naturally. 179 | safe_exit() { 180 | trap - INT TERM EXIT 181 | exit 182 | } 183 | 184 | # }}} 185 | # Main loop {{{ 186 | 187 | # Print help if no arguments were passed. 188 | [[ $# -eq 0 ]] && set -- "--help" 189 | 190 | # Read the options and set stuff 191 | while [[ $1 = -?* ]]; do 192 | case $1 in 193 | -h|--help) usage >&2; safe_exit ;; 194 | --version) out "$(basename $0) $version"; safe_exit ;; 195 | -u|--username) shift; username=$1 ;; 196 | -p|--password) shift; password=$1 ;; 197 | -v|--verbose) verbose=1 ;; 198 | -q|--quiet) quiet=1 ;; 199 | -i|--interactive) interactive=1 ;; 200 | -f|--force) force=1 ;; 201 | --endopts) shift; break ;; 202 | *) die "invalid option: $1" ;; 203 | esac 204 | shift 205 | done 206 | 207 | # Store the remaining part as arguments. 208 | args+=("$@") 209 | 210 | # }}} 211 | # Run it {{{ 212 | 213 | # Uncomment this line if the script requires root privileges. 214 | # [[ $UID -ne 0 ]] && die "You need to be root to run this script" 215 | 216 | if ((interactive)); then 217 | prompt_options 218 | fi 219 | 220 | # You should delegate your logic from the `main` function 221 | main 222 | 223 | # This has to be run last not to rollback changes we've made. 224 | safe_exit 225 | 226 | # }}} 227 | --------------------------------------------------------------------------------