├── .gitignore ├── LICENSE ├── README.md ├── STANDARDS.md ├── base.sh ├── base_init.sh ├── bin ├── base-wrapper ├── caff ├── sort-in-place └── test-command1 ├── company ├── bin │ └── test-command1 └── lib │ └── bashrc ├── demo ├── README.md ├── error_handling_demo.sh ├── error_handling_demo_lib.sh └── logging_demo.sh ├── docs └── img │ └── directory_structure.png ├── lib ├── README.md ├── assertions.sh ├── base_defaults.sh ├── bash_profile ├── bashrc ├── file.sh ├── net.sh ├── shopt.sh └── stdlib.sh ├── team ├── README.md ├── test-team1 │ ├── bin │ │ └── test-command1 │ └── lib │ │ ├── bashrc │ │ └── test-team1.sh └── test-team2 │ ├── bin │ └── test-command1 │ └── lib │ ├── bashrc │ └── test-team2.sh ├── test ├── test_base_init.sh └── test_stdlib.sh └── user ├── common_user.sh ├── test_user1.sh └── test_user2.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore macOS DS_Store files 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ramesh Padmanabhaiah 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 | # **What is Base?** 2 | 3 | ``` 4 | Great things are done by a series of small things brought together. 5 | - Vincent van Gogh 6 | ``` 7 | Base is a sharing platform for shell settings, libraries, and light-weight tools. It gives a structured way for Bash users to organize the following across multiple hosts: 8 | 9 | * .bash_profile 10 | * .bashrc 11 | * generic Bash libraries and commands 12 | * company specific Bash libraries, commands, and configuration 13 | * team specific Bash libraries, commands, and configuration 14 | * user specific settings (aliases, functions, Bash settings) 15 | * Bash libraries, commands, and configuration that are shared across teams 16 | 17 | It can benefit anyone who engages with Mac/Linux command line to get their work done. 18 | 19 | # **Requirements** 20 | 21 | Base needs Bash version 4.2 or above. 22 | 23 | # **How can I get set up?** 24 | 25 | Set up is easy. Essentially, this is what you have to do: 26 | 27 | * Check out Base. The standard location for Base is $HOME/base. In case your git directory is elsewhere, symlink `$HOME/git` to it or specify the path by setting `BASE_HOME` in `$HOME/.baserc` file. 28 | * Consolidate your individual settings from your current `.bash_profile` and `.bashrc` into `$USER.sh` file. Place this file under `base/user` directory and check it in to git. 29 | * Make a backup of your `.bash_profile`. Replace this file with a symlink to `base/lib/bash_profile`. 30 | * Make a backup of your `.bashrc`. Replace this file with a symlink to `base/lib/bashrc`. 31 | 32 | Log out and log back in or just do `exec bash` and you are all set! 33 | 34 | Here is an example: 35 | 36 | cd $HOME 37 | mkdir git && cd git 38 | git clone git@github.com:codeforester/base.git 39 | cd $HOME 40 | mv .bash_profile .bash_profile.safe && ln -sf $HOME/base/lib/bash_profile .bash_profile 41 | mv .bashrc .bashrc.safe && ln -sf $HOME/base/lib/bashrc .bashrc 42 | cp $USER.sh $HOME/base/user 43 | cd $HOME/base 44 | git add user/$USER.sh 45 | git commit -m "Adding the initial version of $USER.sh" 46 | git push 47 | 48 | If you don't want to disturb your `.bash_profile` and `.bashrc`, you can still use Base in a less full-fledged manner. See the FAQ section for details. 49 | 50 | # **How does Base work?** 51 | 52 | In a typical setting, `.bashrc` sources in `$BASE_HOME/base_init.sh` which does the following: 53 | 54 | * source in `lib/stdlib.sh` 55 | * source in `company/lib/company.sh` if it exists 56 | * source in `company/lib/bashrc` if it exists, if the shell is interactive 57 | * source in `user/$USER.sh` if it exists and if the shell is interactive 58 | * source in team specific bashrc from `team//lib/bashrc` for each team defined in `BASE_TEAM` and `BASE_SHARED_TEAMS` variables, if the shell is interactive. Note that `BASE_TEAM` and `BASE_SHARED_TEAMS` should be ideally set in `user/$USER.sh`. 59 | * source in team specific library from `team//lib/.sh` for each team defined in `BASE_TEAM` and `BASE_SHARED_TEAMS` variables, if they exist 60 | * update `$PATH` to include the relevant `bin` directories 61 | * `$BASE_HOME/bin` is always added 62 | * `$BASE_HOME/team/$BASE_TEAM/bin` is added if `$BASE_TEAM` is set in `user/$USER.sh` 63 | * `$BASE_HOME/team/$BASE_TEAM/bin` is added for each team defined in `$BASE_SHARED_TEAMS` (space-separated string), set in `user/$USER.sh` 64 | * `$BASE_HOME/company/bin` is always added 65 | 66 | # **Directory structure** 67 | 68 | [![Screenshot of directory structure](./docs/img/directory_structure.png)](./docs/img/directory_structure.png) 69 | 70 | # **Environment variables** 71 | 72 | * BASE_HOME 73 | * BASE_DEBUG 74 | * BASE_TEAM 75 | * BASE_SHARED_TEAMS 76 | * BASE_OS 77 | * BASE_HOST 78 | * BASE_SOURCES 79 | 80 | # **Functions exported by base_init.sh** 81 | 82 | * import - sources in libraries from any place under `BASE_HOME` directory 83 | * base_update - does a `git pull` on Base git directory; add it to `user/.sh` to "auto update" Base 84 | 85 | # **FAQ** 86 | 87 | ## My git location is not `$HOME/base`. What should I do? 88 | 89 | You can either 90 | 91 | * specify your Base location in `$HOME/.baserc`, like 92 | 93 | BASE_HOME=/path/to/base 94 | 95 | * symlink `$HOME/base` to the right place 96 | 97 | You need to do this on every host where you want Base. 98 | 99 | ## I want to keep my personal settings private, and not in git. What should I do? 100 | 101 | * write a one-liner in `user/$USER.sh` like this: 102 | 103 | source /path/to/your.settings 104 | 105 | You would need to manage this file outside of Base. 106 | 107 | ## I do want to use the default settings. What should I do? 108 | 109 | Add this to your `user/$USER.sh` file: 110 | 111 | import lib/base_defaults.sh 112 | 113 | ## I want to make sure I keep my Base repository updated always. How can I do it? 114 | 115 | Add this to your `user/$USER.sh` file: 116 | 117 | base_update 118 | 119 | ## I don't want to reorganize my `.bash_profile` or `.bashrc`. Can I still use Base? 120 | 121 | Yes, you can, though you will lose the flexibility of keeping your `.bash_profile` and `.bashrc` synced across hosts in case you are working with multiple hosts. 122 | 123 | To turn on Base upon login, add this to your `.bash_profile`: 124 | 125 | export BASE_HOME=/path/to/base 126 | source "$BASE_HOME/base_init.sh" 127 | 128 | after making sure you have the base repo checked out under `$BASE_HOME` directory. 129 | 130 | If you don't want to change your `.bash_profile` at all, you can still turn Base on and off as needed. First, make sure BASE_HOME is set appropriately, ideally in your `.bash_profile`. 131 | 132 | Run this command to get a shell with Base turned on: 133 | 134 | $BASE_HOME/base.sh shell 135 | 136 | or 137 | $BASE_HOME/base.sh 138 | 139 | # **Debugging** 140 | 141 | * You can turn on debug mode by touching `$HOME/.base_debug` file. You can also do the same by setting environment variable `BASE_DEBUG` to 1. 142 | * You can add `set -x` to `$HOME/.baserc` file to trace the execution in detail. 143 | -------------------------------------------------------------------------------- /STANDARDS.md: -------------------------------------------------------------------------------- 1 | # Standards followed in this framework 2 | 3 | 0. Use four spaces for indentation. No tabs. 4 | 1. Shell/local variables and function names follow "snake_case" - only lowercase letters, underscores, and digits. 5 | 2. Environment variables use all uppercase names. For example, BASE_HOME, BASE_HOST, BASE_OS, BASE_SOURCES. 6 | See: https://stackoverflow.com/a/42290320/6862601 7 | 3. In rare cases of global variables being shared between library functions and their callers, use all uppercase names. 8 | For example: OUTPUT and OUTPUT_ARRAY 9 | 4. Place most code inside functions and invoke the main function at the bottom of the script. 10 | 5. In libraries, have top level code that prevents the file from being sourced more than once. For example: 11 | ```bash 12 | [[ $__stdlib_sourced__ ]] && return 13 | __stdlib_sourced__=1 14 | ``` 15 | 6. Make sure all local variables inside functions are declared local. 16 | 7. Use __func__ naming convention for special purpose variables and functions. Use a leading underscore for "private" variables and functions. 17 | 8. Double quote all variable expansions, except: 18 | - inside [[ ]] or (( )) 19 | - places where we need word splitting to take place 20 | 21 | 9. Use [[ $var ]] to check if var has non-zero length, instead of [[ -n $var ]]. 22 | See: https://stackoverflow.com/a/49825114/6862601 23 | 10. Use "compact" style for if statements and loops: 24 | ```bash 25 | if condition; then 26 | ... 27 | fi 28 | 29 | while condition; do 30 | ... 31 | done 32 | 33 | for ((i=0; i < limit; i++)); do 34 | ... 35 | done 36 | ``` 37 | 11. Make sure the code passes https://shellcheck.net checks. 38 | -------------------------------------------------------------------------------- /base.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # base.sh 5 | # 6 | # This is a wrapper or a common entry point for Base. It helps in two main ways: 7 | # 8 | # 1. Install base or change Base settings (install, embrace, set-team, set-shared-teams) 9 | # 2. Do things with the installed version of Base (status, run, shell) 10 | # 11 | # In shell mode, we would create a Bash shell: 12 | # a) using Base's bash_profile in case Base is installed 13 | # b) using the in-line common bash_profile in case Base is not installed 14 | # 15 | 16 | error_exit() { printf 'ERROR: %s\n' "$@" >&2; exit 1; } 17 | exit_if_error() { local ec=$1; shift; (($ec)) && error_exit "$@"; } 18 | cd_base() { cd -- "$BASE_HOME" || exit_if_error 1 "Can't cd to BASE_HOME at '$BASE_HOME'"; } 19 | usage_error() { 20 | printf '%s\n' "$@" >&2 21 | show_common_help >&2 22 | exit 2 23 | } 24 | 25 | show_common_help() { 26 | cat << EOF 27 | Usage: base [-b DIR] [-t TEAM] [-x] [install|embrace|update|run|status|shell|set-team|set-shared-teams|version|help] ... 28 | -b DIR - use DIR as BASE_HOME directory 29 | -t TEAM - use TEAM as BASE_TEAM 30 | -s TEAM - use TEAM as BASE_SHARED_TEAMS [use space delimited strings for multiple teams] 31 | -f - ignore the existing installation and force install [relevant only for 'base install' command] 32 | -v - show the CLI version 33 | -x - turn on bash debug mode 34 | 35 | install - install Base 36 | embrace - override .bash_profile and .bashrc so that Base gets enabled upon login 37 | update - update Base by running 'git pull' in BASE_HOME directory 38 | run - run the rest of the command line after initializing Base 39 | shell - if Base is installed, create an interactive Bash shell with Base initialized 40 | if Base is not installed, create an interactive Bash shell with default settings 41 | status - check if Base is installed or not 42 | set-team TEAM - set BASE_TEAM in $HOME/.baserc 43 | set-shared-teams TEAM - set shared BASE_SHARED_TEAMS in $HOME/.baserc [use space delimited strings for multiple teams] 44 | version - show the CLI version 45 | help - show this help message 46 | man - print one line summary of all Base scripts, use '-t team' to filter by team 47 | 48 | Invoking without any arguments would result in an interactive Bash shell with default settings. 49 | EOF 50 | } 51 | 52 | base_init() { 53 | local base_init=$BASE_HOME/base_init.sh 54 | [[ -f $base_init ]] && source "$base_init" 55 | } 56 | 57 | get_base_home() { 58 | if [[ ! $HOME ]]; then 59 | error_exit "Environment variable 'HOME' is not set!" 60 | fi 61 | if [[ ! -d $HOME ]]; then 62 | error_exit "\$HOME '$HOME' is not a directory!" 63 | fi 64 | 65 | # if BASE_HOME is not already set, source .baserc to see it is defined there 66 | if [[ ! $BASE_HOME ]]; then 67 | local baserc=$HOME/.baserc 68 | [[ -f $baserc ]] && source "$baserc" 69 | fi 70 | 71 | # if BASE_HOME is still not set, go with the default value 72 | BASE_HOME=${BASE_HOME:-$HOME/base} 73 | } 74 | 75 | create_base_home() { 76 | # if set, BASE_HOME must hold a directory name 77 | if [[ -e $BASE_HOME && ! -d $BASE_HOME ]]; then 78 | error_exit "$BASE_HOME exists but it is not a directory!" 79 | else 80 | mkdir -- "$BASE_HOME" 81 | exit_if_error $? "Can't create directory '$BASE_HOME'" 82 | fi 83 | } 84 | 85 | verify_base() { 86 | # now make sure BASE_HOME directory is actually a git repo 87 | local git=$BASE_HOME/.git 88 | if [[ ! -d $git ]]; then 89 | glb_error_message="Directory '$BASE_HOME' isn't a git repo; check if Base is installed" 90 | return 1 91 | else 92 | local oldpwd=$PWD 93 | cd -- "$git" || { glb_error_message="Can't cd to '$git' directory"; return 1; } 94 | if ! git rev-parse --git-dir &>/dev/null; then 95 | glb_error_message="Directory '$git' isn't a git repo; check if Base is installed" 96 | return 1 97 | fi 98 | local file missing=() 99 | for file in base_init.sh lib/bash_profile lib/bashrc; do 100 | if [[ ! -f $BASE_HOME/$file ]]; then 101 | missing+=($file) 102 | fi 103 | done 104 | cd -- "$oldpwd" 105 | if (( ${#missing[@]} > 0)); then 106 | glb_error_message="Files missing in Base repo: ${missing[@]}" 107 | return 1 108 | fi 109 | fi 110 | return 0 111 | } 112 | 113 | patch_baserc() { 114 | local var value base_text_array=() grep_expr 115 | local marker="# BASE_MARKER, do not delete" 116 | local baserc=$HOME/.baserc baserc_temp=$HOME/.baserc.temp 117 | for var; do 118 | value=${!var} 119 | if [[ $value ]]; then 120 | base_text_array+=("export $var=\"$value\" $marker") 121 | fi 122 | if [[ $grep_expr ]]; then 123 | grep_expr="$grep_expr|$var=.*$marker" 124 | else 125 | grep_expr="$var=.*$marker" 126 | fi 127 | done 128 | if [[ ! -f $baserc ]]; then 129 | touch -- "$baserc" 130 | exit_if_error $? "Couldn't create '$baserc'" 131 | fi 132 | rm -f "$baserc_temp" 133 | if [[ $grep_expr ]]; then 134 | grep -Ev -- "$grep_expr" "$baserc" > "$baserc_temp" 135 | else 136 | touch -- "$baserc_temp" 137 | fi 138 | [[ -f $baserc_temp ]] || exit_if_error 1 "Couldn't create '$baserc_temp'" 139 | printf '%s\n' "${base_text_array[@]}" >> "$baserc_temp" 140 | exit_if_error $? "Couldn't append to '$baserc_temp'" 141 | mv -f -- "$baserc_temp" "$baserc" 142 | exit_if_error $? "Couldn't overwrite '$baserc'" 143 | return 0 144 | } 145 | 146 | do_install() { 147 | local repo="ssh://git@github.com:codeforester/base.git" 148 | if [[ -d $BASE_HOME ]]; then 149 | if ((force_install)); then 150 | local base_home_backup=$BASE_HOME.$current_time 151 | if mv -- "$BASE_HOME" "$base_home_backup"; then 152 | printf '%s\n' "Moved current base home directory '$BASE_HOME' to '$base_home_backup'" 153 | else 154 | exit_if_error 1 "Couldn't move current base home directory '$BASE_HOME' to '$base_home_backup'" 155 | fi 156 | else 157 | printf '%s\n' "Base is already installed at '$BASE_HOME'" 158 | exit 0 159 | fi 160 | fi 161 | 162 | git clone "$repo" "$BASE_HOME" 163 | exit_if_error $? "Couldn't install Base" 164 | printf '%s\n' "Installed Base at '$BASE_HOME'" 165 | 166 | # 167 | # patch .baserc 168 | # This is how we remember custom BASE_HOME path and BASE_TEAM values. 169 | # The user is free to put custom code into the .baserc file. 170 | # A marker is appended to the lines managed by base CLI. 171 | # 172 | BASE_TEAM=$base_team 173 | BASE_SHARED_TEAMS=$base_shared_teams 174 | patch_baserc BASE_HOME BASE_TEAM BASE_SHARED_TEAMS 175 | 176 | exit 0 177 | } 178 | 179 | do_embrace() { 180 | if ! verify_base; then 181 | error_exit "$glb_error_message" 182 | fi 183 | local base_bash_profile=$BASE_HOME/lib/bash_profile 184 | local base_bashrc=$BASE_HOME/lib/bashrc 185 | local bash_profile=$HOME/.bash_profile 186 | local bashrc=$HOME/.bashrc 187 | if [[ -L $bash_profile ]]; then 188 | local bash_profile_link=$(readlink "$bash_profile") 189 | fi 190 | if [[ -L $bashrc ]]; then 191 | local bashrc_link=$(readlink "$bashrc") 192 | fi 193 | if [[ $bash_profile_link = $base_bash_profile ]]; then 194 | printf '%s\n' "$bash_profile is already symlinked to $base_bash_profile" 195 | else 196 | if [[ -f $bash_profile ]]; then 197 | local bash_profile_backup=$HOME/.bash_profile.$current_time 198 | printf '%s\n' "Backing up $bash_profile to $bash_profile_backup and overriding it with $base_bash_profile" 199 | if ! cp -- "$bash_profile" "$bash_profile_backup"; then 200 | exit_if_error $? "ERROR: can't create a backup of $bash_profile" 201 | fi 202 | fi 203 | if ln -sf -- "$base_bash_profile" "$bash_profile"; then 204 | printf '%s\n' "Symlinked '$bash_profile' to '$base_bash_profile'" 205 | fi 206 | fi 207 | if [[ $bashrc_link = $base_bashrc ]]; then 208 | printf '%s\n' "$bashrc is already symlinked to $base_bashrc" 209 | else 210 | if [[ -f $bashrc ]]; then 211 | local bashrc_backup=$HOME/.bashrc.$current_time 212 | printf '%s\n' "Backing up $bashrc to $bashrc_backup and overriding it with $base_bashrc" 213 | if ! cp -- "$bashrc" "$bashrc_backup"; then 214 | exit_if_error $? "ERROR: can't create a backup of $bashrc" 215 | fi 216 | fi 217 | if ln -sf -- "$base_bashrc" "$bashrc"; then 218 | printf '%s\n' "Symlinked '$bash_profile' to '$base_bash_profile'" 219 | fi 220 | fi 221 | } 222 | 223 | do_update() { 224 | if [[ -d $BASE_HOME ]]; then 225 | cd -- "$BASE_HOME" || error_exit "Can't cd to BASE_HOME at '$BASE_HOME'" 226 | git pull 227 | else 228 | printf '%s\n' "ERROR: Base is not installed at BASE_HOME '$BASE_HOME'" 229 | exit 1 230 | fi 231 | } 232 | 233 | do_run() { 234 | if ! verify_base; then 235 | error_exit "$glb_error_message" 236 | fi 237 | base_init 238 | "$@" 239 | } 240 | 241 | do_status() { 242 | if [[ ! -d $BASE_HOME ]]; then 243 | printf '%s\n' "Base is not installed at '$BASE_HOME'" 244 | exit 1 245 | fi 246 | 247 | if ! verify_base; then 248 | error_exit "$glb_error_message" 249 | else 250 | printf '%s\n' "Base is installed at $BASE_HOME" 251 | fi 252 | exit 0 253 | } 254 | 255 | do_shell() { 256 | local base_bash_profile=$BASE_HOME/lib/bash_profile 257 | if [[ -d $BASE_HOME && -f $base_bash_profile ]]; then 258 | export BASE_SHELL=1 259 | exec bash --rcfile "$base_bash_profile" 260 | else 261 | do_common_shell 262 | fi 263 | } 264 | 265 | do_common_shell() { 266 | local common_bash_profile=${0%/*}/bash_profile 267 | if [[ -f $common_bash_profile ]]; then 268 | exec bash --rcfile "$common_bash_profile" 269 | else 270 | error_exit "Common bash profile '$common_bash_profile' not found" 271 | fi 272 | } 273 | 274 | do_version() { 275 | printf '%s\n' "base version $BASE_VERSION" 276 | } 277 | 278 | do_man() { 279 | local dir bin desc dirs team 280 | local -A teams 281 | if ! verify_base; then 282 | error_exit "$glb_error_message" 283 | fi 284 | if ! command -v base-wrapper >/dev/null; then 285 | error_exit "base-wrapper needs to be in your \$PATH to be able to run this command." 286 | fi 287 | dirs=(bin company/bin) 288 | [[ $base_team ]] || base_team=$BASE_TEAM 289 | if [[ $base_team ]]; then 290 | dirs+=(team/$base_team/bin) 291 | teams[$base_team]=1 292 | fi 293 | # note: BASE_SHARED_TEAMS could be a space delimited string or an array 294 | for team in $BASE_SHARED_TEAMS "${BASE_SHARED_TEAMS[@]}"; do 295 | [[ ${teams[$team]} ]] && continue 296 | dirs+=(team/$team/bin) 297 | teams[$team]=1 298 | done 299 | for dir in "${dirs[@]}"; do 300 | dir=$BASE_HOME/$dir 301 | [[ -d $dir ]] || continue 302 | cd -- "$dir" || continue 303 | printf '%s\n' "${dir#$BASE_HOME/}:" 304 | for bin in *; do 305 | [[ -f $bin ]] || continue 306 | if head -1 "$bin" | grep -Eq '!/usr/bin/env[[:space:]]+base-wrapper[[:space:]]*'; then 307 | desc=$(./"$bin" --describe) 308 | printf '\t\t%s\n' "$bin: $desc" 309 | fi 310 | done 311 | done 312 | } 313 | 314 | do_set_team() { 315 | if (($# > 1)); then 316 | usage_error "Got too many arguments" 317 | else 318 | if [[ $1 ]]; then 319 | BASE_TEAM=$1 320 | else 321 | BASE_TEAM=$base_team 322 | fi 323 | if [[ ! $BASE_TEAM ]]; then 324 | usage_error "Usage: base set-team TEAM" 325 | fi 326 | fi 327 | 328 | patch_baserc BASE_TEAM 329 | } 330 | 331 | do_set_shared_teams() { 332 | if (($# > 0)); then 333 | BASE_SHARED_TEAMS=$* 334 | elif [[ $base_shared_teams ]]; then 335 | BASE_SHARED_TEAMS=$base_shared_teams 336 | else 337 | usage_error "Usage: base set-shared-teams TEAM ..." 338 | fi 339 | patch_baserc BASE_SHARED_TEAMS 340 | } 341 | 342 | assert_bash_version() { 343 | local curr_version=$1 min_needed_version=$2 344 | if ((curr_version < min_needed_version)); then 345 | error_exit "Need Bash version >= $min_needed_version; your version is $curr_version" 346 | fi 347 | } 348 | 349 | main() { 350 | local bash_version="${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" timefmt="%Y-%m-%d:%H:%M:%S" 351 | if [[ $1 =~ -h|--help|-help|help ]]; then 352 | show_common_help 353 | exit 0 354 | fi 355 | if [[ $1 =~ --version|-version|-v ]]; then 356 | do_version 357 | exit 0 358 | fi 359 | force_install=0 360 | if ((bash_version >= 42)); then 361 | printf -v current_time "%($timefmt)T" -1 362 | else 363 | current_time=$(date +"$timefmt") 364 | fi 365 | while getopts "fhb:s:t:vx" opt; do 366 | case $opt in 367 | b) export BASE_HOME=$OPTARG;; 368 | t) export base_team=$OPTARG;; 369 | s) export base_shared_teams=$OPTARG;; 370 | f) force_install=1;; 371 | v) do_version 372 | exit 0;; 373 | x) set -x;; 374 | *) show_common_help >&2 375 | exit 2;; 376 | esac 377 | done 378 | shift $((OPTIND-1)) 379 | command=$1 380 | shift 2>/dev/null 381 | get_base_home 382 | case $command in 383 | embrace) do_embrace;; 384 | help) show_common_help;; 385 | install) assert_bash_version "$bash_version" 42; do_install;; 386 | run) do_run "$@";; 387 | set-shared-teams) do_set_shared_teams "$@";; 388 | set-team) do_set_team "$@";; 389 | shell) do_shell;; 390 | status) do_status;; 391 | update) do_update;; 392 | version) do_version;; 393 | man) do_man;; 394 | "") do_common_shell;; 395 | *) usage_error "Unrecognized command: $command";; 396 | esac 397 | } 398 | 399 | main "$@" 400 | -------------------------------------------------------------------------------- /base_init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # base_init.sh: top level script that should be sourced in by login/interactive shells 5 | # 6 | # lib/bashrc invokes this 7 | # 8 | 9 | [[ $__base_init_sourced__ ]] && return 10 | __base_init_sourced__=1 11 | 12 | check_bash_version() { 13 | local major=${1:-4} 14 | local minor=$2 15 | local rc=0 16 | local num_re='^[0-9]+$' 17 | 18 | if [[ ! $major =~ $num_re ]] || [[ $minor && ! $minor =~ $num_re ]]; then 19 | printf '%s\n' "ERROR: version numbers should be numeric" 20 | return 1 21 | fi 22 | if [[ $minor ]]; then 23 | local bv=${BASH_VERSINFO[0]}${BASH_VERSINFO[1]} 24 | local vstring=$major.$minor 25 | local vnum=$major$minor 26 | else 27 | local bv=${BASH_VERSINFO[0]} 28 | local vstring=$major 29 | local vnum=$major 30 | fi 31 | ((bv < vnum)) && { 32 | printf '%s\n' "ERROR: Base needs Bash version $vstring or above, your version is ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" 33 | rc=1 34 | } 35 | return $rc 36 | } 37 | 38 | do_init() { 39 | local rc=0 40 | [[ -f $HOME/.base_debug ]] && export BASE_DEBUG=1 41 | if [[ $BASH ]]; then 42 | # Bash 43 | base_debug() { [[ $BASE_DEBUG ]] && printf '%(%Y-%m-%d:%H:%M:%S)T %s\n' -1 "DEBUG ${BASH_SOURCE[0]}:${BASH_LINENO[1]} $@" >&2; } 44 | base_error() { printf '%(%Y-%m-%d:%H:%M:%S)T %s\n' -1 "ERROR ${BASH_SOURCE[0]}:${BASH_LINENO[1]} $@" >&2; } 45 | elif [[ $ZSH_VERSION ]]; then 46 | # 47 | # for zsh - it doesn't support time in printf 48 | # 49 | base_debug() { [[ $BASE_DEBUG ]] && printf '%s\n' "$(date) DEBUG ${BASH_SOURCE[0]}:${BASH_LINENO[1]} $@" >&2; } 50 | base_error() { printf '%s\n' "$(date) ERROR ${BASH_SOURCE[0]}:${BASH_LINENO[1]} $@" >&2; } 51 | else 52 | printf '%s\n' "ERROR: Unsupported shell - need Bash or zsh" >&2 53 | rc=1 54 | fi 55 | 56 | BASE_OS=$(uname -s) 57 | BASE_HOST=$(hostname -s) 58 | export BASE_SOURCES=() BASE_OS BASE_HOST 59 | 60 | return $rc 61 | } 62 | 63 | set_base_home() { 64 | script=$HOME/.baserc 65 | [[ -f $script ]] && [[ -z $_baserc_sourced ]] && { 66 | base_debug "Sourcing $script" 67 | # shellcheck source=/dev/null 68 | source "$script" 69 | _baserc_sourced=1 70 | } 71 | 72 | # set BASE_HOME to default in case it is not set 73 | [[ -z $BASE_HOME ]] && { 74 | local dir=$HOME/base 75 | base_debug "BASE_HOME not set; defaulting it to '$dir'" 76 | BASE_HOME=$dir 77 | } 78 | 79 | export BASE_HOME 80 | } 81 | 82 | # 83 | # check for existence of the library, source it, add its name to BASE_SOURCES array 84 | # Usage: source_it [-i] library_file 85 | # -i - source only if the shell is interactive 86 | # 87 | source_it() { 88 | local lib iflag=0 sourced=0 89 | [[ $1 = "-i" ]] && { iflag=1; shift; } 90 | lib=$1 91 | if ((iflag)); then 92 | # shellcheck source=/dev/null 93 | ((_interactive)) && [[ -f $lib ]] && { base_debug "(interactive) Sourcing $lib"; source "$lib"; sourced=1; } 94 | else 95 | # shellcheck source=/dev/null 96 | [[ -f $lib ]] && { base_debug "Sourcing $lib"; source "$lib"; sourced=1; } 97 | fi 98 | ((sourced)) && BASE_SOURCES+=("$lib") 99 | } 100 | 101 | # 102 | # source in libraries, starting from the top (lowest precedence) to the bottom (highest precedence) 103 | # 104 | import_libs_and_profiles() { 105 | local lib script team 106 | local -A teams 107 | 108 | source_it "$BASE_HOME/lib/stdlib.sh" # common library 109 | source_it "$BASE_HOME/company/lib/company.sh" # company specific library 110 | source_it -i "$BASE_HOME/company/lib/bashrc" # company specific bashrc for interactive shells 111 | source_it -i "$BASE_HOME/user/$USER.sh" # user specific bashrc in the repo for interactive shells 112 | source_it -i "$HOME/.baserc-$USER" # user specific bashrc outside the repo for interactive shells 113 | 114 | # 115 | # team specific actions 116 | # 117 | # Users choose teams by setting the "BASE_TEAM" variable in their user specific startup script 118 | # For example: BASE_TEAM=teamX 119 | # 120 | # Users can also set "BASE_SHARED_TEAMS" to more teams so as to share from those teams. 121 | # For example: BASE_SHARED_TEAMS="teamY teamZ" or 122 | # BASE_SHARED_TEAMS=(teamY teamZ) 123 | # 124 | # We source the team specific startup script add the team bin directory to PATH, in the same order 125 | # 126 | teams=() 127 | for team in $BASE_TEAM $BASE_SHARED_TEAMS "${BASE_SHARED_TEAMS[@]}"; do 128 | [[ ${teams[$team]} ]] && continue # skip if team was seen already 129 | source_it "$BASE_HOME/team/$team/lib/$team.sh" # team specific library 130 | source_it -i "$BASE_HOME/team/$team/lib/bashrc" # team specific bashrc for interactive shells 131 | add_to_path "$BASE_HOME/team/$team/bin" # add team bin to PATH (gets priority over company bin) 132 | teams[$team]=1 133 | done 134 | 135 | # add company bin to PATH; team bins, if any, take priority over company bin 136 | add_to_path "$BASE_HOME/company/bin" 137 | } 138 | 139 | # 140 | # A shortcut to refresh the base git repo; users can add it to user/.sh file so that base is automatically 141 | # updated upon login. 142 | # 143 | base_update() ( 144 | [[ -d $BASE_HOME ]] && { 145 | cd "$BASE_HOME" && git pull --rebase 146 | } 147 | ) 148 | 149 | base_main() { 150 | check_bash_version 4 2 || return $? 151 | do_init || return $? 152 | [[ $- = *i* ]] && _interactive=1 || _interactive=0 153 | set_base_home 154 | if [[ -d $BASE_HOME ]]; then 155 | import_libs_and_profiles 156 | add_to_path "$BASE_HOME/bin" 157 | else 158 | base_error "BASE_HOME '$BASE_HOME' is not a directory or is not accessible" 159 | fi 160 | 161 | # 162 | # these functions need to be available to user's subprocesses 163 | # 164 | export -f base_update import 165 | } 166 | 167 | # 168 | # start here 169 | # 170 | base_main 171 | -------------------------------------------------------------------------------- /bin/base-wrapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # base_wrapper 5 | # 6 | # This script is meant to be used in the shebang line of Base scripts, like this: 7 | # 8 | # #!/usr/bin/env base-wrapper 9 | # 10 | # What does this wrapper do? 11 | # 12 | # It discovers base_init and sources it. It also looks at the command line options and interprets a few of those, 13 | # like --debug, --help, --describe etc. It calls the main function with the special options removed from the argument list. 14 | # The main function is expected to be defined by the wrapped script. 15 | # 16 | 17 | _bw_name="base-wrapper" 18 | _bw_description="Run shell scripts in a standardized environment enriched with Base features" 19 | _bw_check_and_execute() { 20 | local cmd=$1; shift 21 | if command -v -- "$cmd" &>/dev/null; then 22 | "$cmd" "$@" 23 | exit $? 24 | else 25 | print_error "Function '$cmd' not implemented" 26 | exit 1 27 | fi 28 | } 29 | 30 | _bw_describe() { 31 | printf '%s\n' "$_bw_description" 32 | } 33 | 34 | _bw_usage() { 35 | printf '%s\n' "$_bw_name: $_bw_description" 36 | printf '%s\n' "Usage: $_bw_name [options] script [args]" 37 | cat << EOF 38 | Options: 39 | -debug|--debug - Turn on DEBUG logging 40 | -shell-debug|--shell-debug - Turn shell debug on through 'set -x' 41 | -describe|--describe - Invoke base_describe function if defined in script 42 | -help|--help - Invoke base_help function if defined in script; if no script specified, show this help 43 | EOF 44 | } 45 | 46 | base_wrapper() { 47 | local arg args script 48 | 49 | ## 50 | ## Do Base set up 51 | ## 52 | 53 | [[ $1 = "-d" ]] && { grab_debug=1; shift; } 54 | [[ $BASE_HOME ]] || { printf '%s\n' "ERROR: BASE_HOME is not set" >&2; exit 1; } 55 | [[ -d $BASE_HOME ]] || { printf '%s\n' "ERROR: BASE_HOME '$BASE_HOME'is not a directory or is not readable" >&2; exit 1; } 56 | script=$BASE_HOME/base_init.sh 57 | [[ -f $script ]] || { printf '%s\n' "ERROR: base_init script '$script'is not present or is not readable" >&2; exit 1; } 58 | # shellcheck source=/dev/null 59 | source "$script" 60 | import lib/stdlib.sh 61 | 62 | ## 63 | ## Execute the script after processing the special command line arguments 64 | ## 65 | 66 | script=$1 67 | if [[ ! $script || $script = -* ]]; then 68 | _bw_usage >&2 69 | exit 2 70 | elif [[ ! -f $script ]]; then 71 | print_error "Script '$script' does not exist" 72 | _bw_usage >&2 73 | exit 1 74 | fi 75 | source "$1" 76 | shift 77 | for arg; do 78 | if [[ $arg =~ ^-debug$|^--debug$ ]]; then 79 | export BASE_DEBUG=1 80 | set_log_level DEBUG 81 | elif [[ $arg =~ ^-shell-debug$|^--shell-debug$ ]]; then 82 | set -x 83 | elif [[ $arg =~ ^-describe$|^--describe$|^-desc$|^--desc$ ]]; then 84 | _bw_check_and_execute base_describe 85 | elif [[ $arg =~ ^-help$|^--help$|^-h$|^--help$ ]]; then 86 | _bw_check_and_execute base_help 87 | else 88 | args+=("$arg") 89 | fi 90 | done 91 | 92 | set -- "${args[@]}" 93 | log_debug "Invoking 'main' with arguments: $@" 94 | _bw_check_and_execute main "$@" 95 | } 96 | 97 | base_wrapper "$@" 98 | -------------------------------------------------------------------------------- /bin/caff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env base-wrapper 2 | 3 | # 4 | # caff: call caffeinate for a named process 5 | # 6 | 7 | name=caff 8 | description="Caffeinate a named process" 9 | base_help() { 10 | cat << EOUSAGE 11 | $description 12 | Usage: caff process_name 13 | If process_name matches multiple running process, we pick up the first one. 14 | EOUSAGE 15 | } 16 | 17 | base_describe() { 18 | printf '%s\n' "$description" 19 | } 20 | 21 | main() { 22 | silent=0 23 | if [[ $1 = "-s" ]]; then 24 | silent=1 25 | shift 26 | fi 27 | if ! type -ft caffeinate >/dev/null; then 28 | print_error "There is no caffeinate command on your system" 29 | exit 1 30 | fi 31 | 32 | (($# != 1)) && { 33 | print_error "need an argument" 34 | base_help 35 | exit 2 36 | } 37 | 38 | process_name=$1 39 | local vpid=$(pgrep "$process_name" | head -1) 40 | if [[ ! $vpid ]]; then 41 | print_warn "'$process_name' process not running" 42 | exit 1 43 | fi 44 | local cpid=$(pgrep caffeinate) 45 | if [[ $cpid ]]; then 46 | caffeinate_pid=$(ps -o args -p "$cpid" | awk 'NR==2 {print $3}') 47 | if [[ $caffeinate_pid == $vpid ]]; then 48 | ((silent)) || printf '%s\n' "Alreading caffeinating: $process_name pid=$vpid, caffeinate pid=$cpid" 49 | exit 0 50 | fi 51 | fi 52 | 53 | printf '%s\n' "Caffeinating PID $vpid" 54 | caffeinate -iw "$vpid" & disown 55 | exit $? 56 | } 57 | -------------------------------------------------------------------------------- /bin/sort-in-place: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env base-wrapper 2 | 3 | # 4 | # sort-in-place: sort files in place 5 | # 6 | 7 | name=sort-in-place 8 | description="Sort text files in place" 9 | base_describe() { 10 | printf '%s\n' "$description" 11 | } 12 | 13 | base_help() { 14 | printf '%s\n' "$name: $description" 15 | printf '%s\n' "Usage: $name [-u] file ..." 16 | } 17 | 18 | main() { 19 | local u_flag file temp rc=0 20 | if [[ $1 = "-u" ]]; then 21 | u_flag=$1 22 | shift 23 | fi 24 | 25 | for file; do 26 | if [[ ! -f $file ]]; then 27 | print_warning "$file is not a regular file; skipping" 28 | continue 29 | fi 30 | temp="$file._tmp" 31 | if [[ -f "$temp" ]]; then 32 | print_warning "$temp already exists; skipping $file" 33 | continue 34 | fi 35 | sort $u_flag "$file" > "$temp" 36 | if (($? != 0)); then 37 | print_error "Can't write to '$temp'" 38 | rc=1 39 | else 40 | if ! mv -- "$temp" "$file"; then 41 | print_error "Can't move '$temp' to '$file'" 42 | rc=1 43 | fi 44 | fi 45 | done 46 | 47 | exit $rc 48 | } 49 | -------------------------------------------------------------------------------- /bin/test-command1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env base-wrapper 2 | # 3 | # ^^^ this shebang line is needed 4 | # 5 | 6 | # 7 | # test command: 8 | # 9 | 10 | name=test-command1 11 | description="Test command 1" 12 | 13 | # 14 | # base-wrapper invokes this function when the script is invoked with '--describe' or '-describe' option 15 | # 16 | base_describe() { 17 | printf '%s\n' "$description" 18 | } 19 | 20 | # 21 | # base-wrapper invokes this function when the script is invoked with '--help' or '-help' or '-h' option 22 | # 23 | base_help() { 24 | printf '%s\n' "$name: $description" 25 | printf '%s\n' "Usage: $name ..." 26 | } 27 | 28 | # 29 | # base-wrapper invokes the main function after stripping out standard options like --describe, --debug, and --help from 30 | # the command line arguments list 31 | # 32 | main() { 33 | if (($# != 1)); then 34 | print_error "Invalid arguments" 35 | base_help 36 | exit 2 37 | fi 38 | 39 | # 40 | # base-wrapper imports stdlib.sh. So, all standard functions, including logging, are available. 41 | # 42 | log_info "Starting" 43 | 44 | # do something 45 | log_info "Finished" 46 | } 47 | 48 | # 49 | # no need to call main function since that responsibility is delegated to base-wrapper 50 | # 51 | -------------------------------------------------------------------------------- /company/bin/test-command1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env base-wrapper 2 | 3 | # 4 | # see bin/test-command1 to understand the structure of a Base-wrapped script 5 | # 6 | 7 | # 8 | # test command for team 9 | # 10 | main() { 11 | log_info "Starting" 12 | # do something 13 | log_info "Finished" 14 | } 15 | -------------------------------------------------------------------------------- /company/lib/bashrc: -------------------------------------------------------------------------------- 1 | # 2 | # Company specific bashrc for interactive shells 3 | # 4 | 5 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Logging demo 2 | 3 | ``` bash 4 | $ ./logging_demo.sh 5 | 2019-04-09:14:08:04 DEBUG ./logging_demo.sh:31 This is a debug log 6 | 2019-04-09:14:08:04 INFO ./logging_demo.sh:32 This is an info log 7 | 2019-04-09:14:08:04 WARN ./logging_demo.sh:33 This is a warning 8 | 2019-04-09:14:08:04 ERROR ./logging_demo.sh:34 This is an error 9 | 2019-04-09:14:08:04 FATAL ./logging_demo.sh:35 This is a fatal error 10 | 2019-04-09:14:08:04 VERBOSE ./logging_demo.sh:38 This is a verbose log 11 | 2019-04-09:14:08:04 WARN ./logging_demo.sh:44 This is a warning 12 | 2019-04-09:14:08:04 INFO ./logging_demo.sh:10 Entering function test_func 13 | 2019-04-09:14:08:04 DEBUG ./logging_demo.sh:11 Entering function test_func 14 | 2019-04-09:14:08:04 VERBOSE ./logging_demo.sh:12 Entering function test_func 15 | 2019-04-09:14:08:04 INFO ./logging_demo.sh:14 Leaving function test_func 16 | 2019-04-09:14:08:04 DEBUG ./logging_demo.sh:15 Leaving function test_func 17 | 2019-04-09:14:08:04 VERBOSE ./logging_demo.sh:16 Leaving function test_func 18 | 2019-04-09:14:08:04 DEBUG ../lib/stdlib.sh:161 Contents of file '/tmp/__log_demo__.txt': 19 | first line 20 | second line 21 | third line 22 | 2019-04-09:14:08:04 DEBUG ../lib/stdlib.sh:161 Contents of file '/tmp/__log_demo__.txt': 23 | first line 24 | second line 25 | third line 26 | 2019-04-09:14:08:04 DEBUG ../lib/stdlib.sh:161 Contents of file '/tmp/__log_demo__.txt': 27 | first line 28 | second line 29 | third line 30 | ERROR: This is a plain error 31 | WARN: This is a plain warning 32 | This is a plain info 33 | ``` 34 | # Error handling demo 35 | 36 | ```bash 37 | $ ./error_handling_demo.sh 38 | 2019-04-09:14:07:33 DEBUG ./error_handling_demo.sh:10 Entering function test_func1 39 | 2019-04-09:14:07:33 INFO ./error_handling_demo.sh:11 Calling test_func2 40 | 2019-04-09:14:07:33 DEBUG ./error_handling_demo.sh:18 Entering function test_func2 41 | 2019-04-09:14:07:33 INFO ./error_handling_demo.sh:19 Calling test_func3 42 | 2019-04-09:14:07:33 DEBUG ./error_handling_demo.sh:26 Entering function test_func3 43 | 2019-04-09:14:07:33 DEBUG ./error_handling_demo.sh:21 Leaving function test_func2 44 | 2019-04-09:14:07:33 FATAL ../lib/stdlib.sh:240 test_func2 failed 45 | Encountered a fatal error 46 | at exit_if_error (../lib/stdlib.sh:241) 47 | at test_func1 (./error_handling_demo.sh:13) 48 | at main (./error_handling_demo.sh:34) 49 | at main (./error_handling_demo.sh:38) 50 | 2019-04-09:14:07:33 DEBUG ./error_handling_demo_lib.sh:6 Entering function demo_lib_func 51 | 2019-04-09:14:07:33 FATAL ../lib/stdlib.sh:240 Deliberately exiting! 52 | Encountered a fatal error 53 | at exit_if_error (../lib/stdlib.sh:241) 54 | at demo_lib_func (./error_handling_demo_lib.sh:7) 55 | at main (./error_handling_demo.sh:35) 56 | at main (./error_handling_demo.sh:38) 57 | ``` 58 | -------------------------------------------------------------------------------- /demo/error_handling_demo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | trap 'rm -f -- "$tempfile"' EXIT 4 | 5 | # run this from demo directory 6 | source ../lib/stdlib.sh 7 | source ./error_handling_demo_lib.sh 8 | 9 | test_func1() { 10 | log_debug_enter 11 | log_info "Calling test_func2" 12 | test_func2 13 | exit_if_error $? "test_func2 failed" 14 | log_debug_leave 15 | } 16 | 17 | test_func2() { 18 | log_debug_enter 19 | log_info "Calling test_func3" 20 | test_func3; ret=$? 21 | log_debug_leave 22 | return $ret 23 | } 24 | 25 | test_func3() { 26 | log_debug_enter 27 | return 1 28 | log_debug_leave 29 | } 30 | 31 | main() { 32 | set_log_level DEBUG 33 | # run tests in subshell so that the parent can continue even after error handler calls exit 34 | (test_func1) 35 | (demo_lib_func) # this is defined in error_handling_demo_lib.sh 36 | } 37 | 38 | main 39 | -------------------------------------------------------------------------------- /demo/error_handling_demo_lib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ../lib/stdlib.sh 4 | 5 | demo_lib_func() { 6 | log_debug_enter 7 | exit_if_error 1 "Deliberately exiting!" 8 | log_debug_leave 9 | } 10 | -------------------------------------------------------------------------------- /demo/logging_demo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | trap 'rm -f -- "$tempfile"' EXIT 4 | 5 | # run this from demo directory 6 | source ../lib/stdlib.sh 7 | 8 | test_func() { 9 | set_log_level VERBOSE 10 | log_info_enter 11 | log_debug_enter 12 | log_verbose_enter 13 | 14 | log_info_leave 15 | log_debug_leave 16 | log_verbose_leave 17 | } 18 | 19 | test_file_logging() { 20 | tempfile=/tmp/__log_demo__.txt 21 | printf '%s\n' "first line" "second line" "third line" > $tempfile 22 | set_log_level VERBOSE 23 | log_info_file "$tempfile" 24 | log_debug_file "$tempfile" 25 | log_verbose_file "$tempfile" 26 | rm -f -- "$tempfile" 27 | } 28 | 29 | set_log_level DEBUG 30 | log_verbose "This verbose log won't print" 31 | log_debug "This is a debug log" 32 | log_info "This is an info log" 33 | log_warn "This is a warning" 34 | log_error "This is an error" 35 | log_fatal "This is a fatal error" 36 | 37 | set_log_level VERBOSE 38 | log_verbose "This is a verbose log" 39 | 40 | set_log_level WARN 41 | log_verbose "This verbose log won't print" 42 | log_debug "This debug log won't print" 43 | log_info "This info log that won't print" 44 | log_warn "This is a warning" 45 | 46 | test_func 47 | test_file_logging 48 | 49 | print_error "This is a plain error" 50 | print_warn "This is a plain warning" 51 | print_info "This is a plain info" 52 | -------------------------------------------------------------------------------- /docs/img/directory_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforester/base/dd9707dd35619649a3df3e2224da7a1426e0719c/docs/img/directory_structure.png -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | ## Logging examples 2 | 3 | ### Basic logging 4 | 5 | ``` bash 6 | source stdlib.sh 7 | 8 | SECONDS=0 9 | log_info "Program started" 10 | 11 | # the default log level is INFO; override it to DEBUG 12 | set_log_level DEBUG 13 | 14 | log_debug "Running step 1" 15 | # code for step 1 16 | 17 | log_warn "This is a warning" 18 | 19 | required_file=/path/to/required/file 20 | if [[ ! -f $required_file ]]; then 21 | log_error "File '$required_file' does not exist, skipping this step" 22 | else 23 | # log the contents of this file in DEBUG mode 24 | log_debug_file "$required_file" 25 | 26 | log_verbose "Calling process_file" 27 | process_file "$required_file" 28 | log_verbose "Call to process_file finished" 29 | fi 30 | 31 | log_info "Program finished, elapsed time = $SECONDS seconds" 32 | ``` 33 | 34 | ### Multiline logs 35 | 36 | ``` 37 | # Pass each log line as a separate argument 38 | log_info "This is a multi-line log" "This is the second line" "This is the last line" 39 | 40 | # store log lines in an array and pass the array to the logging function 41 | log_lines=("log line 1" "log line 2" "log line 3") 42 | log_debug "${log_lines[@]}" 43 | ``` 44 | 45 | ## Error handling example 46 | 47 | ``` bash 48 | source stdlib.sh 49 | 50 | path=/path/to/required/dir 51 | [[ -d $path ]] || fatal_error "Directory '$path' does not exist" 52 | 53 | call_some_function; exit_if_error $? "Some function failed" 54 | ``` 55 | -------------------------------------------------------------------------------- /lib/assertions.sh: -------------------------------------------------------------------------------- 1 | ## 2 | ## Assertions 3 | ## 4 | 5 | [[ $__assertions_sourced__ ]] && return 6 | __assertions_sourced__=1 7 | 8 | # 9 | # Given a version like x.y, where x and y are numbers, asserts that 10 | # bash version is at least x.y 11 | # 12 | assert_minimum_bash_version() { 13 | local version=$1 message=$2 14 | local version_array curr_version version_re='^[0-9]+\.[0-9]+$' 15 | assert_arg_count $# 1 "Usage: assert_minimum_bash_version version" 16 | assert_regex_match "$version" "$version_re" "Version should be in the format x.y where x and y are integers" 17 | version_array=(${version//\./ }) 18 | ((BASH_VERSINFO[0] < version_array[0] || 19 | ((BASH_VERSINFO[0] == version_array[0] && BASH_VERSINFO[1] < version_array[1])))) && { 20 | curr_version="${BASH_VERSINFO[@]:0:4}" 21 | [[ $message ]] || message="Running with Bash version ${curr_version// /.}; need $version or above" 22 | printf '%s\n' "$message" >&2 23 | exit 1 24 | } 25 | return 0 26 | } 27 | 28 | # 29 | # exit if number of arguments passed doesn't meet expectations 30 | # example call: 31 | # assert_arg_count $# 2 "Need exactly two arguments" 32 | # 33 | assert_arg_count() { 34 | local actual=$1 expected=$2 message=$3 35 | ((actual != expected)) && { 36 | [[ $message ]] || message="Expected $expected arguments, got $actual arguments" 37 | printf '%s\n' "$message" >&2 38 | exit 1 39 | } 40 | } 41 | 42 | assert_regex_match() { 43 | local string=$1 regex=$2 message=$3 44 | [[ $string =~ $regex ]] || { 45 | [[ $message ]] || message="String '$string' does not match regex '$regex'" 46 | printf '%s\n' "$message" >&2 47 | exit 1 48 | } 49 | } 50 | 51 | # 52 | # assert if variables are set 53 | # if any variable is not set, exit 1 (when -f option is set) or return 1 otherwise 54 | # 55 | # Usage: assert_var_not_null [-f] variable ... 56 | # 57 | assert_var_not_null() { 58 | local fatal var num_null=0 59 | [[ "$1" = "-f" ]] && { shift; fatal=1; } 60 | for var in "$@"; do 61 | [[ -z "${!var}" ]] && printf '%s\n' "Variable '$var' not set" >&2 && 62 | ((num_null++)) 63 | done 64 | 65 | if ((num_null > 0)); then 66 | [[ "$fatal" ]] && exit 1 67 | return 1 68 | fi 69 | return 0 70 | } 71 | 72 | # 73 | # assert if $1 is a valid URL 74 | # 75 | assert_valid_url() { 76 | (($#)) || return 0 77 | local url=$1 78 | curl --fail --head -o /dev/null --silent "$url" || fatal_error "Invalid URL - '$url'" 79 | } 80 | 81 | # 82 | # assert if directories do exist 83 | # 84 | assert_dir_exists() { 85 | local dir 86 | local rc=0 87 | for dir; do 88 | if [[ ! -d "$dir" ]]; then 89 | log_error "Directory '$dir' does not exist" 90 | ((rc++)) 91 | fi 92 | done 93 | ((rc)) && exit 1 94 | } 95 | 96 | # 97 | # assert if directories do exist 98 | # 99 | assert_file_exists() { 100 | local file 101 | local rc=0 102 | for file; do 103 | if [[ ! -f "$file" ]]; then 104 | log_error "File '$file' does not exist" 105 | ((rc++)) 106 | fi 107 | done 108 | ((rc)) && exit 1 109 | } 110 | 111 | # 112 | # assert that we are running on the right OS 113 | # 114 | assert_os() { 115 | (($#)) || return 116 | if [[ $BASE_OS != "$1" ]]; then 117 | fatal_error "Required OS: $1, current OS: $BASE_OS" 118 | fi 119 | } 120 | -------------------------------------------------------------------------------- /lib/base_defaults.sh: -------------------------------------------------------------------------------- 1 | # 2 | # base_defaults.sh 3 | # 4 | 5 | ### 6 | ### Aliases 7 | ### 8 | alias rm='rm -i' 9 | alias cp='cp -i' 10 | alias mv='mv -i' 11 | 12 | ### 13 | ### Command editing 14 | ### 15 | set -o vi 16 | export EDITOR=vi 17 | 18 | ### 19 | ### vi/vim 20 | ### 21 | export EXINIT="set ts=4 sw=4 ai nows nosm expandtab" 22 | 23 | ### 24 | ### Prompt 25 | ### 26 | export PS1='\[\033[0;35m\]\T \h\[\033[0;33m\] \w\[\033[00m\]: ' 27 | 28 | ### 29 | ### Bash history 30 | ### 31 | export HISTCONTROL=ignoredups:erasedups 32 | export HISTTIMEFORMAT="[%F %T] " 33 | shopt -s histappend 34 | -------------------------------------------------------------------------------- /lib/bash_profile: -------------------------------------------------------------------------------- 1 | # 2 | # .bash_profile 3 | # 4 | 5 | do_init() { 6 | [[ -f $HOME/.base_debug ]] && export BASE_DEBUG=1 7 | base_debug() { 8 | [[ $BASE_DEBUG ]] && 9 | printf '%(%Y-%m-%d:%H:%M:%S)T %s\n' -1 "DEBUG ${BASH_SOURCE[0]}:$LINENO $@" >&2 10 | } 11 | base_debug "Running .bash_profile" 12 | } 13 | 14 | # 15 | # Source global profile 16 | # 17 | source_global_profile() { 18 | if shopt -q login_shell; then 19 | local global_profile=/etc/profile 20 | if [[ -f $global_profile ]]; then 21 | base_debug "Sourcing $global_profile" 22 | source "$global_profile" 23 | fi 24 | fi 25 | } 26 | 27 | # 28 | # Source .bashrc 29 | # 30 | source_bashrc() { 31 | local bashrc=$HOME/.bashrc 32 | if [[ -f $bashrc ]]; then 33 | base_debug "Invoking $bashrc from .bash_profile" 34 | source "$bashrc" 35 | fi 36 | } 37 | 38 | do_init 39 | source_global_profile 40 | source_bashrc 41 | -------------------------------------------------------------------------------- /lib/bashrc: -------------------------------------------------------------------------------- 1 | # 2 | # .bashrc 3 | # 4 | 5 | do_init() { 6 | [[ -f $HOME/.base_debug ]] && export BASE_DEBUG=1 7 | base_debug() { 8 | [[ $BASE_DEBUG ]] && 9 | printf '%(%Y-%m-%d:%H:%M:%S)T %s\n' -1 "DEBUG ${BASH_SOURCE[0]} $@" >&2 10 | } 11 | base_error() { 12 | printf '%(%Y-%m-%d:%H:%M:%S)T %s\n' -1 "ERROR ${BASH_SOURCE[0]} $@" >&2 13 | } 14 | base_debug "Running .bashrc" 15 | } 16 | 17 | # 18 | # Source global bashrc 19 | # 20 | global_init() { 21 | local global_bashrc=/etc/bashrc 22 | if [[ -f $global_bashrc ]]; then 23 | base_debug "Sourcing $global_bashrc" 24 | source "$global_bashrc" 25 | fi 26 | } 27 | 28 | # 29 | # base stuff 30 | # 31 | base_init() { 32 | local script 33 | 34 | script=$HOME/.baserc 35 | [[ -f $script ]] && { 36 | base_debug "Sourcing $script" 37 | source "$script" 38 | _baserc_sourced=1 39 | } 40 | 41 | # set BASE_HOME to default in case it is not set 42 | [[ -z $BASE_HOME ]] && { 43 | dir=$HOME/base 44 | base_debug "Defaulting BASE_HOME to '$dir'" 45 | export BASE_HOME=$dir 46 | } 47 | 48 | [[ ! -d "$BASE_HOME" ]] && { 49 | base_error "BASE_HOME directory '$BASE_HOME' does not exist" 50 | return 51 | } 52 | 53 | script=$BASE_HOME/base_init.sh 54 | if [[ -f $script ]]; then 55 | base_debug "Sourcing $script" 56 | source "$script" 57 | else 58 | base_error "base init script '$script' does not exist; check your git repository" 59 | return 60 | fi 61 | } 62 | 63 | do_init 64 | global_init 65 | base_init 66 | -------------------------------------------------------------------------------- /lib/file.sh: -------------------------------------------------------------------------------- 1 | ## 2 | ## file related functions 3 | ## 4 | 5 | dirname2() { 6 | local path=$1 7 | [[ $path =~ ^[^/]+$ ]] && dir=. || { # if path has no slashes, set dir to . 8 | [[ $path =~ ^/+$ ]] && dir=/ || { # if path has only slashes, set dir to / 9 | local IFS=/ dir_a i 10 | read -ra dir_a <<< "$path" # read the components of path into an array 11 | dir="${dir_a[0]}" 12 | for ((i=1; i < ${#dir_a[@]}; i++)); do # strip out any repeating slashes 13 | [[ ${dir_a[i]} ]] && dir="$dir/${dir_a[i]}" # append unless it is an empty element 14 | done 15 | } 16 | } 17 | 18 | [[ $dir ]] && printf '%s\n' "$dir" # print only if not empty 19 | } 20 | 21 | # 22 | # Attempt to create a list of directories; throw fatal error lazily in case any mkdir fails 23 | # 24 | base_mkdir() { 25 | local dir fails=0 failed_dirs=() 26 | for dir; do 27 | mkdir -p -- "$dir" 28 | if (($? != 0)); then 29 | ((fails++)) 30 | failed_dirs+=("$dir") 31 | fi 32 | done 33 | if ((fails == 1)); then 34 | fatal_error "Couldn't create directory '${failed_dirs[0]}'" 35 | elif ((fails > 1)); then 36 | fatal_error "Couldn't create these directories: ${failed_dirs[@]}" 37 | fi 38 | return 0 39 | } 40 | 41 | # 42 | # Simple copy with error check 43 | # 44 | base_simple_cp() { 45 | local src=$1 dest=$2 46 | if (($# != 2)); then 47 | fatal_error "Usage: base_simple_cp source dest" 48 | fi 49 | 50 | if ! command cp -- "$src" "$dest"; then 51 | fatal_error "Can't cp '$src' to '$dest'" 52 | fi 53 | } 54 | 55 | # 56 | # Simple move with error check 57 | # 58 | base_simple_mv() { 59 | local src=$1 dest=$2 60 | if (($# != 2)); then 61 | fatal_error "Usage: base_simple_mv source dest" 62 | fi 63 | 64 | if ! command mv -- "$src" "$dest"; then 65 | fatal_error "Can't mv '$src' to '$dest'" 66 | fi 67 | } 68 | -------------------------------------------------------------------------------- /lib/net.sh: -------------------------------------------------------------------------------- 1 | # 2 | # networking related functions 3 | # 4 | 5 | import lib/assertions.sh 6 | 7 | # 8 | # check if $1 is a valid IPV4 address; return 0 if true, 1 otherwise 9 | # 10 | validate_ip4() { 11 | local arr element 12 | IFS=. read -r -a arr <<< "$1" 13 | [[ ${#arr[@]} != 4 ]] && return 1 14 | for element in "${arr[@]}"; do 15 | [[ (! $element =~ ^[0-9]+$) || 16 | $element =~ ^0[1-9]+$ 17 | ]] && return 1 18 | ((element < 0 || element > 255)) && return 1 19 | done 20 | return 0 21 | } 22 | 23 | # 24 | # check if URL is valid. Credit: https://stackoverflow.com/a/12199125/6862601 25 | # 26 | is_valid_url() { 27 | assert_arg_count $# 1 "is_valid_url: expected 1 argument, got $#" 28 | curl --output /dev/null --silent --head --fail "$1" 29 | } 30 | 31 | is_valid_url_no_head() { 32 | assert_arg_count $# 1 "is_valid_url_no_head: expected 1 argument, got $#" 33 | curl --output /dev/null --silent --fail -r 0-0 "$1" 34 | } 35 | -------------------------------------------------------------------------------- /lib/shopt.sh: -------------------------------------------------------------------------------- 1 | # 2 | # shopt.sh: Make it easy to turn shopt options on/off and restore the earlier settings 3 | # in a clean way, preventing any side effects. 4 | # 5 | 6 | # 7 | # associative array to hold state 8 | # 9 | declare -gA _shopt_restore 10 | 11 | # 12 | # shopt set one or more options 13 | # 14 | shopt_set() { 15 | local opt 16 | for opt; do 17 | if ! shopt -q "$opt"; then 18 | echo "$opt not set, setting it" 19 | shopt -s "$opt" 20 | _shopt_restore[$opt]=1 21 | else 22 | echo "$opt set already" 23 | fi 24 | done 25 | } 26 | 27 | # 28 | # shopt unset one or more options 29 | # 30 | shopt_unset() { 31 | local opt restore_type 32 | for opt; do 33 | restore_type=${_shopt_restore[$opt]} 34 | if shopt -q "$opt"; then 35 | echo "$opt set, unsetting it" 36 | shopt -u "$opt" 37 | _shopt_restore[$opt]=2 38 | else 39 | echo "$opt unset already" 40 | fi 41 | if [[ $restore_type == 1 ]]; then 42 | unset _shopt_restore[$opt] 43 | fi 44 | done 45 | } 46 | 47 | # 48 | # restore one or more shopt options which were changed earlier; if no options passed, restore all 49 | # 50 | shopt_restore() { 51 | local opt opts restore_type 52 | if (($# > 0)); then 53 | opts=("$@") 54 | else 55 | opts=("${!_shopt_restore[@]}") 56 | fi 57 | for opt in "${opts[@]}"; do 58 | restore_type=${_shopt_restore[$opt]} 59 | case $restore_type in 60 | 1) 61 | echo "unsetting $opt" 62 | shopt -u "$opt" 63 | unset _shopt_restore[$opt] 64 | ;; 65 | 2) 66 | echo "setting $opt" 67 | shopt -s "$opt" 68 | unset _shopt_restore[$opt] 69 | ;; 70 | *) 71 | echo "$opt wasn't changed earlier" 72 | ;; 73 | esac 74 | done 75 | } 76 | 77 | # 78 | # shop what options set or unset currently 79 | # 80 | shopt_show() { 81 | local opt restore_type 82 | for opt in "${!_shopt_restore[@]}"; do 83 | restore_type=${_shopt_restore[$opt]} 84 | if [[ $restore_type == 1 ]]; then 85 | echo "$opt set" 86 | elif [[ $restore_type == 2 ]]; then 87 | echo "$opt unset" 88 | else 89 | echo "$opt - unknown status '$restore_type'" 90 | fi 91 | done 92 | } 93 | -------------------------------------------------------------------------------- /lib/stdlib.sh: -------------------------------------------------------------------------------- 1 | ### 2 | ### stdlib.sh - foundation library for Bash scripts 3 | ### Need Bash version 4.3 or above - see http://tiswww.case.edu/php/chet/bash/NEWS 4 | ### 5 | ### Areas covered: 6 | ### - PATH manipulation 7 | ### - logging 8 | ### - error handling 9 | ### 10 | 11 | ################################################# INITIALIZATION ####################################################### 12 | 13 | # 14 | # make sure we do nothing in case the library is sourced more than once in the same shell 15 | # 16 | [[ $__stdlib_sourced__ ]] && return 17 | __stdlib_sourced__=1 18 | 19 | # 20 | # The only code that executes when the library is sourced 21 | # 22 | __stdlib_init__() { 23 | __log_init__ 24 | 25 | # call future init functions here 26 | } 27 | 28 | ################################################# LIBRARY IMPORTER ##################################################### 29 | 30 | # 31 | # import: source a library from $BASE_HOME 32 | # Example: 33 | # import lib/assertions.sh company/lib/xyz.sh ... 34 | # 35 | # IMPORTANT NOTE: If your library has global variables declared with 'declare' statement, you need to add -g flag to those. 36 | # Since the library gets sourced inside the `import` function, globals declared without the -g option would 37 | # be local to the function and hence be unavailable to other functions. 38 | import() { 39 | local lib rc=0 40 | [[ $BASE_HOME ]] || { printf '%s\n' "ERROR: BASE_HOME not set; import functionality needs it" >&2; return 1; } 41 | for lib; do 42 | lib=$BASE_HOME/$lib 43 | if [[ -f "$lib" ]]; then 44 | source "$lib" 45 | else 46 | printf 'ERROR: %s\n' "Library '$lib' does not exist" >&2 47 | rc=1 48 | fi 49 | done 50 | return $rc 51 | } 52 | 53 | ################################################# PATH MANIPULATION #################################################### 54 | 55 | # add a new directory to $PATH 56 | add_to_path() { 57 | local dir re prepend=0 opt strict=1 58 | OPTIND=1 59 | while getopts sp opt; do 60 | case "$opt" in 61 | n) strict=0 ;; # don't care if directory exists or not before adding it to PATH 62 | p) prepend=1 ;; # prepend the directory to PATH instead of appending 63 | *) log_error "add_to_path - invalid option '$opt'" 64 | return 65 | ;; 66 | esac 67 | done 68 | 69 | shift $((OPTIND-1)) 70 | for dir; do 71 | ((strict)) && [[ ! -d $dir ]] && continue 72 | re="(^$dir:|:$dir:|:$dir$)" 73 | if ! [[ $PATH =~ $re ]]; then 74 | ((prepend)) && PATH="$dir:$PATH" || PATH="$PATH:$dir" 75 | fi 76 | done 77 | } 78 | 79 | # remove duplicates in $PATH 80 | dedupe_path() { PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"; } 81 | 82 | # print directories in $PATH, one per line 83 | print_path() { 84 | local -a dirs; local dir 85 | IFS=: read -ra dirs <<< "$PATH" 86 | for dir in "${dirs[@]}"; do printf '%s\n' "$dir"; done 87 | } 88 | 89 | #################################################### LOGGING ########################################################### 90 | 91 | __log_init__() { 92 | if [[ -t 1 ]]; then 93 | # colors for logging in interactive mode 94 | [[ $COLOR_BOLD ]] || COLOR_BOLD="\033[1m" 95 | [[ $COLOR_RED ]] || COLOR_RED="\033[0;31m" 96 | [[ $COLOR_GREEN ]] || COLOR_GREEN="\033[0;34m" 97 | [[ $COLOR_YELLOW ]] || COLOR_YELLOW="\033[0;33m" 98 | [[ $COLOR_BLUE ]] || COLOR_BLUE="\033[0;32m" 99 | [[ $COLOR_OFF ]] || COLOR_OFF="\033[0m" 100 | else 101 | # no colors to be used if non-interactive 102 | COLOR_RED= COLOR_GREEN= COLOR_YELLOW= COLOR_BLUE= COLOR_OFF= 103 | fi 104 | readonly COLOR_RED COLOR_GREEN COLOR_YELLOW COLOR_BLUE COLOR_OFF 105 | 106 | # 107 | # map log level strings (FATAL, ERROR, etc.) to numeric values 108 | # 109 | # Note the '-g' option passed to declare - it is essential 110 | # 111 | unset _log_levels _loggers_level_map 112 | declare -gA _log_levels _loggers_level_map 113 | _log_levels=([FATAL]=0 [ERROR]=1 [WARN]=2 [INFO]=3 [DEBUG]=4 [VERBOSE]=5) 114 | 115 | # 116 | # hash to map loggers to their log levels 117 | # the default logger "default" has INFO as its default log level 118 | # 119 | _loggers_level_map["default"]=3 # the log level for the default logger is INFO 120 | } 121 | 122 | # 123 | # set_log_level 124 | # 125 | set_log_level() { 126 | local logger=default in_level l 127 | [[ $1 = "-l" ]] && { logger=$2; shift 2 2>/dev/null; } 128 | in_level="${1:-INFO}" 129 | if [[ $logger ]]; then 130 | l="${_log_levels[$in_level]}" 131 | if [[ $l ]]; then 132 | _loggers_level_map[$logger]=$l 133 | else 134 | printf '%(%Y-%m-%d:%H:%M:%S)T %-7s %s\n' -1 WARN \ 135 | "${BASH_SOURCE[2]}:${BASH_LINENO[1]} Unknown log level '$in_level' for logger '$logger'; setting to INFO" 136 | _loggers_level_map[$logger]=3 137 | fi 138 | else 139 | printf '%(%Y-%m-%d:%H:%M:%S)T %-7s %s\n' -1 WARN \ 140 | "${BASH_SOURCE[2]}:${BASH_LINENO[1]} Option '-l' needs an argument" >&2 141 | fi 142 | } 143 | 144 | # 145 | # Core and private log printing logic to be called by all logging functions. 146 | # Note that we don't make use of any external commands like 'date' and hence we don't fork at all. 147 | # We use the Bash's printf builtin instead. 148 | # 149 | _print_log() { 150 | local in_level=$1; shift 151 | local logger=default log_level_set log_level 152 | [[ $1 = "-l" ]] && { logger=$2; shift 2; } 153 | log_level="${_log_levels[$in_level]}" 154 | log_level_set="${_loggers_level_map[$logger]}" 155 | if [[ $log_level_set ]]; then 156 | ((log_level_set >= log_level)) && { 157 | printf '%(%Y-%m-%d:%H:%M:%S)T %-7s %s ' -1 "$in_level" "${BASH_SOURCE[2]}:${BASH_LINENO[1]}" 158 | printf '%s\n' "$@" 159 | } 160 | else 161 | printf '%(%Y-%m-%d:%H:%M:%S)T %-7s %s\n' -1 WARN "${BASH_SOURCE[2]}:${BASH_LINENO[1]} Unknown logger '$logger'" 162 | fi 163 | } 164 | 165 | # 166 | # core function for logging contents of a file 167 | # 168 | _print_log_file() { 169 | local in_level=$1; shift 170 | local logger=default log_level_set log_level file 171 | [[ $1 = "-l" ]] && { logger=$2; shift 2; } 172 | file=$1 173 | log_level="${_log_levels[$in_level]}" 174 | log_level_set="${_loggers_level_map[$logger]}" 175 | if [[ $log_level_set ]]; then 176 | if ((log_level_set >= log_level)) && [[ -f $file ]]; then 177 | log_debug "Contents of file '$1':" 178 | cat -- "$1" 179 | fi 180 | else 181 | printf '%(%Y-%m-%d:%H:%M:%S)T %s\n' -1 "WARN ${BASH_SOURCE[2]}:${BASH_LINENO[1]} Unknown logger '$logger'" 182 | fi 183 | } 184 | 185 | # 186 | # main logging functions 187 | # 188 | log_fatal() { _print_log FATAL "$@"; } 189 | log_error() { _print_log ERROR "$@"; } 190 | log_warn() { _print_log WARN "$@"; } 191 | log_info() { _print_log INFO "$@"; } 192 | log_debug() { _print_log DEBUG "$@"; } 193 | log_verbose() { _print_log VERBOSE "$@"; } 194 | # 195 | # logging file content 196 | # 197 | log_info_file() { _print_log_file INFO "$@"; } 198 | log_debug_file() { _print_log_file DEBUG "$@"; } 199 | log_verbose_file() { _print_log_file VERBOSE "$@"; } 200 | # 201 | # logging for function entry and exit 202 | # 203 | log_info_enter() { _print_log INFO "Entering function ${FUNCNAME[1]}"; } 204 | log_debug_enter() { _print_log DEBUG "Entering function ${FUNCNAME[1]}"; } 205 | log_verbose_enter() { _print_log VERBOSE "Entering function ${FUNCNAME[1]}"; } 206 | log_info_leave() { _print_log INFO "Leaving function ${FUNCNAME[1]}"; } 207 | log_debug_leave() { _print_log DEBUG "Leaving function ${FUNCNAME[1]}"; } 208 | log_verbose_leave() { _print_log VERBOSE "Leaving function ${FUNCNAME[1]}"; } 209 | 210 | # 211 | # THe print routines don't prefix the messages with the timestamp 212 | # 213 | 214 | print_error() { 215 | { 216 | printf "${COLOR_RED}ERROR: " 217 | printf '%s\n' "$@" 218 | printf "$COLOR_OFF" 219 | } >&2 220 | } 221 | 222 | print_warn() { 223 | printf "${COLOR_YELLOW}WARN: " 224 | printf '%s\n' "$@" 225 | printf "$COLOR_OFF" 226 | } 227 | 228 | print_info() { 229 | printf "$COLOR_BLUE" 230 | printf '%s\n' "$@" 231 | printf "$COLOR_OFF" 232 | } 233 | 234 | print_success() { 235 | printf "${COLOR_GREEN}SUCCESS: " 236 | printf '%s\n' "$@" 237 | printf "$COLOR_OFF" 238 | } 239 | 240 | print_bold() { 241 | printf '%b\n' "$COLOR_BOLD$@$COLOR_OFF" 242 | } 243 | 244 | print_message() { 245 | printf '%s\n' "$@" 246 | } 247 | 248 | # print only if output is going to terminal 249 | print_tty() { 250 | if [[ -t 1 ]]; then 251 | printf '%s\n' "$@" 252 | fi 253 | } 254 | 255 | ################################################## ERROR HANDLING ###################################################### 256 | 257 | dump_trace() { 258 | local frame=0 line func source n=0 259 | while caller "$frame"; do 260 | ((frame++)) 261 | done | while read line func source; do 262 | ((n++ == 0)) && { 263 | printf 'Encountered a fatal error\n' 264 | } 265 | printf '%4s at %s\n' " " "$func ($source:$line)" 266 | done 267 | } 268 | 269 | exit_if_error() { 270 | (($#)) || return 271 | local num_re='^[0-9]+' 272 | local rc=$1; shift 273 | local message="${@:-No message specified}" 274 | if ! [[ $rc =~ $num_re ]]; then 275 | log_error "'$rc' is not a valid exit code; it needs to be a number greater than zero. Treating it as 1." 276 | rc=1 277 | fi 278 | ((rc)) && { 279 | log_fatal "$message" 280 | dump_trace "$@" 281 | exit $rc 282 | } 283 | return 0 284 | } 285 | 286 | fatal_error() { 287 | local ec=$? # grab the current exit code 288 | ((ec == 0)) && ec=1 # if it is zero, set exit code to 1 289 | exit_if_error "$ec" "$@" 290 | } 291 | 292 | # 293 | # run a simple command (no compound statements or pipelines) and exit if it exits with non-zero 294 | # 295 | run_simple() { 296 | log_debug "Running command: $*" 297 | "$@" 298 | exit_if_error $? "run failed: $@" 299 | } 300 | 301 | # 302 | # safe cd 303 | # 304 | base_cd() { 305 | local dir=$1 306 | [[ $dir ]] || fatal_error "No arguments or an empty string passed to base_cd" 307 | cd -- "$dir" || fatal_error "Can't cd to '$dir'" 308 | } 309 | 310 | base_cd_nonfatal() { 311 | local dir=$1 312 | [[ $dir ]] || return 1 313 | cd -- "$dir" || return 1 314 | return 0 315 | } 316 | 317 | # 318 | # safe_unalias 319 | # 320 | safe_unalias() { 321 | # Ref: https://stackoverflow.com/a/61471333/6862601 322 | local alias_name 323 | for alias_name; do 324 | [[ ${BASH_ALIASES[$alias_name]} ]] && unalias "$alias_name" 325 | done 326 | return 0 327 | } 328 | 329 | ################################################# MISC FUNCTIONS ####################################################### 330 | # 331 | # For functions that need to return a single value, we use the global variable OUTPUT. 332 | # For functions that need to return multiple values, we use the global variable OUTPUT_ARRAY. 333 | # These global variables eliminate the need for a subshell when the caller wants to retrieve the 334 | # returned values. 335 | # 336 | # Each function that makes use of these global variables would call __clear_output__ as the very first step. 337 | # 338 | __clear_output__() { unset OUTPUT OUTPUT_ARRAY; } 339 | 340 | # 341 | # return path to parent script's source directory 342 | # 343 | get_my_source_dir() { 344 | __clear_output__ 345 | 346 | # Reference: https://stackoverflow.com/a/246128/6862601 347 | OUTPUT="$(cd "$(dirname "${BASH_SOURCE[1]}")" >/dev/null 2>&1 && pwd -P)" 348 | } 349 | 350 | # 351 | # wait for user to hit Enter key 352 | # 353 | wait_for_enter() { 354 | local prompt=${1:-"Press Enter to continue"} 355 | read -r -n1 -s -p "Press Enter to continue" .sh` script. 4 | - Place team-specific commands inside `/bin` directory 5 | - Place team-specific shell libraries inside `/lib` directory 6 | - Two libraries under `/lib` have special meaning: 7 | 1. `.sh` - always sourced by `base_init.sh` 8 | 2. `bashrc` - sourced by `base_init.sh` if the invoking shell is interactive 9 | - `base_init.sh` adds `$BASE_HOME/team/$BASE_TEAM/bin` to `PATH`. 10 | 11 | # Sharing among teams 12 | 13 | - To share libraries and commands of other teams, set `BASE_SHARED_TEAMS` variable inside `user/.sh` script. 14 | 1. `base_init.sh` adds shared teams bin directory to `PATH` 15 | 2. Shared team's `/lib/.sh` libraries or `bashrc` are not automatically sourced in 16 | 17 | - To source in a team's library, use the `import` function (defined in `lib/stdlib.sh`): 18 | ```bash 19 | import lib/.sh 20 | import team//.sh 21 | ``` 22 | -------------------------------------------------------------------------------- /team/test-team1/bin/test-command1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env base-wrapper 2 | 3 | # 4 | # see bin/test-command1 to understand the structure of a Base-wrapped script 5 | # 6 | 7 | # 8 | # test command for team 9 | # 10 | main() { 11 | log_info "Starting" 12 | # do something 13 | log_info "Finished" 14 | } 15 | -------------------------------------------------------------------------------- /team/test-team1/lib/bashrc: -------------------------------------------------------------------------------- 1 | # 2 | # team specific bashrc file for interactive shells 3 | # 4 | -------------------------------------------------------------------------------- /team/test-team1/lib/test-team1.sh: -------------------------------------------------------------------------------- 1 | # 2 | # any team specific aliases, functions, and other settings 3 | # 4 | -------------------------------------------------------------------------------- /team/test-team2/bin/test-command1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env base-wrapper 2 | 3 | # 4 | # see bin/test-command1 to understand the structure of a Base-wrapped script 5 | # 6 | 7 | # 8 | # test command for team 9 | # 10 | main() { 11 | log_info "Starting" 12 | # do something 13 | log_info "Finished" 14 | } 15 | -------------------------------------------------------------------------------- /team/test-team2/lib/bashrc: -------------------------------------------------------------------------------- 1 | # 2 | # team specific bashrc file for interactive shells 3 | # 4 | -------------------------------------------------------------------------------- /team/test-team2/lib/test-team2.sh: -------------------------------------------------------------------------------- 1 | # 2 | # any team specific aliases, functions, and other settings 3 | # 4 | -------------------------------------------------------------------------------- /test/test_base_init.sh: -------------------------------------------------------------------------------- 1 | # 2 | # test base_init.sh 3 | # 4 | 5 | source ../base_init.sh 6 | -------------------------------------------------------------------------------- /test/test_stdlib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Note: this script needs to be run from the test directory itself. 5 | # 6 | 7 | source ../lib/stdlib.sh 8 | 9 | test_log_func() { 10 | [[ $1 = "-e" ]] && { local sub=_enter; shift; } 11 | [[ $1 = "-l" ]] && { local sub=_leave; shift; } 12 | local level=$1 func=$2 expected=$3 log 13 | set_log_level "$level" 14 | log=$(log_$func$sub "test $level" 2>&1) 15 | if ((expected)) && ! [[ $log ]]; then 16 | printf 'Log level %-7s function %-11s: %s\n' "$level" "log_$func" FAIL 17 | ((fail++)) 18 | else 19 | ((verbose)) && printf 'Log level %-7s function %-11s: %s\n' "$level" "log_$func" SUCCESS 20 | fi 21 | } 22 | 23 | test_logging() { 24 | test_log_func ERROR error 1 25 | test_log_func ERROR warn 0 26 | test_log_func ERROR info 0 27 | test_log_func ERROR debug 0 28 | test_log_func ERROR verbose 0 29 | 30 | test_log_func WARN error 1 31 | test_log_func WARN warn 1 32 | test_log_func WARN info 0 33 | test_log_func WARN debug 0 34 | test_log_func WARN verbose 0 35 | 36 | test_log_func INFO error 1 37 | test_log_func INFO warn 1 38 | test_log_func INFO info 1 39 | test_log_func INFO debug 0 40 | test_log_func INFO verbose 0 41 | 42 | test_log_func DEBUG error 1 43 | test_log_func DEBUG warn 1 44 | test_log_func DEBUG info 1 45 | test_log_func DEBUG debug 1 46 | test_log_func DEBUG verbose 0 47 | 48 | test_log_func VERBOSE error 1 49 | test_log_func VERBOSE warn 1 50 | test_log_func VERBOSE info 1 51 | test_log_func VERBOSE debug 1 52 | test_log_func VERBOSE verbose 1 53 | 54 | for func_type in e l; do 55 | test_log_func -$func_type INFO info 1 56 | test_log_func -$func_type INFO debug 0 57 | test_log_func -$func_type INFO verbose 0 58 | test_log_func -$func_type DEBUG info 1 59 | test_log_func -$func_type DEBUG debug 1 60 | test_log_func -$func_type DEBUG verbose 0 61 | test_log_func -$func_type VERBOSE info 1 62 | test_log_func -$func_type VERBOSE debug 1 63 | test_log_func -$func_type VERBOSE verbose 1 64 | done 65 | } 66 | 67 | test_get_my_source_dir() { 68 | get_my_source_dir 69 | if [[ $OUTPUT != $PWD ]]; then 70 | ((++fail)) 71 | printf '%s\n' "get_my_source_dir: expected '$PWD' as output, but got '$OUTPUT' - FAIL" 72 | else 73 | ((verbose)) && printf '%s\n' "get_my_source_dir returned '$OUTPUT' - SUCCESS" 74 | fi 75 | } 76 | 77 | main() { 78 | verbose=0 fail=0 rc=0 79 | [[ $1 = -v ]] && verbose=1 80 | test_logging 81 | test_get_my_source_dir 82 | ((fail)) && rc=1 83 | exit $rc 84 | } 85 | 86 | main "$@" 87 | -------------------------------------------------------------------------------- /user/common_user.sh: -------------------------------------------------------------------------------- 1 | import lib/base_defaults.sh 2 | 3 | # 4 | # vi 5 | # 6 | export EXINIT="set ts=4 expandtab sw=4 nows ai nosm|syntax off" 7 | export EDITOR=vi 8 | 9 | # 10 | # kubernetes 11 | # 12 | alias kc=kubectl 13 | -------------------------------------------------------------------------------- /user/test_user1.sh: -------------------------------------------------------------------------------- 1 | BASE_TEAM=test-team1 2 | BASE_SHARED_TEAMS=test-team2 3 | 4 | import lib/base_defaults.sh 5 | 6 | # 7 | # generic aliases 8 | # 9 | alias mv='mv -i' 10 | alias cp='cp -i' 11 | alias rm='rm -i' 12 | alias h='history' 13 | alias l='ls -ltr' 14 | 15 | # 16 | # refresh base 17 | # 18 | base_update 19 | -------------------------------------------------------------------------------- /user/test_user2.sh: -------------------------------------------------------------------------------- 1 | BASE_TEAM=test-team2 2 | 3 | import lib/base_defaults.sh 4 | 5 | # 6 | # generic aliases 7 | # 8 | alias mv='mv -i' 9 | alias cp='cp -i' 10 | alias rm='rm -i' 11 | alias h='history' 12 | alias l='ls -ltr' 13 | 14 | # 15 | # refresh base 16 | # 17 | base_update 18 | --------------------------------------------------------------------------------