├── .editorconfig ├── BEST_PRACTICES.md ├── LICENSE ├── README.md └── scripts └── perf_subshell.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /BEST_PRACTICES.md: -------------------------------------------------------------------------------- 1 | # Best Practices 2 | 3 | These Best Practices are more specific to Basalt; for general reference, see the the [Bash Hackers Wiki](https://wiki.bash-hackers.org/doku.php) and [Greg's Wiki](https://mywiki.wooledge.org). 4 | 5 | Things not mentioned here are already handled by [Basalt](https://github.com/hyperupcall/basalt) (ex. `set -ET`). 6 | 7 | ## Names for executables and functions 8 | 9 | Executables should match the regex `[[:alpha:]][[:alnum:]-]*`. 10 | 11 | Functions should match the regex `[[:alpha:]][[:alnum:]_:.]*`. 12 | 13 | Use `::` or `.` to namespace your functions. `.` is _highly_ preferred. For example: 14 | 15 | ```sh 16 | bash_core.log_info() { :; } 17 | bash_core.log_warn() { :; } 18 | bash_core.log_error() { :; } 19 | ``` 20 | 21 | ## Names for variables 22 | 23 | Different variable types have special naming rules, per convention. 24 | 25 | When naming variables after command-line flags, prefix with `flag_`: 26 | 27 | ```sh 28 | local flag_verbose= 29 | ``` 30 | 31 | When naming global variables, prefix with `g_`: 32 | 33 | ```sh 34 | declare -g g_something= 35 | ``` 36 | 37 | When naming dynamic variables, prefix with `d_`: 38 | 39 | ```sh 40 | local d_something= 41 | ``` 42 | 43 | When naming indirect variables, pre with `_` (`__` for libraries) or postfix with `_name`: 44 | 45 | ```sh 46 | local -n _final_value= 47 | local -n key_name= 48 | ``` 49 | 50 | ## Private functions 51 | 52 | When developing Bash libraries, it may be helpful to denote functions as private for internal use anyways. Do so by prefixing an underscore: 53 | 54 | ```sh 55 | bash_core._trim_whitespace() { 56 | : 57 | } 58 | ``` 59 | 60 | ## Utility and helper functions 61 | 62 | If there are many utility or helper functions, it may be helpful to name the functions after the file it's contained in. For example, a file called `util-db.sh` may contain the following functions: 63 | 64 | - `util.db_read()` 65 | - `util.db_write()` 66 | 67 | And so on... 68 | 69 | ## Exiting 70 | 71 | When exiting, _always_ supply a number: 72 | 73 | ```sh 74 | if [ -n "$foo" ]; then 75 | : 76 | else 77 | exit 0 78 | fi 79 | ``` 80 | 81 | ## Bashisms 82 | 83 | Try keep Bashisms to a minimum. This keeps the code portable, in the sense that's it's easy to port to a POSIX shell. If you're not familiar with what Bashisms were introduced in what versions of Bash, it also makes your shell scripts more compatible. 84 | 85 | For example: 86 | 87 | ```sh 88 | # Use `fn() { ...` instead of `function() { ...` or 89 | # `function fn { ...` 90 | fn() { 91 | # OK; although `case` is slightly better, the 92 | # convenience likely justifies it 93 | if [[ $a == *glob* ]]; then 94 | : 95 | # Use single-equals and single-brackets 96 | elif [ "$a" = "b" ]; then 97 | : 98 | fi 99 | } 100 | ``` 101 | 102 | Linters like ShellCheck won't print warnings on these if your shell is set to Bash. Unfortunately, more sophisticated linters aren't available yet; until then, you can use regular expressions like I do in [`checkstyle.py`](https://github.com/asdf-vm/asdf/blob/master/scripts/checkstyle.py) for the [asdf](https://github.com/asdf-vm/asdf) project. 103 | 104 | ## REPLY-Pattern 105 | 106 | Bash only allows "returning" from a function with a numerical exit code. To work around this, use this pattern, inspired by the `read` and `select` builtins. 107 | 108 | ### Example 109 | 110 | Simply assign the value you wish to return to `REPLY`, then return from the function: 111 | 112 | ```sh 113 | my_basename() { 114 | unset -v REPLY; REPLY= 115 | local filepath=$1 116 | 117 | local result={filepath%/} 118 | result=${result##*/} 119 | 120 | REPLY=$result 121 | } 122 | 123 | my_basename "$0" 124 | printf '%s\n' "The basename of '$0' is '$REPLY'" 125 | ``` 126 | 127 | There are several things to note: 128 | 129 | - **`unset -v REPLY; REPLY=`**: Since the function is known to "return something", initialize `REPLY` to an empty string to be sure sure its value is well-known. It's not strictly necessary if every code path is guaranteed to set `REPLY` (accounting for `errexit`, traps, etc.) but it makes the code much easier to read, especially if the REPLY-pattern is frequently used. 130 | 131 | - **`unset -v REPLY`** guarantees the type of `REPLY` is reset (by default, to a string). If this isn't done, then the type of the previous result of string can potentially stay the same. For instance, if `REPLY` was previously an index array, then `REPLY=` simply makes the array empty. 132 | 133 | - Unsetting the shopt option `localvar_inherit`and and setting `localvar_unset` obviates the aforementioned code, but there may be times in which this behavior is expected, and adding the single line makes the code more portable. 134 | 135 | - **`REPLY=`** guarantees that the variable is set, to prevent any mishaps on `nounset`. 136 | 137 | - If there are multiple return values, you may use the shortcut: `unset -v REPLY{1,2,3,4}` 138 | 139 | - Do **NOT** use the name `REPLIES` if returning an index array - stick with `REPLY` for all variables types. It's confusing. 140 | 141 | - Do **NOT** do `REPLY+=$something`. Use an intermediate variable and assign it at end. `REPLY=$var` or `REPLY=("${arr[@]}")` 142 | 143 | - If you need a different name (due to Bash dynamic scope, etc.) then naming it `REPLY_OUTER` or `REPLY_INNER_*`, or `REPLY_{name}` is O.K. 144 | 145 | - Try to only set REPLY at the beginning or end of the function 146 | 147 | ### Other benefits 148 | 149 | This pattern makes it simple and straightforward to return multiple variables from an array. It also makes it possible to return index arrays and associative arrays. 150 | 151 | Lastly, it also performs better. See [perf_subshell.sh](./perf_subshell.sh) for more details. 152 | 153 |
154 | 155 | 👇 Performance Results 156 | 157 | ```sh 158 | $ for fn in slow_basename fast_basename faster_basename; do hyperfine -N --warmup 1000 --runs 1000 "./scripts/perf_subshell.sh $fn"; done 159 | Benchmark 1: ./script.sh slow_basename 160 | Time (mean ± σ): 3.6 ms ± 0.2 ms [User: 2.0 ms, System: 1.4 ms] 161 | Range (min … max): 3.4 ms … 8.8 ms 1000 runs 162 | 163 | Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options. 164 | 165 | Benchmark 1: ./script.sh fast_basename 166 | Time (mean ± σ): 3.2 ms ± 0.4 ms [User: 1.6 ms, System: 1.4 ms] 167 | Range (min … max): 2.9 ms … 15.1 ms 1000 runs 168 | 169 | Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options. 170 | 171 | Benchmark 1: ./script.sh faster_basename 172 | Time (mean ± σ): 2.7 ms ± 0.3 ms [User: 1.3 ms, System: 1.2 ms] 173 | Range (min … max): 2.5 ms … 8.9 ms 1000 runs 174 | 175 | Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options. 176 | ``` 177 | 178 |
179 | 180 | Considering real performance, I have heard that x10 performance improvement is not uncommon. 181 | 182 | ## Loops 183 | 184 | You may wish to harden your loop code. Take the following example: 185 | 186 | ```sh 187 | #!/usr/bin/env bash 188 | 189 | str.repeat() { 190 | unset REPLY; REPLY= 191 | local str=$1 192 | local -i count=$2 193 | 194 | local i= 195 | for ((i=0; i foxfoxfox 202 | ``` 203 | 204 | - **`local i=`**: Ensure the variable is function scoped instead of globally scoped. 205 | 206 | - **`unset -v i`**: Prevent Bash's default dynamic scoping from making this variable accessible in later contexts. 207 | 208 | Admittedly, this hardening is a bit extraneous, so I do not strongly recommend it. 209 | 210 | ## Error handling 211 | 212 | ## Stacktrace 213 | 214 | Printing a stacktrace on `ERR` helps debugging Bash scripts tremendously. It's never been easier to do this with [`bash-core`](https://github.com/hyperupcall/bash-core): 215 | 216 | ```sh 217 | #!/usr/bin/env bash 218 | 219 | # This script loads Bash dependencies (including bash-core). See 220 | # Basalt for more information: https://github.com/hyperupcall/basalt 221 | eval "$(basalt-package-init)" 222 | basalt.package-init || exit 223 | basalt.package-load 224 | 225 | # The meat 226 | err_handler() { 227 | core.print_stacktrace 228 | } 229 | core.trap_add 'err_handler' ERR 230 | 231 | fn() { 232 | # Do something naughty 233 | false 234 | } 235 | fn 236 | ``` 237 | 238 | ```txt 239 | $ ./script.sh 240 | Stacktrace: 241 | in core.print_stacktrace (/home/edwin/.local/share/basalt/store/packages/github.com/hyperupcall/bash-core@v0.12.0/pkg/src/public/bash-core.sh:0) 242 | in err_handler (/home/edwin/groups/Bash/woof/.hidden/blah.sh:11) 243 | in core.private.util.trap_handler_common (/home/edwin/.local/share/basalt/store/packages/github.com/hyperupcall/bash-core@v0.12.0/pkg/src/util/util.sh:31) 244 | in core.private.trap_handler_ERR (/home/edwin/.local/share/basalt/store/packages/github.com/hyperupcall/bash-core@v0.12.0/pkg/src/public/bash-core.sh:42) 245 | in fn (/home/edwin/groups/Bash/woof/.hidden/blah.sh:17) 246 | ``` 247 | 248 | ### Exit codes 249 | 250 | If you wish to print a nice error message on failures, it is not straight-forward. Use the following guidelines. I assume that you have `errexit` enabled and have setup stacktrace printing (as I mentioned above). 251 | 252 | ❌ Incorrect 253 | 254 | This prints an ugly stacktrace to the end-user. 255 | 256 | ```sh 257 | work() { 258 | curl "$@" 259 | } 260 | ``` 261 | 262 | ❌ Incorrect 263 | 264 | This prints to standard _output_, `!` clobbers the exit code, and executing doesn't stop. 265 | 266 | ```sh 267 | work() { 268 | if ! curl "$@"; then 269 | printf '%s\n' "Error: Failed to download URL: ${!#} (code $?)" 270 | fi 271 | } 272 | ``` 273 | 274 | ✅ Correct: 275 | 276 | ```sh 277 | work() { 278 | curl "$@" || { 279 | local code=$? 280 | printf '%s\n' "Error: Failed to download URL: ${!#} (code $?)" >&2 281 | exit $code 282 | } 283 | } 284 | ``` 285 | 286 | ✅ Correct: 287 | 288 | ```sh 289 | work() { 290 | if curl "$@"; then :; else 291 | local code=$? 292 | printf '%s\n' "Error: Failed to download URL: ${!#} (code $?)" >&2 293 | exit $code 294 | fi 295 | } 296 | 297 | ``` 298 | 299 | ## Project Layout 300 | 301 | The layout of Basalt projects should be consistent. My personal layout was informed with the following goals: 302 | 303 | 1. Make it easier to run static code analysis on Bash projects 304 | 2. Make it relatively straight-forward to distribute on other system (via Basalt or system package manager) 305 | 306 | ```txt 307 | .git/ 308 | .gitignore 309 | .gitattributes 310 | basalt.toml 311 | pkg/ 312 | bin/ 313 | NAME 314 | src/ 315 | bin/ 316 | NAME.sh 317 | public/ 318 | util/ 319 | completions/ 320 | man/ 321 | man1/ 322 | NAME.1 323 | share/ 324 | 325 | docs/ 326 | tests/ 327 | ``` 328 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | Toucan 123 | Over 100 word limit 124 | We’re working to increase this limit and keep load times short. In the meantime, try highlighting up to 100 words at one time to translate. 125 | Don’t show again 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awesome Basalt 2 | 3 | All links are packages for [Basalt](https://github.com/hyperupcall/basalt), a Bash package manager. 4 | 5 | This list contains projects written with a shell language that are either (1) Bash or POSIX Shell applications that are globally installable or (2) _Bash libraries_ that are locally (per-project) installable. 6 | 7 | Keep in mind that everything listed should be considered at least _bleeding edge_. Notwithstanding the label, many could be considered "stable" to comprehensive test suites and minimalist functionality. I'll also add that I'm dogfooding everything listed here, and most things _just work_ on _my machine_. 8 | 9 | There is also a [best practices](./BEST_PRACTICES.md) file, which contains some reminders for how to write defensive Bash (which is a _must_ when writing Basalt packages). 10 | 11 | ## General use 12 | 13 | ### Applications 14 | 15 | - [hyperupcall/bake](https://github.com/hyperupcall/bake) - A Bash-based Make alternative (FEATURED) 16 | - [hyperupcall/woof](https://github.com/hyperupcall/woof) - The version manager to end all version managers 17 | - [hyperupcall/hookah](https://github.com/hyperupcall/hookah) - An elegantly minimal solution for Git hooks 18 | - [hyperupcall/bash_config](https://github.com/hyperupcall/bash_config) - [`fish_config`](https://fishshell.com/docs/current/cmds/fish_config.html) for Bash 19 | - [hyperupcall/shelltest](https://github.com/hyperupcall/shelltest) - A test runner for POSIX-compliant shells 20 | - [hyperupcall/neodkms](https://github.com/hyperupcall/neodkms) - An improved DKMS 21 | 22 | ### Libraries 23 | 24 | - [hyperupcall/bash-object](https://github.com/hyperupcall/bash-object) - Manipulate heterogenous data hierarchies in Bash (FEATURED) 25 | - [hyperupcall/bash-utility](https://github.com/hyperupcall/bash-utility) - A standard utility library for Bash (previously `bash-std`) 26 | - [hyperupcall/bash-args](https://github.com/hyperupcall/bash-args) - A cute little Bash library for blazing fast argument parsing 27 | - [hyperupcall/bash-term](https://github.com/hyperupcall/bash-term) - Bash library for terminal escape sequences 28 | - [hyperupcall/bash-toml](https://github.com/hyperupcall/bash-toml) - A kickass Toml parser written in pure Bash 29 | - [hyperupcall/bash-json](https://github.com/hyperupcall/bash-json) - A Parse JSON in Bash 30 | - [hyperupcall/bats-all](https://github.com/hyperupcall/bats-all) - Aggregation of the three most popular Bats utility libraries 31 | - [hyperupcall/bash-core](https://github.com/hyperupcall/bash-core) - Core functions for any Bash program 32 | - [hyperupcall/bash-algo](https://github.com/hyperupcall/bash-algo) - Common algorithms implemented in pure Bash 33 | - [hyperupcall/bash-http](https://github.com/hyperupcall/bash-http) - Bash web servers for everyone! (not released) 34 | - [hyperupcall/bash-tui](https://github.com/hyperupcall/bash-tui) - Bash library for making TUI's (not released) 35 | 36 | ## Miscellaneous 37 | 38 | ### Similar Lists 39 | 40 | - [awesome-bash](https://github.com/awesome-lists/awesome-bash) 41 | - [awesome-shell](https://github.com/alebcay/awesome-shell) 42 | 43 | ### Similar Package Managers 44 | 45 | Basalt works with existing "packages", as defined by both Basher and pkg. Browse their lists as well: 46 | 47 | - [Basher](https://www.basher.it/package) 48 | - [bpkg](https://bpkg.sh/packages/name) 49 | -------------------------------------------------------------------------------- /scripts/perf_subshell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | fast_basename_work() { 4 | local path="$1" 5 | path=${path%/} 6 | printf '%s\n' "{path##*/}" 7 | } 8 | 9 | slow_basename() { 10 | unset -v REPLY 11 | REPLY=$(basename "$1") 12 | } 13 | 14 | fast_basename() { 15 | unset -v REPLY 16 | REPLY=$(fast_basename_work "$1") 17 | } 18 | 19 | faster_basename() { 20 | unset -v REPLY; REPLY= 21 | local path="$1" 22 | path=${path%/} 23 | path=${path##*/} 24 | REPLY=$path 25 | } 26 | 27 | main() { 28 | "$1" "$PWD" 29 | local _result="$REPLY" 30 | } 31 | 32 | main "$@" 33 | --------------------------------------------------------------------------------