├── .github └── workflows │ ├── bash_ci.yml │ └── shellcheck.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION.md ├── docs ├── _config.yml ├── bash-boilerplate.jpg └── index.md ├── script.sh ├── tests ├── bash_unit ├── test_basic └── test_script └── usage ├── CHANGELOG.md ├── EXAMPLES.md ├── EXPLAIN.md ├── INSPIRATION.md ├── INSTALL.md ├── TODO.md └── create.sh /.github/workflows/bash_ci.yml: -------------------------------------------------------------------------------- 1 | name: Bash CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: list scripts to be tested 16 | run: ls ./*.sh 17 | 18 | - name: Check for basic execution 19 | run: ls ./*.sh | xargs bash 20 | 21 | - name: Unit testing with bash_unit 22 | run: tests/bash_unit tests/test_* 23 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Shellcheck CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | shellcheck: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Check for code quality errors 16 | run: ls ./*.sh | xargs shellcheck 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | .*/ 3 | log/ 4 | tmp/ 5 | cache/* 6 | tests/log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Peter Forret 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Bash CI](https://github.com/pforret/bash-boilerplate/workflows/Bash%20CI/badge.svg) 2 | ![Shellcheck CI](https://github.com/pforret/bash-boilerplate/workflows/Shellcheck%20CI/badge.svg) 3 | ![version](https://img.shields.io/github/v/release/pforret/bash-boilerplate?include_prereleases) 4 | ![activity](https://img.shields.io/github/commit-activity/y/pforret/bash-boilerplate) 5 | ![license](https://img.shields.io/github/license/pforret/bash-boilerplate) 6 | ![repo size](https://img.shields.io/github/repo-size/pforret/bash-boilerplate) 7 | 8 | ### BASH BOILERPLATE 9 | 10 | UPDATE: This repo is no longer under development!! 11 | please check [github.com/pforret/bashew](https://github.com/pforret/bashew) for a more modern version of this project 12 | 13 | It's like a mini console framework for bash shell scripting. 14 | 15 | Just use one of 4 methods to generate a new script, that has all the functionality to 16 | 17 | 1. one self-contained file, no external dependencies 18 | 2. parse options and parameters 19 | 3. generate clean usage 20 | 4. run in silent/quiet or verbose mode 21 | 5. create and clean up temporary folder/files 22 | 6. better error reporting 23 | 7. Bash CI (Github Actions) 24 | 8. self-initialisation for new scripts (`script.sh init`) 25 | 26 | flag|h|help|show usage 27 | flag|q|quiet|no output 28 | flag|v|verbose|output more 29 | flag|f|force|do not ask for confirmation 30 | option|l|logd|folder for log files |log 31 | option|t|tmpd|folder for temp files|.tmp 32 | param|1|action|action to perform: LIST/TEST/... 33 | param|1|output|output file 34 | # there can only be 1 param|n and it should be the last 35 | param|n|inputs|input files 36 | 37 | becomes 38 | 39 | ### USAGE 40 | Program: script.sh by @email 41 | Version: @version (L:591-MD:6523f7) 42 | Updated: Jul 31 20:07:24 2020 43 | Usage: script.sh [-h] [-q] [-v] [-f] [-l ] [-t ] 44 | Flags, options and parameters: 45 | -h|--help : [flag] show usage [default: off] 46 | -q|--quiet : [flag] no output [default: off] 47 | -v|--verbose : [flag] output more [default: off] 48 | -f|--force : [flag] do not ask for confirmation (always yes) [default: off] 49 | -l|--logd : [optn] folder for log files [default: log] 50 | -t|--tmpd : [optn] folder for temp files [default: .tmp] 51 | : [parameter] action to perform: init/list/test/... 52 | : [parameter] output file 53 | : [parameters] input files (1 or more) 54 | 55 | ### SCRIPT AUTHORING TIPS 56 | * use out to show any kind of output, except when option --quiet is specified 57 | out "User is [$USER]" 58 | * use progress to show one line of progress that will be overwritten by the next output 59 | progress "Now generating file $nb of $total ..." 60 | * use is_empty and is_not_empty to test for variables 61 | if is_empty "$email" ; then ; echo "Need Email!" ; fi 62 | * use die to show error message and exit program 63 | if [[ ! -f $output ]] ; then ; die "could not create output" ; fi 64 | * use alert to show alert message but continue 65 | if [[ ! -f $output ]] ; then ; alert "could not create output" ; fi 66 | * use success to show success message but continue 67 | if [[ -f $output ]] ; then ; success "output was created!" ; fi 68 | * use announce to show the start of a task 69 | announce "now generating the reports" 70 | * use log to show information that will only be visible when -v is specified 71 | log "input file: [$inputname] - [$inputsize] MB" 72 | * use escape to extra escape '/' paths in regex 73 | sed 's/$(escape $path)//g' 74 | * use lcase and ucase to convert to upper/lower case 75 | param=$(lcase $param) 76 | * use confirm for interactive confirmation before doing something 77 | if ! confirm "Delete file"; then ; echo "skip deletion" ; fi 78 | * use ask for interactive setting of variables 79 | ask NAME "What is your name" "Peter" 80 | * use on_mac/on_linux/'on_32bit'/'on_64bit' to only run things on certain platforms 81 | on_mac && log "Running on MacOS" 82 | * use folder_prep to create a folder if needed and otherwise clean up old files 83 | folder_prep "$logd" 7 # delete all files olders than 7 days 84 | 85 | ### VERSION HISTORY 86 | 87 | * v1.7: all editable content to the front of the file 88 | * v1.6: introduce semver versioning, Bash CI 89 | * v1.5: fixed last shellcheck warnings - https://github.com/koalaman/shellcheck 90 | * v1.4: fix md5sum problem, add script authoring tips, automated README creation 91 | * v1.3: robuster parameter parsing 92 | * v1.2: better error trap and versioning info 93 | * v1.1: better single and multi param parsing 94 | * v1.0: first release 95 | 96 | 97 | ### CREATE NEW BASH SCRIPT 98 | 99 | #### Option 1: clone this repo 100 | 101 | git clone https://github.com/pforret/bash-boilerplate.git 102 | cp bash-boilerplate/script.sh my-new-script.sh 103 | 104 | #### Option 2: create new Github repo from template 105 | 106 | go to https://github.com/pforret/bash-boilerplate - press "Use this template" 107 | 108 | #### Option 3: download the script directly 109 | 110 | wget https://raw.githubusercontent.com/pforret/bash-boilerplate/master/script.sh 111 | mv script.sh my-new-script.sh 112 | 113 | #### Option 4: customize parameters and copy/paste 114 | 115 | go to [toolstud.io/data/bash.php](https://toolstud.io/data/bash.php) 116 | 117 | ### EXAMPLES 118 | 119 | These scripts were made with some version of [bash-boilerplate](https://github.com/pforret/bash-boilerplate) 120 | 121 | * [github.com/pforret/crontask](https://github.com/pforret/crontask) 122 | * [github.com/pforret/networkcheck](https://github.com/pforret/networkcheck) 123 | * [github.com/cinemapub/signage_prep](https://github.com/cinemapub/signage_prep) 124 | * send me your example repos! 125 | 126 | ### ACKNOWLEDGEMENTS 127 | 128 | I learned a lot of tips from these sources: 129 | 130 | * Daniel Mills, [e36freak](https://github.com/e36freak) 131 | * Kfir Lavi [www.kfirlavi.com](http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming) 132 | * Aaron Maxwell [redsymbol.net](http://redsymbol.net/articles/unofficial-bash-strict-mode/) 133 | * Bash Variables [gnu.org](https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html) 134 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | 1.7.1 2 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/bash-boilerplate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pforret/bash-boilerplate/ea9a87dbdb8e50daff23f18c88a2d1782b34823c/docs/bash-boilerplate.jpg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ![Bash CI](https://github.com/pforret/bash-boilerplate/workflows/Bash%20CI/badge.svg) 2 | ![Shellcheck CI](https://github.com/pforret/bash-boilerplate/workflows/Shellcheck%20CI/badge.svg) 3 | ![version](https://img.shields.io/github/v/release/pforret/bash-boilerplate?include_prereleases) 4 | ![activity](https://img.shields.io/github/commit-activity/y/pforret/bash-boilerplate) 5 | ![license](https://img.shields.io/github/license/pforret/bash-boilerplate) 6 | ![repo size](https://img.shields.io/github/repo-size/pforret/bash-boilerplate) 7 | 8 | ### BASH BOILERPLATE 9 | 10 | It's like a mini console framework for bash shell scripting. 11 | 12 | Just use one of 4 methods to generate a new script, that has all the functionality to 13 | 14 | 1. one self-contained file, no external dependencies 15 | 2. parse options and parameters 16 | 3. generate clean usage 17 | 4. run in silent/quiet or verbose mode 18 | 5. create and clean up temporary folder/files 19 | 6. better error reporting 20 | 7. Bash CI (Github Actions) 21 | 8. self-initialisation for new scripts (`script.sh init`) 22 | 23 | flag|h|help|show usage 24 | flag|q|quiet|no output 25 | flag|v|verbose|output more 26 | flag|f|force|do not ask for confirmation 27 | option|l|logd|folder for log files |log 28 | option|t|tmpd|folder for temp files|.tmp 29 | param|1|action|action to perform: LIST/TEST/... 30 | param|1|output|output file 31 | # there can only be 1 param|n and it should be the last 32 | param|n|inputs|input files 33 | 34 | becomes 35 | 36 | ### USAGE 37 | Program: script.sh by @email 38 | Version: @version (L:591-MD:6523f7) 39 | Updated: Jul 31 20:07:24 2020 40 | Usage: script.sh [-h] [-q] [-v] [-f] [-l ] [-t ] 41 | Flags, options and parameters: 42 | -h|--help : [flag] show usage [default: off] 43 | -q|--quiet : [flag] no output [default: off] 44 | -v|--verbose : [flag] output more [default: off] 45 | -f|--force : [flag] do not ask for confirmation (always yes) [default: off] 46 | -l|--logd : [optn] folder for log files [default: log] 47 | -t|--tmpd : [optn] folder for temp files [default: .tmp] 48 | : [parameter] action to perform: init/list/test/... 49 | : [parameter] output file 50 | : [parameters] input files (1 or more) 51 | 52 | ### SCRIPT AUTHORING TIPS 53 | * use out to show any kind of output, except when option --quiet is specified 54 | out "User is [$USER]" 55 | * use progress to show one line of progress that will be overwritten by the next output 56 | progress "Now generating file $nb of $total ..." 57 | * use is_empty and is_not_empty to test for variables 58 | if is_empty "$email" ; then ; echo "Need Email!" ; fi 59 | * use die to show error message and exit program 60 | if [[ ! -f $output ]] ; then ; die "could not create output" ; fi 61 | * use alert to show alert message but continue 62 | if [[ ! -f $output ]] ; then ; alert "could not create output" ; fi 63 | * use success to show success message but continue 64 | if [[ -f $output ]] ; then ; success "output was created!" ; fi 65 | * use announce to show the start of a task 66 | announce "now generating the reports" 67 | * use log to show information that will only be visible when -v is specified 68 | log "input file: [$inputname] - [$inputsize] MB" 69 | * use escape to extra escape '/' paths in regex 70 | sed 's/$(escape $path)//g' 71 | * use lcase and ucase to convert to upper/lower case 72 | param=$(lcase $param) 73 | * use confirm for interactive confirmation before doing something 74 | if ! confirm "Delete file"; then ; echo "skip deletion" ; fi 75 | * use ask for interactive setting of variables 76 | ask NAME "What is your name" "Peter" 77 | * use on_mac/on_linux/'on_32bit'/'on_64bit' to only run things on certain platforms 78 | on_mac && log "Running on MacOS" 79 | * use folder_prep to create a folder if needed and otherwise clean up old files 80 | folder_prep "$logd" 7 # delete all files olders than 7 days 81 | 82 | ### VERSION HISTORY 83 | 84 | * v1.7: all editable content to the front of the file 85 | * v1.6: introduce semver versioning, Bash CI 86 | * v1.5: fixed last shellcheck warnings - https://github.com/koalaman/shellcheck 87 | * v1.4: fix md5sum problem, add script authoring tips, automated README creation 88 | * v1.3: robuster parameter parsing 89 | * v1.2: better error trap and versioning info 90 | * v1.1: better single and multi param parsing 91 | * v1.0: first release 92 | 93 | 94 | ### CREATE NEW BASH SCRIPT 95 | 96 | #### Option 1: clone this repo 97 | 98 | git clone https://github.com/pforret/bash-boilerplate.git 99 | cp bash-boilerplate/script.sh my-new-script.sh 100 | 101 | #### Option 2: create new Github repo from template 102 | 103 | go to https://github.com/pforret/bash-boilerplate - press "Use this template" 104 | 105 | #### Option 3: download the script directly 106 | 107 | wget https://raw.githubusercontent.com/pforret/bash-boilerplate/master/script.sh 108 | mv script.sh my-new-script.sh 109 | 110 | #### Option 4: customize parameters and copy/paste 111 | 112 | go to [toolstud.io/data/bash.php](https://toolstud.io/data/bash.php) 113 | 114 | ### EXAMPLES 115 | 116 | These scripts were made with some version of [bash-boilerplate](https://github.com/pforret/bash-boilerplate) 117 | 118 | * [github.com/pforret/crontask](https://github.com/pforret/crontask) 119 | * [github.com/pforret/networkcheck](https://github.com/pforret/networkcheck) 120 | * [github.com/cinemapub/signage_prep](https://github.com/cinemapub/signage_prep) 121 | * send me your example repos! 122 | 123 | ### ACKNOWLEDGEMENTS 124 | 125 | I learned a lot of tips from these sources: 126 | 127 | * Daniel Mills, [e36freak](https://github.com/e36freak) 128 | * Kfir Lavi [www.kfirlavi.com](http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming) 129 | * Aaron Maxwell [redsymbol.net](http://redsymbol.net/articles/unofficial-bash-strict-mode/) 130 | * Bash Variables [gnu.org](https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html) -------------------------------------------------------------------------------- /script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ============================================================================== 3 | ### SO HOW DO YOU PROCEED WITH YOUR SCRIPT? 4 | ### 1. run "script.sh init" 5 | ### 2. define the options/parameters and defaults you need in list_options() 6 | ### 3. define functions your might need by changing/adding to perform_action1() 7 | ### 4. add binaries your script needs (e.g. ffmpeg) to verify_programs awk (...) wc 8 | ### 5. implement the different actions you defined in 2. in main() 9 | ### ============================================================================== 10 | 11 | readonly PROGVERS="@version" 12 | readonly PROGAUTH="@email" 13 | # uncomment next line to have time prefix for every output line 14 | #prefix_fmt='+%H:%M:%S | ' 15 | readonly prefix_fmt="" 16 | 17 | # runasroot = 0 :: don't check anything 18 | # runasroot = 1 :: script MUST run as root 19 | # runasroot = -1 :: script MAY NOT run as root 20 | runasroot=-1 21 | 22 | list_options() { 23 | ### Change the next lines to reflect which flags/options/parameters you need 24 | ### flag: switch a flag 'on' / no extra parameter / e.g. "-v" for verbose 25 | ### flag|||| 26 | ### option: set an option value / 1 extra parameter / e.g. "-l error.log" for logging to file 27 | ### option|||| 28 | ### param: comes after the options 29 | ### param||| 30 | ### where = 1 for single parameters or = n for (last) parameter that can be a list 31 | echo -n " 32 | #commented lines will be filtered 33 | flag|h|help|show usage 34 | flag|q|quiet|no output 35 | flag|v|verbose|output more 36 | flag|f|force|do not ask for confirmation (always yes) 37 | option|l|logd|folder for log files |log 38 | option|t|tmpd|folder for temp files|.tmp 39 | #you could also use /tmp/$PROGNAME as the default temp folder 40 | #option|u|user|USER to use|$USER 41 | #secret|p|pass|password to use 42 | param|1|action|action to perform: init/list/test/... 43 | # there can only be 1 param|n and it should be the last 44 | param|1|output|output file 45 | param|n|inputs|input files 46 | " | grep -v '^#' 47 | } 48 | 49 | ## Put your helper scripts here 50 | 51 | perform_action1(){ 52 | OUTPUT="$1" 53 | shift 54 | echo INPUTS = "$*" 55 | echo OUTPUT = "$OUTPUT" 56 | # < "$1" do_stuff > "$2" 57 | } 58 | 59 | perform_action2(){ 60 | OUTPUT="$1" 61 | shift 62 | echo INPUTS = "$*" 63 | echo OUTPUT = "$OUTPUT" 64 | # < "$1" do_stuff > "$2" 65 | } 66 | 67 | ##################################################################### 68 | ## Put your main script here 69 | ##################################################################### 70 | 71 | main() { 72 | log "Program: $PROGFNAME $PROGVERS ($PROGUUID)" 73 | log "Updated: $PROGDATE" 74 | log "Run as : $USER@$HOSTNAME" 75 | # add programs you need in your script here, like tar, wget, ffmpeg, rsync ... 76 | verify_programs awk basename cut date dirname find grep head mkdir sed stat tput uname wc 77 | prep_log_and_temp_dir 78 | 79 | action=$(lcase "$action") 80 | case $action in 81 | init ) 82 | create_script_from_template 83 | ;; 84 | 85 | test ) 86 | run_tests 87 | ;; 88 | 89 | action2 ) 90 | #perform_action2 "$output" $inputs 91 | ;; 92 | 93 | *) 94 | die "param [$action] not recognized" 95 | esac 96 | } 97 | 98 | ##################################################################### 99 | ################### DO NOT MODIFY BELOW THIS LINE ################### 100 | 101 | # set strict mode - via http://redsymbol.net/articles/unofficial-bash-strict-mode/ 102 | # removed -e because it made basic [[ testing ]] difficult 103 | set -uo pipefail 104 | IFS=$'\n\t' 105 | hash(){ 106 | if [[ -n $(which md5sum) ]] ; then 107 | # regular linux 108 | md5sum | cut -c1-6 109 | else 110 | # macos 111 | md5 | cut -c1-6 112 | fi 113 | } 114 | 115 | # change program version to your own release logic 116 | readonly PROGNAME=$(basename "$0" .sh) 117 | readonly PROGFNAME=$(basename "$0") 118 | readonly PROGDIRREL=$(dirname "$0") 119 | if [[ -z "$PROGDIRREL" ]] ; then 120 | # script called without path specified ; must be in $PATH somewhere 121 | readonly PROGFULLPATH=$(which "$0") 122 | readonly PROGDIR=$(dirname "$PROGFULLPATH") 123 | else 124 | readonly PROGDIR=$(cd "$PROGDIRREL" && pwd) 125 | readonly PROGFULLPATH="$PROGDIR/$PROGFNAME" 126 | fi 127 | readonly PROGLINES=$(< "$PROGFULLPATH" awk 'END {print NR}') 128 | readonly PROGHASH=$(< "$PROGFULLPATH" hash) 129 | readonly PROGUUID="L:${PROGLINES}-MD:${PROGHASH}" 130 | # this is version of bash-boilerplate - replace by versioning of your script; start at 1.0.0 131 | readonly TODAY=$(date "+%Y-%m-%d") 132 | readonly PROGIDEN="«${PROGNAME} ${PROGVERS}»" 133 | [[ -z "${TEMP:-}" ]] && TEMP=/tmp 134 | 135 | PROGDATE="??" 136 | os_uname=$(uname -s) 137 | [[ "$os_uname" = "Linux" ]] && PROGDATE=$(stat -c %y "$0" 2>/dev/null | cut -c1-16) # generic linux 138 | [[ "$os_uname" = "Darwin" ]] && PROGDATE=$(stat -f "%Sm" "$0" 2>/dev/null) # for MacOS 139 | 140 | verbose=0 141 | quiet=0 142 | piped=0 143 | force=0 144 | help=0 145 | tmpd="$TEMP/$PROGNAME" 146 | logd="./log" 147 | 148 | [[ $# -gt 0 ]] && [[ $1 == "-v" ]] && verbose=1 149 | #to enable verbose even for option parsing 150 | 151 | [[ -t 1 ]] && piped=0 || piped=1 # detect if out put is piped 152 | [[ $(echo -e '\xe2\x82\xac') == '€' ]] && unicode=1 || unicode=0 # detect if unicode is supported 153 | 154 | # Defaults 155 | 156 | if [[ $piped -eq 0 ]] ; then 157 | readonly col_reset="\033[0m" 158 | readonly col_red="\033[1;31m" 159 | readonly col_grn="\033[1;32m" 160 | readonly col_ylw="\033[1;33m" 161 | else 162 | # no colors for piped content 163 | readonly col_reset="" 164 | readonly col_red="" 165 | readonly col_grn="" 166 | readonly col_ylw="" 167 | fi 168 | 169 | if [[ $unicode -gt 0 ]] ; then 170 | readonly char_succ="✔" 171 | readonly char_fail="✖" 172 | readonly char_alrt="➨" 173 | readonly char_wait="…" 174 | else 175 | # no unicode chars if not supported 176 | readonly char_succ="OK " 177 | readonly char_fail="!! " 178 | readonly char_alrt="?? " 179 | readonly char_wait="..." 180 | fi 181 | 182 | readonly nbcols=$(tput cols || echo 80) 183 | readonly wprogress=$((nbcols - 5)) 184 | #readonly nbrows=$(tput lines) 185 | 186 | tmpfile="" 187 | logfile="" 188 | 189 | out() { 190 | ((quiet)) && return 191 | local message="$*" 192 | local prefix="" 193 | if is_not_empty "$prefix_fmt" ; then 194 | prefix=$(date "$prefix_fmt") 195 | fi 196 | printf '%b\n' "$prefix$message"; 197 | } 198 | #TIP: use «out» to show any kind of output, except when option --quiet is specified 199 | #TIP:> out "User is [$USER]" 200 | 201 | progress() { 202 | ((quiet)) && return 203 | local message="$*" 204 | if ((piped)); then 205 | printf '%b\n' "$message"; 206 | # \r makes no sense in file or pipe 207 | else 208 | printf "... %-${wprogress}b\r" "$message "; 209 | # next line will overwrite this line 210 | fi 211 | } 212 | #TIP: use «progress» to show one line of progress that will be overwritten by the next output 213 | #TIP:> progress "Now generating file $nb of $total ..." 214 | 215 | error_prefix="${col_red}>${col_reset}" 216 | trap "die \"ERROR \$? after \$SECONDS seconds \n\ 217 | \${error_prefix} last command : '\$BASH_COMMAND' \" \ 218 | \$(< \$PROGFULLPATH awk -v lineno=\$LINENO \ 219 | 'NR == lineno {print \"\${error_prefix} from line \" lineno \" : \" \$0}')" INT TERM EXIT 220 | # cf https://askubuntu.com/questions/513932/what-is-the-bash-command-variable-good-for 221 | # trap 'echo ‘$BASH_COMMAND’ failed with error code $?' ERR 222 | safe_exit() { 223 | [[ -n "$tmpfile" ]] && [[ -f "$tmpfile" ]] && rm "$tmpfile" 224 | trap - INT TERM EXIT 225 | exit 0 226 | } 227 | 228 | is_set() { [[ "$1" -gt 0 ]]; } 229 | is_empty() { [[ -z "$1" ]] ; } 230 | is_not_empty() { [[ -n "$1" ]] ; } 231 | #TIP: use «is_empty» and «is_not_empty» to test for variables 232 | #TIP:> if is_empty "$email" ; then ; echo "Need Email!" ; fi 233 | 234 | is_file() { [[ -f "$1" ]] ; } 235 | is_dir() { [[ -d "$1" ]] ; } 236 | 237 | 238 | die() { tput bel; out "${col_red}${char_fail} $PROGIDEN${col_reset}: $*" >&2; safe_exit; } 239 | fail() { tput bel; out "${col_red}${char_fail} $PROGIDEN${col_reset}: $*" >&2; safe_exit; } 240 | #TIP: use «die» to show error message and exit program 241 | #TIP:> if [[ ! -f $output ]] ; then ; die "could not create output" ; fi 242 | 243 | alert() { out "${col_red}${char_alrt}${col_reset}: $*" >&2 ; } # print error and continue 244 | #TIP: use «alert» to show alert message but continue 245 | #TIP:> if [[ ! -f $output ]] ; then ; alert "could not create output" ; fi 246 | 247 | success() { out "${col_grn}${char_succ}${col_reset} $*"; } 248 | #TIP: use «success» to show success message but continue 249 | #TIP:> if [[ -f $output ]] ; then ; success "output was created!" ; fi 250 | 251 | announce(){ out "${col_grn}${char_wait}${col_reset} $*"; sleep 1 ; } 252 | #TIP: use «announce» to show the start of a task 253 | #TIP:> announce "now generating the reports" 254 | 255 | log() { is_set $verbose && out "${col_ylw}# $* ${col_reset}" ; } 256 | debug() { is_set $verbose && out "${col_ylw}# $* ${col_reset}" ; } 257 | #TIP: use «log» to show information that will only be visible when -v is specified 258 | #TIP:> log "input file: [$inputname] - [$inputsize] MB" 259 | 260 | escape() { echo "$*" | sed 's/\//\\\//g' ; } 261 | #TIP: use «escape» to extra escape '/' paths in regex 262 | #TIP:> sed 's/$(escape $path)//g' 263 | 264 | lcase() { echo "$*" | awk '{print tolower($0)}' ; } 265 | ucase() { echo "$*" | awk '{print toupper($0)}' ; } 266 | #TIP: use «lcase» and «ucase» to convert to upper/lower case 267 | #TIP:> param=$(lcase $param) 268 | 269 | confirm() { is_set $force && return 0; read -r -p "$1 [y/N] " -n 1; echo " "; [[ $REPLY =~ ^[Yy]$ ]];} 270 | #TIP: use «confirm» for interactive confirmation before doing something 271 | #TIP:> if ! confirm "Delete file"; then ; echo "skip deletion" ; fi 272 | 273 | ask() { 274 | # $1 = variable name 275 | # $2 = question 276 | # $3 = default value 277 | # not using read -i because that doesn't work on MacOS 278 | local ANSWER 279 | read -r -p "$2 ($3) > " ANSWER 280 | if [[ -z "$ANSWER" ]] ; then 281 | eval "$1=\"$3\"" 282 | else 283 | eval "$1=\"$ANSWER\"" 284 | fi 285 | } 286 | #TIP: use «ask» for interactive setting of variables 287 | #TIP:> ask NAME "What is your name" "Peter" 288 | 289 | 290 | os_uname=$(uname -s) 291 | os_bits=$(uname -m) 292 | os_version=$(uname -v) 293 | 294 | on_mac() { [[ "$os_uname" = "Darwin" ]] ; } 295 | on_linux() { [[ "$os_uname" = "Linux" ]] ; } 296 | 297 | on_32bit() { [[ "$os_bits" = "i386" ]] ; } 298 | on_64bit() { [[ "$os_bits" = "x86_64" ]] ; } 299 | #TIP: use «on_mac»/«on_linux»/'on_32bit'/'on_64bit' to only run things on certain platforms 300 | #TIP:> on_mac && log "Running on MacOS" 301 | 302 | usage() { 303 | out "Program: ${col_grn}$PROGFNAME${col_reset} by ${col_ylw}$PROGAUTH${col_reset}" 304 | out "Version: ${col_grn}$PROGVERS${col_reset} (${col_ylw}$PROGUUID${col_reset})" 305 | out "Updated: ${col_grn}$PROGDATE${col_reset}" 306 | 307 | echo -n "Usage: $PROGFNAME" 308 | list_options \ 309 | | awk ' 310 | BEGIN { FS="|"; OFS=" "; oneline="" ; fulltext="Flags, options and parameters:"} 311 | $1 ~ /flag/ { 312 | fulltext = fulltext sprintf("\n -%1s|--%-10s: [flag] %s [default: off]",$2,$3,$4) ; 313 | oneline = oneline " [-" $2 "]" 314 | } 315 | $1 ~ /option/ { 316 | fulltext = fulltext sprintf("\n -%1s|--%s <%s>: [optn] %s",$2,$3,"val",$4) ; 317 | if($5!=""){fulltext = fulltext " [default: " $5 "]"; } 318 | oneline = oneline " [-" $2 " <" $3 ">]" 319 | } 320 | $1 ~ /secret/ { 321 | fulltext = fulltext sprintf("\n -%1s|--%s <%s>: [secr] %s",$2,$3,"val",$4) ; 322 | oneline = oneline " [-" $2 " <" $3 ">]" 323 | } 324 | $1 ~ /param/ { 325 | if($2 == "1"){ 326 | fulltext = fulltext sprintf("\n %-10s: [parameter] %s","<"$3">",$4); 327 | oneline = oneline " <" $3 ">" 328 | } else { 329 | fulltext = fulltext sprintf("\n %-10s: [parameters] %s (1 or more)","<"$3">",$4); 330 | oneline = oneline " <" $3 " …>" 331 | } 332 | } 333 | END {print oneline; print fulltext} 334 | ' 335 | } 336 | 337 | tips(){ 338 | < "$0" grep -v "\$0" \ 339 | | awk " 340 | /TIP: / {\$1=\"\"; gsub(/«/,\"$col_grn\"); gsub(/»/,\"$col_reset\"); print \"*\" \$0} 341 | /TIP:> / {\$1=\"\"; print \" $col_ylw\" \$0 \"$col_reset\"} 342 | " 343 | } 344 | 345 | init_options() { 346 | local init_command 347 | init_command=$(list_options \ 348 | | awk ' 349 | BEGIN { FS="|"; OFS=" ";} 350 | $1 ~ /flag/ && $5 == "" {print $3"=0; "} 351 | $1 ~ /flag/ && $5 != "" {print $3"="$5"; "} 352 | $1 ~ /option/ && $5 == "" {print $3"=\" \"; "} 353 | $1 ~ /option/ && $5 != "" {print $3"="$5"; "} 354 | ') 355 | if [[ -n "$init_command" ]] ; then 356 | #log "init_options: $(echo "$init_command" | wc -l) options/flags initialised" 357 | eval "$init_command" 358 | fi 359 | } 360 | 361 | run_only_show_errors(){ 362 | tmpfile=$(mktemp) 363 | if ( "$@" ) 2>> "$tmpfile" >> "$tmpfile" ; then 364 | #all OK 365 | rm "$tmpfile" 366 | return 0 367 | else 368 | alert "[$*] gave an error" 369 | cat "$tmpfile" 370 | rm "$tmpfile" 371 | return 255 372 | fi 373 | } 374 | 375 | verify_programs(){ 376 | log "Running: on $os_uname ($os_version)" 377 | list_programs=$(echo "$*" | sort -u | tr "\n" " ") 378 | hash_programs=$(echo "$list_programs" | hash) 379 | verify_cache="$PROGDIR/.$PROGNAME.$hash_programs.verified" 380 | if [[ -f "$verify_cache" ]] ; then 381 | log "Verify : $list_programs (cached)" 382 | else 383 | log "Verify : $list_programs" 384 | programs_ok=1 385 | for prog in "$@" ; do 386 | if [[ -z $(which "$prog") ]] ; then 387 | alert "$PROGIDEN needs [$prog] but this program cannot be found on this $os_uname machine" 388 | programs_ok=0 389 | fi 390 | done 391 | if [[ $programs_ok -eq 1 ]] ; then 392 | ( 393 | echo "$PROGNAME: check required programs OK" 394 | echo "$list_programs" 395 | date 396 | ) > "$verify_cache" 397 | fi 398 | fi 399 | } 400 | 401 | folder_prep(){ 402 | if [[ -n "$1" ]] ; then 403 | local folder="$1" 404 | local maxdays=365 405 | if [[ -n "$2" ]] ; then 406 | maxdays=$2 407 | fi 408 | if [ ! -d "$folder" ] ; then 409 | log "Create folder [$folder]" 410 | mkdir "$folder" 411 | else 412 | log "Cleanup: [$folder] - delete files older than $maxdays day(s)" 413 | find "$folder" -mtime "+$maxdays" -type f -exec rm {} \; 414 | fi 415 | fi 416 | } 417 | #TIP: use «folder_prep» to create a folder if needed and otherwise clean up old files 418 | #TIP:> folder_prep "$logd" 7 # delete all files olders than 7 days 419 | 420 | expects_single_params(){ 421 | list_options | grep 'param|1|' > /dev/null 422 | } 423 | 424 | expects_multi_param(){ 425 | list_options | grep 'param|n|' > /dev/null 426 | } 427 | 428 | parse_options() { 429 | if [[ $# -eq 0 ]] ; then 430 | usage >&2 ; safe_exit 431 | fi 432 | 433 | ## first process all the -x --xxxx flags and options 434 | #set -x 435 | while true; do 436 | # flag is savec as $flag = 0/1 437 | # option