├── LICENSE ├── README.md ├── TODO.md ├── bash-modules ├── COPYING.GPLv2 ├── COPYING.LGPL-2.1 ├── README.md ├── examples │ ├── chain-of-errors.sh │ ├── hw.sh │ ├── mk-svg-placeholder │ ├── mktemp.sh │ ├── module-template.sh │ ├── showcase-arguments.sh │ ├── showcase-deep-stack-trace.sh │ ├── showcase-log.sh │ ├── simple-panic.sh │ ├── td │ ├── test_mktemp.sh │ └── test_td.sh ├── install.sh ├── spec │ └── bash-modules.spec ├── src │ ├── bash-modules │ │ ├── arguments.sh │ │ ├── cd_to_bindir.sh │ │ ├── date.sh │ │ ├── log.sh │ │ ├── meta.sh │ │ ├── renice.sh │ │ ├── settings.sh │ │ ├── strict.sh │ │ ├── string.sh │ │ ├── timestamped_log.sh │ │ └── unit.sh │ └── import.sh └── test │ ├── test.sh │ ├── test_arguments.sh │ ├── test_cd_to_bindir.sh │ ├── test_date.sh │ ├── test_import.sh │ ├── test_install.sh │ ├── test_log.sh │ ├── test_meta.sh │ ├── test_settings.sh │ ├── test_string.sh │ └── test_timestamped_log.sh ├── docs ├── LICENSE ├── README.html ├── README.md ├── TODO.html ├── TODO.md ├── arguments.html ├── arguments.md ├── cd_to_bindir.html ├── cd_to_bindir.md ├── date.html ├── date.md ├── import.sh.html ├── import.sh.md ├── index.html ├── index.md ├── log.html ├── log.md ├── meta.html ├── meta.md ├── renice.html ├── renice.md ├── settings.html ├── settings.md ├── strict.html ├── strict.md ├── string.html ├── string.md ├── timestamped_log.html ├── timestamped_log.md ├── unit.html └── unit.md ├── images ├── showcase-arguments-1.png ├── showcase-arguments-2.png ├── showcase-arguments-3.png ├── showcase-log-1.png ├── showcase-log-2.png └── showcase-strict-mode.png ├── import.sh └── mkdocs.sh /README.md: -------------------------------------------------------------------------------- 1 | * [bash-modules](#bash-modules) 2 | * [Simple module system for bash.](#simple-module-system-for-bash) 3 | * [Syntax](#syntax) 4 | * [Example](#example) 5 | * [License](#license) 6 | * [Vision](#vision) 7 | * [Features](#features) 8 | * [TODO](#todo) 9 | * [Showcase - log module](#showcase---log-module) 10 | * [Showcase - arguments module](#showcase---arguments-module) 11 | * [Showcase - strict mode](#showcase---strict-mode) 12 | * [Error handling](#error-handling) 13 | * [Chain of errors](#chain-of-errors) 14 | * [Panic](#panic) 15 | 16 | bash-modules 17 | ============ 18 | 19 | See documentation in [HTML format](http://vlisivka.github.io/bash-modules/). 20 | 21 | ## Simple module system for bash. 22 | 23 | Module loader and collection of modules for bash scripts, to quickly write safe bash scripts in unofficial bash strict mode. 24 | 25 | Currently, bash-modules project is targetting users of Linux OS, such as system administrators. 26 | 27 | bash-modules is developed at Fedora Linux and requires bash 4 or higher. 28 | 29 | ## Syntax 30 | 31 | To include module(s) into your script (note the "." at the beginning of the line): 32 | 33 | ``` 34 | . import.sh MODULE[...] 35 | ``` 36 | 37 | To list available modules and show their documentation call import.sh as a command: 38 | 39 | ``` 40 | import.sh [OPTIONS] 41 | ``` 42 | 43 | NOTE: Don't be confused by `import` (without `.sh`) command from `ImageMagick` package. `bash-modules` uses `import.sh`, not `import`. 44 | 45 | ## Example 46 | 47 | ```bash 48 | #!/bin/bash 49 | . import.sh log 50 | info "Hello, world!" 51 | ``` 52 | 53 | See more examples in [bash-modules/examples](https://github.com/vlisivka/bash-modules/tree/master/bash-modules/examples) directory. 54 | 55 | ## License 56 | 57 | `bash-modules` is licensed under terms of LGPL2+ license, like glibc. You are not allowed to copy-paste the code of this project into an your non-GPL-ed project, but you are free to use, modify, or distribute `bash-modules` as a separate library. 58 | 59 | ## Vision 60 | 61 | My vision for the project is to create a loadable set of bash subroutines, which are: 62 | 63 | * useful; 64 | * work in strict mode (set -ue); 65 | * correctly handle strings with spaces and special characters; 66 | * use as little external commands as possible; 67 | * easy to use; 68 | * well documented; 69 | * well covered by test cases. 70 | 71 | ## Features 72 | 73 | * module for logging; 74 | * module for parsing of arguments; 75 | * module for unit testing; 76 | * full support for unofficial strict mode. 77 | 78 | ## Installation 79 | 80 | Use `install.sh` script in bash-modules directory to install bash-modules 81 | to `~/.local` (default for a user) or `/usr/local/bin` (default for a 82 | root user). See `./install.sh --help` for options. 83 | 84 | 85 | ## TODO 86 | 87 | * [x] Implement module loader. 88 | * [x] Implement few modules with frequently used functions and routines. 89 | * [ ] Cooperate with other bash-related projects. 90 | * [ ] Implement a repository for extra modules. 91 | * [ ] Implement a package manager for modules or integrate with an existing PM. 92 | 93 | ## Showcase - log module 94 | 95 | ```bash 96 | #!/bin/bash 97 | . import.sh strict log arguments 98 | 99 | main() { 100 | debug "A debug message (use --debug option to show debug messages)." 101 | 102 | info "An information message. Arguments: $*" 103 | 104 | warn "A warning message." 105 | 106 | error "An error message." 107 | 108 | todo "A todo message." 109 | 110 | unimplemented "Not implemented." 111 | } 112 | 113 | arguments::parse -- "$@" || panic "Cannot parse arguments." 114 | 115 | dbg ARGUMENTS 116 | 117 | main "${ARGUMENTS[@]}" 118 | ``` 119 | 120 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-log-1.png) 121 | 122 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-log-2.png) 123 | 124 | ## Showcase - arguments module 125 | 126 | 127 | ```bash 128 | #!/bin/bash 129 | . import.sh strict log arguments 130 | 131 | NAME="John" 132 | AGE=42 133 | MARRIED="no" 134 | 135 | 136 | main() { 137 | info "Name: $NAME" 138 | info "Age: $AGE" 139 | info "Married: $MARRIED" 140 | info "Other arguments: $*" 141 | } 142 | 143 | arguments::parse \ 144 | "-n|--name)NAME;String,Required" \ 145 | "-a|--age)AGE;Number,(( AGE >= 18 ))" \ 146 | "-m|--married)MARRIED;Boolean" \ 147 | -- "$@" || panic "Cannot parse arguments. Use \"--help\" to show options." 148 | 149 | main "${ARGUMENTS[@]}" || exit $? 150 | 151 | # Comments marked by "#>>" are shown by --help. 152 | # Comments marked by "#>" and "#>>" are shown by --man. 153 | 154 | #> Example of a script with parsing of arguments. 155 | #>> 156 | #>> Usage: showcase-arguments.sh [OPTIONS] [--] [ARGUMENTS] 157 | #>> 158 | #>> OPTIONS: 159 | #>> 160 | #>> -h|--help show this help screen. 161 | #>> --man show complete manual. 162 | #>> -n|--name NAME set name. Name must not be empty. Default name is "John". 163 | #>> -a|--age AGE set age. Age must be >= 18. Default age is 42. 164 | #>> -m|--married set married flag to "yes". Default value is "no". 165 | #>> 166 | ``` 167 | 168 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-arguments-1.png) 169 | 170 | 171 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-arguments-2.png) 172 | 173 | 174 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-arguments-3.png) 175 | 176 | ## Showcase - strict mode 177 | 178 | ```bash 179 | #!/bin/bash 180 | . import.sh strict log 181 | a() { 182 | b 183 | } 184 | b() { 185 | c 186 | } 187 | c() { 188 | d 189 | } 190 | d() { 191 | false 192 | } 193 | 194 | a 195 | 196 | ``` 197 | 198 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-strict-mode.png) 199 | 200 | ## Error handling 201 | 202 | `bash-modules` `log` module supports two strategies to handle errors: 203 | 204 | ### Chain of errors 205 | 206 | The first, strategy is to report the error to user, and then return error code from the function, to produce chain of errors. This technique allows for system administrator to understand faster - why script failed and what it tried to achieve. 207 | 208 | ```bash 209 | #!/bin/bash 210 | . import.sh strict log 211 | foo() { 212 | xxx || { error "Cannot execute xxx."; return 1; } 213 | } 214 | 215 | bar() { 216 | foo || { error "Cannot perform foo."; return 1; } 217 | } 218 | 219 | main() { 220 | bar || { error "Cannot perform bar."; return 1; } 221 | } 222 | 223 | main "$@" || exit $? 224 | ``` 225 | 226 | ```text 227 | $ ./chain-of-errors.sh 228 | ./chain-of-errors.sh: line 4: xxx: command not found 229 | [chain-of-errors.sh] ERROR: Cannot execute xxx. 230 | [chain-of-errors.sh] ERROR: Cannot perform foo. 231 | [chain-of-errors.sh] ERROR: Cannot perform bar. 232 | ``` 233 | 234 | ### Panic 235 | 236 | The second strategy is just to panic, when error happened, and abort script. Stacktrace is printed automatically in this case. 237 | 238 | ```bash 239 | #!/bin/bash 240 | . import.sh strict log 241 | xxx || panic "Cannot execute xxx." 242 | ``` 243 | 244 | ```text 245 | $ ./simple-panic.sh 246 | ./simple-panic.sh: line 3: xxx: command not found 247 | [simple-panic.sh] PANIC: Cannot execute xxx. 248 | at main(./simple-panic.sh:3) 249 | ``` 250 | 251 | NOTE: If error happened in a subshell, then script author need to add another panic handler after end of subshell, e.g. `( false || panic "foo" ) || panic "bar"`. 252 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * [x] Create test case for import.sh. 2 | * [x] Create test case for documentation parser. 3 | * [x] Create test case for install.sh script. 4 | * [x] Add support for installation to a custom directory. 5 | * [x] Add support for installation to HOME or HOME/.local. 6 | * [x] Add datetime module 7 | * [x] Fix timestamped_log to use bash built-in for date instead of date command. 8 | * [x] Move logic to wrap functions from timestamped_log to separate module. 9 | * [x] Update TOC in top README. 10 | * [x] Update examples in README. 11 | * [x] Release version 4.0.0beta. 12 | * [x] Ask other bash-scripters for peer review. 13 | * [x] Publish an article at linux.org.ua. 14 | * [x] Install to XDG dir or HOME/.local, when install run by user. 15 | * [x] Install to /usr/local by default, when run by root. 16 | * [x] Update path to modules and configuration in import.sh during installation. 17 | * [x] Add support for configuration file in other places than /etc. 18 | * [x] Add an Install section to README. 19 | * [x] Add dispatch function. 20 | * [x] Kill useless cat. 21 | * [x] Update help function to support arbitrary prefixes for built-in documentation. 22 | * [x] Generate documentation in markdown format and convert it to HTML. 23 | * [ ] Publish an article in Fedora Magazine. 24 | * [ ] Ask bash developers for strict mode support, like in zsh, because it's critical for this project. 25 | * [ ] Use realpath instead of readlink -f, when possible. 26 | * [ ] Write a package manager for bash: bash-modules. 27 | * [ ] SPDX-License-Identifier: LGPL-2.1-or-later 28 | * [ ] Add __END__ function, which will just exit, and support for line delimited built-in help, like in perl. 29 | * [ ] Write an markdown parser for terminal, to show built-in manual with colors? 30 | * [ ] Add path module. 31 | * [ ] Add lock module. 32 | * [ ] Add is module. 33 | * [ ] Add log2file module. 34 | -------------------------------------------------------------------------------- /bash-modules/README.md: -------------------------------------------------------------------------------- 1 | Very simple module system for bash. 2 | 3 | Author: Volodymyr M. Lisivka 4 | 5 | Home page: https://github.com/vlisivka/bash-modules/ 6 | 7 | 8 | Usage: 9 | 10 | . import.sh module[...] || exit 11 | 12 | Bash modules should work OK wih -u (nounset) and -e (errexit) shell 13 | options. It is good idea to always use "set -ueo pipefail" in your own 14 | applications (or use "strict" module). 15 | -------------------------------------------------------------------------------- /bash-modules/examples/chain-of-errors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log 3 | 4 | # Error handling strategy: print error message and return error code to 5 | # upper layer. 6 | 7 | foo() { 8 | xxx || { error "Cannot execute xxx."; return 1; } 9 | } 10 | 11 | bar() { 12 | foo || { error "Cannot perform foo."; return 1; } 13 | } 14 | 15 | main() { 16 | bar || { error "Cannot perform bar."; return 1; } 17 | } 18 | 19 | main "$@" || exit $? 20 | -------------------------------------------------------------------------------- /bash-modules/examples/hw.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log arguments 3 | 4 | # Name of someone to greet 5 | NAME="${HW_NAME:-world}" 6 | 7 | # Number of times to greet someone 8 | TIMES_TO_GREET=1 9 | 10 | hw() { 11 | local name="${1:?ERROR: Argument is required: a name to greet.}" 12 | 13 | info "Hello, $name!" 14 | 15 | return 0 16 | } 17 | 18 | main() { 19 | local i 20 | for((i=0; i" is doc comment, which will be shown by --man option. 34 | # "#>>" is help comment, which will be shown by --help and --man. 35 | 36 | #>> Usage: hw.sh [OPTIONS] 37 | #>> 38 | #>> Options: 39 | #>> -h | --help show this help text. 40 | #>> --man show documentation. 41 | #>> -n NAME | --name[=]NAME name of someone to greet. Default value: "world". 42 | #>> -m | --more greet one more time. Up to 3 times. 43 | #>> 44 | #> Environment variables: 45 | #> 46 | #> HW_NAME - name of someone to greet. Default value: "world". 47 | #> 48 | #> Examples: 49 | #> * ./hw.sh --name user 50 | #> * HW_NAME="user" ./hw.sh 51 | -------------------------------------------------------------------------------- /bash-modules/examples/mk-svg-placeholder: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh log strict arguments 3 | 4 | BG_COLOR="white" 5 | FG_COLOR="gray" 6 | IMAGE_WIDTH="600" 7 | IMAGE_HEIGHT="400" 8 | IMAGE_TYPE="svg" 9 | FONT_SIZE="30" 10 | 11 | main() { 12 | local image_text="\ 13 | \ 14 | $IMAGE_WIDTH×$IMAGE_HEIGHT\ 15 | " 16 | 17 | local image_b64=$( echo -n "$image_text" | base64 --wrap=0 ) 18 | 19 | echo "data:image/$IMAGE_TYPE;base64,$image_b64" 20 | } 21 | 22 | 23 | arguments::parse \ 24 | "-b|--bg-color)BG_COLOR;String" \ 25 | "-f|--fg-color)FG_COLOR;String" \ 26 | "-W|--image-width)IMAGE_WIDTH;Number" \ 27 | "-H|--image-height)IMAGE_HEIGHT;Number" \ 28 | "-s|--font-size)FONT_SIZE;Number" \ 29 | -- "${@:+$@}" || panic "Cannot parse arguments." 30 | 31 | main "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" || exit $? 32 | 33 | #>> Usage: mk-svg-placeholder [-h|--help|--man] | [-b|--bg-color BG_COLOR] [-f|--fg-color FG_COLOR] [-W|--image-width IMAGE_WIDTH] [-H|--image-height IMAGE_HEIGHT] [-s|--font-size FONT_SIZE] 34 | #> 35 | #> Generate placeholder SVG image in base64 encoding, e.g. white rectangle 100x100. 36 | #> 37 | #> Options: 38 | #> -b|--bg-color BG_COLOR set background color for image, e.g. "white", or "black", or "transparent". Default value is "white". 39 | #> -f|--fg-color FG_COLOR set foreground color for text, eg. "rgba(0,0,0,0.5)" or "#666". Default value is "gray". 40 | #> -W|--image-width IMAGE_WIDTH set image width in pixels. Default value is "600". 41 | #> -H|--image-height IMAGE_HEIGHT set image height in pixels. Default value is "400". 42 | #> -s|--font-size FONT_SIZE set font size. Default value is "30", 43 | #> 44 | #> Example: 45 | #> 46 | #> mk-svg-placeholder -b gray -fg white -W 100 -H 100 -s 20 47 | -------------------------------------------------------------------------------- /bash-modules/examples/mktemp.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | . import.sh strict arguments string 3 | 4 | CREATE_DIR="no" 5 | DRY_RUN="no" 6 | QUIET="no" 7 | USE_TMP_DIR="no" 8 | TEMPORARY_DIR="" 9 | 10 | main() { 11 | local TEMPLATE="${1:-tmp.XXXXXXXXXX}" 12 | [ -n "${1:-}" ] || USE_TMP_DIR="yes" 13 | 14 | if [ -n "$TEMPORARY_DIR" ] 15 | then 16 | # --tmpdir option is used 17 | USE_TMP_DIR="yes" 18 | else 19 | # Initialize with default value for -t option 20 | TEMPORARY_DIR="${TMPDIR:-/tmp}" 21 | fi 22 | 23 | # When use temporary dir, use only last path element of template 24 | [ "$USE_TMP_DIR" == "no" ] || string::basename TEMPLATE "$TEMPLATE" 25 | 26 | # Create private owned files and directories (0700) 27 | umask 077 28 | set -o noclobber 29 | 30 | local i 31 | for((i=0; i<100; i++)) 32 | do 33 | local file_name="" random_string 34 | case "$TEMPLATE" in 35 | *XXXXXXXXXXXXXXXXXXXXXXXXX*) string::random_string random_string 25 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXXXXXXXX/$random_string} ;; 36 | *XXXXXXXXXXXXXXXXXXXXXXXX*) string::random_string random_string 24 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXXXXXXX/$random_string} ;; 37 | *XXXXXXXXXXXXXXXXXXXXXXX*) string::random_string random_string 23 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXXXXXX/$random_string} ;; 38 | *XXXXXXXXXXXXXXXXXXXXXX*) string::random_string random_string 22 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXXXXX/$random_string} ;; 39 | *XXXXXXXXXXXXXXXXXXXXX*) string::random_string random_string 21 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXXXX/$random_string} ;; 40 | *XXXXXXXXXXXXXXXXXXXX*) string::random_string random_string 20 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXXX/$random_string} ;; 41 | *XXXXXXXXXXXXXXXXXXX*) string::random_string random_string 19 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXXX/$random_string} ;; 42 | *XXXXXXXXXXXXXXXXXX*) string::random_string random_string 18 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXXX/$random_string} ;; 43 | *XXXXXXXXXXXXXXXXX*) string::random_string random_string 17 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXXX/$random_string} ;; 44 | *XXXXXXXXXXXXXXXX*) string::random_string random_string 16 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXXX/$random_string} ;; 45 | *XXXXXXXXXXXXXXX*) string::random_string random_string 15 ; file_name=${TEMPLATE//XXXXXXXXXXXXXXX/$random_string} ;; 46 | *XXXXXXXXXXXXXX*) string::random_string random_string 14 ; file_name=${TEMPLATE//XXXXXXXXXXXXXX/$random_string} ;; 47 | *XXXXXXXXXXXXX*) string::random_string random_string 13 ; file_name=${TEMPLATE//XXXXXXXXXXXXX/$random_string} ;; 48 | *XXXXXXXXXXXX*) string::random_string random_string 12 ; file_name=${TEMPLATE//XXXXXXXXXXXX/$random_string} ;; 49 | *XXXXXXXXXXX*) string::random_string random_string 11 ; file_name=${TEMPLATE//XXXXXXXXXXX/$random_string} ;; 50 | *XXXXXXXXXX*) string::random_string random_string 10 ; file_name=${TEMPLATE//XXXXXXXXXX/$random_string} ;; 51 | *XXXXXXXXX*) string::random_string random_string 9 ; file_name=${TEMPLATE//XXXXXXXXX/$random_string} ;; 52 | *XXXXXXXX*) string::random_string random_string 8 ; file_name=${TEMPLATE//XXXXXXXX/$random_string} ;; 53 | *XXXXXXX*) string::random_string random_string 7 ; file_name=${TEMPLATE//XXXXXXX/$random_string} ;; 54 | *XXXXXX*) string::random_string random_string 6 ; file_name=${TEMPLATE//XXXXXX/$random_string} ;; 55 | *XXXXX*) string::random_string random_string 5 ; file_name=${TEMPLATE//XXXXX/$random_string} ;; 56 | *XXXX*) string::random_string random_string 4 ; file_name=${TEMPLATE//XXXX/$random_string} ;; 57 | *XXX*) string::random_string random_string 3 ; file_name=${TEMPLATE//XXX/$random_string} ;; 58 | *) 59 | error "Bad template string. TEMPLATE must contain at least 3 consecutive \"X\"s." 60 | exit 1 61 | ;; 62 | esac 63 | 64 | [ "$USE_TMP_DIR" == "no" ] || { 65 | file_name="$TEMPORARY_DIR/$file_name" 66 | } 67 | 68 | [ ! -e "$file_name" ] || continue 69 | 70 | [ "$DRY_RUN" == "no" ] || { 71 | echo "$file_name" 72 | return 0 73 | } 74 | 75 | if [ "$CREATE_DIR" == "no" ] 76 | then 77 | # Try to create file (noclobber option will not allow to 78 | # overwrite file). 79 | : >"$file_name" || continue 80 | else 81 | 82 | # Try to create director (mkdir will fail if directory 83 | # is already exists). 84 | mkdir "$file_name" 2>/dev/null || continue 85 | fi 86 | 87 | echo "$file_name" 88 | return 0 89 | 90 | done 91 | 92 | [ "$QUIET" == "no" ] || error "Cannot create temporary file after 100 attempts." 93 | return 1 94 | } 95 | 96 | arguments::parse \ 97 | "-d|--directory)CREATE_DIR;Yes" \ 98 | "-u|--dry-run)DRY_RUN;Yes" \ 99 | "-q|--quiet)QUIET;Yes" \ 100 | "-t|--tmpdir)USE_TMP_DIR;Yes" \ 101 | "--tmpdir)TEMPORARY_DIR;String" \ 102 | -- "${@:+$@}" || { 103 | error "Cannot parse mktemp command line arguments." 104 | mktemp::usage 105 | exit 1 106 | } 107 | 108 | [ "${#ARGUMENTS[@]}" -le 1 ] || { 109 | error "Incorrect number of mktemp command line arguments. Put options before template." 110 | mktemp::usage 111 | exit 1 112 | } 113 | 114 | main "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" || exit $? 115 | 116 | 117 | #>>> mktemp - implementation of mktemp in pure bash for portability. 118 | #>> 119 | #>> USAGE 120 | #>> mktemp [OPTION]... [TEMPLATE] 121 | #> 122 | #> DESCRIPTION 123 | #> Create a temporary file or directory, safely, and print its name. 124 | #> TEMPLATE must contain at least 3 consecutive "X"s in last 125 | #> component. If TEMPLATEis not specified, use tmp.XXXXXXXXXX, and 126 | #> --tmpdir is implied. 127 | #>> 128 | #>> OPTIONS 129 | #>> -h | --help 130 | #>> Print this help page. 131 | #>> 132 | #>> --man 133 | #>> Show manual. 134 | #>> 135 | #>> -d | --directory 136 | #>> create a directory, not a file 137 | #>> 138 | #>> -u | --dry-run 139 | #>> do not create anything; merely print a name (unsafe) 140 | #>> 141 | #>> -q | --quiet 142 | #>> suppress diagnostics about file/dir-creation failure 143 | #>> 144 | #>> --suffix=SUFF 145 | #>> append SUFF to TEMPLATE. SUFF must not contain slash. 146 | #>> This option is implied if TEMPLATE does not end in X. 147 | #>> 148 | #>> -t | --tmpdir[=DIR] 149 | #>> interpret TEMPLATE relative to DIR. If DIR is not 150 | #>> specified, use $TMPDIR if set, else /tmp. With this 151 | #>> option, TEMPLATE must not be an absolute name. TEMPLATE 152 | #>> may contain slashes, but mktemp creates only the final 153 | #>> component. 154 | #>> 155 | -------------------------------------------------------------------------------- /bash-modules/examples/module-template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) YEAR AUTHOR , All Rights Reserved 3 | # YOUR LICENSE FOR MODULE (LGPL2+, MIT, etc.) 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `template` - todo: summary 8 | 9 | #> 10 | #> ## FUNCTIONS 11 | 12 | #>> 13 | #>> * `template::echo ARGUMENTS` - todo: function summary 14 | template::echo() { 15 | echo "$@" 16 | } 17 | -------------------------------------------------------------------------------- /bash-modules/examples/showcase-arguments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log arguments 3 | 4 | NAME="John" 5 | AGE=42 6 | MARRIED="no" 7 | 8 | 9 | main() { 10 | info "Name: $NAME" 11 | info "Age: $AGE" 12 | info "Married: $MARRIED" 13 | info "Other arguments: $*" 14 | } 15 | 16 | arguments::parse \ 17 | "-n|--name)NAME;String,Required" \ 18 | "-a|--age)AGE;Number,(( AGE >= 18 ))" \ 19 | "-m|--married)MARRIED;Yes" \ 20 | -- "$@" || panic "Cannot parse arguments. Use \"--help\" to show options." 21 | 22 | main "${ARGUMENTS[@]}" || exit 23 | 24 | # Comments marked by "#>>" are shown by --help. 25 | # Comments marked by "#>" and "#>>" are shown by --man. 26 | 27 | #> Example of a script with parsing of arguments. 28 | #>> 29 | #>> Usage: showcase-arguments.sh [OPTIONS] [--] [ARGUMENTS] 30 | #>> 31 | #>> OPTIONS: 32 | #>> 33 | #>> -h|--help show this help screen. 34 | #>> --man show complete manual. 35 | #>> -n|--name NAME set name. Name must not be empty. Default name is "John". 36 | #>> -a|--age AGE set age. Age must be >= 18. Default age is 42. 37 | #>> -m|--married set married flag to "yes". Default value is "no". 38 | #>> 39 | -------------------------------------------------------------------------------- /bash-modules/examples/showcase-deep-stack-trace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log 3 | a() { 4 | b 5 | } 6 | b() { 7 | c 8 | } 9 | c() { 10 | d 11 | } 12 | d() { 13 | false 14 | } 15 | 16 | a 17 | -------------------------------------------------------------------------------- /bash-modules/examples/showcase-log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log arguments 3 | 4 | main() { 5 | debug "A debug message (use --debug option to show debug messages)." 6 | 7 | info "An information message. Arguments: $*" 8 | 9 | warn "A warning message." 10 | 11 | error "An error message." 12 | 13 | todo "A todo message." 14 | 15 | unimplemented "Not implemented." 16 | } 17 | 18 | arguments::parse -- "$@" || panic "Cannot parse arguments." 19 | 20 | dbg ARGUMENTS 21 | 22 | main "${ARGUMENTS[@]}" || exit 23 | -------------------------------------------------------------------------------- /bash-modules/examples/simple-panic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log 3 | 4 | # Error handling strategy: just panic in case of an error. 5 | 6 | xxx || panic "Cannot execute xxx." 7 | -------------------------------------------------------------------------------- /bash-modules/examples/test_mktemp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . import.sh strict log unit string cd_to_bindir 3 | 4 | MKTEMP="./mktemp.sh" 5 | 6 | unit::set_up() { 7 | unset TMPDIR 8 | } 9 | 10 | ############################################### 11 | # Test cases 12 | 13 | test_mktemp_without_options() { 14 | local file_name="$($MKTEMP)" 15 | 16 | unit::assert "File was not created by mktemp." [ -f "$file_name" ] 17 | unit::assert "Temporary file must be placed in /tmp." "string::starts_with '$file_name' '/tmp/'" 18 | unit::assert "Template must be replaced by a random string." "! string::contains '$file_name' 'XXX'" 19 | 20 | rm -f "$file_name" 21 | } 22 | 23 | test_mktemp_with_dry_run_option() { 24 | local file_name="$($MKTEMP --dry-run)" 25 | 26 | unit::assert "File must not be created by mktemp with --dry-run option." '[ ! -f "$file_name" ]' 27 | unit::assert "Temporary file must be placed in /tmp." "string::starts_with '$file_name' '/tmp/'" 28 | unit::assert "Template must be replaced by a random string." "! string::contains '$file_name' 'XXX'" 29 | } 30 | 31 | test_mktemp_with_template() { 32 | local file_name="$($MKTEMP /var/tmp/foooXXXXXXXXX.bar)" 33 | 34 | unit::assert_not_equal "$file_name" "/var/tmp/foooXXXXXXXXX.bar" "XXXXX in template is not replaced by random string." 35 | unit::assert "File was not created by mktemp." '[ -f "$file_name" ]' 36 | unit::assert "Temporary file must be placed in /var/tmp in this case." "string::starts_with '$file_name' '/var/tmp/fooo'" 37 | unit::assert "Template must be replaced by a random string." "! string::contains '$file_name' 'XXX'" 38 | 39 | rm -f "$file_name" 40 | } 41 | 42 | test_mktemp_with_d_option() { 43 | local dir_name="$($MKTEMP -d)" 44 | 45 | unit::assert "Directory was not created by mktemp." '[ -d "$dir_name" ]' 46 | unit::assert "Template must be replaced by a random string." "! string::contains '$dir_name' 'XXX'" 47 | 48 | rm -rf "$dir_name" 49 | } 50 | 51 | test_mktemp_with_t_option() { 52 | # Option -t is deprecated in mktemp, because it's confusing. 53 | local file_name="$($MKTEMP -t /var/var/foooXXXXXXXXX.bar)" 54 | 55 | unit::assert "File was not created by mktemp." '[ -f "$file_name" ]' 56 | unit::assert "Temporary file must be placed in /tmp in this case." "string::starts_with '$file_name' '/tmp/'" 57 | unit::assert "Template must be replaced by a random string." "! string::contains '$file_name' 'XXX'" 58 | 59 | rm -f "$file_name" 60 | } 61 | 62 | test_mktemp_with_tmpdir_option() { 63 | local file_name="$($MKTEMP --tmpdir=/var/tmp /tmp/foooXXXXXXXXX.bar)" 64 | 65 | unit::assert "File was not created by mktemp." [ -f "$file_name" ] 66 | unit::assert "Temporary file must be placed in /var/tmp in this case." "string::starts_with '$file_name' '/var/tmp/fooo'" 67 | unit::assert "Template must be replaced by a random string." "! string::contains '$file_name' 'XXX'" 68 | 69 | rm -f "$file_name" 70 | } 71 | 72 | 73 | # Helper function. Create bunch of temporary files, write content into 74 | # these files, then check read content back and check is it was overwritten 75 | # or not. 76 | mktemp_test_file_worker() { 77 | local WORKER_NUMBER="$1" 78 | local NUMBER_OF_FILES="$2" 79 | shift 2 80 | 81 | local FILES=( ) 82 | for((I=0; I "$FILE" 87 | 88 | FILES[${#FILES[@]}]="$FILE" 89 | done 90 | 91 | for((I=0; I/dev/null || panic "Cannot create new TODO.md file." 17 | } 18 | 19 | unit::tear_down() { 20 | set -ue 21 | 22 | rm -rf "$DIR" 23 | } 24 | 25 | ############################################### 26 | # Test cases 27 | 28 | test_error_without_file() { 29 | rm -f "TODO.md" || panic "Cannot remove TODO.md in $(pwd)." 30 | 31 | OUT="$(! "$TD" 2>&1 >/dev/null)" || panic "td returned zero exit code when TODO.md file is not found." 32 | 33 | unit::assert_equal "$OUT" "[td] ERROR: Cannot find \"TODO.md\" file in current or upper directories." "Unexpected error message returned by td." 34 | } 35 | 36 | test_create_new_file() { 37 | rm -f "TODO.md" || panic "Cannot remove TODO.md in $(pwd)." 38 | 39 | OUT="$("$TD" -n 2>&1)" || panic "td returned non-zero exit code when creating new TODO.md file." 40 | 41 | unit::assert_equal "$OUT" $'\n'"[td] INFO: \"TODO.md\" is created." "Unexpected message returned by td." 42 | } 43 | 44 | test_create_new_item() { 45 | OUT="$("$TD" foo bar baz 2>&1)" || panic "Cannot create new TODO item." 46 | 47 | unit::assert_equal "$OUT" $'\n'"* [ ] foo bar baz" "Unexpected message returned by td." 48 | } 49 | 50 | test_mark_item_as_wip() { 51 | OUT="$("$TD" a task 1 2>&1)" || panic "Cannot create new TODO item." 52 | OUT="$("$TD" -w 2>&1)" || panic "Cannot mark item as WIP." 53 | 54 | unit::assert_equal "$OUT" $'\n'"* [.] a task 1" "Unexpected message returned by td." 55 | 56 | # With message 57 | OUT="$("$TD" -d 2>&1)" || panic "Cannot mark TODO item as done." 58 | OUT="$("$TD" a task 2 2>&1)" || panic "Cannot create new TODO item." 59 | OUT="$("$TD" -w 16:15 2>&1)" || panic "Cannot mark item as WIP." 60 | 61 | unit::assert_equal "$OUT" $'\n'"* [.] a task 2 - 16:15" "Unexpected message returned by td." 62 | 63 | # Try to mark WIP message as WIP again 64 | OUT="$("$TD" -w 2>&1)" || panic "Cannot mark TODO item as WIP." 65 | 66 | unit::assert_equal "$OUT" $'\n'"[td] INFO: Top TODO item is already in progress."$'\n'"* [.] a task 2 - 16:15" "Unexpected message returned by td." 67 | } 68 | 69 | test_mark_item_as_done() { 70 | OUT="$("$TD" a message 1 2>&1)" || panic "Cannot create new TODO item." 71 | OUT="$("$TD" -d 2>&1)" || panic "Cannot mark item as done." 72 | 73 | unit::assert_equal "$OUT" $'\n'"* [x] a message 1" "Unexpected message returned by td." 74 | 75 | # With message 76 | OUT="$("$TD" a message 2 2>&1)" || panic "Cannot create new TODO item." 77 | OUT="$("$TD" -d 16:20 2>&1)" || panic "Cannot mark item as done." 78 | 79 | unit::assert_equal "$OUT" $'\n'"* [x] a message 2 - 16:20" "Unexpected message returned by td." 80 | } 81 | 82 | test_mark_item_as_canceled() { 83 | OUT="$("$TD" a task 1 2>&1)" || panic "Cannot create new TODO item." 84 | OUT="$("$TD" -c 2>&1)" || panic "Cannot mark item as canceled." 85 | 86 | unit::assert_equal "$OUT" $'\n'"* [-] a task 1" "Unexpected message returned by td." 87 | 88 | # With explanation 89 | OUT="$("$TD" a task 2 2>&1)" || panic "Cannot create new TODO item." 90 | OUT="$("$TD" -c an explanation 2>&1)" || panic "Cannot mark item as canceled." 91 | 92 | unit::assert_equal "$OUT" $'\n'"* [-] a task 2 - an explanation" "Unexpected message returned by td." 93 | } 94 | 95 | unit::run_test_cases "$@" 96 | -------------------------------------------------------------------------------- /bash-modules/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euEo pipefail # Unofficial strict mode 3 | APP_DIR="$(dirname "$0")" 4 | 5 | if (( $EUID == 0 )) 6 | then 7 | PREFIX="/usr/local" 8 | else 9 | PREFIX="${XDG_DATA_HOME:-$HOME/.local}" 10 | fi 11 | 12 | MODULES_DIR="@PREFIX@/share/bash-modules" 13 | BIN_DIR="@PREFIX@/bin" 14 | 15 | cd "$APP_DIR" 16 | 17 | main() { 18 | echo "Modules directory: \"$MODULES_DIR\"." 19 | echo "Bin directory: \"$BIN_DIR\"." 20 | 21 | mkdir -p "$MODULES_DIR" || { echo "ERROR: Cannot create directory \"$MODULES_DIR\"." >&2 ; return 1 ;} 22 | install -t "$MODULES_DIR" src/bash-modules/*.sh || { echo "ERROR: Cannot install modules to directory \"$MODULES_DIR\"." >&2 ; return 1 ;} 23 | 24 | mkdir -p "$BIN_DIR" || { echo "ERROR: Cannot create directory \"$BIN_DIR\"." >&2 ; return 1 ;} 25 | install -D src/import.sh "$BIN_DIR/import.sh" || { echo "ERROR: Cannot install import.sh script to \"$MODULES_DIR\"." >&2 ; return 1 ;} 26 | 27 | # Update hardcoded path to directory with modules 28 | sed -i "s@/usr/share/bash-modules@$MODULES_DIR@g" "$BIN_DIR/import.sh" \ 29 | || { echo "ERROR: Cannot replace path to modules directory in \"$BIN_DIR/import.sh\" from \"/usr/share/bash-modules\" to \"$MODULES_DIR\"." >&2 ; return 1; } 30 | } 31 | 32 | while (( $# > 0 )) 33 | do 34 | case "$1" in 35 | -p|--prefix) 36 | PREFIX="${2:?Value is required for option: prefix for directories, e.g. \"$HOME/.local\" or \"/usr/local\".}" 37 | shift 1 38 | ;; 39 | 40 | -m|--modules-dir) 41 | MODULES_DIR="${2:?Value is required for option: path to directory with modules, e.g. \"/usr/local/share/bash-modules\".}" 42 | shift 1 43 | ;; 44 | 45 | -b|--bin-dir) 46 | BIN_DIR="${2:?Value is required for option: path to bin directory, e.g. \"$HOME/bin\" or \"/usr/local/bin\".}" 47 | shift 1 48 | ;; 49 | 50 | -h|--help) 51 | echo "Usage: install.sh [-p|--prefix PREFIX] [-m|--modules-dir DIR] [-b|--bin-dir DIR] 52 | 53 | OPTIONS 54 | 55 | -h|--help 56 | 57 | This help screen. 58 | 59 | -p|--prefix PREFIX 60 | 61 | Install bash-modules import.sh to PREFIX/bin, and modules to 62 | PREFIX/share/bash-modules. Default value is \"$HOME/.local\" for 63 | non-root user and \"/usr/local\" for a root user. 64 | 65 | -m|--modules-dir DIR 66 | 67 | Directory to store modules. Default value is \"@PREFIX@/share/bash-modules\". 68 | 69 | -b|--bin-dir DIR 70 | 71 | Directory to store import.sh script. Default value is \"@PREFIX/bin\". 72 | 73 | " 74 | exit 0 75 | ;; 76 | 77 | *) 78 | echo "ERROR: Unknown option: \"$1\"." >&2 79 | return 1 80 | ;; 81 | esac 82 | 83 | shift 1 84 | done 85 | 86 | MODULES_DIR="${MODULES_DIR/@PREFIX@/$PREFIX}" 87 | BIN_DIR="${BIN_DIR/@PREFIX@/$PREFIX}" 88 | 89 | main || exit $? 90 | -------------------------------------------------------------------------------- /bash-modules/spec/bash-modules.spec: -------------------------------------------------------------------------------- 1 | 2 | Name: bash-modules 3 | Version: 4.0.1 4 | Release: 1%{?dist} 5 | Summary: Modules for bash 6 | 7 | Group: System Environment/Libraries 8 | URL: https://github.com/vlisivka/bash-modules 9 | License: LGPL-2.1+ 10 | Source0: %{name}.tar.gz 11 | BuildArch: noarch 12 | 13 | %define homedir /usr/share/bash-modules 14 | 15 | %description 16 | Optional modules to use with bash, like logging, argument parsing, etc. 17 | 18 | %prep 19 | %setup -q -n %{name} 20 | 21 | %build 22 | # Nothing to do 23 | 24 | # Execute tests 25 | ( 26 | cd test 27 | exec /bin/bash ./test.sh -q 28 | ) 29 | 30 | %install 31 | install -D src/import.sh "$RPM_BUILD_ROOT%_bindir/import.sh" 32 | 33 | mkdir -p "$RPM_BUILD_ROOT%homedir/" 34 | cp -a src/bash-modules/* "$RPM_BUILD_ROOT%homedir/" 35 | 36 | %clean 37 | rm -rf "$RPM_BUILD_ROOT" 38 | 39 | %files 40 | %defattr(644,root,root,755) 41 | %doc COPYING* README.md examples/ 42 | 43 | %attr(0755,root,root) %_bindir/import.sh 44 | %homedir 45 | 46 | 47 | %changelog 48 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/arguments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `arguments` - contains function to parse arguments and assign option values to variables. 8 | 9 | #>> 10 | #>> ## FUNCTIONS 11 | 12 | #>> 13 | #>> * `arguments::parse [-S|--FULL)VARIABLE;FLAGS[,COND]...]... -- [ARGUMENTS]...` 14 | #> 15 | #> Where: 16 | #> 17 | #> * `-S` - short option name. 18 | #> 19 | #> * `--FULL` - long option name. 20 | #> 21 | #> * `VARIABLE` - name of shell variable to assign value to. 22 | #> 23 | #> * `FLAGS` - one of (case sensitive): 24 | #> * `Y | Yes` - set variable value to "yes"; 25 | #> * `No` - set variable value to "no"; 26 | #> * `I | Inc | Incremental` - incremental (no value) - increment variable value by one; 27 | #> * `S | Str | String` - string value; 28 | #> * `N | Num | Number` - numeric value; 29 | #> * `A | Arr | Array` - array of string values (multiple options); 30 | #> * `C | Com | Command` - option name will be assigned to the variable. 31 | #> 32 | #> * `COND` - post conditions: 33 | #> * `R | Req | Required` - option value must be not empty after end of parsing. 34 | #> Set initial value to empty value to require this option; 35 | #> * any code - executed after option parsing to check post conditions, e.g. "(( FOO > 3 )), (( FOO > BAR ))". 36 | #> 37 | #> * -- - the separator between option descriptions and script commandline arguments. 38 | #> 39 | #> * `ARGUMENTS` - command line arguments to parse. 40 | #> 41 | #> **LIMITATION:** grouping of one-letter options is NOT supported. Argument `-abc` will be parsed as 42 | #> option `-abc`, not as `-a -b -c`. 43 | #> 44 | #> **NOTE:** bash4 requires to use `"${@:+$@}"` to expand empty list of arguments in strict mode (`-u`). 45 | #> 46 | #> By default, function supports `-h|--help`, `--man` and `--debug` options. 47 | #> Options `--help` and `--man` are calling `arguments::help()` function with `2` or `1` as 48 | #> argument. Override that function if you want to provide your own help. 49 | #> 50 | #> Unlike many other parsers, this function stops option parsing at first 51 | #> non-option argument. 52 | #> 53 | #> Use `--` in commandline arguments to strictly separate options and arguments. 54 | #> 55 | #> After option parsing, unparsed command line arguments are stored in 56 | #> `ARGUMENTS` array. 57 | #> 58 | #> **Example:** 59 | #> 60 | #> ```bash 61 | #> # Boolean variable ("yes" or "no") 62 | #> FOO="no" 63 | #> # String variable 64 | #> BAR="" 65 | #> # Indexed array 66 | #> declare -a BAZ=( ) 67 | #> # Integer variable 68 | #> declare -i TIMES=0 69 | #> 70 | #> arguments::parse \\ 71 | #> "-f|--foo)FOO;Yes" \\ 72 | #> "-b|--bar)BAR;String,Required" \\ 73 | #> "-B|--baz)BAZ;Array" \\ 74 | #> "-i|--inc)TIMES;Incremental,((TIMES<3))" \\ 75 | #> -- \\ 76 | #> "${@:+$@}" 77 | #> 78 | #> # Print name and value of variables 79 | #> dbg FOO BAR BAZ TIMES ARGUMENTS 80 | #> ``` 81 | arguments::parse() { 82 | 83 | # Global array to hold command line arguments 84 | ARGUMENTS=( ) 85 | 86 | # Local variables 87 | local OPTION_DESCRIPTIONS PARSER 88 | declare -a OPTION_DESCRIPTIONS 89 | # Initialize array, because declare -a is not enough anymore for -u opt 90 | OPTION_DESCRIPTIONS=( ) 91 | 92 | # Split arguments list at "--" 93 | while [ $# -gt 0 ] 94 | do 95 | [ "$1" != "--" ] || { 96 | shift 97 | break 98 | } 99 | 100 | OPTION_DESCRIPTIONS[${#OPTION_DESCRIPTIONS[@]}]="$1" # Append argument to end of array 101 | shift 102 | done 103 | 104 | # Generate option parser and execute it 105 | PARSER="$(arguments::generate_parser "${OPTION_DESCRIPTIONS[@]:+${OPTION_DESCRIPTIONS[@]}}")" || return 1 106 | eval "$PARSER" || return 1 107 | arguments::parse_options "${@:+$@}" || return $? 108 | } 109 | 110 | #>> 111 | #>> * `arguments::generate_parser OPTIONS_DESCRIPTIONS` - generate parser for options. 112 | #> Will create function `arguments::parse_options()`, which can be used to parse arguments. 113 | #> Use `declare -fp arguments::parse_options` to show generated source. 114 | arguments::generate_parser() { 115 | 116 | local OPTION_DESCRIPTION OPTION_CASE OPTION_FLAGS OPTION_TYPE OPTION_OPTIONS OPTIONS_PARSER="" OPTION_POSTCONDITIONS="" 117 | 118 | # Parse option description and generate code to parse that option from script arguments 119 | while [ $# -gt 0 ] 120 | do 121 | # Parse option description 122 | OPTION_DESCRIPTION="$1" ; shift 123 | 124 | # Check option syntax 125 | case "$OPTION_DESCRIPTION" in 126 | *')'*';'*) ;; # OK 127 | *) 128 | error "Incorrect syntax of option: \"$OPTION_DESCRIPTION\". Option syntax: -S|--FULL)VARIABLE;TYPE[,CHECK]... . Example: '-f|--foo)FOO;String,Required'." 129 | __log__DEBUG=yes; stacktrace 130 | return 1 131 | ;; 132 | esac 133 | 134 | OPTION_CASE="${OPTION_DESCRIPTION%%)*}" # Strip everything after first ')': --foo)BAR -> --foo 135 | OPTION_VARIABLE="${OPTION_DESCRIPTION#*)}" # Strip everything before first ')': --foo)BAR -> BAR 136 | OPTION_FLAGS="${OPTION_VARIABLE#*;}" # Strip everything before first ';': BAR;Baz -> Baz 137 | OPTION_VARIABLE="${OPTION_VARIABLE%%;*}" # String everything after first ';': BAR;Baz -> BAR 138 | 139 | IFS=',' read -a OPTION_OPTIONS <<<"$OPTION_FLAGS" # Convert string into array: 'a,b,c' -> [ a b c ] 140 | OPTION_TYPE="${OPTION_OPTIONS[0]:-}" ; unset OPTION_OPTIONS[0] ; # First element of array is option type 141 | 142 | # Generate the parser for option 143 | case "$OPTION_TYPE" in 144 | 145 | Y|Yes) # Set variable to "yes", no arguments 146 | OPTIONS_PARSER="$OPTIONS_PARSER 147 | $OPTION_CASE) 148 | $OPTION_VARIABLE=\"yes\" 149 | shift 1 150 | ;; 151 | " 152 | ;; 153 | 154 | No) # Set variable to "no", no arguments 155 | OPTIONS_PARSER="$OPTIONS_PARSER 156 | $OPTION_CASE) 157 | $OPTION_VARIABLE=\"no\" 158 | shift 1 159 | ;; 160 | " 161 | ;; 162 | 163 | C|Com|Command) # Set variable to name of the option, no arguments 164 | OPTIONS_PARSER="$OPTIONS_PARSER 165 | $OPTION_CASE) 166 | $OPTION_VARIABLE=\"\$1\" 167 | shift 1 168 | ;; 169 | " 170 | ;; 171 | 172 | 173 | I|Incr|Incremental) # Incremental - any use of this option will increment variable by 1 174 | OPTIONS_PARSER="$OPTIONS_PARSER 175 | $OPTION_CASE) 176 | let $OPTION_VARIABLE++ || : 177 | shift 1 178 | ;; 179 | " 180 | ;; 181 | 182 | S|Str|String) # Regular option with string value 183 | OPTIONS_PARSER="$OPTIONS_PARSER 184 | $OPTION_CASE) 185 | $OPTION_VARIABLE=\"\${2:?ERROR: String value is required for \\\"$OPTION_CASE\\\" option. See --help for details.}\" 186 | shift 2 187 | ;; 188 | ${OPTION_CASE//|/=*|}=*) 189 | $OPTION_VARIABLE=\"\${1#*=}\" 190 | shift 1 191 | ;; 192 | " 193 | ;; 194 | 195 | N|Num|Number) # Same as string 196 | OPTIONS_PARSER="$OPTIONS_PARSER 197 | $OPTION_CASE) 198 | $OPTION_VARIABLE=\"\${2:?ERROR: Numeric value is required for \\\"$OPTION_CASE\\\" option. See --help for details.}\" 199 | shift 2 200 | ;; 201 | ${OPTION_CASE//|/=*|}=*) 202 | $OPTION_VARIABLE=\"\${1#*=}\" 203 | shift 1 204 | ;; 205 | " 206 | ;; 207 | 208 | A|Arr|Array) # Array of strings 209 | OPTIONS_PARSER="$OPTIONS_PARSER 210 | $OPTION_CASE) 211 | ${OPTION_VARIABLE}[\${#${OPTION_VARIABLE}[@]}]=\"\${2:?Value is required for \\\"$OPTION_CASE\\\". See --help for details.}\" 212 | shift 2 213 | ;; 214 | ${OPTION_CASE//|/=*|}=*) 215 | ${OPTION_VARIABLE}[\${#${OPTION_VARIABLE}[@]}]=\"\${1#*=}\" 216 | shift 1 217 | ;; 218 | " 219 | ;; 220 | 221 | *) 222 | echo "ERROR: Unknown option type: \"$OPTION_TYPE\"." >&2 223 | return 1 224 | ;; 225 | esac 226 | 227 | # Parse option options, e.g "Required". Any other text is treated as condition, e.g. (( VAR > 10 && VAR < 20 )) 228 | local OPTION_OPTION 229 | for OPTION_OPTION in "${OPTION_OPTIONS[@]:+${OPTION_OPTIONS[@]}}" 230 | do 231 | case "$OPTION_OPTION" in 232 | R|Req|Required) 233 | OPTION_POSTCONDITIONS="$OPTION_POSTCONDITIONS 234 | [ -n \"\$${OPTION_VARIABLE}\" ] || { echo \"ERROR: Option \\\"$OPTION_CASE\\\" is required. See --help for details.\" >&2; return 1; } 235 | " 236 | ;; 237 | *) # Any other code after option type i 238 | OPTION_POSTCONDITIONS="$OPTION_POSTCONDITIONS 239 | $OPTION_OPTION || { echo \"ERROR: Condition for \\\"$OPTION_CASE\\\" option is failed. See --help for details.\" >&2; return 1; } 240 | " 241 | ;; 242 | esac 243 | done 244 | 245 | done 246 | echo " 247 | arguments::parse_options() { 248 | # Global array to hold command line arguments 249 | ARGUMENTS=( ) 250 | 251 | while [ \$# -gt 0 ] 252 | do 253 | case \"\$1\" in 254 | # User options. 255 | $OPTIONS_PARSER 256 | 257 | # Built-in options. 258 | -h|--help) 259 | arguments::help 2 260 | exit 0 261 | ;; 262 | --man) 263 | arguments::help 1 264 | exit 0 265 | ;; 266 | --debug) 267 | log::enable_debug_mode 268 | shift 269 | ;; 270 | --) 271 | shift; break; # Do not parse rest of the command line arguments 272 | ;; 273 | -*) 274 | echo \"ERROR: Unknown option: \\\"\$1\\\".\" >&2 275 | arguments::help 3 276 | return 1 277 | ;; 278 | *) 279 | break; # Do not parse rest of the command line 280 | ;; 281 | esac 282 | done 283 | [ \$# -eq 0 ] || ARGUMENTS=( \"\$@\" ) # Store rest of the command line arguments into the ARGUMENTS array 284 | $OPTION_POSTCONDITIONS 285 | } 286 | " 287 | } 288 | 289 | #>> 290 | #>> * `arguments::help LEVEL` - display embeded documentation. 291 | #>> LEVEL - level of documentation: 292 | #>> * 3 - summary (`#>>>` comments), 293 | #>> * 2 - summary and usage ( + `#>>` comments), 294 | #>> * 1 - full documentation (+ `#>` comments). 295 | arguments::help() { 296 | local LEVEL="${1:-3}" 297 | case "$LEVEL" in 298 | 2|3) import::show_documentation "$LEVEL" "$IMPORT__BIN_FILE" ;; 299 | 1) import::show_documentation "$LEVEL" "$IMPORT__BIN_FILE" | less ;; 300 | esac 301 | } 302 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/cd_to_bindir.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | #>> # NAME 5 | #>>> `cd_to_bindir` - change working directory to the directory where main script file is located. 6 | #>> 7 | #>> # DESCRIPTION 8 | #>> 9 | #>> Just import this cwdir module to change current working directory to a directory, 10 | #>> where main script file is located. 11 | 12 | # Get file name of the main source file 13 | __CD_TO_BINDIR__BIN_FILE="${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}" 14 | 15 | # If file name doesn't contains "/", then use `which` to find path to file name. 16 | [[ "$__CD_TO_BINDIR__BIN_FILE" == */* ]] || __CD_TO_BINDIR__BIN_FILE=$( which "$__CD_TO_BINDIR__BIN_FILE" ) 17 | 18 | # Strip everything after last "/" to get directoru: "/foo/bar/baz" -> "/foo/bar", "./foo" -> "./". 19 | # Then cd to the directory and get it path. 20 | __CD_TO_BINDIR_DIRECTORY=$( cd "${__CD_TO_BINDIR__BIN_FILE%/*}/" ; pwd ) 21 | 22 | unset __CD_TO_BINDIR__BIN_FILE 23 | 24 | #>> 25 | #>> # FUNCTIONS 26 | #>> 27 | #>> * `ch_bin_dir` - Change working directory to directory where script is located, which is usually called "bin dir". 28 | cd_to_bindir() { 29 | cd "$__CD_TO_BINDIR_DIRECTORY" 30 | } 31 | 32 | # Call this function at import. 33 | cd_to_bindir 34 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/date.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `date` - date-time functions. 8 | 9 | #> 10 | #> ## FUNCTIONS 11 | 12 | #>> 13 | #>> * `date::timestamp VARIABLE` - return current time in seconds since UNIX epoch 14 | date::timestamp() { 15 | printf -v "$1" "%(%s)T" "-1" 16 | } 17 | 18 | #>> 19 | #>> * `date::current_datetime VARIABLE FORMAT` - return current date time in given format. 20 | #> See `man 3 strftime` for details. 21 | date::current_datetime() { 22 | printf -v "$1" "%($2)T" "-1" 23 | } 24 | 25 | #>> 26 | #>> * `date::print_current_datetime FORMAT` - print current date time in given format. 27 | #> See `man 3 strftime` for details. 28 | date::print_current_datetime() { 29 | printf "%($1)T" "-1" 30 | } 31 | 32 | #>> 33 | #>> * `date::datetime VARIABLE FORMAT TIMESTAMP` - return current date time in given format. 34 | #> See `man 3 strftime` for details. 35 | date::datetime() { 36 | printf -v "$1" "%($2)T" "$3" 37 | } 38 | 39 | #>> 40 | #>> * `date::print_elapsed_time` - print value of SECONDS variable in human readable form: "Elapsed time: 0 days 00:00:00." 41 | #> It's useful to know time of execution of a long script, so here is function for that. 42 | #> Assign 0 to SECONDS variable to reset counter. 43 | date::print_elapsed_time() { 44 | printf "Elapsed time: %d days %02d:%02d:%02d.\n" $((SECONDS/(24*60*60))) $(((SECONDS/(60*60))%24)) $(((SECONDS/60)%60)) $((SECONDS%60)) 45 | } 46 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/log.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `log` - various functions related to logging. 8 | 9 | #> 10 | #> ## VARIABLES 11 | 12 | #export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}}: '. 13 | 14 | #> * `__log__APP` - name of main file without path. 15 | __log__APP="${IMPORT__BIN_FILE##*/}" # Strip everything before last "/" 16 | 17 | #> * `__log__DEBUG` - set to yes to enable printing of debug messages and stacktraces. 18 | #> * `__log__STACKTRACE` - set to yes to enable printing of stacktraces. 19 | 20 | #>> 21 | #>> ## FUNCTIONS 22 | 23 | #>> 24 | #>> * `stacktrace [INDEX]` - display functions and source line numbers starting 25 | #>> from given index in stack trace, when debugging or back tracking is enabled. 26 | log::stacktrace() { 27 | [ "${__log__DEBUG:-}" != "yes" -a "${__log__STACKTRACE:-}" != "yes" ] || { 28 | local BEGIN="${1:-1}" # Display line numbers starting from given index, e.g. to skip "log::stacktrace" and "error" functions. 29 | local I 30 | for(( I=BEGIN; I<${#FUNCNAME[@]}; I++ )) 31 | do 32 | echo $'\t\t'"at ${FUNCNAME[$I]}(${BASH_SOURCE[$I]}:${BASH_LINENO[$I-1]})" >&2 33 | done 34 | echo 35 | } 36 | } 37 | 38 | #>> 39 | #>> * `error MESAGE...` - print error message and stacktrace (if enabled). 40 | error() { 41 | if [ -t 2 ] 42 | then 43 | # STDERR is tty 44 | local __log_ERROR_BEGIN=$'\033[91m' 45 | local __log_ERROR_END=$'\033[39m' 46 | echo "[$__log__APP] ${__log_ERROR_BEGIN}ERROR${__log_ERROR_END}: ${*:-}" >&2 47 | else 48 | echo "[$__log__APP] ERROR: ${*:-}" >&2 49 | fi 50 | log::stacktrace 2 51 | } 52 | 53 | #>> 54 | #>> * `warn MESAGE...` - print warning message and stacktrace (if enabled). 55 | warn() { 56 | if [ -t 2 ] 57 | then 58 | # STDERR is tty 59 | local __log_WARN_BEGIN=$'\033[96m' 60 | local __log_WARN_END=$'\033[39m' 61 | echo "[$__log__APP] ${__log_WARN_BEGIN}WARN${__log_WARN_END}: ${*:-}" >&2 62 | else 63 | echo "[$__log__APP] WARN: ${*:-}" >&2 64 | fi 65 | log::stacktrace 2 66 | } 67 | 68 | #>> 69 | #>> * `info MESAGE...` - print info message. 70 | info() { 71 | if [ -t 1 ] 72 | then 73 | # STDOUT is tty 74 | local __log_INFO_BEGIN=$'\033[92m' 75 | local __log_INFO_END=$'\033[39m' 76 | echo "[$__log__APP] ${__log_INFO_BEGIN}INFO${__log_INFO_END}: ${*:-}" 77 | else 78 | echo "[$__log__APP] INFO: ${*:-}" 79 | fi 80 | } 81 | 82 | #>> 83 | #>> * `debug MESAGE...` - print debug message, when debugging is enabled only. 84 | debug() { 85 | [ "${__log__DEBUG:-}" != yes ] || echo "[$__log__APP] DEBUG: ${*:-}" 86 | } 87 | 88 | #>> 89 | #>> * `log::fatal LEVEL MESSAGE...` - print a fatal-like LEVEL: MESSAGE to STDERR. 90 | log::fatal() { 91 | local LEVEL="$1" ; shift 92 | if [ -t 2 ] 93 | then 94 | # STDERR is tty 95 | local __log_ERROR_BEGIN=$'\033[95m' 96 | local __log_ERROR_END=$'\033[39m' 97 | echo "[$__log__APP] ${__log_ERROR_BEGIN}$LEVEL${__log_ERROR_END}: ${*:-}" >&2 98 | else 99 | echo "[$__log__APP] $LEVEL: ${*:-}" >&2 100 | fi 101 | } 102 | 103 | #>> 104 | #>> * `log::error LEVEL MESSAGE...` - print error-like LEVEL: MESSAGE to STDERR. 105 | log::error() { 106 | local LEVEL="$1" ; shift 107 | if [ -t 2 ] 108 | then 109 | # STDERR is tty 110 | local __log_ERROR_BEGIN=$'\033[91m' 111 | local __log_ERROR_END=$'\033[39m' 112 | echo "[$__log__APP] ${__log_ERROR_BEGIN}$LEVEL${__log_ERROR_END}: ${*:-}" >&2 113 | else 114 | echo "[$__log__APP] $LEVEL: ${*:-}" >&2 115 | fi 116 | } 117 | 118 | #>> 119 | #>> * `log::warn LEVEL MESSAGE...` - print warning-like LEVEL: MESSAGE to STDERR. 120 | log::warn() { 121 | local LEVEL="${1:-WARN}" ; shift 122 | if [ -t 2 ] 123 | then 124 | # STDERR is tty 125 | local __log_WARN_BEGIN=$'\033[96m' 126 | local __log_WARN_END=$'\033[39m' 127 | echo "[$__log__APP] ${__log_WARN_BEGIN}$LEVEL${__log_WARN_END}: ${*:-}" >&2 128 | else 129 | echo "[$__log__APP] $LEVEL: ${*:-}" >&2 130 | fi 131 | } 132 | 133 | #>> 134 | #>> * `log::info LEVEL MESSAGE...` - print info-like LEVEL: MESSAGE to STDOUT. 135 | log::info() { 136 | local LEVEL="${1:-INFO}" ; shift 137 | if [ -t 1 ] 138 | then 139 | # STDOUT is tty 140 | local __log_INFO_BEGIN=$'\033[92m' 141 | local __log_INFO_END=$'\033[39m' 142 | echo "[$__log__APP] ${__log_INFO_BEGIN}${LEVEL}${__log_INFO_END}: ${*:-}" 143 | else 144 | echo "[$__log__APP] ${LEVEL}: ${*:-}" 145 | fi 146 | } 147 | 148 | #>> 149 | #>> * `panic MESAGE...` - print error message and stacktrace, then exit with error code 1. 150 | panic() { 151 | log::fatal "PANIC" "${*:-}" 152 | log::enable_stacktrace 153 | log::stacktrace 2 154 | exit 1 155 | } 156 | 157 | #>> 158 | #>> * `unimplemented MESAGE...` - print error message and stacktrace, then exit with error code 42. 159 | unimplemented() { 160 | log::fatal "UNIMPLEMENTED" "${*:-}" 161 | log::enable_stacktrace 162 | log::stacktrace 2 163 | exit 42 164 | } 165 | 166 | 167 | #>> 168 | #>> * `todo MESAGE...` - print todo message and stacktrace. 169 | todo() { 170 | log::warn "TODO" "${*:-}" 171 | local __log__STACKTRACE="yes" 172 | log::stacktrace 2 173 | } 174 | 175 | #>> 176 | #>> * `dbg VARIABLE...` - print name of variable and it content to stderr 177 | dbg() { 178 | local __dbg_OUT=$( declare -p "$@" ) 179 | 180 | if [ -t 2 ] 181 | then 182 | # STDERR is tty 183 | local __log_DBG_BEGIN=$'\033[96m' 184 | local __log_DBG_END=$'\033[39m' 185 | echo "[$__log__APP] ${__log_DBG_BEGIN}DBG${__log_DBG_END}: ${__dbg_OUT//declare -? /}" >&2 186 | else 187 | echo "[$__log__APP] DBG: ${__dbg_OUT//declare -? /}" >&2 188 | fi 189 | } 190 | 191 | #>> 192 | #>> * `log::enable_debug_mode` - enable debug messages and stack traces. 193 | log::enable_debug_mode() { 194 | __log__DEBUG="yes" 195 | } 196 | 197 | #>> 198 | #>> * `log::disable_debug_mode` - disable debug messages and stack traces. 199 | log::disable_debug_mode() { 200 | __log__DEBUG="no" 201 | } 202 | 203 | #>> 204 | #>> * `log::enable_stacktrace` - enable stack traces. 205 | log::enable_stacktrace() { 206 | __log__STACKTRACE="yes" 207 | } 208 | 209 | #>> 210 | #>> * `log::disable_stacktrace` - disable stack traces. 211 | log::disable_stacktrace() { 212 | __log__STACKTRACE="no" 213 | } 214 | 215 | #>> 216 | #>> ## NOTES 217 | #>> 218 | #>> - If STDOUT is connected to tty, then 219 | #>> * info and info-like messages will be printed with message level higlighted in green, 220 | #>> * warn and warn-like messages will be printed with message level higlighted in yellow, 221 | #>> * error and error-like messages will be printed with message level higlighted in red. 222 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/meta.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `meta` - functions for working with bash functions. 8 | 9 | #>> 10 | #>> ## FUNCTIONS 11 | 12 | #>> 13 | #>> * `meta::copy_function FUNCTION_NAME NEW_FUNCTION_PREFIX` - copy function to new function with prefix in name. 14 | #> Create copy of function with new prefix. 15 | #> Old function can be redefined or `unset -f`. 16 | meta::copy_function() { 17 | local FUNCTION_NAME="$1" 18 | local PREFIX="$2" 19 | 20 | eval "$PREFIX$(declare -fp $FUNCTION_NAME)" 21 | } 22 | 23 | #>> 24 | #>> * `meta::wrap BEFORE AFTER FUNCTION_NAME[...]` - wrap function. 25 | #> Create wrapper for a function(s). Execute given commands before and after 26 | #> each function. Original function is available as meta::orig_FUNCTION_NAME. 27 | meta::wrap() { 28 | local BEFORE="$1" 29 | local AFTER="$2" 30 | shift 2 31 | 32 | local FUNCTION_NAME 33 | for FUNCTION_NAME in "$@" 34 | do 35 | # Rename original function 36 | meta::copy_function "$FUNCTION_NAME" "meta::orig_" || return 1 37 | 38 | # Redefine function 39 | eval " 40 | function $FUNCTION_NAME() { 41 | $BEFORE 42 | 43 | local __meta__EXIT_CODE=0 44 | meta::orig_$FUNCTION_NAME \"\$@\" || __meta__EXIT_CODE=\$? 45 | 46 | $AFTER 47 | 48 | return \$__meta__EXIT_CODE 49 | } 50 | " 51 | done 52 | } 53 | 54 | 55 | #>> 56 | #>> * `meta::functions_with_prefix PREFIX` - print list of functions with given prefix. 57 | meta::functions_with_prefix() { 58 | compgen -A function "$1" 59 | } 60 | 61 | #>> 62 | #>> * `meta::is_function FUNC_NAME` Checks is given name corresponds to a function. 63 | meta::is_function() { 64 | declare -F "$1" >/dev/null 65 | } 66 | 67 | #>> 68 | #>> * `meta::dispatch PREFIX COMMAND [ARGUMENTS...]` - execute function `PREFIX__COMMAND [ARGUMENTS]` 69 | #> 70 | #> For example, it can be used to execute functions (commands) by name, e.g. 71 | #> `main() { meta::dispatch command__ "$@" ; }`, when called as `man hw world` will execute 72 | #> `command_hw "$world"`. When command handler is not found, dispatcher will try 73 | #> to call `PREFIX__DEFAULT` function instead, or return error code when defaulf handler is not found. 74 | meta::dispatch() { 75 | local prefix="${1:?Prefix is required.}" 76 | local command="${2:?Command is required.}" 77 | shift 2 78 | 79 | local fn="${prefix}${command}" 80 | 81 | # Is handler function exists? 82 | meta::is_function "$fn" || { 83 | # Is default handler function exists? 84 | meta::is_function "${prefix}__DEFAULT" || { echo "ERROR: Function \"$fn\" is not found." >&2; return 1; } 85 | fn="${prefix}__DEFAULT" 86 | } 87 | 88 | "$fn" "${@:+$@}" || return $? 89 | } 90 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/renice.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `renice` - reduce priority of current shell to make it low priority task (`renice 19` to self). 8 | #>> 9 | #>> ## USAGE 10 | #>> 11 | #>> `. import.sh renice` 12 | 13 | # Run this script as low priority task 14 | renice 19 -p $$ >/dev/null 15 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/settings.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | . import.sh log arguments 5 | 6 | #>> ## NAME 7 | #>> 8 | #>>> `settigngs` - import settings from configuration files and configuration directories. 9 | #>> Also known as "configuration directory" pattern. 10 | 11 | #>> 12 | #>> ## FUNCTIONS 13 | 14 | #>> * `settings::import [-e|--ext EXTENSION] FILE|DIRECTORY...` - Import settings 15 | #> (source them into current program as shell script) when 16 | #> file or directory exists. For directories, all files with given extension 17 | #> (`".sh"` by default) are imported, without recursion. 18 | #> 19 | #> **WARNING:** this method is powerful, but unsafe, because user can put any shell 20 | #> command into the configuration file, which will be executed by script. 21 | #> 22 | #> **TODO:** implement file parsing instead of sourcing. 23 | settings::import() { 24 | local __settings_EXTENSION="sh" 25 | arguments::parse '-e|--ext)__settings_EXTENSION;String,Required' -- "$@" || panic "Cannot parse arguments." 26 | 27 | local __settings_ENTRY 28 | for __settings_ENTRY in "${@:+$@}" 29 | do 30 | if [ -f "$__settings_ENTRY" -a -r "$__settings_ENTRY" -a -s "$__settings_ENTRY" ] 31 | then 32 | # Just source configuration file into this script. 33 | source "$__settings_ENTRY" || { 34 | error "Cannot import settings from \"$__settings_ENTRY\" file: non-zero exit code returned: $?." >&2 35 | return 1 36 | } 37 | elif [ -d "$__settings_ENTRY" -a -x "$__settings_ENTRY" ] 38 | then 39 | # Just source each configuration file in the directory into this script. 40 | local __settings_FILE 41 | for __settings_FILE in "$__settings_ENTRY"/*."$__settings_EXTENSION" 42 | do 43 | if [ -f "$__settings_FILE" -a -r "$__settings_FILE" -a -s "$__settings_FILE" ] 44 | then 45 | source "$__settings_FILE" || { 46 | error "Cannot import settings from \"$__settings_FILE\" file: non-zero exit code returned: $?." >&2 47 | return 1 48 | } 49 | fi 50 | done 51 | fi 52 | done 53 | return 0 54 | } 55 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/strict.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `strict` - unofficial strict mode for bash 8 | #>> 9 | #>> Just import this module, to enabe strict mode: `set -euEo pipefail`. 10 | #> 11 | #> ## NOTE 12 | #> 13 | #> * Option `-e` is not working when command is part of a compound command, 14 | #> or in subshell. See bash manual for details. For example, `-e` may not working 15 | #> in a `for` cycle. 16 | 17 | set -euEo pipefail 18 | trap 'panic "Uncaught error."' ERR 19 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/string.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> string - various functions to manipulate strings. 8 | 9 | #>> 10 | #>> ## FUNCTIONS 11 | 12 | #>> 13 | #>> * `string::trim_spaces VARIABLE VALUE` 14 | #> Trim white space characters around value and assign result to variable. 15 | string::trim() { 16 | local -n __string__VAR="$1" 17 | local __string__VALUE="${2:-}" 18 | 19 | # remove leading whitespace characters 20 | __string__VALUE="${__string__VALUE#"${__string__VALUE%%[![:space:]]*}"}" 21 | # remove trailing whitespace characters 22 | __string__VALUE="${__string__VALUE%"${__string__VALUE##*[![:space:]]}"}" 23 | 24 | __string__VAR="$__string__VALUE" 25 | } 26 | 27 | #>> 28 | #>> * `string::trim_start VARIABLE VALUE` 29 | #> Trim white space characters at begining of the value and assign result to the variable. 30 | string::trim_start() { 31 | local -n __string__VAR="$1" 32 | local __string__VALUE="${2:-}" 33 | 34 | # remove leading whitespace characters 35 | __string__VALUE="${__string__VALUE#"${__string__VALUE%%[![:space:]]*}"}" #" 36 | 37 | __string__VAR="$__string__VALUE" 38 | } 39 | 40 | #>> 41 | #>> * `string::trim_end VARIABLE VALUE` 42 | #> Trim white space characters at the end of the value and assign result to the variable. 43 | string::trim_end() { 44 | local -n __string__VAR="$1" 45 | local __string__VALUE="${2:-}" 46 | 47 | # remove trailing whitespace characters 48 | __string__VALUE="${__string__VALUE%"${__string__VALUE##*[![:space:]]}"}" #" 49 | 50 | __string__VAR="$__string__VALUE" 51 | } 52 | 53 | #>> 54 | #>> * `string::insert VARIABLE POSITION VALUE` 55 | #> Insert `VALUE` into `VARIABLE` at given `POSITION`. 56 | #> Example: 57 | #> 58 | #> ```bash 59 | #> v="abba" 60 | #> string::insert v 2 "cc" 61 | #> # now v=="abccba" 62 | #> ``` 63 | string::insert() { 64 | local -n __string__VAR="$1" 65 | local __string__POSITION="$2" 66 | local __string__VALUE="${3:-}" 67 | 68 | __string__VALUE="${__string__VAR::$__string__POSITION}${__string__VALUE}${__string__VAR:$__string__POSITION}" 69 | 70 | __string__VAR="$__string__VALUE" 71 | } 72 | 73 | #>> 74 | #>> * `string::split_by_delimiter ARRAY DELIMITERS VALUE` 75 | #> Split value by delimiter(s) and assign result to array. Use 76 | #> backslash to escape delimiter in string. 77 | #> NOTE: Temporary file will be used. 78 | string::split_by_delimiter() { 79 | local __string__VAR="$1" 80 | local IFS="$2" 81 | local __string__VALUE="${3:-}" 82 | 83 | # We can use "for" loop and strip elements item by item, but we are 84 | # unable to assign result to named array, so we must use "read -a" and "<<<" here. 85 | 86 | # TODO: use regexp and loop instead. 87 | read -a "$__string__VAR" <<<"${__string__VALUE:-}" 88 | } 89 | 90 | #>> 91 | #>> * `string::basename VARIABLE FILE [EXT]` 92 | #> Strip path and optional extension from full file name and store 93 | #> file name in variable. 94 | string::basename() { 95 | local -n __string__VAR="$1" 96 | local __string__FILE="${2:-}" 97 | local __string__EXT="${3:-}" 98 | 99 | __string__FILE="${__string__FILE##*/}" # Strip everything before last "/" 100 | __string__FILE="${__string__FILE%$__string__EXT}" # Strip .sh extension 101 | 102 | __string__VAR="$__string__FILE" 103 | } 104 | 105 | #>> 106 | #>> * `string::dirname VARIABLE FILE` 107 | #> Strip file name from path and store directory name in variable. 108 | string::dirname() { 109 | local -n __string__VAR="$1" 110 | local __string__FILE="${2:-}" 111 | 112 | local __string__DIR="" 113 | case "$__string__FILE" in 114 | */*) 115 | __string__DIR="${__string__FILE%/*}" # Strip everything after last "/' 116 | ;; 117 | *) 118 | __string__DIR="." 119 | ;; 120 | esac 121 | 122 | __string__VAR="$__string__DIR" 123 | } 124 | 125 | #>> 126 | #>> * `string::random_string VARIABLE LENGTH` 127 | #> Generate random string of given length using [a-zA-Z0-9] 128 | #> characters and store it into variable. 129 | string::random_string() { 130 | local -n __string__VAR="$1" 131 | local __string__LENGTH="${2:-8}" 132 | 133 | local __string__ALPHABET="0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM" 134 | local __string__ALPHABET_LENGTH=${#__string__ALPHABET} 135 | 136 | local __string__I __string__RESULT="" 137 | for((__string__I=0; __string__I<__string__LENGTH; __string__I++)) 138 | do 139 | __string__RESULT="$__string__RESULT${__string__ALPHABET:RANDOM%__string__ALPHABET_LENGTH:1}" 140 | done 141 | 142 | __string__VAR="$__string__RESULT" 143 | } 144 | 145 | #>> 146 | #>> * `string::chr VARIABLE CHAR_CODE` 147 | #> Convert decimal character code to its ASCII representation. 148 | string::chr() { 149 | local __string__VAR="$1" 150 | local __string__CODE="$2" 151 | 152 | local __string__OCTAL_CODE 153 | printf -v __string__OCTAL_CODE '%03o' "$__string__CODE" 154 | printf -v "$__string__VAR" "\\$__string__OCTAL_CODE" 155 | } 156 | 157 | #>> 158 | #>> * `string::ord VARIABLE CHAR` 159 | #> Converts ASCII character to its decimal value. 160 | string::ord() { 161 | local __string__VAR="$1" 162 | local __string__CHAR="$2" 163 | 164 | printf -v "$__string__VAR" '%d' "'$__string__CHAR" 165 | } 166 | 167 | # Alternative version of function: 168 | # string::quote_to_bash_format() { 169 | # local -n __string__VAR="$1" 170 | # local __string__STRING="$2" 171 | # 172 | # local __string__QUOTE="'\\''" 173 | # local __string__QUOTE="'\"'\"'" 174 | # __string__VAR="'${__string__STRING//\'/$__string__QUOTE}'" 175 | # } 176 | 177 | #>> 178 | #>> * `string::quote_to_bash_format VARIABLE STRING` 179 | #> Quote the argument in a way that can be reused as shell input. 180 | string::quote_to_bash_format() { 181 | local __string__VAR="$1" 182 | local __string__STRING="$2" 183 | 184 | printf -v "$__string__VAR" "%q" "$__string__STRING" 185 | } 186 | 187 | #>> 188 | #>> * `string::unescape_backslash_sequences VARIABLE STRING` 189 | #> Expand backslash escape sequences. 190 | string::unescape_backslash_sequences() { 191 | local __string__VAR="$1" 192 | local __string__STRING="$2" 193 | 194 | printf -v "$__string__VAR" "%b" "$__string__STRING" 195 | } 196 | 197 | #>> 198 | #>> * `string::to_identifier VARIABLE STRING` 199 | #> Replace all non-alphanumeric characters in string by underscore character. 200 | string::to_identifier() { 201 | local -n __string__VAR="$1" 202 | local __string__STRING="$2" 203 | 204 | # We need a-zA-Z letters only. 205 | # 'z' can be in middle of alphabet on some locales. 206 | __string__VAR="${__string__STRING//[^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]/_}" 207 | } 208 | 209 | #>> 210 | #>> * `string::find_string_with_prefix VAR PREFIX [STRINGS...]` 211 | #> Find first string with given prefix and assign it to VAR. 212 | string::find_string_with_prefix() { 213 | local -n __string__VAR="$1" 214 | local __string__PREFIX="$2" 215 | shift 2 216 | 217 | local __string__I 218 | for __string__I in "$@" 219 | do 220 | [[ "${__string__I}" != "${__string__PREFIX}"* ]] || { 221 | __string__VAR="${__string__I}" 222 | return 0 223 | } 224 | done 225 | return 1 226 | } 227 | 228 | #>> 229 | #>> * `string::contains STRING SUBSTRING` 230 | #> Returns zero exit code (true), when string contains substring 231 | string::contains() { 232 | case "$1" in 233 | *"$2"*) return 0 ;; 234 | *) return 1 ;; 235 | esac 236 | } 237 | 238 | #>> 239 | #>> * `string::starts_with STRING SUBSTRING` 240 | #> Returns zero exit code (true), when string starts with substring 241 | string::starts_with() { 242 | case "$1" in 243 | "$2"*) return 0 ;; 244 | *) return 1 ;; 245 | esac 246 | } 247 | 248 | #>> 249 | #>> * `string::ends_with STRING SUBSTRING` 250 | #> Returns zero exit code (true), when string ends with substring 251 | string::ends_with() { 252 | case "$1" in 253 | *"$2") return 0 ;; 254 | *) return 1 ;; 255 | esac 256 | } 257 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/timestamped_log.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | # Import log module and then override some functions 6 | . import.sh log date meta 7 | 8 | #>> ## NAME 9 | #>> 10 | #>>> `timestamped_log` - print timestamped logs. Drop-in replacement for `log` module. 11 | 12 | #> 13 | #> ## VARIABLES 14 | 15 | #> 16 | #> * `__timestamped_log_format` - format of timestamp. Default value: "%F %T" (full date and time). 17 | __timestamped_log_format="%F %T " 18 | 19 | 20 | #>> 21 | #>> ## FUNCTIONS 22 | 23 | #>> 24 | #>> * `timestamped_log::set_format FORMAT` - Set format for date. Default value is "%F %T". 25 | timestamped_log::set_format() { 26 | __timestamped_log_format="$1" 27 | } 28 | 29 | #>> 30 | #>> ## Wrapped functions: 31 | #>> 32 | #>> `log::info`, `info`, `debug` - print timestamp to stdout and then log message. 33 | meta::wrap \ 34 | 'date::print_current_datetime "$__timestamped_log_format"' \ 35 | '' \ 36 | log::info \ 37 | info \ 38 | debug 39 | 40 | #>> 41 | #>> `log::error`, `log::warn`, `error`, `warn` - print timestamp to stderr and then log message. 42 | meta::wrap \ 43 | 'date::print_current_datetime "$__timestamped_log_format" >&2' \ 44 | '' \ 45 | log::error \ 46 | log::warn \ 47 | error \ 48 | warn \ 49 | panic 50 | 51 | #>> 52 | #>> ## NOTES 53 | #>> 54 | #>> See `log` module usage for details about log functions. Original functions 55 | #>> are available with prefix `"timestamped_log::orig_"`. 56 | -------------------------------------------------------------------------------- /bash-modules/src/bash-modules/unit.sh: -------------------------------------------------------------------------------- 1 | ##!/bin/bash 2 | # Copyright (c) 2009-2021 Volodymyr M. Lisivka , All Rights Reserved 3 | # License: LGPL2+ 4 | 5 | #>> ## NAME 6 | #>> 7 | #>>> `unit` - functions for unit testing. 8 | 9 | . import.sh log arguments 10 | 11 | #>> 12 | #>> ## FUNCTIONS 13 | 14 | #>> 15 | #>> * `unit::assert_yes VALUE [MESSAGE]` - Show error message, when `VALUE` is not equal to `"yes"`. 16 | unit::assert_yes() { 17 | local VALUE="${1:-}" 18 | local MESSAGE="${2:-Value is not \"yes\".}" 19 | 20 | [ "${VALUE:-}" == "yes" ] || { 21 | log::error "ASSERT FAILED" "$MESSAGE" 22 | exit 1 23 | } 24 | } 25 | 26 | #>> 27 | #>> * `unit::assert_no VALUE [MESSAGE]` - Show error message, when `VALUE` is not equal to `"no"`. 28 | unit::assert_no() { 29 | local VALUE="$1" 30 | local MESSAGE="${2:-Value is not \"no\".}" 31 | 32 | [ "$VALUE" == "no" ] || { 33 | log::error "ASSERT FAILED" "$MESSAGE" 34 | exit 1 35 | } 36 | } 37 | 38 | #>> 39 | #>> * `unit::assert_not_empty VALUE [MESSAGE]` - Show error message, when `VALUE` is empty. 40 | unit::assert_not_empty() { 41 | local VALUE="${1:-}" 42 | local MESSAGE="${2:-Value is empty.}" 43 | 44 | [ -n "${VALUE:-}" ] || { 45 | log::error "ASSERT FAILED" "$MESSAGE" 46 | exit 1 47 | } 48 | } 49 | 50 | #>> 51 | #>> * `unit::assert_equal ACTUAL EXPECTED [MESSAGE]` - Show error message, when values are not equal. 52 | unit::assert_equal() { 53 | local ACTUAL="${1:-}" 54 | local EXPECTED="${2:-}" 55 | local MESSAGE="${3:-Values are not equal.}" 56 | 57 | [ "${ACTUAL:-}" == "${EXPECTED:-}" ] || { 58 | log::error "ASSERT FAILED" "$MESSAGE Actual value: \"${ACTUAL:-}\", expected value: \"${EXPECTED:-}\"." 59 | exit 1 60 | } 61 | } 62 | 63 | #>> 64 | #>> * `unit::assert_arrays_are_equal MESSAGE VALUE1... -- VALUE2...` - Show error message when arrays are not equal in size or content. 65 | unit::assert_arrays_are_equal() { 66 | local MESSAGE="${1:-Arrays are not equal.}" ; shift 67 | local ARGS=( $@ ) 68 | 69 | local I LEN1='' 70 | for((I=0;I<${#ARGS[@]};I++)) 71 | do 72 | [ "${ARGS[I]}" != "--" ] || { 73 | LEN1="$I" 74 | break 75 | } 76 | done 77 | 78 | [ -n "${LEN1:-}" ] || { 79 | error "Array separator is not found. Put \"--\" between two arrays." 80 | exit 1 81 | } 82 | 83 | local LEN2=$(($# - LEN1 - 1)) 84 | local MIN=$(( (LEN1> 103 | #>> * `unit::assert_not_equal ACTUAL_VALUE UNEXPECTED_VALUE [MESSAGE]` - Show error message, when values ARE equal. 104 | unit::assert_not_equal() { 105 | local ACTUAL_VALUE="${1:-}" 106 | local UNEXPECTED_VALUE="${2:-}" 107 | local MESSAGE="${3:-values are equal but must not.}" 108 | 109 | [ "${ACTUAL_VALUE:-}" != "${UNEXPECTED_VALUE:-}" ] || { 110 | log::error "ASSERT FAILED" "$MESSAGE Actual value: \"${ACTUAL_VALUE:-}\", unexpected value: \"$UNEXPECTED_VALUE\"." 111 | exit 1 112 | } 113 | } 114 | 115 | #>> 116 | #>> * `unit::assert MESSAGE TEST[...]` - Evaluate test and show error message when it returns non-zero exit code. 117 | unit::assert() { 118 | local MESSAGE="${1:-}"; shift 119 | 120 | eval "$@" || { 121 | log::error "ASSERT FAILED" "${MESSAGE:-}: $@" 122 | exit 1 123 | } 124 | } 125 | 126 | #>> 127 | #>> * `unit::fail [MESSAGE]` - Show error message. 128 | unit::fail() { 129 | local MESSAGE="${1:-This point in test case must not be reached.}"; shift 130 | log::error "FAIL" "$MESSAGE $@" 131 | exit 1 132 | } 133 | 134 | #>> 135 | #>> * `unit::run_test_cases [OPTIONS] [--] [ARGUMENTS]` - Execute all functions with 136 | #>> test* prefix in name in alphabetic order 137 | #> 138 | #> * OPTIONS: 139 | #> * `-t | --test TEST_CASE` - execute single test case, 140 | #> * `-q | --quiet` - do not print informational messages and dots, 141 | #> * `--debug` - enable stack traces. 142 | #> * ARGUMENTS - All arguments, which are passed to run_test_cases, are passed then 143 | #> to `unit::set_up`, `unit::tear_down` and test cases using `ARGUMENTS` array, so you 144 | #> can parametrize your test cases. You can call `run_test_cases` more than 145 | #> once with different arguments. Use `"--"` to strictly separate arguments 146 | #> from options. 147 | #> 148 | #> After execution of `run_test_cases`, following variables will have value: 149 | #> 150 | #> * `NUMBER_OF_TEST_CASES` - total number of test cases executed, 151 | #> * `NUMBER_OF_FAILED_TEST_CASES` - number of failed test cases, 152 | #> * `FAILED_TEST_CASES` - names of functions of failed tests cases. 153 | #> 154 | #> 155 | #> If you want to ignore some test case, just prefix them with 156 | #> underscore, so `unit::run_test_cases` will not see them. 157 | #> 158 | #> If you want to run few subsets of test cases in one file, define each 159 | #> subset in it own subshell and execute `unit::run_test_cases` in each subshell. 160 | #> 161 | #> Each test case is executed in it own subshell, so you can call `exit` 162 | #> in the test case or assign variables without any effect on subsequent test 163 | #> cases. 164 | unit::run_test_cases() { 165 | 166 | NUMBER_OF_TEST_CASES=0 167 | NUMBER_OF_FAILED_TEST_CASES=0 168 | FAILED_TEST_CASES=( ) 169 | 170 | local __QUIET=no __TEST_CASES=( ) 171 | 172 | arguments::parse \ 173 | "-t|test)__TEST_CASES;Array" \ 174 | "-q|--quiet)__QUIET;Yes" \ 175 | -- "$@" || panic "Cannot parse arguments. Arguments: $*" 176 | 177 | # If no test cases are given via options 178 | [ "${#__TEST_CASES[@]}" -gt 0 ] || { 179 | # Then generate list of test cases using compgen 180 | # As alternative, declare -F | cut -d ' ' -f 3 | grep '^test' can be used 181 | __TEST_CASES=( $(compgen -A function test) ) || panic "No test cases are found. Create a function with test_ prefix in the name." 182 | } 183 | 184 | local __TEST __EXIT_CODE=0 185 | 186 | ( set -ueEo pipefail ; FIRST_TEAR_DOWN=yes ; unit::tear_down "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" ) || { 187 | __EXIT_CODE=$? 188 | log::error "FAIL" "tear_down before first test case is failed." 189 | } 190 | 191 | for __TEST in "${__TEST_CASES[@]:+${__TEST_CASES[@]}}" 192 | do 193 | let NUMBER_OF_TEST_CASES++ || : 194 | [ "$__QUIET" == "yes" ] || echo -n "." 195 | 196 | ( 197 | __EXIT_CODE=0 198 | 199 | unit::set_up "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" || { 200 | __EXIT_CODE=$? 201 | unit::fail "unit::set_up failed before test case #$NUMBER_OF_TEST_CASES ($__TEST)." 202 | } 203 | 204 | ( "$__TEST" "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" ) || { 205 | __EXIT_CODE=$? 206 | unit::fail "Test case #$NUMBER_OF_TEST_CASES ($__TEST) failed." 207 | } 208 | 209 | unit::tear_down "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" || { 210 | __EXIT_CODE=$? 211 | unit::fail "unit::tear_down failed after test case #$NUMBER_OF_TEST_CASES ($__TEST)." 212 | } 213 | exit $__EXIT_CODE # Exit from subshell 214 | ) || { 215 | __EXIT_CODE=$? 216 | let NUMBER_OF_FAILED_TEST_CASES++ || : 217 | FAILED_TEST_CASES[${#FAILED_TEST_CASES[@]}]="$__TEST" 218 | } 219 | done 220 | 221 | [ "$__QUIET" == "yes" ] || echo 222 | if [ "$__EXIT_CODE" -eq 0 ] 223 | then 224 | [ "$__QUIET" == "yes" ] || log::info "OK" "Test cases total: $NUMBER_OF_TEST_CASES, failed: $NUMBER_OF_FAILED_TEST_CASES${FAILED_TEST_CASES[@]:+, failed methods: ${FAILED_TEST_CASES[@]}}." 225 | else 226 | log::error "FAIL" "Test cases total: $NUMBER_OF_TEST_CASES, failed: $NUMBER_OF_FAILED_TEST_CASES${FAILED_TEST_CASES[@]:+, failed methods: ${FAILED_TEST_CASES[@]}}." 227 | fi 228 | 229 | return $__EXIT_CODE 230 | } 231 | 232 | #> 233 | #> `unit::run_test_cases` will also call `unit::set_up` and `unit::tear_down` 234 | #> functions before and after each test case. By default, they do nothing. 235 | #> Override them to do something useful. 236 | 237 | #>> 238 | #>> * `unit::set_up` - can set variables which are available for following 239 | #>> test case and `tear_down`. It also can alter `ARGUMENTS` array. Test case 240 | #>> and tear_down are executed in their own subshell, so they cannot change 241 | #>> outer variables. 242 | unit::set_up() { 243 | return 0 244 | } 245 | 246 | #>> 247 | #>> * `unit::tear_down` is called first, before first set_up of first test case, to 248 | #>> cleanup after possible failed run of previous test case. When it 249 | #>> called for first time, `FIRST_TEAR_DOWN` variable with value `"yes"` is 250 | #>> available. 251 | unit::tear_down() { 252 | return 0 253 | } 254 | 255 | 256 | #> 257 | #> ## NOTES 258 | #> 259 | #> All assert functions are executing `exit` instead of returning error code. 260 | -------------------------------------------------------------------------------- /bash-modules/test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict log 6 | 7 | EXIT_CODE=0 8 | 9 | for I in "$APP_DIR"/test_*.sh 10 | do 11 | bash -ue "$I" "$@" || { 12 | EXIT_CODE=$? 13 | } 14 | done 15 | 16 | exit $EXIT_CODE 17 | -------------------------------------------------------------------------------- /bash-modules/test/test_arguments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict log unit arguments 6 | 7 | 8 | ############################################### 9 | # Test cases 10 | 11 | test_yes_option() { 12 | local FOO="no" 13 | 14 | arguments::parse '--foo)FOO;Yes' -- --foo || { 15 | error "Cannot parse Yes option." 16 | return 1 17 | } 18 | 19 | unit::assert_equal "$FOO" "yes" "Yes option parsed incorrectly." 20 | } 21 | 22 | test_no_option() { 23 | local FOO="yes" 24 | 25 | arguments::parse '--foo)FOO;No' -- --foo || { 26 | error "Cannot parse No option." 27 | return 1 28 | } 29 | 30 | unit::assert_equal "$FOO" "no" "No option parsed incorrectly." 31 | } 32 | 33 | test_command_option() { 34 | local FOO="" 35 | 36 | arguments::parse 'b|foo|bar)FOO;Command' -- foo || { 37 | error "Cannot parsee Command option." 38 | return 1 39 | } 40 | 41 | unit::assert_equal "$FOO" "foo" "Command option parsed incorrectly." 42 | } 43 | 44 | test_string_option() { 45 | local FOO="" BAR="" BAZ="" 46 | 47 | arguments::parse '--foo)FOO;S' '--bar)BAR;Str' '--baz)BAZ;String' -- --foo foo --bar=bar --baz baz || { 48 | error "Cannot parse String option." 49 | return 1 50 | } 51 | 52 | unit::assert_equal "$FOO" "foo" "String option foo parsed incorrectly." 53 | unit::assert_equal "$BAR" "bar" "String option bar parsed incorrectly." 54 | unit::assert_equal "$BAZ" "baz" "String option baz parsed incorrectly." 55 | } 56 | 57 | test_string_option_with_multiple_variants() { 58 | local FOO="" BAR="" BAZ="" 59 | 60 | arguments::parse '-f|F|--foo)FOO;S' '-b|BAR|--bar)BAR;Str' '-B|--baz)BAZ;String' -- F foo -b=bar -B baz || { 61 | error "Cannot parse String option." 62 | return 1 63 | } 64 | 65 | unit::assert_equal "$FOO" "foo" "String option foo parsed incorrectly." 66 | unit::assert_equal "$BAR" "bar" "String option bar parsed incorrectly." 67 | unit::assert_equal "$BAZ" "baz" "String option baz parsed incorrectly." 68 | } 69 | 70 | test_number_option() { 71 | local FOO="" BAR="" BAZ="" 72 | 73 | arguments::parse '--foo)FOO;N' '--bar)BAR;Num,(( BAR >= 2 ))' '--baz)BAZ;Number,Required' -- --foo 1 --bar 2 --baz 3 || { 74 | error "Cannot parse Number option." 75 | return 1 76 | } 77 | 78 | unit::assert_equal "$FOO" "1" "Numeric option foo parsed incorrectly." 79 | unit::assert_equal "$BAR" "2" "Numeric option bar parsed incorrectly." 80 | unit::assert_equal "$BAZ" "3" "Numeric option baz parsed incorrectly." 81 | } 82 | 83 | test_required_option() { 84 | local FOO="" BAR="" BAZ="" 85 | 86 | arguments::parse '--foo)FOO;Str,Req' -- 2>/dev/null && { 87 | error "Function must return error code when required option is missed." 88 | return 1 89 | } || : 90 | } 91 | 92 | test_option_postcondition() { 93 | local FOO="" BAR="" BAZ="" 94 | 95 | arguments::parse '--foo)FOO;Num,Req,(( FOO > 2 ))' '--bar)BAR;Num,Req, (( BAR > 1 )), (( BAR > FOO ))' -- --foo 3 --bar 4 || { 96 | error "Cannot parse Number option with option postcondition." 97 | return 1 98 | } 99 | 100 | unit::assert_equal "$FOO" "3" "Numeric option foo parsed incorrectly." 101 | unit::assert_equal "$BAR" "4" "Numeric option bar parsed incorrectly." 102 | } 103 | 104 | test_option_postcondition_failed() { 105 | local FOO="" BAR="" BAZ="" 106 | 107 | arguments::parse '--foo)FOO;Num,Req,(( FOO > 2 ))' -- --foo 2 2>/dev/null && { 108 | error "Function must return error code when option postcondition is failed." 109 | return 1 110 | } || : 111 | } 112 | 113 | test_array_argument() { 114 | local FOO=( ) 115 | 116 | arguments::parse '-f|--foo)FOO;A' -- -f bar --foo=baz || { 117 | error "Cannot parse Array option." 118 | return 1 119 | } 120 | 121 | unit::assert_equal "${#FOO[@]}" "2" "Array option parsed incorrectly: wrong count of elements." 122 | unit::assert_equal "${FOO[0]}" "bar" "Array option parsed incorrectly: wrong first value." 123 | unit::assert_equal "${FOO[1]}" "baz" "Array option parsed incorrectly: wrong second value." 124 | } 125 | 126 | test_incremental_argument() { 127 | local FOO=0 128 | 129 | arguments::parse '-f|-fo|--foo)FOO;I' -- -f --foo -fo || { 130 | error "Cannot parse array argument." 131 | return 1 132 | } 133 | 134 | unit::assert_equal "$FOO" "3" "Incremental option parsed incorrectly: wrong count of options." 135 | } 136 | 137 | unit::run_test_cases "$@" 138 | -------------------------------------------------------------------------------- /bash-modules/test/test_cd_to_bindir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH=( $(readlink -f "$APP_DIR/../src/bash-modules") ) 4 | export PATH="$(readlink -f "$APP_DIR/../src"):$PATH" 5 | . import.sh strict log unit 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | SCRIPT_DIR=$(mktemp -d) 11 | SCRIPT_NAME="test_cd_to_bindir-test-script.sh" 12 | 13 | [ -d "$SCRIPT_DIR" ] || panic "Temporary directory is not created." 14 | 15 | unit::set_up() { 16 | mkdir -p "$SCRIPT_DIR" || panic "Cannot create temporary directory." 17 | 18 | cat > "$SCRIPT_DIR/$SCRIPT_NAME" <"$DIR/a_mod_1.sh" <>> A test 1 module one-line summary. 28 | 29 | #>> 30 | #>> A test function 1 desciption. 31 | fn1() { 32 | echo "This is fn1." 33 | } 34 | 35 | #>> 36 | #>> A test function 12 desciption. 37 | fn12() { 38 | echo "This is fn12." 39 | [[ "\${1:-}" == "stop" ]] || fn21 stop 40 | } 41 | 42 | #> 43 | #> A detailed documentation about test module 1. 44 | #> 45 | 46 | #a_prefix Alternative documentation 1. 47 | END_OF_MODULE 48 | 49 | # Create a test module 2. 50 | cat >"$DIR/a_mod_2.sh" <>> A test 2 module one-line summary. 54 | 55 | #>> 56 | #>> A test function 2 desciption. 57 | fn2() { 58 | echo "This is fn2." 59 | } 60 | 61 | #>> 62 | #>> A test function 21 desciption. 63 | fn21() { 64 | echo "This is fn21." 65 | [[ "\${1:-}" == "stop" ]] || fn12 stop 66 | } 67 | 68 | #> 69 | #> A detailed documentation about test module 2. 70 | #> 71 | END_OF_MODULE 72 | 73 | 74 | } 75 | 76 | tear_down() { 77 | rm -rf "$DIR" || panic "Cannot remove \"$DIR\" directory." 78 | } 79 | 80 | # Test the import of a module by BASH_MODULES_PATH 81 | test_bash_modules_path() { 82 | . import.sh a_mod_1 || panic "Cannot import module a_mod_1." 83 | 84 | # Check content of module lookup path array 85 | local output=$(dbg __IMPORT__BASE_PATH 2>&1) 86 | local expected_output="[test_import.sh] DBG: __IMPORT__BASE_PATH=([0]=\"$DIR\" [1]=\"/usr/share/bash-modules\")" 87 | 88 | [[ "$output" == "$expected_output" ]] || panic "Unexpected value of __IMPORT__BASE_PATH: $output" 89 | } 90 | 91 | # TODO: Test that import path doesn't contain current directory by default. 92 | 93 | test_import_of_modules() { 94 | . import.sh a_mod_1 a_mod_2 || panic "Cannot import module a_mod_1 or a_mod_2." 95 | } 96 | 97 | test_import_of_non_existing_module() { 98 | local output=$(. import.sh non_existing_mod 2>&1 >/dev/null && panic "import.sh must return an error, when module doesn't exists. Actual exit code: $?." ) 99 | local expected_output="[import.sh:import_module] ERROR: Cannot locate module: \"non_existing_mod\". Search path: $DIR /usr/share/bash-modules" 100 | 101 | [[ "$output" == "$expected_output" ]] || panic "Unexpected error message returned by import.sh after trying to import non-existing module: \"$output\"." 102 | } 103 | 104 | test_import_of_module_from_subdir() { 105 | local subdir="$DIR/subdir" 106 | mkdir -p "$subdir" || panic "Cannot create temporary directory \"$subdir\"." 107 | 108 | # Create empty "module" in the subdirectory 109 | echo "# An empty module" > "$subdir/an_empty_mod.sh" 110 | 111 | # Try to import it 112 | . import.sh subdir/an_empty_mod || panic "Cannot import module subdir/an_empty_mod." 113 | } 114 | 115 | test_import_of_module_with_bad_symbols() { 116 | 117 | # Create empty "module" with bad symbols in bame in the directory 118 | echo "# An empty module" > "$DIR/a-b@d-module.sh" 119 | echo "# An empty module" > "$DIR/[]{}!@#\$%^&*()=+~\`\\,?|'\"-.sh" 120 | 121 | # Try to import it 122 | . import.sh "a-b@d-module" || panic "Cannot import module \"a-b@d-module\"." 123 | . import.sh "[]{}!@#\$%^&*()=+~\`\\,?|'\"-" || panic "Cannot import module \"[]{}!@#\$%^&*()=+~\`\\,?|'\\\"-\"." 124 | } 125 | 126 | test_multiple_dirs_in_bash_module_path() { 127 | local subdir1="$DIR/subdir1" 128 | local subdir2="$DIR/subdir2" 129 | 130 | mkdir -p "$subdir1" || panic "Cannot create temporary directory \"$subdir1\"." 131 | mkdir -p "$subdir2" || panic "Cannot create temporary directory \"$subdir2\"." 132 | 133 | # Create empty "module" in the subdirectories 134 | echo "# An empty module 1" > "$subdir1/an_empty_mod_1.sh" 135 | echo "# An empty module 2" > "$subdir2/an_empty_mod_2.sh" 136 | 137 | BASH_MODULES_PATH="$DIR:$subdir1:$subdir2" 138 | 139 | . import.sh a_mod_1 an_empty_mod_1 an_empty_mod_2 || panic "Cannot import module a_mod_1 or an_empty_mod_1 or an_empty_mod_2 when using BASH_MODULES_PATH with multiple directories." 140 | } 141 | 142 | test_that_import_path_contains_no_empty_element_by_default() { 143 | unset BASH_MODULES_PATH 144 | . import.sh || panic "Cannot import nothing." 145 | 146 | # Check content of module lookup path array 147 | local output=$(dbg __IMPORT__BASE_PATH 2>&1) 148 | [[ "$output" == "[test_import.sh] DBG: __IMPORT__BASE_PATH=([0]=\"/usr/share/bash-modules\")" ]] \ 149 | || panic "Unexpected value of __IMPORT__BASE_PATH: $output" 150 | } 151 | 152 | test_module_list() { 153 | export BASH_MODULES_PATH 154 | 155 | local output="$(import.sh -l | grep -v /usr/share/bash-modules || panic "Cannot show list of modules")" 156 | local expected_output="a_mod_1 "$'\t'"$DIR/a_mod_1.sh 157 | a_mod_2 "$'\t'"$DIR/a_mod_2.sh" 158 | 159 | [[ "$output" == "$expected_output" ]] || panic "Unexpected output: \"$output\"." 160 | } 161 | 162 | todo_test_module_summary() { 163 | export BASH_MODULES_PATH 164 | 165 | # TODO: How to filter out summaries of standard modules? 166 | local output="$(import.sh -s | grep -v /usr/share/bash-modules || panic "Cannot show summary for modules")" 167 | local expected_output="" 168 | 169 | [[ "$output" == "$expected_output" ]] || panic "Unexpected output: \"$output\"." 170 | } 171 | 172 | test_module_usage() { 173 | export BASH_MODULES_PATH 174 | 175 | local output="$(import.sh -u a_mod_1)" 176 | local expected_output="A test 1 module one-line summary. 177 | 178 | A test function 1 desciption. 179 | 180 | A test function 12 desciption." 181 | 182 | [[ "$output" == "$expected_output" ]] || panic "Unexpected output: \"$output\"." 183 | } 184 | 185 | test_module_documentation() { 186 | export BASH_MODULES_PATH 187 | 188 | local output="$(import.sh --doc a_mod_1)" 189 | local expected_output="A test 1 module one-line summary. 190 | 191 | A test function 1 desciption. 192 | 193 | A test function 12 desciption. 194 | 195 | A detailed documentation about test module 1." 196 | 197 | [[ "$output" == "$expected_output" ]] || panic "Unexpected output: \"$output\"." 198 | } 199 | 200 | test_alternatvive_module_documentation() { 201 | export BASH_MODULES_PATH 202 | . import.sh 203 | 204 | local output="$(import::show_documentation "#a_prefix" "$DIR/a_mod_1.sh")" 205 | local expected_output="Alternative documentation 1." 206 | 207 | [[ "$output" == "$expected_output" ]] || panic "Unexpected output: \"$output\"." 208 | } 209 | 210 | 211 | run_test_cases() { 212 | local exit_code=0 213 | local test_cases=( $(compgen -A function test) ) 214 | 215 | set_up || panic "set_up failed." 216 | 217 | for test_case in "${test_cases[@]}" 218 | do 219 | ( 220 | set_up || panic "set_up failed." 221 | 222 | ( "$test_case" ) || { error "Test \"$test_case\" failed." ; exit_code=1 ; } 223 | 224 | echo -n "." 225 | 226 | tear_down || panic "tear_down failed." 227 | ) 228 | done 229 | echo 230 | 231 | (( $exit_code != 0 )) || info "OK." 232 | 233 | return $exit_code 234 | } 235 | 236 | run_test_cases 237 | exit 238 | -------------------------------------------------------------------------------- /bash-modules/test/test_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH=( $(readlink -f "$APP_DIR/../src/bash-modules") ) 4 | export PATH="$(readlink -f "$APP_DIR/.."):$(readlink -f "$APP_DIR/../src"):$PATH" 5 | . import.sh strict log unit 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | DIR=$(mktemp -d) 11 | 12 | [ -d "$DIR" ] || panic "Temporary directory is not created." 13 | 14 | unit::set_up() { 15 | mkdir -p "$DIR" || panic "Cannot create temporary directory." 16 | } 17 | 18 | unit::tear_down() { 19 | rm -rf "$DIR" || panic "Cannot remove \"$DIR\" directory." 20 | } 21 | 22 | test_install_to_custom_directory() { 23 | local output="$(install.sh --prefix "$DIR")" 24 | local expected_output="Modules directory: \"$DIR/share/bash-modules\". 25 | Bin directory: \"$DIR/bin\"." 26 | 27 | unit::assert_equal "$output" "$expected_output" "Unexpected output from install.sh command." 28 | 29 | [ -s "$DIR/bin/import.sh" ] || unit::fail "Script import.sh does not exists or empty after installation." 30 | [ -x "$DIR/bin/import.sh" ] || unit::fail "Script import.sh must have executable permission set after installation." 31 | [ -s "$DIR/share/bash-modules/log.sh" ] || unit::fail "Module log is not exists or empty after installation." 32 | } 33 | 34 | test_install_to_custom_directories() { 35 | local output="$(install.sh --bin-dir "$DIR/main" --modules-dir "$DIR/modules")" 36 | local expected_output="Modules directory: \"$DIR/modules\". 37 | Bin directory: \"$DIR/main\"." 38 | 39 | unit::assert_equal "$output" "$expected_output" "Unexpected output from install.sh command." 40 | 41 | [ -s "$DIR/main/import.sh" ] || unit::fail "Script import.sh does not exists or empty after installation." 42 | [ -x "$DIR/main/import.sh" ] || unit::fail "Script import.sh must have executable permission set after installation." 43 | [ -s "$DIR/modules/log.sh" ] || unit::fail "Module log is not exists or empty after installation." 44 | } 45 | 46 | unit::run_test_cases "$@" 47 | -------------------------------------------------------------------------------- /bash-modules/test/test_log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict log unit 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | test_info() { 11 | unit::assert_equal "$( info Test 2>/dev/null )" "[test_log.sh] INFO: Test" 12 | } 13 | 14 | test_warn() { 15 | unit::assert_equal "$( warn Test 2>&1 1>/dev/null )" "[test_log.sh] WARN: Test" 16 | } 17 | 18 | test_error() { 19 | unit::assert_equal "$( error Test 2>&1 1>/dev/null )" "[test_log.sh] ERROR: Test" 20 | } 21 | 22 | test_log_info() { 23 | unit::assert_equal "$( log::info foo Test 2>/dev/null )" "[test_log.sh] foo: Test" 24 | } 25 | 26 | test_log_warn() { 27 | unit::assert_equal "$( log::warn foo Test 2>&1 1>/dev/null )" "[test_log.sh] foo: Test" 28 | } 29 | 30 | test_log_error() { 31 | unit::assert_equal "$( log::error foo Test 2>&1 1>/dev/null )" "[test_log.sh] foo: Test" 32 | } 33 | 34 | test_log_fatal() { 35 | unit::assert_equal "$( log::fatal foo Test 2>&1 1>/dev/null | head -n 1)" "[test_log.sh] foo: Test" 36 | } 37 | 38 | test_panic() { 39 | unit::assert_equal "$( panic Test 2>&1 1>/dev/null | head -n 1)" "[test_log.sh] PANIC: Test" 40 | } 41 | 42 | test_unimplemented() { 43 | unit::assert_equal "$( unimplemented Test 2>&1 1>/dev/null | head -n 1)" "[test_log.sh] UNIMPLEMENTED: Test" 44 | } 45 | 46 | test_todo() { 47 | unit::assert_equal "$( todo Test 2>&1 1>/dev/null | head -n 1)" "[test_log.sh] TODO: Test" 48 | } 49 | 50 | test_dbg() { 51 | local foo="bar" 52 | unit::assert_equal "$( dbg foo 2>&1 )" '[test_log.sh] DBG: foo="bar"' 53 | } 54 | 55 | 56 | unit::run_test_cases "$@" 57 | -------------------------------------------------------------------------------- /bash-modules/test/test_meta.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict log unit meta 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | test_copy_function() { 11 | a_fn() { 12 | return 42 13 | } 14 | meta::copy_function a_fn prefix_ 15 | 16 | local exit_code=0 17 | prefix_a_fn || exit_code=$? 18 | 19 | unit::assert_equal "$exit_code" "42" "Unexpected return code is returned by copy of the function." 20 | } 21 | 22 | test_wrap() { 23 | a_fn() { 24 | return 42 25 | } 26 | meta::wrap "true" "true" a_fn 27 | 28 | local exit_code=0 29 | a_fn || exit_code=$? 30 | 31 | unit::assert_equal "$exit_code" "42" "Unexpected return code is returned by wrapped function." 32 | 33 | meta::orig_a_fn || exit_code=$? 34 | 35 | unit::assert_equal "$exit_code" "42" "Unexpected return code is returned by the original function." 36 | } 37 | 38 | test_wrap_code() { 39 | a_fn() { 40 | echo -n " body " 41 | } 42 | meta::wrap "echo -n before" "echo -n after" a_fn 43 | 44 | local output="$(a_fn)" 45 | local expected_output="before body after" 46 | 47 | unit::assert_equal "$output" "$expected_output" "Unexpected output from wrapped function." 48 | } 49 | 50 | 51 | test_functions_with_prefix() { 52 | pr_1() { :; } 53 | pr_2() { :; } 54 | pr_3() { :; } 55 | 56 | local output="$( meta::functions_with_prefix pr_ )" 57 | local expected_output="pr_1 58 | pr_2 59 | pr_3" 60 | 61 | unit::assert_equal "$output" "$expected_output" "Unexpected output from meta::functions_with_prefix." 62 | } 63 | 64 | test_is_function() { 65 | unit::assert "is_function FUNCTION must return true" meta::is_function test_is_function 66 | unit::assert "is_function FUNCTION must return true" meta::is_function meta::is_function 67 | 68 | local a_var="foo" 69 | unit::assert "is_function VAR must return false" ! meta::is_function a_var 70 | 71 | unit::assert "is_function SHELL_BUILTIN must return false" ! meta::is_function cd 72 | unit::assert "is_function COMMAND must return false" ! meta::is_function /usr/bin/ls 73 | } 74 | 75 | test_dispatch() { 76 | 77 | local fresult="" 78 | 79 | f1() { 80 | fresult="f1 $*" 81 | } 82 | f2() { 83 | fresult="f2 $*" 84 | } 85 | 86 | meta::dispatch f 1 foo bar || unit::fail "Cannot dispatch to function." 87 | unit::assert_equal "$fresult" "f1 foo bar" "Unexpected result of call to f1 via dispatch." 88 | 89 | meta::dispatch f 2 bar baz || unit::fail "Cannot dispatch to function." 90 | unit::assert_equal "$fresult" "f2 bar baz" "Unexpected result of call to f2 via dispatch." 91 | 92 | meta::dispatch f 3 bar baz 2>/dev/null && unit::fail "Dispatch function must not call an non-existing function or a command." || : 93 | meta::dispatch l s bar baz 2>/dev/null && unit::fail "Dispatch function must not call an non-existing function or a command." || : 94 | meta::dispatch c d bar baz 2>/dev/null && unit::fail "Dispatch function must not call an non-existing function or a command." || : 95 | } 96 | 97 | 98 | test_dispatch_with_default_handler() { 99 | 100 | local fresult="" 101 | 102 | f1() { 103 | fresult="f1 $*" 104 | } 105 | f__DEFAULT() { 106 | fresult="fD $*" 107 | } 108 | 109 | meta::dispatch f 1 foo bar || unit::fail "Cannot dispatch to function." 110 | unit::assert_equal "$fresult" "f1 foo bar" "Unexpected result of call to f1 via dispatch." 111 | 112 | meta::dispatch f 2 bar baz || unit::fail "Cannot dispatch to function." 113 | unit::assert_equal "$fresult" "fD bar baz" "Unexpected result of call to f__DEFAULT via dispatch." 114 | } 115 | 116 | unit::run_test_cases "$@" 117 | -------------------------------------------------------------------------------- /bash-modules/test/test_settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict log unit settings 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | test_import_from_single_file() { 11 | local FILE="$(mktemp)" 12 | echo "TEST=foo" >"$FILE" 13 | 14 | settings::import "$FILE" 15 | 16 | unit::assert_equal "${TEST:-}" "foo" 17 | 18 | rm -f "$FILE" 19 | } 20 | 21 | test_import_from_multiple_files() { 22 | local FILE1="$(mktemp)" 23 | echo "TEST1=foo" >"$FILE1" 24 | local FILE2="$(mktemp)" 25 | echo "TEST2=bar" >"$FILE2" 26 | 27 | settings::import "$FILE1" "$FILE2" 28 | 29 | unit::assert_equal "${TEST1:-}" "foo" 30 | unit::assert_equal "${TEST2:-}" "bar" 31 | 32 | rm -f "$FILE1" "$FILE2" 33 | } 34 | 35 | test_import_from_dir() { 36 | local DIR="$(mktemp -d)" 37 | local FILE1="$DIR/file1.sh" 38 | echo "TEST1=foo" >"$FILE1" 39 | local FILE2="$DIR/file2.sh" 40 | echo "TEST2=bar" >"$FILE2" 41 | 42 | settings::import "$DIR" 43 | 44 | unit::assert_equal "${TEST1:-}" "foo" 45 | unit::assert_equal "${TEST2:-}" "bar" 46 | 47 | rm -rf "$DIR" 48 | } 49 | 50 | unit::run_test_cases "$@" 51 | -------------------------------------------------------------------------------- /bash-modules/test/test_string.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict log unit string 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | test_trim() { 11 | string::trim BAR " aaaa " 12 | unit::assert_equal "$BAR" "aaaa" 13 | 14 | string::trim BAR " aaaa *" 15 | unit::assert_equal "$BAR" "aaaa *" 16 | 17 | string::trim BAR "& aaaa " 18 | unit::assert_equal "$BAR" "& aaaa" 19 | } 20 | 21 | test_trim_start() { 22 | string::trim_start BAR " aaaa " 23 | unit::assert_equal "$BAR" "aaaa " 24 | } 25 | 26 | test_trim_end() { 27 | string::trim_end BAR " aaaa " 28 | unit::assert_equal "$BAR" " aaaa" 29 | 30 | string::trim_end BAR " aaaa "$'\n' 31 | unit::assert_equal "$BAR" " aaaa" 32 | } 33 | 34 | test_insert() { 35 | v="abba" 36 | string::insert v 0 "cc" 37 | unit::assert_equal "$v" "ccabba" 38 | 39 | v="abba" 40 | string::insert v 2 "cc" 41 | unit::assert_equal "$v" "abccba" 42 | 43 | v="abba" 44 | string::insert v 4 "cc" 45 | unit::assert_equal "$v" "abbacc" 46 | } 47 | 48 | test_split_by_delimiter() { 49 | string::split_by_delimiter FOO ":;+-" "a:b;c+d-e" 50 | unit::assert_arrays_are_equal "Wrong array returned by split_by_delimiter." "${FOO[@]}" -- a b c d e 51 | } 52 | 53 | test_split_by_delimiter_with_escape() { 54 | string::split_by_delimiter FOO ":;+-" "a\:b;c\+d-e" 55 | unit::assert_arrays_are_equal "Wrong array returned by split_by_delimiter." "${FOO[@]}" -- 'a:b' 'c+d' e 56 | } 57 | 58 | test_strings_basename_with_full_path() { 59 | string::basename FOO "/a/b.c" ".c" 60 | unit::assert_equal "$FOO" "b" 61 | } 62 | 63 | test_strings_basename_without_path() { 64 | string::basename FOO "b.c" ".c" 65 | unit::assert_equal "$FOO" "b" 66 | } 67 | 68 | test_strings_basename_without_ext() { 69 | string::basename FOO "b.c" 70 | unit::assert_equal "$FOO" "b.c" 71 | } 72 | 73 | test_strings_basename_with_different_ext() { 74 | string::basename FOO "b.c" ".e" 75 | unit::assert_equal "$FOO" "b.c" 76 | } 77 | 78 | test_strings_dirname_with_full_path() { 79 | string::dirname FOO "/a/b.c" 80 | unit::assert_equal "$FOO" "/a" 81 | } 82 | 83 | test_strings_dirname_with_relative_path() { 84 | string::dirname FOO "../a/b.c" 85 | unit::assert_equal "$FOO" "../a" 86 | } 87 | 88 | test_strings_dirname_with_file_name_only() { 89 | string::dirname FOO "b.c" 90 | unit::assert_equal "$FOO" "." 91 | } 92 | 93 | test_random_string() { 94 | string::random_string FOO 8 95 | unit::assert_equal "${#FOO}" 8 96 | } 97 | 98 | test_chr() { 99 | string::chr FOO 65 100 | unit::assert_equal "$FOO" "A" 101 | } 102 | 103 | test_ord() { 104 | string::ord FOO "A" 105 | unit::assert_equal "$FOO" "65" 106 | } 107 | 108 | test_quote_to_bash_format() { 109 | STRING="a' b [] & \$ % \" \\ 110 | ; 111 | G^&^%&^%&^ 112 | " 113 | string::quote_to_bash_format FOO "$STRING" 114 | eval "BAR=$FOO" 115 | unit::assert_equal "$BAR" "$STRING" 116 | } 117 | 118 | test_unescape_backslash_sequences() { 119 | STRING1="a\nb\tc\071" 120 | STRING2=$'a\nb\tc\071' 121 | 122 | string::unescape_backslash_sequences FOO "$STRING1" 123 | unit::assert_equal "$FOO" "$STRING2" 124 | } 125 | 126 | test_to_identifier() { 127 | string::to_identifier FOO "Hello, world!" 128 | unit::assert_equal "$FOO" "Hello__world_" 129 | } 130 | 131 | test_contains() { 132 | unit::assert "String contains substring" "string::contains 'aba' 'b'" 133 | unit::assert "String contains substring" "string::contains 'ab*a' '*'" 134 | unit::assert "String contains substring" "string::contains 'aba\\' '\\'" 135 | 136 | unit::assert "String doesn't contain substring" "! string::contains 'aba' 'c'" 137 | unit::assert "String doesn't contain substring" "! string::contains 'aba' '*'" 138 | unit::assert "String doesn't contain substring" "! string::contains 'aba' '\\'" 139 | } 140 | 141 | test_starts_with() { 142 | unit::assert "String starts with substring" "string::starts_with 'abac' 'a'" 143 | unit::assert "String starts with substring" "string::starts_with '*aba' '*'" 144 | unit::assert "String starts with substring" "string::starts_with '\\aba' '\\'" 145 | 146 | unit::assert "String doesn't start with substring" "! string::starts_with 'aba' 'c'" 147 | unit::assert "String doesn't start with substring" "! string::starts_with 'aba' '*'" 148 | unit::assert "String doesn't start with substring" "! string::starts_with 'aba' '\\'" 149 | } 150 | 151 | test_ends_with() { 152 | unit::assert "String ends with substring" "string::ends_with 'caba' 'a'" 153 | unit::assert "String ends with substring" "string::ends_with 'aba*' '*'" 154 | unit::assert "String ends with substring" "string::ends_with 'aba\\' '\\'" 155 | 156 | unit::assert "String doesn't end with substring" "! string::ends_with 'aba' 'c'" 157 | unit::assert "String doesn't end with substring" "! string::ends_with 'aba' '*'" 158 | unit::assert "String doesn't end with substring" "! string::ends_with 'aba' '\\'" 159 | } 160 | 161 | unit::run_test_cases "$@" 162 | -------------------------------------------------------------------------------- /bash-modules/test/test_timestamped_log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR="$(dirname "$0")" 3 | export __IMPORT__BASE_PATH="$APP_DIR/../src/bash-modules" 4 | export PATH="$APP_DIR/../src:$PATH" 5 | . import.sh strict timestamped_log unit 6 | 7 | ############################################### 8 | # Test cases 9 | 10 | test_info() { 11 | unit::assert_equal "$( info Test 2>/dev/null )" "placeholder[test_timestamped_log.sh] INFO: Test" 12 | } 13 | 14 | test_warn() { 15 | unit::assert_equal "$( warn Test 2>&1 1>/dev/null )" "placeholder[test_timestamped_log.sh] WARN: Test" 16 | } 17 | 18 | test_error() { 19 | unit::assert_equal "$( error Test 2>&1 1>/dev/null )" "placeholder[test_timestamped_log.sh] ERROR: Test" 20 | } 21 | 22 | test_timestamped_log_info() { 23 | unit::assert_equal "$( log::info foo Test 2>/dev/null )" "placeholder[test_timestamped_log.sh] foo: Test" 24 | } 25 | 26 | test_timestamped_log_warn() { 27 | unit::assert_equal "$( log::warn foo Test 2>&1 1>/dev/null )" "placeholder[test_timestamped_log.sh] foo: Test" 28 | } 29 | 30 | test_timestamped_log_error() { 31 | unit::assert_equal "$( log::error foo Test 2>&1 1>/dev/null )" "placeholder[test_timestamped_log.sh] foo: Test" 32 | } 33 | 34 | timestamped_log::set_format "placeholder" 35 | 36 | unit::run_test_cases "$@" 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | * [bash-modules](#bash-modules) 2 | * [Simple module system for bash.](#simple-module-system-for-bash) 3 | * [Syntax](#syntax) 4 | * [Example](#example) 5 | * [License](#license) 6 | * [Vision](#vision) 7 | * [Features](#features) 8 | * [TODO](#todo) 9 | * [Showcase - log module](#showcase---log-module) 10 | * [Showcase - arguments module](#showcase---arguments-module) 11 | * [Showcase - strict mode](#showcase---strict-mode) 12 | * [Error handling](#error-handling) 13 | * [Chain of errors](#chain-of-errors) 14 | * [Panic](#panic) 15 | 16 | bash-modules 17 | ============ 18 | 19 | See documentation in [HTML format](http://vlisivka.github.io/bash-modules/). 20 | 21 | ## Simple module system for bash. 22 | 23 | Module loader and collection of modules for bash scripts, to quickly write safe bash scripts in unofficial bash strict mode. 24 | 25 | Currently, bash-modules project is targetting users of Linux OS, such as system administrators. 26 | 27 | bash-modules is developed at Fedora Linux and requires bash 4 or higher. 28 | 29 | ## Syntax 30 | 31 | To include module(s) into your script (note the "." at the beginning of the line): 32 | 33 | ``` 34 | . import.sh MODULE[...] 35 | ``` 36 | 37 | To list available modules and show their documentation call import.sh as a command: 38 | 39 | ``` 40 | import.sh [OPTIONS] 41 | ``` 42 | 43 | NOTE: Don't be confused by `import` (without `.sh`) command from `ImageMagick` package. `bash-modules` uses `import.sh`, not `import`. 44 | 45 | ## Example 46 | 47 | ```bash 48 | #!/bin/bash 49 | . import.sh log 50 | info "Hello, world!" 51 | ``` 52 | 53 | See more examples in [bash-modules/examples](https://github.com/vlisivka/bash-modules/tree/master/bash-modules/examples) directory. 54 | 55 | ## License 56 | 57 | `bash-modules` is licensed under terms of LGPL2+ license, like glibc. You are not allowed to copy-paste the code of this project into an your non-GPL-ed project, but you are free to use, modify, or distribute `bash-modules` as a separate library. 58 | 59 | ## Vision 60 | 61 | My vision for the project is to create a loadable set of bash subroutines, which are: 62 | 63 | * useful; 64 | * work in strict mode (set -ue); 65 | * correctly handle strings with spaces and special characters; 66 | * use as little external commands as possible; 67 | * easy to use; 68 | * well documented; 69 | * well covered by test cases. 70 | 71 | ## Features 72 | 73 | * module for logging; 74 | * module for parsing of arguments; 75 | * module for unit testing; 76 | * full support for unofficial strict mode. 77 | 78 | ## Installation 79 | 80 | Use `install.sh` script in bash-modules directory to install bash-modules 81 | to `~/.local` (default for a user) or `/usr/local/bin` (default for a 82 | root user). See `./install.sh --help` for options. 83 | 84 | 85 | ## TODO 86 | 87 | * [x] Implement module loader. 88 | * [x] Implement few modules with frequently used functions and routines. 89 | * [ ] Cooperate with other bash-related projects. 90 | * [ ] Implement a repository for extra modules. 91 | * [ ] Implement a package manager for modules or integrate with an existing PM. 92 | 93 | ## Showcase - log module 94 | 95 | ```bash 96 | #!/bin/bash 97 | . import.sh strict log arguments 98 | 99 | main() { 100 | debug "A debug message (use --debug option to show debug messages)." 101 | 102 | info "An information message. Arguments: $*" 103 | 104 | warn "A warning message." 105 | 106 | error "An error message." 107 | 108 | todo "A todo message." 109 | 110 | unimplemented "Not implemented." 111 | } 112 | 113 | arguments::parse -- "$@" || panic "Cannot parse arguments." 114 | 115 | dbg ARGUMENTS 116 | 117 | main "${ARGUMENTS[@]}" 118 | ``` 119 | 120 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-log-1.png) 121 | 122 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-log-2.png) 123 | 124 | ## Showcase - arguments module 125 | 126 | 127 | ```bash 128 | #!/bin/bash 129 | . import.sh strict log arguments 130 | 131 | NAME="John" 132 | AGE=42 133 | MARRIED="no" 134 | 135 | 136 | main() { 137 | info "Name: $NAME" 138 | info "Age: $AGE" 139 | info "Married: $MARRIED" 140 | info "Other arguments: $*" 141 | } 142 | 143 | arguments::parse \ 144 | "-n|--name)NAME;String,Required" \ 145 | "-a|--age)AGE;Number,(( AGE >= 18 ))" \ 146 | "-m|--married)MARRIED;Boolean" \ 147 | -- "$@" || panic "Cannot parse arguments. Use \"--help\" to show options." 148 | 149 | main "${ARGUMENTS[@]}" || exit $? 150 | 151 | # Comments marked by "#>>" are shown by --help. 152 | # Comments marked by "#>" and "#>>" are shown by --man. 153 | 154 | #> Example of a script with parsing of arguments. 155 | #>> 156 | #>> Usage: showcase-arguments.sh [OPTIONS] [--] [ARGUMENTS] 157 | #>> 158 | #>> OPTIONS: 159 | #>> 160 | #>> -h|--help show this help screen. 161 | #>> --man show complete manual. 162 | #>> -n|--name NAME set name. Name must not be empty. Default name is "John". 163 | #>> -a|--age AGE set age. Age must be >= 18. Default age is 42. 164 | #>> -m|--married set married flag to "yes". Default value is "no". 165 | #>> 166 | ``` 167 | 168 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-arguments-1.png) 169 | 170 | 171 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-arguments-2.png) 172 | 173 | 174 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-arguments-3.png) 175 | 176 | ## Showcase - strict mode 177 | 178 | ```bash 179 | #!/bin/bash 180 | . import.sh strict log 181 | a() { 182 | b 183 | } 184 | b() { 185 | c 186 | } 187 | c() { 188 | d 189 | } 190 | d() { 191 | false 192 | } 193 | 194 | a 195 | 196 | ``` 197 | 198 | ![Output](https://raw.githubusercontent.com/vlisivka/bash-modules/master/images/showcase-strict-mode.png) 199 | 200 | ## Error handling 201 | 202 | `bash-modules` `log` module supports two strategies to handle errors: 203 | 204 | ### Chain of errors 205 | 206 | The first, strategy is to report the error to user, and then return error code from the function, to produce chain of errors. This technique allows for system administrator to understand faster - why script failed and what it tried to achieve. 207 | 208 | ```bash 209 | #!/bin/bash 210 | . import.sh strict log 211 | foo() { 212 | xxx || { error "Cannot execute xxx."; return 1; } 213 | } 214 | 215 | bar() { 216 | foo || { error "Cannot perform foo."; return 1; } 217 | } 218 | 219 | main() { 220 | bar || { error "Cannot perform bar."; return 1; } 221 | } 222 | 223 | main "$@" || exit $? 224 | ``` 225 | 226 | ```text 227 | $ ./chain-of-errors.sh 228 | ./chain-of-errors.sh: line 4: xxx: command not found 229 | [chain-of-errors.sh] ERROR: Cannot execute xxx. 230 | [chain-of-errors.sh] ERROR: Cannot perform foo. 231 | [chain-of-errors.sh] ERROR: Cannot perform bar. 232 | ``` 233 | 234 | ### Panic 235 | 236 | The second strategy is just to panic, when error happened, and abort script. Stacktrace is printed automatically in this case. 237 | 238 | ```bash 239 | #!/bin/bash 240 | . import.sh strict log 241 | xxx || panic "Cannot execute xxx." 242 | ``` 243 | 244 | ```text 245 | $ ./simple-panic.sh 246 | ./simple-panic.sh: line 3: xxx: command not found 247 | [simple-panic.sh] PANIC: Cannot execute xxx. 248 | at main(./simple-panic.sh:3) 249 | ``` 250 | 251 | NOTE: If error happened in a subshell, then script author need to add another panic handler after end of subshell, e.g. `( false || panic "foo" ) || panic "bar"`. 252 | -------------------------------------------------------------------------------- /docs/TODO.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TODO 8 | 166 | 169 | 170 | 171 |
172 |

TODO

173 |
174 |
    175 |
  • 177 |
  • 179 |
  • 181 |
  • 183 |
  • 185 |
  • 187 |
  • 189 |
  • 191 |
  • 193 |
  • 195 |
  • 197 |
  • 199 |
  • 201 |
  • 203 |
  • 205 |
  • 207 |
  • 209 |
  • 211 |
  • 213 |
  • 215 |
  • 217 |
  • 219 |
  • 222 |
  • 223 |
  • 225 |
  • 227 |
  • 230 |
  • 232 |
  • 234 |
  • 236 |
  • 239 |
  • 241 |
  • 242 |
  • 243 |
  • 244 |
245 | 246 | 247 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | * [x] Create test case for import.sh. 2 | * [x] Create test case for documentation parser. 3 | * [x] Create test case for install.sh script. 4 | * [x] Add support for installation to a custom directory. 5 | * [x] Add support for installation to HOME or HOME/.local. 6 | * [x] Add datetime module 7 | * [x] Fix timestamped_log to use bash built-in for date instead of date command. 8 | * [x] Move logic to wrap functions from timestamped_log to separate module. 9 | * [x] Update TOC in top README. 10 | * [x] Update examples in README. 11 | * [x] Release version 4.0.0beta. 12 | * [x] Ask other bash-scripters for peer review. 13 | * [x] Publish an article at linux.org.ua. 14 | * [x] Install to XDG dir or HOME/.local, when install run by user. 15 | * [x] Install to /usr/local by default, when run by root. 16 | * [x] Update path to modules and configuration in import.sh during installation. 17 | * [x] Add support for configuration file in other places than /etc. 18 | * [x] Add an Install section to README. 19 | * [x] Add dispatch function. 20 | * [x] Kill useless cat. 21 | * [x] Update help function to support arbitrary prefixes for built-in documentation. 22 | * [x] Generate documentation in markdown format and convert it to HTML. 23 | * [ ] Formalize syntax for classes/objects: class::static_method, class.object_method self. 24 | * [ ] Release version 4.0. 25 | * [ ] Publish an article in Fedora Magazine. 26 | * [ ] Report bug in bash 5.1.0. 27 | * [ ] Ask bash developers for strict mode support, like in zsh, because it's critical for this project. 28 | * [ ] Use realpath instead of readlink -f, when possible. 29 | * [ ] Write a package manager for bash: bum, for strict bash code only. 30 | * [ ] SPDX-License-Identifier: LGPL-2.1-or-later 31 | * [ ] Add __END__ function, which will just exit, and support for line delimited built-in help, like in perl. 32 | * [ ] Write an markdown parser for terminal, to show built-in manual with colors? 33 | * [ ] Add path module. 34 | * [ ] Add lock module. 35 | * [ ] Add is module. 36 | -------------------------------------------------------------------------------- /docs/arguments.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `arguments` - contains function to parse arguments and assign option values to variables. 4 | 5 | ## FUNCTIONS 6 | 7 | * `arguments::parse [-S|--FULL)VARIABLE;FLAGS[,COND]...]... -- [ARGUMENTS]...` 8 | 9 | Where: 10 | 11 | * `-S` - short option name. 12 | 13 | * `--FULL` - long option name. 14 | 15 | * `VARIABLE` - name of shell variable to assign value to. 16 | 17 | * `FLAGS` - one of (case sensitive): 18 | * `Y | Yes` - set variable value to "yes"; 19 | * `No` - set variable value to "no"; 20 | * `I | Inc | Incremental` - incremental (no value) - increment variable value by one; 21 | * `S | Str | String` - string value; 22 | * `N | Num | Number` - numeric value; 23 | * `A | Arr | Array` - array of string values (multiple options); 24 | * `C | Com | Command` - option name will be assigned to the variable. 25 | 26 | * `COND` - post conditions: 27 | * `R | Req | Required` - option value must be not empty after end of parsing. 28 | Set initial value to empty value to require this option; 29 | * any code - executed after option parsing to check post conditions, e.g. "(( FOO > 3 )), (( FOO > BAR ))". 30 | 31 | * -- - the separator between option descriptions and script commandline arguments. 32 | 33 | * `ARGUMENTS` - command line arguments to parse. 34 | 35 | **LIMITATION:** grouping of one-letter options is NOT supported. Argument `-abc` will be parsed as 36 | option `-abc`, not as `-a -b -c`. 37 | 38 | **NOTE:** bash4 requires to use `"${@:+$@}"` to expand empty list of arguments in strict mode (`-u`). 39 | 40 | By default, function supports `-h|--help`, `--man` and `--debug` options. 41 | Options `--help` and `--man` are calling `arguments::help()` function with `2` or `1` as 42 | argument. Override that function if you want to provide your own help. 43 | 44 | Unlike many other parsers, this function stops option parsing at first 45 | non-option argument. 46 | 47 | Use `--` in commandline arguments to strictly separate options and arguments. 48 | 49 | After option parsing, unparsed command line arguments are stored in 50 | `ARGUMENTS` array. 51 | 52 | **Example:** 53 | 54 | ```bash 55 | # Boolean variable ("yes" or "no") 56 | FOO="no" 57 | # String variable 58 | BAR="" 59 | # Indexed array 60 | declare -a BAZ=( ) 61 | # Integer variable 62 | declare -i TIMES=0 63 | 64 | arguments::parse \ 65 | "-f|--foo)FOO;Yes" \ 66 | "-b|--bar)BAR;String,Required" \ 67 | "-B|--baz)BAZ;Array" \ 68 | "-i|--inc)TIMES;Incremental,((TIMES<3))" \ 69 | -- \ 70 | "${@:+$@}" 71 | 72 | # Print name and value of variables 73 | dbg FOO BAR BAZ TIMES ARGUMENTS 74 | ``` 75 | 76 | * `arguments::generate_parser OPTIONS_DESCRIPTIONS` - generate parser for options. 77 | Will create function `arguments::parse_options()`, which can be used to parse arguments. 78 | Use `declare -fp arguments::parse_options` to show generated source. 79 | 80 | * `arguments::help LEVEL` - display embeded documentation. 81 | LEVEL - level of documentation: 82 | * 3 - summary (`#>>>` comments), 83 | * 2 - summary and usage ( + `#>>` comments), 84 | * 1 - full documentation (+ `#>` comments). 85 | -------------------------------------------------------------------------------- /docs/cd_to_bindir.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | cd_to_bindir 8 | 166 | 169 | 170 | 171 |
172 |

cd_to_bindir

173 |
174 |

NAME

175 |

cd_to_bindir - change working directory to the directory 176 | where main script file is located.

177 |

DESCRIPTION

178 |

Just import this cwdir module to change current working directory to 179 | a directory, where main script file is located.

180 |

FUNCTIONS

181 |
    182 |
  • ch_bin_dir - Change working directory to directory 183 | where script is located, which is usually called “bin dir”.
  • 184 |
185 | 186 | 187 | -------------------------------------------------------------------------------- /docs/cd_to_bindir.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | `cd_to_bindir` - change working directory to the directory where main script file is located. 3 | 4 | # DESCRIPTION 5 | 6 | Just import this cwdir module to change current working directory to a directory, 7 | where main script file is located. 8 | 9 | # FUNCTIONS 10 | 11 | * `ch_bin_dir` - Change working directory to directory where script is located, which is usually called "bin dir". 12 | -------------------------------------------------------------------------------- /docs/date.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | date 8 | 166 | 169 | 170 | 171 |
172 |

date

173 |
174 |

NAME

175 |

date - date-time functions.

176 |

FUNCTIONS

177 |
    178 |
  • date::timestamp VARIABLE - return current time in 179 | seconds since UNIX epoch

  • 180 |
  • date::current_datetime VARIABLE FORMAT - return 181 | current date time in given format. See man 3 strftime for 182 | details.

  • 183 |
  • date::print_current_datetime FORMAT - print current 184 | date time in given format. See man 3 strftime for 185 | details.

  • 186 |
  • date::datetime VARIABLE FORMAT TIMESTAMP - return 187 | current date time in given format. See man 3 strftime for 188 | details.

  • 189 |
  • date::print_elapsed_time - print value of SECONDS 190 | variable in human readable form: “Elapsed time: 0 days 00:00:00.” It’s 191 | useful to know time of execution of a long script, so here is function 192 | for that. Assign 0 to SECONDS variable to reset counter.

  • 193 |
194 | 195 | 196 | -------------------------------------------------------------------------------- /docs/date.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `date` - date-time functions. 4 | 5 | ## FUNCTIONS 6 | 7 | * `date::timestamp VARIABLE` - return current time in seconds since UNIX epoch 8 | 9 | * `date::current_datetime VARIABLE FORMAT` - return current date time in given format. 10 | See `man 3 strftime` for details. 11 | 12 | * `date::print_current_datetime FORMAT` - print current date time in given format. 13 | See `man 3 strftime` for details. 14 | 15 | * `date::datetime VARIABLE FORMAT TIMESTAMP` - return current date time in given format. 16 | See `man 3 strftime` for details. 17 | 18 | * `date::print_elapsed_time` - print value of SECONDS variable in human readable form: "Elapsed time: 0 days 00:00:00." 19 | It's useful to know time of execution of a long script, so here is function for that. 20 | Assign 0 to SECONDS variable to reset counter. 21 | -------------------------------------------------------------------------------- /docs/import.sh.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `import.sh` - import bash modules into scripts or into interactive shell 4 | 5 | ## SYNOPSIS 6 | 7 | ### In a scipt: 8 | 9 | * `. import.sh MODULE[...]` - import module(s) into script or shell 10 | * `source import.sh MODULE[...]` - same as above, but with `source` instead of `.` 11 | 12 | 13 | ### At command line: 14 | 15 | * `import.sh --help|-h` - print this help text 16 | * `import.sh --man` - show manual 17 | * `import.sh --list` - list modules with their path 18 | * `import.sh --summary|-s [MODULE...]` - list module(s) with summary 19 | * `import.sh --usage|-u MODULE[...]` - print module help text 20 | * `import.sh --doc|-d MODULE[...]` - print module documentation 21 | 22 | ## DESCRIPTION 23 | 24 | Imports given module(s) into current shell. 25 | 26 | Use: 27 | 28 | * `import.sh --list` - to print list of available modules. 29 | * `import.sh --summary` - to print list of available modules with short description. 30 | * `import.sh --usage MODULE[...]` - to print longer description of given module(s). 31 | 32 | ## CONFIGURATION 33 | 34 | * `BASH_MODULES_PATH` - ':' separated list of your own directories with modules, 35 | which will be prepended to module search path. You can set `__IMPORT__BASE_PATH` array in 36 | script at begining, in `/etc/bash-modules/config.sh`, or in `~/.config/bash-modules/config.sh` file. 37 | 38 | * `__IMPORT__BASE_PATH` - array with list of directories for module search. It's reserved for internal use by bash-modules. 39 | 40 | * `/etc/bash-modules/config.sh` - system wide configuration file. 41 | WARNING: Code in this script will affect all scripts. 42 | 43 | ### Example configration file 44 | 45 | Put following snippet into `~/.config/bash-modules/config.sh` file: 46 | 47 | ```bash 48 | 49 | # Enable stack trace printing for warnings and errors, 50 | # like with --debug option: 51 | __log__STACKTRACE=="yes" 52 | 53 | # Add additional directory to module search path: 54 | BASH_MODULES_PATH="/home/user/my-bash-modules" 55 | 56 | ``` 57 | 58 | * `~/.config/bash-modules/config.sh` - user configuration file. 59 | **WARNING:** Code in this script will affect all user scripts. 60 | 61 | ## VARIABLES 62 | 63 | * `IMPORT__BIN_FILE` - script main file name, e.g. `/usr/bin/my-script`, as in `$0` variable in main file. 64 | 65 | ## FUNCTIONS 66 | 67 | * `import::import_module MODULE` - import single module only. 68 | 69 | * `import::import_modules MODULE[...]` - import module(s). 70 | 71 | * `import::list_modules FUNC [MODULE]...` - print various information about module(s). 72 | `FUNC` is a function to call on each module. Function will be called with two arguments: 73 | path to module and module name. 74 | Rest of arguments are module names. No arguments means all modules. 75 | 76 | * `import::show_documentation LEVEL PARSER FILE` - print module built-in documentation. 77 | This function scans given file for lines with "#>" prefix (or given prefix) and prints them to stdout with prefix stripped. 78 | * `LEVEL` - documentation level (one line summary, usage, full manual): 79 | - 1 - for manual (`#>` and `#>>` and `#>>>`), 80 | - 2 - for usage (`#>>` and `#>>>`), 81 | - 3 - for one line summary (`#>>>`), 82 | - or arbitrary prefix, e.g. `##`. 83 | * `FILE` - path to file with built-in documentation. 84 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | index 8 | 166 | 169 | 170 | 171 |
172 |

index

173 |
174 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | * [README](README.html) 2 | * [TODO](TODO.html) 3 | * [LICENSE](LICENSE) 4 | * [import.sh main script](import.sh.html) 5 | * [arguments module](arguments.html) 6 | * [cd_to_bindir module](cd_to_bindir.html) 7 | * [date module](date.html) 8 | * [log module](log.html) 9 | * [meta module](meta.html) 10 | * [renice module](renice.html) 11 | * [settings module](settings.html) 12 | * [strict module](strict.html) 13 | * [string module](string.html) 14 | * [timestamped_log module](timestamped_log.html) 15 | * [unit module](unit.html) 16 | -------------------------------------------------------------------------------- /docs/log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | log 8 | 166 | 169 | 170 | 171 |
172 |

log

173 |
174 |

NAME

175 |

log - various functions related to logging.

176 |

VARIABLES

177 |
    178 |
  • __log__APP - name of main file without path.
  • 179 |
  • __log__DEBUG - set to yes to enable printing of debug 180 | messages and stacktraces.
  • 181 |
  • __log__STACKTRACE - set to yes to enable printing of 182 | stacktraces.
  • 183 |
184 |

FUNCTIONS

185 |
    186 |
  • stacktrace [INDEX] - display functions and source 187 | line numbers starting from given index in stack trace, when debugging or 188 | back tracking is enabled.

  • 189 |
  • error MESAGE... - print error message and stacktrace 190 | (if enabled).

  • 191 |
  • warn MESAGE... - print warning message and 192 | stacktrace (if enabled).

  • 193 |
  • info MESAGE... - print info message.

  • 194 |
  • debug MESAGE... - print debug message, when 195 | debugging is enabled only.

  • 196 |
  • log::fatal LEVEL MESSAGE... - print a fatal-like 197 | LEVEL: MESSAGE to STDERR.

  • 198 |
  • log::error LEVEL MESSAGE... - print error-like 199 | LEVEL: MESSAGE to STDERR.

  • 200 |
  • log::warn LEVEL MESSAGE... - print warning-like 201 | LEVEL: MESSAGE to STDERR.

  • 202 |
  • log::info LEVEL MESSAGE... - print info-like LEVEL: 203 | MESSAGE to STDOUT.

  • 204 |
  • panic MESAGE... - print error message and 205 | stacktrace, then exit with error code 1.

  • 206 |
  • unimplemented MESAGE... - print error message and 207 | stacktrace, then exit with error code 42.

  • 208 |
  • todo MESAGE... - print todo message and 209 | stacktrace.

  • 210 |
  • dbg VARIABLE... - print name of variable and it 211 | content to stderr

  • 212 |
  • log::enable_debug_mode - enable debug messages and 213 | stack traces.

  • 214 |
  • log::disable_debug_mode - disable debug messages and 215 | stack traces.

  • 216 |
  • log::enable_stacktrace - enable stack 217 | traces.

  • 218 |
  • log::disable_stacktrace - disable stack 219 | traces.

  • 220 |
221 |

NOTES

222 |
    223 |
  • If STDOUT is connected to tty, then 224 |
      225 |
    • info and info-like messages will be printed with message level 226 | higlighted in green,
    • 227 |
    • warn and warn-like messages will be printed with message level 228 | higlighted in yellow,
    • 229 |
    • error and error-like messages will be printed with message level 230 | higlighted in red.
    • 231 |
  • 232 |
233 | 234 | 235 | -------------------------------------------------------------------------------- /docs/log.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `log` - various functions related to logging. 4 | 5 | ## VARIABLES 6 | * `__log__APP` - name of main file without path. 7 | * `__log__DEBUG` - set to yes to enable printing of debug messages and stacktraces. 8 | * `__log__STACKTRACE` - set to yes to enable printing of stacktraces. 9 | 10 | ## FUNCTIONS 11 | 12 | * `stacktrace [INDEX]` - display functions and source line numbers starting 13 | from given index in stack trace, when debugging or back tracking is enabled. 14 | 15 | * `error MESAGE...` - print error message and stacktrace (if enabled). 16 | 17 | * `warn MESAGE...` - print warning message and stacktrace (if enabled). 18 | 19 | * `info MESAGE...` - print info message. 20 | 21 | * `debug MESAGE...` - print debug message, when debugging is enabled only. 22 | 23 | * `log::fatal LEVEL MESSAGE...` - print a fatal-like LEVEL: MESSAGE to STDERR. 24 | 25 | * `log::error LEVEL MESSAGE...` - print error-like LEVEL: MESSAGE to STDERR. 26 | 27 | * `log::warn LEVEL MESSAGE...` - print warning-like LEVEL: MESSAGE to STDERR. 28 | 29 | * `log::info LEVEL MESSAGE...` - print info-like LEVEL: MESSAGE to STDOUT. 30 | 31 | * `panic MESAGE...` - print error message and stacktrace, then exit with error code 1. 32 | 33 | * `unimplemented MESAGE...` - print error message and stacktrace, then exit with error code 42. 34 | 35 | * `todo MESAGE...` - print todo message and stacktrace. 36 | 37 | * `dbg VARIABLE...` - print name of variable and it content to stderr 38 | 39 | * `log::enable_debug_mode` - enable debug messages and stack traces. 40 | 41 | * `log::disable_debug_mode` - disable debug messages and stack traces. 42 | 43 | * `log::enable_stacktrace` - enable stack traces. 44 | 45 | * `log::disable_stacktrace` - disable stack traces. 46 | 47 | ## NOTES 48 | 49 | - If STDOUT is connected to tty, then 50 | * info and info-like messages will be printed with message level higlighted in green, 51 | * warn and warn-like messages will be printed with message level higlighted in yellow, 52 | * error and error-like messages will be printed with message level higlighted in red. 53 | -------------------------------------------------------------------------------- /docs/meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | meta 8 | 166 | 169 | 170 | 171 |
172 |

meta

173 |
174 |

NAME

175 |

meta - functions for working with bash functions.

176 |

FUNCTIONS

177 |
    178 |
  • meta::copy_function FUNCTION_NAME NEW_FUNCTION_PREFIX 179 | - copy function to new function with prefix in name. Create copy of 180 | function with new prefix. Old function can be redefined or 181 | unset -f.

  • 182 |
  • meta::wrap BEFORE AFTER FUNCTION_NAME[...] - wrap 183 | function. Create wrapper for a function(s). Execute given commands 184 | before and after each function. Original function is available as 185 | meta::orig_FUNCTION_NAME.

  • 186 |
  • meta::functions_with_prefix PREFIX - print list of 187 | functions with given prefix.

  • 188 |
  • meta::is_function FUNC_NAME Checks is given name 189 | corresponds to a function.

  • 190 |
  • meta::dispatch PREFIX COMMAND [ARGUMENTS...] - 191 | execute function PREFIX__COMMAND [ARGUMENTS]

    192 |

    For example, it can be used to execute functions (commands) by name, 193 | e.g. main() { meta::dispatch command__ "$@" ; }, when 194 | called as man hw world will execute 195 | command_hw "$world". When command handler is not found, 196 | dispatcher will try to call PREFIX__DEFAULT function 197 | instead, or return error code when defaulf handler is not 198 | found.

  • 199 |
200 | 201 | 202 | -------------------------------------------------------------------------------- /docs/meta.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `meta` - functions for working with bash functions. 4 | 5 | ## FUNCTIONS 6 | 7 | * `meta::copy_function FUNCTION_NAME NEW_FUNCTION_PREFIX` - copy function to new function with prefix in name. 8 | Create copy of function with new prefix. 9 | Old function can be redefined or `unset -f`. 10 | 11 | * `meta::wrap BEFORE AFTER FUNCTION_NAME[...]` - wrap function. 12 | Create wrapper for a function(s). Execute given commands before and after 13 | each function. Original function is available as meta::orig_FUNCTION_NAME. 14 | 15 | * `meta::functions_with_prefix PREFIX` - print list of functions with given prefix. 16 | 17 | * `meta::is_function FUNC_NAME` Checks is given name corresponds to a function. 18 | 19 | * `meta::dispatch PREFIX COMMAND [ARGUMENTS...]` - execute function `PREFIX__COMMAND [ARGUMENTS]` 20 | 21 | For example, it can be used to execute functions (commands) by name, e.g. 22 | `main() { meta::dispatch command__ "$@" ; }`, when called as `man hw world` will execute 23 | `command_hw "$world"`. When command handler is not found, dispatcher will try 24 | to call `PREFIX__DEFAULT` function instead, or return error code when defaulf handler is not found. 25 | -------------------------------------------------------------------------------- /docs/renice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | renice 8 | 166 | 169 | 170 | 171 |
172 |

renice

173 |
174 |

NAME

175 |

renice - reduce priority of current shell to make it low 176 | priority task (renice 19 to self).

177 |

USAGE

178 |

. import.sh renice

179 | 180 | 181 | -------------------------------------------------------------------------------- /docs/renice.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `renice` - reduce priority of current shell to make it low priority task (`renice 19` to self). 4 | 5 | ## USAGE 6 | 7 | `. import.sh renice` 8 | -------------------------------------------------------------------------------- /docs/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | settings 8 | 166 | 169 | 170 | 171 |
172 |

settings

173 |
174 |

NAME

175 |

settigngs - import settings from configuration files and 176 | configuration directories. Also known as “configuration directory” 177 | pattern.

178 |

FUNCTIONS

179 |
    180 |
  • settings::import [-e|--ext EXTENSION] FILE|DIRECTORY... 181 | - Import settings (source them into current program as shell script) 182 | when file or directory exists. For directories, all files with given 183 | extension (".sh" by default) are imported, without 184 | recursion.
  • 185 |
186 |

WARNING: this method is powerful, but unsafe, 187 | because user can put any shell command into the configuration file, 188 | which will be executed by script.

189 |

TODO: implement file parsing instead of 190 | sourcing.

191 | 192 | 193 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `settigngs` - import settings from configuration files and configuration directories. 4 | Also known as "configuration directory" pattern. 5 | 6 | ## FUNCTIONS 7 | * `settings::import [-e|--ext EXTENSION] FILE|DIRECTORY...` - Import settings 8 | (source them into current program as shell script) when 9 | file or directory exists. For directories, all files with given extension 10 | (`".sh"` by default) are imported, without recursion. 11 | 12 | **WARNING:** this method is powerful, but unsafe, because user can put any shell 13 | command into the configuration file, which will be executed by script. 14 | 15 | **TODO:** implement file parsing instead of sourcing. 16 | -------------------------------------------------------------------------------- /docs/strict.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | strict 8 | 166 | 169 | 170 | 171 |
172 |

strict

173 |
174 |

NAME

175 |

strict - unofficial strict mode for bash

176 |

Just import this module, to enabe strict mode: 177 | set -euEo pipefail.

178 |

NOTE

179 |
    180 |
  • Option -e is not working when command is part of a 181 | compound command, or in subshell. See bash manual for details. For 182 | example, -e may not working in a for 183 | cycle.
  • 184 |
185 | 186 | 187 | -------------------------------------------------------------------------------- /docs/strict.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `strict` - unofficial strict mode for bash 4 | 5 | Just import this module, to enabe strict mode: `set -euEo pipefail`. 6 | 7 | ## NOTE 8 | 9 | * Option `-e` is not working when command is part of a compound command, 10 | or in subshell. See bash manual for details. For example, `-e` may not working 11 | in a `for` cycle. 12 | -------------------------------------------------------------------------------- /docs/string.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | string 8 | 230 | 233 | 234 | 235 |
236 |

string

237 |
238 |

NAME

239 |

string - various functions to manipulate strings.

240 |

FUNCTIONS

241 |
    242 |
  • string::trim_spaces VARIABLE VALUE Trim white space 243 | characters around value and assign result to variable.

  • 244 |
  • string::trim_start VARIABLE VALUE Trim white space 245 | characters at begining of the value and assign result to the 246 | variable.

  • 247 |
  • string::trim_end VARIABLE VALUE Trim white space 248 | characters at the end of the value and assign result to the 249 | variable.

  • 250 |
  • string::insert VARIABLE POSITION VALUE Insert 251 | VALUE into VARIABLE at given 252 | POSITION. Example:

    253 |
    v="abba"
    255 | string::insert v 2 "cc"
    256 | # now v=="abccba"
  • 257 |
  • string::split_by_delimiter ARRAY DELIMITERS VALUE 258 | Split value by delimiter(s) and assign result to array. Use backslash to 259 | escape delimiter in string. NOTE: Temporary file will be used.

  • 260 |
  • string::basename VARIABLE FILE [EXT] Strip path and 261 | optional extension from full file name and store file name in 262 | variable.

  • 263 |
  • string::dirname VARIABLE FILE Strip file name from 264 | path and store directory name in variable.

  • 265 |
  • string::random_string VARIABLE LENGTH Generate 266 | random string of given length using [a-zA-Z0-9] characters and store it 267 | into variable.

  • 268 |
  • string::chr VARIABLE CHAR_CODE Convert decimal 269 | character code to its ASCII representation.

  • 270 |
  • string::ord VARIABLE CHAR Converts ASCII character 271 | to its decimal value.

  • 272 |
  • string::quote_to_bash_format VARIABLE STRING Quote 273 | the argument in a way that can be reused as shell input.

  • 274 |
  • string::unescape_backslash_sequences VARIABLE STRING 275 | Expand backslash escape sequences.

  • 276 |
  • string::to_identifier VARIABLE STRING Replace all 277 | non-alphanumeric characters in string by underscore character.

  • 278 |
  • string::find_string_with_prefix VAR PREFIX [STRINGS...] 279 | Find first string with given prefix and assign it to VAR.

  • 280 |
  • string::contains STRING SUBSTRING Returns zero exit 281 | code (true), when string contains substring

  • 282 |
  • string::starts_with STRING SUBSTRING Returns zero 283 | exit code (true), when string starts with substring

  • 284 |
  • string::ends_with STRING SUBSTRING Returns zero exit 285 | code (true), when string ends with substring

  • 286 |
287 | 288 | 289 | -------------------------------------------------------------------------------- /docs/string.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | string - various functions to manipulate strings. 4 | 5 | ## FUNCTIONS 6 | 7 | * `string::trim_spaces VARIABLE VALUE` 8 | Trim white space characters around value and assign result to variable. 9 | 10 | * `string::trim_start VARIABLE VALUE` 11 | Trim white space characters at begining of the value and assign result to the variable. 12 | 13 | * `string::trim_end VARIABLE VALUE` 14 | Trim white space characters at the end of the value and assign result to the variable. 15 | 16 | * `string::insert VARIABLE POSITION VALUE` 17 | Insert `VALUE` into `VARIABLE` at given `POSITION`. 18 | Example: 19 | 20 | ```bash 21 | v="abba" 22 | string::insert v 2 "cc" 23 | # now v=="abccba" 24 | ``` 25 | 26 | * `string::split_by_delimiter ARRAY DELIMITERS VALUE` 27 | Split value by delimiter(s) and assign result to array. Use 28 | backslash to escape delimiter in string. 29 | NOTE: Temporary file will be used. 30 | 31 | * `string::basename VARIABLE FILE [EXT]` 32 | Strip path and optional extension from full file name and store 33 | file name in variable. 34 | 35 | * `string::dirname VARIABLE FILE` 36 | Strip file name from path and store directory name in variable. 37 | 38 | * `string::random_string VARIABLE LENGTH` 39 | Generate random string of given length using [a-zA-Z0-9] 40 | characters and store it into variable. 41 | 42 | * `string::chr VARIABLE CHAR_CODE` 43 | Convert decimal character code to its ASCII representation. 44 | 45 | * `string::ord VARIABLE CHAR` 46 | Converts ASCII character to its decimal value. 47 | 48 | * `string::quote_to_bash_format VARIABLE STRING` 49 | Quote the argument in a way that can be reused as shell input. 50 | 51 | * `string::unescape_backslash_sequences VARIABLE STRING` 52 | Expand backslash escape sequences. 53 | 54 | * `string::to_identifier VARIABLE STRING` 55 | Replace all non-alphanumeric characters in string by underscore character. 56 | 57 | * `string::find_string_with_prefix VAR PREFIX [STRINGS...]` 58 | Find first string with given prefix and assign it to VAR. 59 | 60 | * `string::contains STRING SUBSTRING` 61 | Returns zero exit code (true), when string contains substring 62 | 63 | * `string::starts_with STRING SUBSTRING` 64 | Returns zero exit code (true), when string starts with substring 65 | 66 | * `string::ends_with STRING SUBSTRING` 67 | Returns zero exit code (true), when string ends with substring 68 | -------------------------------------------------------------------------------- /docs/timestamped_log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | timestamped_log 8 | 166 | 169 | 170 | 171 |
172 |

timestamped_log

173 |
174 |

NAME

175 |

timestamped_log - print timestamped logs. Drop-in 176 | replacement for log module.

177 |

VARIABLES

178 |
    179 |
  • __timestamped_log_format - format of timestamp. Default 180 | value: “%F %T” (full date and time).
  • 181 |
182 |

FUNCTIONS

183 |
    184 |
  • timestamped_log::set_format FORMAT - Set format for 185 | date. Default value is “%F %T”.
  • 186 |
187 |

Wrapped functions:

188 |

log::info, info, debug - print 189 | timestamp to stdout and then log message.

190 |

log::error, log::warn, error, 191 | warn - print timestamp to stderr and then log message.

192 |

NOTES

193 |

See log module usage for details about log functions. 194 | Original functions are available with prefix 195 | "timestamped_log::orig_".

196 | 197 | 198 | -------------------------------------------------------------------------------- /docs/timestamped_log.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `timestamped_log` - print timestamped logs. Drop-in replacement for `log` module. 4 | 5 | ## VARIABLES 6 | 7 | * `__timestamped_log_format` - format of timestamp. Default value: "%F %T" (full date and time). 8 | 9 | ## FUNCTIONS 10 | 11 | * `timestamped_log::set_format FORMAT` - Set format for date. Default value is "%F %T". 12 | 13 | ## Wrapped functions: 14 | 15 | `log::info`, `info`, `debug` - print timestamp to stdout and then log message. 16 | 17 | `log::error`, `log::warn`, `error`, `warn` - print timestamp to stderr and then log message. 18 | 19 | ## NOTES 20 | 21 | See `log` module usage for details about log functions. Original functions 22 | are available with prefix `"timestamped_log::orig_"`. 23 | -------------------------------------------------------------------------------- /docs/unit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | unit 8 | 166 | 169 | 170 | 171 |
172 |

unit

173 |
174 |

NAME

175 |

unit - functions for unit testing.

176 |

FUNCTIONS

177 |
    178 |
  • unit::assert_yes VALUE [MESSAGE] - Show error 179 | message, when VALUE is not equal to 180 | "yes".

  • 181 |
  • unit::assert_no VALUE [MESSAGE] - Show error 182 | message, when VALUE is not equal to 183 | "no".

  • 184 |
  • unit::assert_not_empty VALUE [MESSAGE] - Show error 185 | message, when VALUE is empty.

  • 186 |
  • unit::assert_equal ACTUAL EXPECTED [MESSAGE] - Show 187 | error message, when values are not equal.

  • 188 |
  • unit::assert_arrays_are_equal MESSAGE VALUE1... -- VALUE2... 189 | - Show error message when arrays are not equal in size or 190 | content.

  • 191 |
  • unit::assert_not_equal ACTUAL_VALUE UNEXPECTED_VALUE [MESSAGE] 192 | - Show error message, when values ARE equal.

  • 193 |
  • unit::assert MESSAGE TEST[...] - Evaluate test and 194 | show error message when it returns non-zero exit code.

  • 195 |
  • unit::fail [MESSAGE] - Show error message.

  • 196 |
  • unit::run_test_cases [OPTIONS] [--] [ARGUMENTS] - 197 | Execute all functions with test* prefix in name in alphabetic order

    198 |
      199 |
    • OPTIONS: 200 |
        201 |
      • -t | --test TEST_CASE - execute single test case,
      • 202 |
      • -q | --quiet - do not print informational messages and 203 | dots,
      • 204 |
      • --debug - enable stack traces.
      • 205 |
    • 206 |
    • ARGUMENTS - All arguments, which are passed to run_test_cases, are 207 | passed then to unit::set_up, unit::tear_down 208 | and test cases using ARGUMENTS array, so you can 209 | parametrize your test cases. You can call run_test_cases 210 | more than once with different arguments. Use "--" to 211 | strictly separate arguments from options.
    • 212 |
  • 213 |
214 |

After execution of run_test_cases, following variables 215 | will have value:

216 |
    217 |
  • NUMBER_OF_TEST_CASES - total number of test cases 218 | executed,
  • 219 |
  • NUMBER_OF_FAILED_TEST_CASES - number of failed test 220 | cases,
  • 221 |
  • FAILED_TEST_CASES - names of functions of failed tests 222 | cases.
  • 223 |
224 |

If you want to ignore some test case, just prefix them with 225 | underscore, so unit::run_test_cases will not see them.

226 |

If you want to run few subsets of test cases in one file, define each 227 | subset in it own subshell and execute unit::run_test_cases 228 | in each subshell.

229 |

Each test case is executed in it own subshell, so you can call 230 | exit in the test case or assign variables without any 231 | effect on subsequent test cases.

232 |

unit::run_test_cases will also call 233 | unit::set_up and unit::tear_down functions 234 | before and after each test case. By default, they do nothing. Override 235 | them to do something useful.

236 |
    237 |
  • unit::set_up - can set variables which are available 238 | for following test case and tear_down. It also can alter 239 | ARGUMENTS array. Test case and tear_down are executed in 240 | their own subshell, so they cannot change outer variables.

  • 241 |
  • unit::tear_down is called first, before first set_up 242 | of first test case, to cleanup after possible failed run of previous 243 | test case. When it called for first time, FIRST_TEAR_DOWN 244 | variable with value "yes" is available.

  • 245 |
246 |

NOTES

247 |

All assert functions are executing exit instead of 248 | returning error code.

249 | 250 | 251 | -------------------------------------------------------------------------------- /docs/unit.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | `unit` - functions for unit testing. 4 | 5 | ## FUNCTIONS 6 | 7 | * `unit::assert_yes VALUE [MESSAGE]` - Show error message, when `VALUE` is not equal to `"yes"`. 8 | 9 | * `unit::assert_no VALUE [MESSAGE]` - Show error message, when `VALUE` is not equal to `"no"`. 10 | 11 | * `unit::assert_not_empty VALUE [MESSAGE]` - Show error message, when `VALUE` is empty. 12 | 13 | * `unit::assert_equal ACTUAL EXPECTED [MESSAGE]` - Show error message, when values are not equal. 14 | 15 | * `unit::assert_arrays_are_equal MESSAGE VALUE1... -- VALUE2...` - Show error message when arrays are not equal in size or content. 16 | 17 | * `unit::assert_not_equal ACTUAL_VALUE UNEXPECTED_VALUE [MESSAGE]` - Show error message, when values ARE equal. 18 | 19 | * `unit::assert MESSAGE TEST[...]` - Evaluate test and show error message when it returns non-zero exit code. 20 | 21 | * `unit::fail [MESSAGE]` - Show error message. 22 | 23 | * `unit::run_test_cases [OPTIONS] [--] [ARGUMENTS]` - Execute all functions with 24 | test* prefix in name in alphabetic order 25 | 26 | * OPTIONS: 27 | * `-t | --test TEST_CASE` - execute single test case, 28 | * `-q | --quiet` - do not print informational messages and dots, 29 | * `--debug` - enable stack traces. 30 | * ARGUMENTS - All arguments, which are passed to run_test_cases, are passed then 31 | to `unit::set_up`, `unit::tear_down` and test cases using `ARGUMENTS` array, so you 32 | can parametrize your test cases. You can call `run_test_cases` more than 33 | once with different arguments. Use `"--"` to strictly separate arguments 34 | from options. 35 | 36 | After execution of `run_test_cases`, following variables will have value: 37 | 38 | * `NUMBER_OF_TEST_CASES` - total number of test cases executed, 39 | * `NUMBER_OF_FAILED_TEST_CASES` - number of failed test cases, 40 | * `FAILED_TEST_CASES` - names of functions of failed tests cases. 41 | 42 | 43 | If you want to ignore some test case, just prefix them with 44 | underscore, so `unit::run_test_cases` will not see them. 45 | 46 | If you want to run few subsets of test cases in one file, define each 47 | subset in it own subshell and execute `unit::run_test_cases` in each subshell. 48 | 49 | Each test case is executed in it own subshell, so you can call `exit` 50 | in the test case or assign variables without any effect on subsequent test 51 | cases. 52 | 53 | `unit::run_test_cases` will also call `unit::set_up` and `unit::tear_down` 54 | functions before and after each test case. By default, they do nothing. 55 | Override them to do something useful. 56 | 57 | * `unit::set_up` - can set variables which are available for following 58 | test case and `tear_down`. It also can alter `ARGUMENTS` array. Test case 59 | and tear_down are executed in their own subshell, so they cannot change 60 | outer variables. 61 | 62 | * `unit::tear_down` is called first, before first set_up of first test case, to 63 | cleanup after possible failed run of previous test case. When it 64 | called for first time, `FIRST_TEAR_DOWN` variable with value `"yes"` is 65 | available. 66 | 67 | ## NOTES 68 | 69 | All assert functions are executing `exit` instead of returning error code. 70 | -------------------------------------------------------------------------------- /images/showcase-arguments-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlisivka/bash-modules/3cac9c645e2d378297636eae2a38d09815f39502/images/showcase-arguments-1.png -------------------------------------------------------------------------------- /images/showcase-arguments-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlisivka/bash-modules/3cac9c645e2d378297636eae2a38d09815f39502/images/showcase-arguments-2.png -------------------------------------------------------------------------------- /images/showcase-arguments-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlisivka/bash-modules/3cac9c645e2d378297636eae2a38d09815f39502/images/showcase-arguments-3.png -------------------------------------------------------------------------------- /images/showcase-log-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlisivka/bash-modules/3cac9c645e2d378297636eae2a38d09815f39502/images/showcase-log-1.png -------------------------------------------------------------------------------- /images/showcase-log-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlisivka/bash-modules/3cac9c645e2d378297636eae2a38d09815f39502/images/showcase-log-2.png -------------------------------------------------------------------------------- /images/showcase-strict-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlisivka/bash-modules/3cac9c645e2d378297636eae2a38d09815f39502/images/showcase-strict-mode.png -------------------------------------------------------------------------------- /import.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ue 3 | 4 | # Wrapper script for compatibility with basher, to use bash-modules 5 | # without installation. 6 | 7 | __BASH_MODULES_IMPORT_SH="$(readlink -f "${BASH_SOURCE[0]}")" 8 | __BASH_MODULES_PROJECT_DIR="${__BASH_MODULES_IMPORT_SH%/*}" 9 | export BASH_MODULES_PATH="${BASH_MODULES_PATH:+$BASH_MODULES_PATH:}$__BASH_MODULES_PROJECT_DIR/bash-modules/src/bash-modules/" 10 | 11 | # If this is top level code and program name is .../import.sh 12 | if [ "${FUNCNAME:+x}" == "" -a "${0##*/}" == "import.sh" ] 13 | then 14 | exec "$__BASH_MODULES_PROJECT_DIR/bash-modules/src/import.sh" "$@" 15 | else 16 | . "$__BASH_MODULES_PROJECT_DIR/bash-modules/src/import.sh" "$@" 17 | fi 18 | -------------------------------------------------------------------------------- /mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . ./import.sh strict log arguments 3 | 4 | # Generate documentation for each source file and store it into ./docs/ 5 | # directory as .md (markdown) files, then use static site generator to 6 | # convert makdown to HTML. 7 | 8 | DOCS_DIR="./docs" 9 | INDEX="index.md" 10 | 11 | main() { 12 | mkdir -p "$DOCS_DIR" || panic "Can't create \"$DOCS_DIR\" directory to store documentation." 13 | 14 | info "Copying documentation files to \"$DOCS_DIR\"." 15 | cp -f README.md TODO.md LICENSE "$DOCS_DIR/" || panic "Can't copy documentation files to \"$DOCS_DIR\"." 16 | 17 | echo -n "" > "$DOCS_DIR/$INDEX" 18 | echo "* [README](README.html)" >> "$DOCS_DIR/$INDEX" 19 | echo "* [TODO](TODO.html)" >> "$DOCS_DIR/$INDEX" 20 | echo "* [LICENSE](LICENSE)" >> "$DOCS_DIR/$INDEX" 21 | 22 | info "Generating doucumentation for import.sh script." 23 | ./import.sh --man >"$DOCS_DIR/import.sh.md" || panic "Can't generate documetation in Markdown format for \"import.sh\" script." 24 | 25 | echo "* [import.sh main script](import.sh.html)" >> "$DOCS_DIR/$INDEX" 26 | 27 | local source_file module 28 | for source_file in ./bash-modules/src/bash-modules/*.sh 29 | do 30 | module="${source_file##*/}" 31 | module="${module%.sh}" 32 | 33 | info "Generating doucumentation for \"$module\" module." 34 | ./import.sh --doc "$source_file" > "$DOCS_DIR/$module.md" || panic "Can't generate documetation in Markdown format for \"$module\" module." 35 | 36 | echo "* [$module module]($module.html)" >> "$DOCS_DIR/$INDEX" 37 | done 38 | 39 | local md_file html_file title 40 | for md_file in docs/*.md 41 | do 42 | html_file="${md_file%.md}.html" 43 | title="${md_file##*/}" 44 | title="${title%.md}" 45 | 46 | info "Converting \"$md_file\" markdown file to \"$html_file\" HTML file." 47 | pandoc -f markdown -t html --standalone --metadata "title=$title" -o "$html_file" "$md_file" || panic "Can't convert \"$md_file\" markdown file to \"$html_file\" HTML file." 48 | done 49 | } 50 | 51 | main "$@" 52 | --------------------------------------------------------------------------------