├── .githooks └── pre-commit ├── .github └── workflows │ ├── bash_unit.yml │ └── shellcheck.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION.md ├── progressbar ├── progressbar.jpg ├── progressbar.sh ├── terminalizer ├── config.yml ├── operation_that_takes_10_seconds.sh ├── progressbar.gif ├── progressbar_usage.yml └── script.txt └── tests ├── bash_unit └── test_script.sh /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | if git rev-parse --verify HEAD >/dev/null 2>&1 10 | then 11 | against=HEAD 12 | else 13 | # Initial commit: diff against an empty tree object 14 | against=$(git hash-object -t tree /dev/null) 15 | fi 16 | 17 | # Redirect output to stderr. 18 | exec 1>&2 19 | 20 | root_folder=$(git rev-parse --show-toplevel) 21 | 22 | tests/bash_unit tests/*.sh 23 | 24 | shellcheck "$root_folder"/*.sh 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/bash_unit.yml: -------------------------------------------------------------------------------- 1 | name: bash_unit CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | ubuntu: 10 | runs-on: ubuntu-latest 11 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Unit testing with bash_unit 17 | run: tests/bash_unit tests/test_* 18 | 19 | macos: 20 | runs-on: macos-latest 21 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: install gawk 27 | run: brew install gawk 28 | 29 | - name: Unit testing with bash_unit 30 | run: tests/bash_unit tests/test_* 31 | -------------------------------------------------------------------------------- /.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 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Check for code quality errors 17 | run: ls ./*.sh | xargs shellcheck 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmp 2 | .env 3 | .idea 4 | .tmp/* 5 | log/* 6 | tests/.tmp 7 | tests/log/* 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ![Changelog v1.0.0](https://img.shields.io/badge/CHANGELOG-v1.0.0-orange) 2 | # CHANGELOG 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | - work on new features 10 | 11 | ## [0.0.1] - 2020-12-01 12 | ### Added 13 | - create progressbar with [pforret/bashew](https://github.com/pforret/bashew) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 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 | ![GH stars](https://img.shields.io/github/stars/pforret/progressbar) 2 | ![GH tag](https://img.shields.io/github/v/tag/pforret/progressbar) 3 | ![Shellcheck CI](https://github.com/pforret/progressbar/workflows/Shellcheck%20CI/badge.svg) 4 | ![bash_unit CI](https://github.com/pforret/progressbar/workflows/bash_unit%20CI/badge.svg) 5 | ![GH Language](https://img.shields.io/github/languages/top/pforret/progressbar) 6 | ![GH License](https://img.shields.io/github/license/pforret/progressbar) 7 | [![basher install](https://img.shields.io/badge/basher-install-white?logo=gnu-bash&style=flat)](https://basher.gitparade.com/package/) 8 | 9 | 10 | # pforret/progressbar 11 | 12 | ![test](progressbar.jpg) 13 | 14 | Show a CLI progress bar for long-running programs, like rsync, ffmpeg, tar, zip, wget, ... or your own script 15 | 16 | **Add a progressbar to _anything_** 17 | 18 | ## Installation 19 | 20 | with [basher](https://github.com/basherpm/basher) 21 | 22 | $ basher install pforret/progressbar 23 | 24 | or with `git` 25 | 26 | ```bash 27 | $ git clone https://github.com/pforret/progressbar.git 28 | $ cd progressbar 29 | $ sudo ln -s $(pwd)/progressbar /usr/local/bin/ # or someone else in your path 30 | ``` 31 | 32 | ## Usage 33 | 34 | Program: progressbar 1.1.0 by peter@forret.com 35 | Updated: Dec 1 14:39:22 2020 36 | Usage: progressbar [-h] [-q] [-v] [-f] [-l ] [-t ] [-b ] [-c ] 37 | Flags, options and parameters: 38 | -h|--help : [flag] show usage [default: off] 39 | -q|--quiet : [flag] no output [default: off] 40 | -v|--verbose : [flag] output more [default: off] 41 | -f|--force : [flag] do not ask for confirmation (always yes) [default: off] 42 | -l|--log_dir : [optn] folder for log files [default: log] 43 | -t|--tmp_dir : [optn] folder for temp files [default: /tmp/progressbar] 44 | -b|--bar : [optn] format of bar: normal/half/long/short [default: normal] 45 | -c|--char : [optn] character to use a filler [default: #] 46 | : [parameter] lines/seconds/clear/check 47 | : [parameter] input number or operation identifier 48 | 49 | ## Examples 50 | 51 | ![Demo](terminalizer/progressbar.gif) 52 | 53 | ### Simple use: # lines or seconds is known 54 | ```bash 55 | # when the approx number of lines output is known 56 | # e.g. first do a rsync --dry-run to check how many files have to be trasferred 57 | # and then use this number in the actual operation 58 | $ expected_lines=$(rsync --dry-run | awk 'END {print NR}') 59 | $ rsync | progressbar lines "$expected_lines" 60 | 61 | # similarly: when the approx number of seconds is known 62 | $ | progressbar seconds 3600 63 | ``` 64 | 65 | ### Auto-estimate lines/seconds 66 | ```bash 67 | # the first time the script learns the expected # lines/seconds for operation '40-pings-to-google' 68 | $ ping -c 40 www.google.com | progressbar lines 40-pings-to-google 69 | 45 lines / 39 secs … 70 | 71 | # the following times, it can use this information to show a 0-100% progressbar 72 | $ ping -c 40 www.google.com | progressbar lines 40-pings-to-google 73 | [############################--3---------4---------5---------6---------7---------8---------9--------] 28% / 11 secs … 74 | 75 | # can also be used with different progress bar format (here: short) 76 | $ ping -c 40 www.google.com | progressbar -b short lines 40-pings-to-google 77 | [##-------] 28% / 11 secs … 78 | 79 | # use '|' as character for 'done' time 80 | $ ping -c 40 www.google.com | progressbar -c '|' lines 40-pings-to-google 81 | [|||||||||||||||||||||||||||||||||||-----4---------5---------6---------7---------8---------9--------] 35% / 14 secs … 82 | 83 | 84 | # for instance, to use it with rsync, first do a --dry-run (rather fast) to get the estimated # of lines 85 | $ rsync --dry-run -avz source/ destination/ | progressbar lines long-rsync-operation 86 | 87 | # and then run the actual operation, which will take much longer, but will output +- the same # of lines 88 | $ rsync -avz source/ destination/ | progressbar lines long-rsync-operation 89 | 90 | ``` 91 | 92 | 93 | ## Acknowledgements 94 | 95 | * script created with [bashew](https://github.com/pforret/bashew) 96 | 97 | © 2020 Peter Forret 98 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | 1.1.2 2 | -------------------------------------------------------------------------------- /progressbar: -------------------------------------------------------------------------------- 1 | ./progressbar.sh -------------------------------------------------------------------------------- /progressbar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pforret/progressbar/9fdf72c198292775d36c8cc638e7af3f45c4e24b/progressbar.jpg -------------------------------------------------------------------------------- /progressbar.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### Created by Peter Forret ( pforret ) on 2020-12-01 3 | script_version="0.0.0" # if there is a VERSION.md in this script's folder, it will take priority for version number 4 | readonly script_author="peter@forret.com" 5 | readonly script_created="2020-12-01" 6 | readonly run_as_root=-1 # run_as_root: 0 = don't check anything / 1 = script MUST run as root / -1 = script MAY NOT run as root 7 | 8 | list_options() { 9 | echo -n " 10 | #commented lines will be filtered 11 | flag|h|help|show usage 12 | flag|q|quiet|no output 13 | flag|v|verbose|output more 14 | flag|f|force|do not ask for confirmation (always yes) 15 | option|l|log_dir|folder for log files |log 16 | option|t|tmp_dir|folder for temp files|/tmp/$script_prefix 17 | #option|i|infile|file with source data (to calculate MB/s)| 18 | #option|o|outfile|file with generated data (to calculate MB/s)| 19 | option|b|bar|format of bar: normal/half/long/short|normal 20 | option|c|char|character to use a filler|# 21 | param|1|action|lines/seconds/clear/check 22 | param|1|input|input number or operation identifier 23 | " | grep -v '^#' 24 | } 25 | 26 | ##################################################################### 27 | ## Put your main script here 28 | ##################################################################### 29 | 30 | main() { 31 | log "Program: $script_basename $script_version" 32 | log "Created: $script_created" 33 | log "Updated: $script_modified" 34 | log "Run as : $USER@$HOSTNAME" 35 | # add programs that need to be installed, like: tar, wget, ffmpeg, rsync, convert, curl ... 36 | require_binaries tput uname gawk 37 | 38 | if [[ ${piped:-0} -gt 0 ]] ; then 39 | eol="\n" 40 | else 41 | eol="\r" 42 | fi 43 | action=$(lower_case "$action") 44 | # shellcheck disable=SC2154 45 | if is_number "$input" ; then 46 | # $1 is # lines/seconds expected 47 | cache_file="" 48 | else 49 | # $1 is process identifier 50 | # shellcheck disable=SC2154 51 | cache_file="$tmp_dir/$(echo "$input" | hash 12).result.txt" 52 | log "Cache file = [$cache_file]" 53 | fi 54 | case $action in 55 | lines ) 56 | #TIP: use «progressbar lines » to show the progress bar when you can estimate the number of lines that will be sent to stdin 57 | #TIP:> progressbar lines 14550 58 | #TIP: use «progressbar lines » to show the progress bar based on the number of lines it generated before 59 | #TIP:> progressbar lines export-of-all-dbs 60 | # shellcheck disable=SC2154 61 | progress_lines "$input" 62 | ;; 63 | 64 | seconds|time ) 65 | #TIP: use «progressbar seconds » to show the progress bar when you can estimate the number of seconds it will typically take 66 | #TIP:> if ! confirm "Delete file"; then ; echo "skip deletion" ; fi 67 | #TIP: use «progressbar seconds » to show the progress bar based on the number of seconds it took before 68 | #TIP:> progressbar seconds export-of-all-dbs 69 | progress_seconds "$input" 70 | ;; 71 | 72 | clear|erase ) 73 | #TIP: use «progressbar clear » to clear the cached results of a process 74 | #TIP:> progressbar clear export-of-all-dbs 75 | if is_number "$input" ; then 76 | alert "[$script_basename $action] only makes sense if [$input] is not a numeric parameter" 77 | else 78 | if [[ -f "$cache_file" ]] ; then 79 | log "clear cache file [$cache_file]" 80 | rm "$cache_file" 81 | fi 82 | fi 83 | ;; 84 | 85 | check|stats|show ) 86 | #TIP: use «progressbar check » to show the cached results of a process 87 | #TIP:> progressbar check export-of-all-dbs 88 | if is_number "$input" ; then 89 | alert "[$script_basename $action] only makes sense if [$input] is not a numeric parameter" 90 | else 91 | if [[ -f "$cache_file" ]] ; then 92 | out "The last time [$input] was measured the following stats were collected:" 93 | cat "$cache_file" 94 | else 95 | alert "No data cached for [$input]" 96 | fi 97 | fi 98 | ;; 99 | 100 | *) 101 | die "action [$action] not recognized" 102 | esac 103 | } 104 | 105 | ##################################################################### 106 | ## Put your helper scripts here 107 | ##################################################################### 108 | 109 | bar_format(){ 110 | # shellcheck disable=SC2154 111 | case $bar in 112 | none) echo "" ;; 113 | 10|short) echo "----------" ;; 114 | 20|medium) echo "--------------------" ;; 115 | 50|half) echo "0----1----2----3----4----5----6----7----8----9----!" ;; 116 | 60|spaces) echo " " ;; 117 | 100|normal) echo "0---------1---------2---------3---------4---------5---------6---------7---------8---------9---------" ;; 118 | 200|double) echo "0-------------------1-------------------2-------------------3-------------------4-------------------5-------------------6------------------7-------------------8-------------------9-------------------!" ;; 119 | *) echo "0----1----2----3----4----5----6----7----8----9----!" 120 | esac 121 | } 122 | 123 | progress_lines(){ 124 | if is_number "$1" ; then 125 | # $1 is # lines input expected 126 | print_bar_lines "$1" 127 | else 128 | # $1 is process identifier 129 | # shellcheck disable=SC2154 130 | cache_file="$tmp_dir/$(echo "$1" | hash 12).result.txt" 131 | log "Cached: $cache_file" 132 | if [[ ! -f "$cache_file" ]] ; then 133 | # first time this runs, no idea about # lines 134 | showbar_unknown "$cache_file" 135 | else 136 | # take # lines of previous time 137 | lines=$(grep 'lines:' "$cache_file" | cut -d: -f2) 138 | log "Found in cache: $lines lines" 139 | [[ -z "$lines" ]] && lines=100 140 | print_bar_lines "$lines" "$cache_file" 141 | fi 142 | fi 143 | } 144 | 145 | progress_seconds(){ 146 | if is_number "$1" ; then 147 | # $1 is # lines input expected 148 | print_bar_seconds "$1" 149 | else 150 | # $1 is process identifier 151 | cache_file="$tmp_dir/$(echo "$1" | hash 12).result.txt" 152 | log "Cached: $cache_file" 153 | if [[ ! -f "$cache_file" ]] ; then 154 | # first time this runs, no idea about # seconds 155 | showbar_unknown "$cache_file" 156 | else 157 | # take # seconds of previous time 158 | lines=$(grep 'seconds:' "$cache_file" | cut -d: -f2) 159 | log "Found in cache: $lines seconds" 160 | [[ -z "$lines" ]] && lines=100 161 | print_bar_seconds "$lines" "$cache_file" 162 | fi 163 | fi 164 | } 165 | 166 | print_bar(){ 167 | lines="$1" 168 | cache=${2:-} 169 | update_every=${3:-1} 170 | # shellcheck disable=SC2154 171 | gawk \ 172 | -v full100="$(bar_format)" \ 173 | -v cache="$cache" \ 174 | -v expected_lines="$lines" \ 175 | -v eol="$eol" \ 176 | -v char="$char" \ 177 | -v quiet="$quiet" \ 178 | -v update_every="$update_every" ' 179 | function repeat(char,count) { 180 | var ="" 181 | while (count-->0) var = var char; 182 | return var; 183 | } 184 | BEGIN { 185 | len100=length(full100); 186 | fin100=substr(repeat(char,len100),1,len100); 187 | started_at=systime(); 188 | if(quiet < 1) printf("[%s] %d%% / %d secs … %s" , full100 , 0, 0, eol); 189 | } 190 | (NR % update_every) == 0 { 191 | percent=100*NR/expected_lines; 192 | width=int(len100*NR/expected_lines); 193 | if(width>len100){width=len100}; 194 | if(width<1){width=1}; 195 | keep=len100-width; 196 | seconds=systime()-started_at; 197 | partial=substr(fin100,1,width) substr(full100,width+1,keep) ; 198 | if(quiet < 1) printf("[%s] %d%% / %d secs … %s" , partial , percent, seconds, eol); 199 | fflush(); 200 | } 201 | END { 202 | if(quiet < 1) printf("\n"); 203 | seconds=systime()-started_at; 204 | if(length(cache)>3){ 205 | print "lines:" , NR > cache 206 | print "seconds:" , seconds >> cache 207 | } 208 | } 209 | ' 210 | } 211 | 212 | print_bar_lines(){ 213 | lines="$1" 214 | cache=${2:-} 215 | update_every=$(( lines / 250)) 216 | [[ $update_every -lt 1 ]] && update_every=1 217 | [[ $update_every -gt 1000 ]] && update_every=1000 218 | log "showbar lines: expect $lines line(s)" 219 | log "showbar lines: update every $update_every line(s)" 220 | print_bar "$lines" "$cache" "$update_every" 221 | } 222 | 223 | print_bar_seconds(){ 224 | seconds="$1" 225 | cache=${2:-} 226 | log "showbar seconds: expect $seconds seconds" 227 | seq 1 "$seconds" \ 228 | | gawk '{system("sleep 1"); print;}' \ 229 | | print_bar "$seconds" "$cache" 1 230 | } 231 | 232 | showbar_unknown(){ 233 | cache=${1:-} 234 | gawk \ 235 | -v quiet="$quiet" \ 236 | -v eol="$eol" \ 237 | -v cache="$cache" ' 238 | BEGIN { 239 | len100=length(full100); 240 | started_at=systime(); 241 | } 242 | { 243 | seconds=systime()-started_at; 244 | if(quiet < 1) printf("%d lines / %d secs … %s" , NR, seconds,eol); 245 | fflush(); 246 | } 247 | END { 248 | if(quiet < 1) printf("\n"); 249 | if(length(cache)>3){ 250 | print "lines:" , NR > cache 251 | print "seconds:" , systime()-started_at >> cache 252 | } 253 | } 254 | ' 255 | } 256 | ##################################################################### 257 | ################### DO NOT MODIFY BELOW THIS LINE ################### 258 | 259 | # set strict mode - via http://redsymbol.net/articles/unofficial-bash-strict-mode/ 260 | # removed -e because it made basic [[ testing ]] difficult 261 | set -uo pipefail 262 | IFS=$'\n\t' 263 | # shellcheck disable=SC2120 264 | hash(){ 265 | length=${1:-6} 266 | # shellcheck disable=SC2230 267 | if [[ -n $(which md5sum) ]] ; then 268 | # regular linux 269 | md5sum | cut -c1-"$length" 270 | else 271 | # macos 272 | md5 | cut -c1-"$length" 273 | fi 274 | } 275 | 276 | script_modified="??" 277 | os_name=$(uname -s) 278 | 279 | force=0 280 | help=0 281 | 282 | ## ----------- TERMINAL OUTPUT STUFF 283 | 284 | [[ -t 1 ]] && piped=0 || piped=1 # detect if out put is piped 285 | verbose=0 286 | #to enable verbose even before option parsing 287 | [[ $# -gt 0 ]] && [[ $1 == "-v" ]] && verbose=1 288 | quiet=0 289 | #to enable quiet even before option parsing 290 | [[ $# -gt 0 ]] && [[ $1 == "-q" ]] && quiet=1 291 | 292 | [[ $(echo -e '\xe2\x82\xac') == '€' ]] && unicode=1 || unicode=0 # detect if unicode is supported 293 | 294 | 295 | if [[ $piped -eq 0 ]] ; then 296 | col_reset="\033[0m" ; col_red="\033[1;31m" ; col_grn="\033[1;32m" ; col_ylw="\033[1;33m" 297 | else 298 | col_reset="" ; col_red="" ; col_grn="" ; col_ylw="" 299 | fi 300 | 301 | if [[ $unicode -gt 0 ]] ; then 302 | char_succ="✔" ; char_fail="✖" ; char_alrt="➨" ; char_wait="…" 303 | else 304 | char_succ="OK " ; char_fail="!! " ; char_alrt="?? " ; char_wait="..." 305 | fi 306 | 307 | readonly nbcols=$(tput cols 2>/dev/null || echo 80) 308 | readonly wprogress=$((nbcols - 5)) 309 | 310 | out() { ((quiet)) || printf '%b\n' "$*"; } 311 | 312 | progress() { 313 | ((quiet)) || ( 314 | if is_set ${piped:-0} ; then 315 | out "$*" 316 | else 317 | printf "... %-${wprogress}b\r" "$* "; 318 | fi 319 | ) 320 | } 321 | 322 | die() { tput bel 2>/dev/null ; out "${col_red}${char_fail} $script_basename${col_reset}: $*" >&2; safe_exit; } 323 | fail() { tput bel 2>/dev/null ; out "${col_red}${char_fail} $script_basename${col_reset}: $*" >&2; safe_exit; } 324 | alert() { out "${col_red}${char_alrt}${col_reset}: $*" >&2 ; } # print error and continue 325 | success() { out "${col_grn}${char_succ}${col_reset} $*" ; } 326 | announce(){ out "${col_grn}${char_wait}${col_reset} $*"; sleep 1 ; } 327 | log() { ((verbose)) && out "${col_ylw}# $* ${col_reset}" >&2 ; } 328 | lower_case() { echo "$*" | awk '{print tolower($0)}' ; } 329 | upper_case() { echo "$*" | awk '{print toupper($0)}' ; } 330 | confirm() { is_set $force && return 0; read -r -p "$1 [y/N] " -n 1; echo " "; [[ $REPLY =~ ^[Yy]$ ]];} 331 | ask() { 332 | # $1 = variable name 333 | # $2 = question 334 | # $3 = default value 335 | # not using read -i because that doesn't work on MacOS 336 | local ANSWER 337 | read -r -p "$2 ($3) > " ANSWER 338 | if [[ -z "$ANSWER" ]] ; then 339 | eval "$1=\"$3\"" 340 | else 341 | eval "$1=\"$ANSWER\"" 342 | fi 343 | } 344 | 345 | error_prefix="${col_red}>${col_reset}" 346 | trap "die \"ERROR \$? after \$SECONDS seconds \n\ 347 | \${error_prefix} last command : '\$BASH_COMMAND' \" \ 348 | \$(< \$script_install_path awk -v lineno=\$LINENO \ 349 | 'NR == lineno {print \"\${error_prefix} from line \" lineno \" : \" \$0}')" INT TERM EXIT 350 | # cf https://askubuntu.com/questions/513932/what-is-the-bash-command-variable-good-for 351 | # trap 'echo ‘$BASH_COMMAND’ failed with error code $?' ERR 352 | safe_exit() { 353 | [[ -n "${tmp_file:-}" ]] && [[ -f "$tmp_file" ]] && rm "$tmp_file" 354 | trap - INT TERM EXIT 355 | log "$script_basename finished after $SECONDS seconds" 356 | exit 0 357 | } 358 | 359 | is_set() { [[ "$1" -gt 0 ]]; } 360 | is_empty() { [[ -z "$1" ]] ; } 361 | is_not_empty() { [[ -n "$1" ]] ; } 362 | 363 | is_file() { [[ -f "$1" ]] ; } 364 | is_dir() { [[ -d "$1" ]] ; } 365 | 366 | is_number(){ local re='^[0-9]+$'; [[ "$1" =~ $re ]] ; } 367 | 368 | show_usage() { 369 | out "Program: ${col_grn}$script_basename $script_version${col_reset} by ${col_ylw}$script_author${col_reset}" 370 | out "Updated: ${col_grn}$script_modified${col_reset}" 371 | 372 | echo -n "Usage: $script_basename" 373 | list_options \ 374 | | awk ' 375 | BEGIN { FS="|"; OFS=" "; oneline="" ; fulltext="Flags, options and parameters:"} 376 | $1 ~ /flag/ { 377 | fulltext = fulltext sprintf("\n -%1s|--%-10s: [flag] %s [default: off]",$2,$3,$4) ; 378 | oneline = oneline " [-" $2 "]" 379 | } 380 | $1 ~ /option/ { 381 | fulltext = fulltext sprintf("\n -%1s|--%s <%s>: [optn] %s",$2,$3,"val",$4) ; 382 | if($5!=""){fulltext = fulltext " [default: " $5 "]"; } 383 | oneline = oneline " [-" $2 " <" $3 ">]" 384 | } 385 | $1 ~ /secret/ { 386 | fulltext = fulltext sprintf("\n -%1s|--%s <%s>: [secr] %s",$2,$3,"val",$4) ; 387 | oneline = oneline " [-" $2 " <" $3 ">]" 388 | } 389 | $1 ~ /param/ { 390 | if($2 == "1"){ 391 | fulltext = fulltext sprintf("\n %-10s: [parameter] %s","<"$3">",$4); 392 | oneline = oneline " <" $3 ">" 393 | } else { 394 | fulltext = fulltext sprintf("\n %-10s: [parameters] %s (1 or more)","<"$3">",$4); 395 | oneline = oneline " <" $3 " …>" 396 | } 397 | } 398 | END {print oneline; print fulltext} 399 | ' 400 | } 401 | 402 | show_tips(){ 403 | < "${BASH_SOURCE[0]}" grep -v "\$0" \ 404 | | awk " 405 | /TIP: / {\$1=\"\"; gsub(/«/,\"$col_grn\"); gsub(/»/,\"$col_reset\"); print \"*\" \$0} 406 | /TIP:> / {\$1=\"\"; print \" $col_ylw\" \$0 \"$col_reset\"} 407 | " 408 | } 409 | 410 | init_options() { 411 | local init_command 412 | init_command=$(list_options \ 413 | | awk ' 414 | BEGIN { FS="|"; OFS=" ";} 415 | $1 ~ /flag/ && $5 == "" {print $3 "=0; "} 416 | $1 ~ /flag/ && $5 != "" {print $3 "=\"" $5 "\"; "} 417 | $1 ~ /option/ && $5 == "" {print $3 "=\"\"; "} 418 | $1 ~ /option/ && $5 != "" {print $3 "=\"" $5 "\"; "} 419 | ') 420 | if [[ -n "$init_command" ]] ; then 421 | #log "init_options: $(echo "$init_command" | wc -l) options/flags initialised" 422 | eval "$init_command" 423 | fi 424 | } 425 | 426 | require_binaries(){ 427 | os_name=$(uname -s) 428 | os_version=$(uname -sprm) 429 | log "Running: on $os_name ($os_version)" 430 | list_programs=$(echo "$*" | sort -u | tr "\n" " ") 431 | log "Verify : $list_programs" 432 | for prog in "$@" ; do 433 | # shellcheck disable=SC2230 434 | if [[ -z $(which "$prog") ]] ; then 435 | die "$script_basename needs [$prog] but this program cannot be found on this [$os_name] machine" 436 | fi 437 | done 438 | } 439 | 440 | folder_prep(){ 441 | if [[ -n "$1" ]] ; then 442 | local folder="$1" 443 | local max_days=${2:-365} 444 | if [[ ! -d "$folder" ]] ; then 445 | log "Create folder : [$folder]" 446 | mkdir "$folder" 447 | else 448 | log "Cleanup folder: [$folder] - delete files older than $max_days day(s)" 449 | find "$folder" -mtime "+$max_days" -type f -exec rm {} \; 450 | fi 451 | fi 452 | } 453 | 454 | expects_single_params(){ 455 | list_options | grep 'param|1|' > /dev/null 456 | } 457 | expects_optional_params(){ 458 | list_options | grep 'param|?|' > /dev/null 459 | } 460 | expects_multi_param(){ 461 | list_options | grep 'param|n|' > /dev/null 462 | } 463 | 464 | count_words(){ 465 | wc -w \ 466 | | awk '{ gsub(/ /,""); print}' 467 | } 468 | 469 | parse_options() { 470 | if [[ $# -eq 0 ]] ; then 471 | show_usage >&2 ; safe_exit 472 | fi 473 | 474 | ## first process all the -x --xxxx flags and options 475 | while true; do 476 | # flag is saved as $flag = 0/1 477 | # option