├── .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 |
--------------------------------------------------------------------------------