├── .github └── workflows │ └── ci.yml ├── .shellspec ├── LICENSE ├── README.md ├── bash-common-helpers.sh └── spec ├── bash-common-helpers_spec.sh └── spec_helper.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow does continuous integration 2 | 3 | name: Continuous Integration 4 | 5 | # Trigger the workflow on push or pull request events but only for the master branch 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | 15 | # Tests 16 | test: 17 | # The type of runner that this job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # The sequence of tasks that will be executed as part of this job 21 | steps: 22 | # Checks-out the repository under $GITHUB_WORKSPACE, so this job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Runs ShellSpec 26 | - name: Run ShellSpec 27 | run: echo "[TODO] actually run ShellSpec" 28 | 29 | # Static code analysis 30 | lint: 31 | # The type of runner that this job will run on 32 | runs-on: ubuntu-latest 33 | 34 | # The sequence of tasks that will be executed as part of this job 35 | steps: 36 | # Checks-out the repository under $GITHUB_WORKSPACE, so this job can access it 37 | - uses: actions/checkout@v2 38 | 39 | # Runs ShellCheck 40 | - name: Run ShellCheck 41 | # https://github.com/ludeeus/action-shellcheck 42 | uses: ludeeus/action-shellcheck@0.5.0 43 | with: 44 | # Ignore ShellSpec files 45 | ignore: spec 46 | -------------------------------------------------------------------------------- /.shellspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | 3 | ## Default kcov (coverage) options 4 | # --kcov-options "--include-path=. --path-strip-level=1" 5 | # --kcov-options "--include-pattern=.sh" 6 | # --kcov-options "--exclude-pattern=/.shellspec,/spec/,/coverage/,/report/" 7 | 8 | ## Example: Include script "myprog" with no extension 9 | # --kcov-options "--include-pattern=.sh,myprog" 10 | 11 | ## Example: Only specified files/directories 12 | # --kcov-options "--include-pattern=myprog,/lib/" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Martin Burger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bash Common Helpers 2 | 3 | This repository contains Bash utility functions I use in my Bash (and Zsh) 4 | scripts regularly. These functions are provided through a small library 5 | called `bash-common-helpers`. The library saves the trouble to redefine 6 | commonly used helper functions every time you write a shell script – be it 7 | a simple, quickly written script or a more elaborate utility. Especially in the 8 | first case, the library may save a great part of the development time. 9 | 10 | ## Overview 11 | 12 | The library currently provides the following helper functions: 13 | 14 | ### Script Initialization 15 | 16 | - `cmn_init` – supposed to be called at the beginning of every script. 17 | Makes scripts more robust by setting various shell options. 18 | - `cmn_assert_running_as_root` – makes sure that the script is run as 19 | root. 20 | 21 | ### Printing to the Screen 22 | 23 | - `cmn_echo_info` – prints informative message in green letters. 24 | - `cmn_echo_important` – prints message of higher importance in yellow 25 | letters. 26 | - `cmn_echo_warn` – prints warning in red letters. 27 | 28 | ### Error Handling 29 | 30 | - `cmn_die` – writes message in red letters to standard error and exits. 31 | 32 | ### Availability of Commands and Files 33 | 34 | - `cmn_assert_command_is_available` – makes sure that the given command is 35 | available. 36 | - `cmn_assert_file_exists` – makes sure that the given regular file 37 | exists. 38 | - `cmn_assert_file_does_not_exist` – makes sure that the given file does 39 | not exist. 40 | 41 | ### User Interaction 42 | 43 | - `cmn_ask_to_continue` – asks the user whether to continue or not. 44 | - `cmn_ask_for_password` – prompts the user for a password. Instead of 45 | echoing the entered characters, asterisks (`*`) are printed. 46 | - `cmn_ask_for_password_twice` – asks the user for her password twice and 47 | checks if both inputs match. 48 | 49 | ### File Utilities 50 | 51 | - `cmn_replace_in_files` – replaces given string in files. The function 52 | uses perl to provide a robust implementation. 53 | 54 | ### Parsing INI Files 55 | 56 | - `cmn_parse_ini_file` – parses INI file using Ruediger Meier's "simple 57 | INI file parser". 58 | - `cmn_assert_ini_variables_exist` – makes sure that the given INI 59 | variables exist and provides feedback to the user if not. 60 | 61 | Note prefix `cmn_` which is supposed to avoid clashes with the names of 62 | functions defined in your script or in any other included script. 63 | 64 | ## Installation 65 | 66 | In a nutshell: just make a clone of this repository on your harddisk and load 67 | the library by [sourcing](http://ss64.com/bash/source.html) it. 68 | 69 | For instance, go to directory `~/local/lib` and clone the repository there. In 70 | this case, the library files will end up in directory 71 | `~/local/lib/bash-common-helpers`. 72 | 73 | Now, you have to `source` the main library file in your scripts. This basically 74 | reads and executes the commands in the library files in your script's context 75 | and thus makes all the library helper functions available to your script. 76 | 77 | ### Using an Environment Variable 78 | 79 | I prefer to set an environment variable that refers to the library path and 80 | to use that variable to `source` the library file in my scripts. This way, when 81 | I move the library on my hard disk, I do not have to adapt all my scripts but 82 | the environment variable only. 83 | 84 | To set the environment variable, add the following line to your `.bashrc`, 85 | `.zshrc`, or whatever rcfile file you are using: 86 | 87 | export BASH_COMMON_HELPERS_LIB="~/local/lib/bash-common-helpers/bash-common-helpers.sh" 88 | 89 | Then, use the following header in your scripts: 90 | 91 | #!/bin/bash 92 | 93 | # BEGIN: Read functions from bash-common-helpers library. 94 | if [[ -z "${BASH_COMMON_HELPERS_LIB}" ]]; then 95 | echo "Required environment variable is not set: BASH_COMMON_HELPERS_LIB" 96 | exit 1 97 | fi 98 | if [[ ! -f "${BASH_COMMON_HELPERS_LIB}" ]]; then 99 | echo "Required file does not exist: ${BASH_COMMON_HELPERS_LIB}" 100 | exit 2 101 | fi 102 | source "${BASH_COMMON_HELPERS_LIB}" 103 | cmn_init || exit 3 104 | # END: Read functions from bash-common-helpers library. 105 | 106 | # Your actual script starts here. 107 | cmn_echo_info "Could source lib file successfully." 108 | 109 | ### Using Path to Library Directly 110 | 111 | Of course, you can omit the above environment variable and refer to the library 112 | directly: 113 | 114 | #!/bin/bash 115 | 116 | # BEGIN: Read functions from bash-common-helpers library. 117 | BASH_COMMON_HELPERS_LIB="~/local/lib/bash-common-helpers/bash-common-helpers.sh" 118 | if [[ ! -f "${BASH_COMMON_HELPERS_LIB}" ]]; then 119 | echo "Required file does not exist: ${BASH_COMMON_HELPERS_LIB}" 120 | exit 1 121 | fi 122 | source "${BASH_COMMON_HELPERS_LIB}" 123 | cmn_init || exit 2 124 | # END: Read functions from bash-common-helpers library. 125 | 126 | # Your actual script starts here. 127 | cmn_echo_info "Could source lib file successfully." 128 | 129 | ## Documentation 130 | 131 | For the documentation of the library, please refer to the source code: each 132 | function in the library has an explaining comment which is written above it. 133 | That comment contains the purpose of the function as well as an example which 134 | shows how to call the function. 135 | 136 | ## Credits 137 | 138 | Many functions have their roots in various web pages, blog posts, and lastly 139 | in answers provided by the phenomenal Stack Exchange Q&A communities. Whenever 140 | possible, I refer to the most relevant source in the documentation of the 141 | functions. 142 | 143 | Function `cmn_parse_ini_file` uses Ruediger Meier's "simple INI file parser" 144 | which is [available at GitHub](https://github.com/rudimeier/bash_ini_parser). 145 | Actually, the library includes that parser for reasons of convenience. 146 | 147 | Last but not least, the library contains valuable knowledge and experience 148 | of coworkers who showed my one or two tricks. 149 | 150 | ## License 151 | 152 | Released under the MIT License (MIT) – see file LICENSE in this software's 153 | repository. 154 | -------------------------------------------------------------------------------- /bash-common-helpers.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Martin Burger 2 | # Released under the MIT License (MIT) 3 | # https://github.com/martinburger/bash-common-helpers/blob/master/LICENSE 4 | 5 | ################################################################################ 6 | # 7 | # SEE README.MD ON HOW TO USE THE FUNCTIONS PROVIDED BY THIS LIBRARY. 8 | # 9 | ################################################################################ 10 | 11 | # 12 | # SCRIPT INITIALIZATION -------------------------------------------------------- 13 | # 14 | 15 | # cmn_init 16 | # 17 | # Should be called at the beginning of every shell script. 18 | # 19 | # Exits your script if you try to use an uninitialised variable and exits your 20 | # script as soon as any statement fails to prevent errors snowballing into 21 | # serious issues. 22 | # 23 | # Example: 24 | # cmn_init 25 | # 26 | # See: http://www.davidpashley.com/articles/writing-robust-shell-scripts/ 27 | # 28 | function cmn_init { 29 | # Will exit script if we would use an uninitialised variable: 30 | set -o nounset 31 | # Will exit script when a simple command (not a control structure) fails: 32 | set -o errexit 33 | } 34 | 35 | # cmn_assert_running_as_root 36 | # 37 | # Makes sure that the script is run as root. If it is, the function just 38 | # returns; if not, it prints an error message and exits with return code 1 by 39 | # calling `cmn_die`. 40 | # 41 | # Example: 42 | # cmn_assert_running_as_root 43 | # 44 | # Note that this function uses variable $EUID which holds the "effective" user 45 | # ID number; the EUID will be 0 even though the current user has gained root 46 | # priviliges by means of su or sudo. 47 | # 48 | # See: http://www.linuxjournal.com/content/check-see-if-script-was-run-root-0 49 | # 50 | function cmn_assert_running_as_root { 51 | if [[ ${EUID} -ne 0 ]]; then 52 | cmn_die "This script must be run as root!" 53 | fi 54 | } 55 | 56 | # 57 | # PRINTING TO THE SCREEN ------------------------------------------------------- 58 | # 59 | 60 | # cmn_echo_info message ... 61 | # 62 | # Writes the given messages in green letters to standard output. 63 | # 64 | # Example: 65 | # cmn_echo_info "Task completed." 66 | # 67 | function cmn_echo_info { 68 | local green=$(tput setaf 2) 69 | local reset=$(tput sgr0) 70 | echo -e "${green}$@${reset}" 71 | } 72 | 73 | # cmn_echo_important message ... 74 | # 75 | # Writes the given messages in yellow letters to standard output. 76 | # 77 | # Example: 78 | # cmn_echo_important "Please complete the following task manually." 79 | # 80 | function cmn_echo_important { 81 | local yellow=$(tput setaf 3) 82 | local reset=$(tput sgr0) 83 | echo -e "${yellow}$@${reset}" 84 | } 85 | 86 | # cmn_echo_warn message ... 87 | # 88 | # Writes the given messages in red letters to standard output. 89 | # 90 | # Example: 91 | # cmn_echo_warn "There was a failure." 92 | # 93 | function cmn_echo_warn { 94 | local red=$(tput setaf 1) 95 | local reset=$(tput sgr0) 96 | echo -e "${red}$@${reset}" 97 | } 98 | 99 | # 100 | # ERROR HANDLING --------------------------------------------------------------- 101 | # 102 | 103 | # cmn_die message ... 104 | # 105 | # Writes the given messages in red letters to standard error and exits with 106 | # error code 1. 107 | # 108 | # Example: 109 | # cmn_die "An error occurred." 110 | # 111 | function cmn_die { 112 | local red=$(tput setaf 1) 113 | local reset=$(tput sgr0) 114 | echo >&2 -e "${red}$@${reset}" 115 | exit 1 116 | } 117 | 118 | # 119 | # AVAILABILITY OF COMMANDS AND FILES ------------------------------------------- 120 | # 121 | 122 | # cmn_assert_command_is_available command 123 | # 124 | # Makes sure that the given command is available. 125 | # 126 | # Example: 127 | # cmn_assert_command_is_available "ping" 128 | # 129 | # See: http://stackoverflow.com/a/677212/66981 130 | # 131 | function cmn_assert_command_is_available { 132 | local cmd=${1} 133 | type ${cmd} >/dev/null 2>&1 || cmn_die "Cancelling because required command '${cmd}' is not available." 134 | } 135 | 136 | # cmn_assert_file_exists file 137 | # 138 | # Makes sure that the given regular file exists. Thus, is not a directory or 139 | # device file. 140 | # 141 | # Example: 142 | # cmn_assert_file_exists "myfile.txt" 143 | # 144 | function cmn_assert_file_exists { 145 | local file=${1} 146 | if [[ ! -f "${file}" ]]; then 147 | cmn_die "Cancelling because required file '${file}' does not exist." 148 | fi 149 | } 150 | 151 | # cmn_assert_file_does_not_exist file 152 | # 153 | # Makes sure that the given file does not exist. 154 | # 155 | # Example: 156 | # cmn_assert_file_does_not_exist "file-to-be-written-in-a-moment" 157 | # 158 | function cmn_assert_file_does_not_exist { 159 | local file=${1} 160 | if [[ -e "${file}" ]]; then 161 | cmn_die "Cancelling because file '${file}' exists." 162 | fi 163 | } 164 | 165 | # 166 | # USER INTERACTION ------------------------------------------------------------- 167 | # 168 | 169 | # cmn_ask_to_continue message 170 | # 171 | # Asks the user - using the given message - to either hit 'y/Y' to continue or 172 | # 'n/N' to cancel the script. 173 | # 174 | # Example: 175 | # cmn_ask_to_continue "Do you want to delete the given file?" 176 | # 177 | # On yes (y/Y), the function just returns; on no (n/N), it prints a confirmative 178 | # message to the screen and exits with return code 1 by calling `cmn_die`. 179 | # 180 | function cmn_ask_to_continue { 181 | local msg=${1} 182 | local waitingforanswer=true 183 | while ${waitingforanswer}; do 184 | read -p "${msg} (hit 'y/Y' to continue, 'n/N' to cancel) " -n 1 ynanswer 185 | case ${ynanswer} in 186 | [Yy] ) waitingforanswer=false; break;; 187 | [Nn] ) echo ""; cmn_die "Operation cancelled as requested!";; 188 | * ) echo ""; echo "Please answer either yes (y/Y) or no (n/N).";; 189 | esac 190 | done 191 | echo "" 192 | } 193 | 194 | # cmn_ask_for_password variable_name prompt 195 | # 196 | # Asks the user for her password and stores the password in a read-only 197 | # variable with the given name. 198 | # 199 | # The user is asked with the given message prompt. Note that the given prompt 200 | # will be complemented with string ": ". 201 | # 202 | # This function does not echo nor completely hides the input but echos the 203 | # asterisk symbol ('*') for each given character. Furthermore, it allows to 204 | # delete any number of entered characters by hitting the backspace key. The 205 | # input is concluded by hitting the enter key. 206 | # 207 | # Example: 208 | # cmn_ask_for_password "THEPWD" "Please enter your password" 209 | # 210 | # See: http://stackoverflow.com/a/24600839/66981 211 | # 212 | function cmn_ask_for_password { 213 | local VARIABLE_NAME=${1} 214 | local MESSAGE=${2} 215 | 216 | echo -n "${MESSAGE}: " 217 | stty -echo 218 | local CHARCOUNT=0 219 | local PROMPT='' 220 | local CHAR='' 221 | local PASSWORD='' 222 | while IFS= read -p "${PROMPT}" -r -s -n 1 CHAR 223 | do 224 | # Enter -> accept password 225 | if [[ ${CHAR} == $'\0' ]] ; then 226 | break 227 | fi 228 | # Backspace -> delete last char 229 | if [[ ${CHAR} == $'\177' ]] ; then 230 | if [ ${CHARCOUNT} -gt 0 ] ; then 231 | CHARCOUNT=$((CHARCOUNT-1)) 232 | PROMPT=$'\b \b' 233 | PASSWORD="${PASSWORD%?}" 234 | else 235 | PROMPT='' 236 | fi 237 | # All other cases -> read last char 238 | else 239 | CHARCOUNT=$((CHARCOUNT+1)) 240 | PROMPT='*' 241 | PASSWORD+="${CHAR}" 242 | fi 243 | done 244 | stty echo 245 | readonly ${VARIABLE_NAME}=${PASSWORD} 246 | echo 247 | } 248 | 249 | # cmn_ask_for_password_twice variable_name prompt 250 | # 251 | # Asks the user for her password twice. If the two inputs match, the given 252 | # password will be stored in a read-only variable with the given name; 253 | # otherwise, it exits with return code 1 by calling `cmn_die`. 254 | # 255 | # The user is asked with the given message prompt. Note that the given prompt 256 | # will be complemented with string ": " at the first time and with 257 | # " (again): " at the second time. 258 | # 259 | # This function basically calls `cmn_ask_for_password` twice and compares the 260 | # two given passwords. If they match, the password will be stored; otherwise, 261 | # the functions exits by calling `cmn_die`. 262 | # 263 | # Example: 264 | # cmn_ask_for_password_twice "THEPWD" "Please enter your password" 265 | # 266 | function cmn_ask_for_password_twice { 267 | local VARIABLE_NAME=${1} 268 | local MESSAGE=${2} 269 | local VARIABLE_NAME_1="${VARIABLE_NAME}_1" 270 | local VARIABLE_NAME_2="${VARIABLE_NAME}_2" 271 | 272 | cmn_ask_for_password "${VARIABLE_NAME_1}" "${MESSAGE}" 273 | cmn_ask_for_password "${VARIABLE_NAME_2}" "${MESSAGE} (again)" 274 | 275 | if [ "${!VARIABLE_NAME_1}" != "${!VARIABLE_NAME_2}" ] ; then 276 | cmn_die "Error: password mismatch" 277 | fi 278 | 279 | readonly ${VARIABLE_NAME}="${!VARIABLE_NAME_2}" 280 | } 281 | 282 | # 283 | # FILE UTILITIES --------------------------------------------------------------- 284 | # 285 | 286 | # cmn_replace_in_files search replace file ... 287 | # 288 | # Replaces given string 'search' with 'replace' in given files. 289 | # 290 | # Important: The replacement is done in-place. Thus, it overwrites the given 291 | # files, and no backup files are created. 292 | # 293 | # Note that this function is intended to be used to replace fixed strings; i.e., 294 | # it does not interpret regular expressions. It was written to replace simple 295 | # placeholders in sample configuration files (you could say very poor man's 296 | # templating engine). 297 | # 298 | # This functions expects given string 'search' to be found in all the files; 299 | # thus, it expects to replace that string in all files. If a given file misses 300 | # that string, a warning is issued by calling `cmn_echo_warn`. Furthermore, 301 | # if a given file does not exist, a warning is issued as well. 302 | # 303 | # To replace the string, perl is used. Pattern metacharacters are quoted 304 | # (disabled). The search is a global one; thus, all matches are replaced, and 305 | # not just the first one. 306 | # 307 | # Example: 308 | # cmn_replace_in_files placeholder replacement file1.txt file2.txt 309 | # 310 | function cmn_replace_in_files { 311 | 312 | local search=${1} 313 | local replace=${2} 314 | local files=${@:3} 315 | 316 | for file in ${files[@]}; do 317 | if [[ -e "${file}" ]]; then 318 | if ( grep --fixed-strings --quiet "${search}" "${file}" ); then 319 | perl -pi -e "s/\Q${search}/${replace}/g" "${file}" 320 | else 321 | cmn_echo_warn "Could not find search string '${search}' (thus, cannot replace with '${replace}') in file: ${file}" 322 | fi 323 | else 324 | cmn_echo_warn "File '${file}' does not exist (thus, cannot replace '${search}' with '${replace}')." 325 | fi 326 | done 327 | 328 | } 329 | 330 | # 331 | # PARSING INI FILES ------------------------------------------------------------ 332 | # 333 | 334 | # cmn_parse_ini_file [--boolean --prefix=STRING] file 335 | # 336 | # Parses given ini file using Ruediger Meier's "simple INI file parser". 337 | # 338 | # Example: 339 | # cmn_parse_ini_file "mycfg.ini" --prefix "TESTING" 340 | # 341 | # Now, variables in assumed section [somevars] will be available as 342 | # ${TESTING__somevars__varname}. 343 | # 344 | # Important: This function expects that `cmn_init` was called before. 345 | # 346 | # Hint: The default prefix is INI. Thus, if not specified as above, the 347 | # variables names would be: ${INI__somevars__varname} 348 | # 349 | # Please note the the parser is included at the end of this file. Thus, you do 350 | # not need to install that parser. 351 | # 352 | # See: https://github.com/rudimeier/bash_ini_parser 353 | # 354 | function cmn_parse_ini_file { 355 | 356 | set +o nounset 357 | set +o errexit 358 | read_ini $@ && rc=$? || rc=$? 359 | set -o errexit 360 | set -o nounset 361 | 362 | if [[ ${rc} != 0 ]] ; then 363 | cmn_die "read_ini exited with error code ${rc}." 364 | fi 365 | 366 | } 367 | 368 | # cmn_assert_ini_variables_exist variable_name ... 369 | # 370 | # Makes sure that the given INI variables exist. The variables are specified by 371 | # name. 372 | # 373 | # This function is intended to provide the user feedback if her INI file would 374 | # miss some expected variable. 375 | # 376 | # Example: 377 | # cmn_assert_ini_variables_exist "TESTING__somevars__var1" "TESTING__somevars__var2" 378 | # 379 | # This function uses indirect expansion: Bash uses the value of the variable 380 | # formed from the rest of parameter as the name of the variable. This way, 381 | # we can check if a variable with the given name is set. 382 | # 383 | function cmn_assert_ini_variables_exist { 384 | for variable in ${@}; do 385 | if [[ -z "${!variable-}" ]]; then 386 | cmn_die "Missing variable in INI file: ${variable}" 387 | fi 388 | done 389 | } 390 | 391 | 392 | 393 | # 394 | # 395 | # Ruediger Meier's "simple INI file parser" follows. 396 | # Commit: 8fb95e3b335823bc85604fd06c32b0d25f2854c5 397 | # Date: 2014-10-21T08:40:19Z 398 | # 399 | # 400 | 401 | 402 | 403 | # 404 | # Copyright (c) 2009 Kevin Porter / Advanced Web Construction Ltd 405 | # (http://coding.tinternet.info, http://webutils.co.uk) 406 | # Copyright (c) 2010-2014 Ruediger Meier 407 | # (https://github.com/rudimeier/) 408 | # 409 | # License: BSD-3-Clause, see LICENSE file 410 | # 411 | # Simple INI file parser. 412 | # 413 | # See README for usage. 414 | # 415 | # 416 | 417 | 418 | 419 | 420 | function read_ini() 421 | { 422 | # Be strict with the prefix, since it's going to be run through eval 423 | function check_prefix() 424 | { 425 | if ! [[ "${VARNAME_PREFIX}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] ;then 426 | echo "read_ini: invalid prefix '${VARNAME_PREFIX}'" >&2 427 | return 1 428 | fi 429 | } 430 | 431 | function check_ini_file() 432 | { 433 | if [ ! -r "$INI_FILE" ] ;then 434 | echo "read_ini: '${INI_FILE}' doesn't exist or not" \ 435 | "readable" >&2 436 | return 1 437 | fi 438 | } 439 | 440 | # enable some optional shell behavior (shopt) 441 | function pollute_bash() 442 | { 443 | if ! shopt -q extglob ;then 444 | SWITCH_SHOPT="${SWITCH_SHOPT} extglob" 445 | fi 446 | if ! shopt -q nocasematch ;then 447 | SWITCH_SHOPT="${SWITCH_SHOPT} nocasematch" 448 | fi 449 | shopt -q -s ${SWITCH_SHOPT} 450 | } 451 | 452 | # unset all local functions and restore shopt settings before returning 453 | # from read_ini() 454 | function cleanup_bash() 455 | { 456 | shopt -q -u ${SWITCH_SHOPT} 457 | unset -f check_prefix check_ini_file pollute_bash cleanup_bash 458 | } 459 | 460 | local INI_FILE="" 461 | local INI_SECTION="" 462 | 463 | # {{{ START Deal with command line args 464 | 465 | # Set defaults 466 | local BOOLEANS=1 467 | local VARNAME_PREFIX=INI 468 | local CLEAN_ENV=0 469 | 470 | # {{{ START Options 471 | 472 | # Available options: 473 | # --boolean Whether to recognise special boolean values: ie for 'yes', 'true' 474 | # and 'on' return 1; for 'no', 'false' and 'off' return 0. Quoted 475 | # values will be left as strings 476 | # Default: on 477 | # 478 | # --prefix=STRING String to begin all returned variables with (followed by '__'). 479 | # Default: INI 480 | # 481 | # First non-option arg is filename, second is section name 482 | 483 | while [ $# -gt 0 ] 484 | do 485 | 486 | case $1 in 487 | 488 | --clean | -c ) 489 | CLEAN_ENV=1 490 | ;; 491 | 492 | --booleans | -b ) 493 | shift 494 | BOOLEANS=$1 495 | ;; 496 | 497 | --prefix | -p ) 498 | shift 499 | VARNAME_PREFIX=$1 500 | ;; 501 | 502 | * ) 503 | if [ -z "$INI_FILE" ] 504 | then 505 | INI_FILE=$1 506 | else 507 | if [ -z "$INI_SECTION" ] 508 | then 509 | INI_SECTION=$1 510 | fi 511 | fi 512 | ;; 513 | 514 | esac 515 | 516 | shift 517 | done 518 | 519 | if [ -z "$INI_FILE" ] && [ "${CLEAN_ENV}" = 0 ] ;then 520 | echo -e "Usage: read_ini [-c] [-b 0| -b 1]] [-p PREFIX] FILE"\ 521 | "[SECTION]\n or read_ini -c [-p PREFIX]" >&2 522 | cleanup_bash 523 | return 1 524 | fi 525 | 526 | if ! check_prefix ;then 527 | cleanup_bash 528 | return 1 529 | fi 530 | 531 | local INI_ALL_VARNAME="${VARNAME_PREFIX}__ALL_VARS" 532 | local INI_ALL_SECTION="${VARNAME_PREFIX}__ALL_SECTIONS" 533 | local INI_NUMSECTIONS_VARNAME="${VARNAME_PREFIX}__NUMSECTIONS" 534 | if [ "${CLEAN_ENV}" = 1 ] ;then 535 | eval unset "\$${INI_ALL_VARNAME}" 536 | fi 537 | unset ${INI_ALL_VARNAME} 538 | unset ${INI_ALL_SECTION} 539 | unset ${INI_NUMSECTIONS_VARNAME} 540 | 541 | if [ -z "$INI_FILE" ] ;then 542 | cleanup_bash 543 | return 0 544 | fi 545 | 546 | if ! check_ini_file ;then 547 | cleanup_bash 548 | return 1 549 | fi 550 | 551 | # Sanitise BOOLEANS - interpret "0" as 0, anything else as 1 552 | if [ "$BOOLEANS" != "0" ] 553 | then 554 | BOOLEANS=1 555 | fi 556 | 557 | 558 | # }}} END Options 559 | 560 | # }}} END Deal with command line args 561 | 562 | local LINE_NUM=0 563 | local SECTIONS_NUM=0 564 | local SECTION="" 565 | 566 | # IFS is used in "read" and we want to switch it within the loop 567 | local IFS=$' \t\n' 568 | local IFS_OLD="${IFS}" 569 | 570 | # we need some optional shell behavior (shopt) but want to restore 571 | # current settings before returning 572 | local SWITCH_SHOPT="" 573 | pollute_bash 574 | 575 | while read -r line || [ -n "$line" ] 576 | do 577 | #echo line = "$line" 578 | 579 | ((LINE_NUM++)) 580 | 581 | # Skip blank lines and comments 582 | if [ -z "$line" -o "${line:0:1}" = ";" -o "${line:0:1}" = "#" ] 583 | then 584 | continue 585 | fi 586 | 587 | # Section marker? 588 | if [[ "${line}" =~ ^\[[a-zA-Z0-9_]{1,}\]$ ]] 589 | then 590 | 591 | # Set SECTION var to name of section (strip [ and ] from section marker) 592 | SECTION="${line#[}" 593 | SECTION="${SECTION%]}" 594 | eval "${INI_ALL_SECTION}=\"\${${INI_ALL_SECTION}# } $SECTION\"" 595 | ((SECTIONS_NUM++)) 596 | 597 | continue 598 | fi 599 | 600 | # Are we getting only a specific section? And are we currently in it? 601 | if [ ! -z "$INI_SECTION" ] 602 | then 603 | if [ "$SECTION" != "$INI_SECTION" ] 604 | then 605 | continue 606 | fi 607 | fi 608 | 609 | # Valid var/value line? (check for variable name and then '=') 610 | if ! [[ "${line}" =~ ^[a-zA-Z0-9._]{1,}[[:space:]]*= ]] 611 | then 612 | echo "Error: Invalid line:" >&2 613 | echo " ${LINE_NUM}: $line" >&2 614 | cleanup_bash 615 | return 1 616 | fi 617 | 618 | 619 | # split line at "=" sign 620 | IFS="=" 621 | read -r VAR VAL <<< "${line}" 622 | IFS="${IFS_OLD}" 623 | 624 | # delete spaces around the equal sign (using extglob) 625 | VAR="${VAR%%+([[:space:]])}" 626 | VAL="${VAL##+([[:space:]])}" 627 | VAR=$(echo $VAR) 628 | 629 | 630 | # Construct variable name: 631 | # ${VARNAME_PREFIX}__$SECTION__$VAR 632 | # Or if not in a section: 633 | # ${VARNAME_PREFIX}__$VAR 634 | # In both cases, full stops ('.') are replaced with underscores ('_') 635 | if [ -z "$SECTION" ] 636 | then 637 | VARNAME=${VARNAME_PREFIX}__${VAR//./_} 638 | else 639 | VARNAME=${VARNAME_PREFIX}__${SECTION}__${VAR//./_} 640 | fi 641 | eval "${INI_ALL_VARNAME}=\"\${${INI_ALL_VARNAME}# } ${VARNAME}\"" 642 | 643 | if [[ "${VAL}" =~ ^\".*\"$ ]] 644 | then 645 | # remove existing double quotes 646 | VAL="${VAL##\"}" 647 | VAL="${VAL%%\"}" 648 | elif [[ "${VAL}" =~ ^\'.*\'$ ]] 649 | then 650 | # remove existing single quotes 651 | VAL="${VAL##\'}" 652 | VAL="${VAL%%\'}" 653 | elif [ "$BOOLEANS" = 1 ] 654 | then 655 | # Value is not enclosed in quotes 656 | # Booleans processing is switched on, check for special boolean 657 | # values and convert 658 | 659 | # here we compare case insensitive because 660 | # "shopt nocasematch" 661 | case "$VAL" in 662 | yes | true | on ) 663 | VAL=1 664 | ;; 665 | no | false | off ) 666 | VAL=0 667 | ;; 668 | esac 669 | fi 670 | 671 | 672 | # enclose the value in single quotes and escape any 673 | # single quotes and backslashes that may be in the value 674 | VAL="${VAL//\\/\\\\}" 675 | VAL="\$'${VAL//\'/\'}'" 676 | 677 | eval "$VARNAME=$VAL" 678 | done <"${INI_FILE}" 679 | 680 | # return also the number of parsed sections 681 | eval "$INI_NUMSECTIONS_VARNAME=$SECTIONS_NUM" 682 | 683 | cleanup_bash 684 | } 685 | -------------------------------------------------------------------------------- /spec/bash-common-helpers_spec.sh: -------------------------------------------------------------------------------- 1 | Describe "bash-common-helpers' function" 2 | Include ./bash-common-helpers.sh 3 | 4 | Describe "cmn_echo_ ..." 5 | 6 | Parameters 7 | "info()" cmn_echo_info 8 | "important()" cmn_echo_important 9 | "warn()" cmn_echo_warn 10 | End 11 | 12 | It "$1 outputs single given string without space character" 13 | When call $2 "abcdef" 14 | The output should include "abcdef" 15 | End 16 | It "$1 outputs single given string with space character" 17 | When call $2 "abc def" 18 | The output should include "abc def" 19 | End 20 | It "$1 outputs two given strings each one without space character" 21 | When call $2 "abc" "def" 22 | The output should include "abc def" 23 | End 24 | It "$1 outputs two given strings each one with space character" 25 | When call $2 "abc " " def" 26 | The output should include "abc def" 27 | End 28 | 29 | End 30 | 31 | Describe "cmn_die()" 32 | It "exits" 33 | When run cmn_die "exited with status 1" 34 | The error should include "exited with status 1" 35 | The status should be failure 36 | End 37 | End 38 | 39 | End 40 | -------------------------------------------------------------------------------- /spec/spec_helper.sh: -------------------------------------------------------------------------------- 1 | #shellcheck shell=sh 2 | 3 | # set -eu 4 | 5 | # shellspec_spec_helper_configure() { 6 | # shellspec_import 'support/custom_matcher' 7 | # } 8 | --------------------------------------------------------------------------------