├── .dkrc
├── .editorconfig
├── .envrc
├── .gitignore
├── LICENSE
├── README.md
├── bashup.events
├── package.sh
├── script
├── README.md
├── bootstrap
├── cibuild
├── clean
├── console
├── server
├── setup
├── test
└── update
└── specs
└── Misc.cram.md
/.dkrc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # ---
3 | # Add your own commands, functions, and variables here. Define defaults first,
4 | # then `dk use:` the desired devkit modules, and then define any overrides to
5 | # the devkit defaults.
6 | # ---
7 |
8 | # Available modules (uncomment to use):
9 |
10 | dk use: cram # run tests using the "cram" functional test tool
11 | dk use: entr-watch # watch files and re-run tests or other commands
12 | dk use: shell-console # make the "console" command enter a subshell
13 | dk use: bash-kit # enable doing tests/console/etc. in other bash versions
14 | dk use: shellcheck # support running shellcheck (via docker if not installed)
15 |
16 | # Define overrides, new commands, functions, etc. here:
17 |
18 | on "cram_files" ls README.md specs/*.cram.md
19 |
20 | # SC2016 = expressions in single quotes
21 | # SC2145 = prefix or suffix on "$@" or other array
22 | SHELLCHECK_OPTS='-e SC2016,SC2145'
23 |
24 | before "test" export PATH="$LOCO_ROOT:$PATH" # put bashup.events on PATH
25 | on "test" shellcheck bashup.events
26 |
27 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [bashup.events]
2 | indent_size = 4
3 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # basher installation scheme for dependencies; you can change this if you want,
4 | # so long as all the variables are correct. The .devkit/dk script will clone
5 | # basher to $BASHER_ROOT and look for binaries in $BASHER_INSTALL_BIN.
6 |
7 | export BASHER_PREFIX="$PWD/.deps"
8 | export BASHER_INSTALL_BIN="$BASHER_PREFIX/bin"
9 | export BASHER_INSTALL_MAN="$BASHER_PREFIX/man"
10 |
11 | # Dependencies are checked out here:
12 | export BASHER_PACKAGES_PATH="$BASHER_PREFIX"
13 | export BASHER_ROOT="$BASHER_PACKAGES_PATH/basherpm/basher"
14 |
15 | # Activate virtualenv if present
16 | [[ -f $BASHER_INSTALL_BIN/activate && -f $BASHER_INSTALL_BIN/python ]] &&
17 | [[ ! "${VIRTUAL_ENV-}" || $VIRTUAL_ENV != "$BASHER_PREFIX" ]] &&
18 | VIRTUAL_ENV_DISABLE_PROMPT=true source $BASHER_INSTALL_BIN/activate
19 |
20 | # Activate .composer/vendor/bin if PHP project
21 | [[ -f composer.json ]] && export PATH="$PWD/vendor/bin:$PATH"
22 |
23 | # Activate node_modules/.bin if Node project
24 | [[ -f package.json ]] && export PATH="$PWD/node_modules/.bin:$PATH"
25 |
26 | # $BASHER_INSTALL_BIN must be on PATH to use commands installed as deps
27 | [[ :$PATH: == *:$BASHER_INSTALL_BIN:* ]] || export PATH="$BASHER_INSTALL_BIN:$PATH"
28 |
29 | # You can add other variables you want available via direnv. Configuration
30 | # variables for devkit itself, however, should go in .dkrc unless they need
31 | # to be available via direnv as well.
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .deps
2 | .devkit
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Practical Event Listeners for Bash
2 |
3 | `bashup.events` is an event listener/callback API for creating extensible bash programs. It's small (<2.2k), fast (~10k events/second), and highly portable (no bash4-isms or external programs used). Events can be one-time or repeated, listeners can be added or removed, and any string can be an event name. (You can even have "[promises](#promise-like-events)", of a sort!) Callbacks can be any command or function plus any number of arguments, and can even opt to receive [additional arguments supplied by the event](#passing-arguments-to-callbacks).
4 |
5 | Other features include:
6 |
7 | * Running a callback each time something happens ([`event on`](#event-on), [`event emit`](#event-emit))
8 | * Running a callback the *next* time something happens, but not after that ([`event once`](#event-once))
9 | * Alerting subscribers of an event once, making them re-subscribe for future occurrences ([`event fire`](#event-fire) )
10 | * Alerting subscribers of a one-time only event or calculation... not just current subscribers, but *future* ones as well ([`event resolve`](#event-resolve))
11 | * Allowing subscribed callbacks to veto a process (e.g. validation rules), using [`event all`](#event-all)
12 | * Searching for the first callback that can successfully handle something, using [`event any`](#event-any)
13 |
14 |
15 | #### Contents
16 |
17 |
18 |
19 | - [Installation, Requirements And Use](#installation-requirements-and-use)
20 | - [Basic Operations](#basic-operations)
21 | * [event on](#event-on)
22 | * [event emit](#event-emit)
23 | * [event off](#event-off)
24 | * [event has](#event-has)
25 | * [event fire](#event-fire)
26 | - [Passing Arguments To Callbacks](#passing-arguments-to-callbacks)
27 | - [Promise-Like Events](#promise-like-events)
28 | * [event resolve](#event-resolve)
29 | * [event resolved](#event-resolved)
30 | - [Conditional Operations](#conditional-operations)
31 | * [event all](#event-all)
32 | * [event any](#event-any)
33 | - [Other Operations](#other-operations)
34 | * [event once](#event-once)
35 | * [event encode](#event-encode)
36 | * [event decode](#event-decode)
37 | * [event get](#event-get)
38 | * [event list](#event-list)
39 | * [event quote](#event-quote)
40 | * [event error](#event-error)
41 | - [Event Handlers and Subshells](#event-handlers-and-subshells)
42 | - [License](#license)
43 |
44 |
45 |
46 | ### Installation, Requirements And Use
47 |
48 | Copy and paste the [code](bashup.events) into your script, or place it on `PATH` and `source bashup.events`. (If you have [basher](https://github.com/basherpm/basher), you can `basher install bashup/events` to get it installed on your `PATH`.) The code is licensed [CC0](http://creativecommons.org/publicdomain/zero/1.0/), so you are not required to add any attribution or copyright notices to your project.
49 |
50 | ````sh
51 | $ source bashup.events
52 | ````
53 |
54 | This version of bashup.events works with bash 3.2+. If you don't need to support older bash versions, you can use the [bash44 branch](https://github.com/bashup/events/tree/bash44), which is significantly faster (~23k emits/second). Besides the supported bash version, the differences between the two versions are:
55 |
56 | * The 3.2+ version is a bit larger and a lot slower: only around 10K emits/second, even when run with a newer bash.
57 | * The 3.2+ version of `event list` returns sorted keys; the 4.4 version does not give a guaranteed order
58 | * The 4.4+ version uses associative arrays; the 3.2+ version emulates them using individual variables with urlencoded names. Among other things, this means that the 3.2+ version can mask specific events with `local`, but the 4.4 version cannot.
59 | * Other performance characteristics vary, as they use different `event encode` implementations with different performance characteristics. (4.4's is tuned for reasonable performance regardless of character set, while 3.2's is tuned for speed at all costs with a small character set.)
60 |
61 | ### Basic Operations
62 |
63 | Sourcing `bashup.events` exposes one public function, `event`, that provides a variety of subcommands. All of the primary subcommands take an event name as their first argument.
64 |
65 | Event names can be any string, but performance is best if you limit them to pure ASCII alphanumeric or `_` characters, as all other characters have to be encoded at the start of each event command. (And the larger the character set used, the slower the encoding process becomes.)
66 |
67 | #### event on
68 |
69 | `event on` *event cmd [args...]* subscribes *cmd args...* as a callback to *event*, if it's not already added:
70 |
71 | ````sh
72 | $ event on "event1" echo "got event1"
73 | $ event on "event1" echo "is this cool or what?"
74 | ````
75 |
76 | #### event emit
77 |
78 | `event emit` *event data...* invokes all the callbacks for *event*, passing *data...* as additional arguments to any callbacks that [registered to receive them](#passing-arguments-to-callbacks). Callbacks added to the event while the `emit` is occurring will **not** be invoked until a subsequent occurrence of the event, and the already-added callbacks will remain subscribed (unless they unsubscribe themselves, or were registered with [`event once`](#event-once)).
79 |
80 | ````sh
81 | $ event emit "event1"
82 | got event1
83 | is this cool or what?
84 | ````
85 |
86 | #### event off
87 |
88 | `event off` *event [cmd [args...]]* unsubscribes the *cmd args...* callback from *event*. If no callback is given, *all* callbacks are removed.
89 |
90 | ````sh
91 | $ event off "event1" echo "got event1"
92 | $ event emit "event1"
93 | is this cool or what?
94 |
95 | $ event on "event2" echo foo
96 | $ event off "event2"
97 | $ event emit "event2"
98 | ````
99 |
100 | #### event has
101 |
102 | * `event has` *event* returns truth if *event* has any registered callbacks.
103 | * `event has` *event cmd [args...]* returns truth if *cmd args...* has been registered as a callback for *event*.
104 |
105 | ````sh
106 | # `event has` with no callback tests for any callbacks at all
107 |
108 | $ event has "event1" && echo "yes, there are some callbacks"
109 | yes, there are some callbacks
110 |
111 | $ event has "something_else" || echo "but not for this other event"
112 | but not for this other event
113 |
114 | # Test for specific callback susbscription:
115 |
116 | $ event has "event1" echo "is this cool or what?" && echo "cool!"
117 | cool!
118 | $ event has "event1" echo "got event1" || echo "nope!"
119 | nope!
120 |
121 | ````
122 |
123 | #### event fire
124 |
125 | `event fire` *event data...* fires a "one shot" event, by invoking all the callbacks for *event*, passing *data...* as additional arguments to any callbacks that [registered to receive them](#passing-arguments-to-callbacks). All callbacks are removed from the event, and any new callbacks added during the firing will be invoked as soon as all the previously-added callbacks have been invoked (and then are also removed from the event).
126 |
127 | The overall idea is somewhat similar to the Javascript "promise" resolution algorithm, except that you can `fire` an event more than once, and there is no "memory" of the arguments. (See [`event resolve`](#event-resolve) if you want something closer to a JS Promise.)
128 |
129 | ````sh
130 | # `event fire` removes callbacks and handles nesting:
131 |
132 | $ mycallback() { event on event1 echo "nested!"; }
133 | $ event on "event1" mycallback
134 |
135 | $ event fire "event1"
136 | is this cool or what?
137 | nested!
138 |
139 | $ event emit "event1" # all callbacks gone now
140 |
141 | ````
142 |
143 | ### Passing Arguments To Callbacks
144 |
145 | When invoking an event, you can pass additional arguments that will be added to the end of the arguments supplied to the given callbacks. The callbacks, however, will only receive these arguments if they were registered to do so, by adding an extra argument after the event name: an `@` followed by the maximum number of arguments the callback is prepared to receive:
146 |
147 | ````sh
148 | # Callbacks can receive extra arguments sent by emit/fire/resolve/all/any:
149 |
150 | $ event on "event2" @2 echo "Args:" # accept up to 2 arguments
151 | $ event fire "event2" foo bar baz
152 | Args: foo bar
153 |
154 | ````
155 |
156 | The reason an argument count is required, is because one purpose of an event system is to be *extensible*. If an event adds new arguments over time, old callbacks may break if they weren't written in such a way as to ignore the new arguments. Requiring an explicit request for arguments avoids this problem.
157 |
158 | If the nature of the event is that it emits a *variable* number of arguments, however, you can register your callback with `@_`, which means "receive *all* the arguments, no matter how many". You should only use it in places where you can definitely handle any number of arguments, or else you may run into unexpected behavior.
159 |
160 | ````sh
161 | # Why variable arguments lists aren't the default:
162 |
163 | $ event on "cleanup" @_ echo "rm -rf"
164 | $ event emit "cleanup" foo
165 | rm -rf foo
166 |
167 | $ event emit "cleanup" foo / # New release... "cleanup" event added a new argument!
168 | rm -rf foo /
169 |
170 | ````
171 |
172 | [`event on`](#event-on), [`event once`](#event-once), [`event off`](#event-off), and [`event has`](#event-has) all accept argument count specifiers when adding, removing, or checking for callbacks. Callbacks with different argument counts are considered to be *different* callbacks:
173 |
174 | ````sh
175 | # Only one argument:
176 |
177 | $ event on "myevent" @1 echo
178 | $ event emit "myevent" foo bar baz
179 | foo
180 |
181 | # Different count = different callbacks:
182 |
183 | $ event has "myevent" @1 echo && echo got it
184 | got it
185 | $ event has "myevent" @2 echo || echo nope
186 | nope
187 |
188 | # Add 2 argument version (numeric value is what's used):
189 |
190 | $ event on "myevent" @02 echo
191 | $ event emit "myevent" foo bar baz
192 | foo
193 | foo bar
194 |
195 | # Remove the 2-arg version, add unlimited version:
196 |
197 | $ event off "myevent" @2 echo
198 | $ event on "myevent" @_ echo
199 |
200 | $ event emit "myevent" foo bar baz
201 | foo
202 | foo bar baz
203 |
204 | # Unlimited version is distinct, too:
205 |
206 | $ event has "myevent" @_ echo && echo got it
207 | got it
208 | $ event has "myevent" @2 echo || echo nope
209 | nope
210 |
211 | # As is the zero-arg version:
212 |
213 | $ event has "myevent" echo || echo nope
214 | nope
215 |
216 | # But the zero-arg version can be implicit or explicit, w/or without leading zeros:
217 |
218 | $ event on "myevent" echo
219 | $ event has "myevent" echo && echo got it
220 | got it
221 | $ event has "myevent" @0 echo && echo got it
222 | got it
223 |
224 | $ event off "myevent" @00 echo
225 | $ event has "myevent" echo || echo nope
226 | nope
227 | ````
228 |
229 | ### Promise-Like Events
230 |
231 | #### event resolve
232 |
233 | If you have a truly one-time event, but subscribers could "miss it" by subscribing too late, you can use `event resolve` to "permanently [`fire`](#event-fire)" an event with a specific set of arguments. Once this is done, all future `event on` calls for that event will invoke the callback *immediately* with the previously-given arguments.
234 |
235 | There is no way to "unresolve" a resolved event within the current shell. Trying to `resolve`, `emit`, `fire`, `any` or `all` an already-resolved event will result in an error message and a failure return of 70 (`EX_SOFTWARE`).
236 |
237 | ````sh
238 | # Subscribers before the resolve will be fired upon resolve:
239 |
240 | $ event on "promised" event on "promised" @1 echo "Nested:"
241 | $ event on "promised" @1 echo "Plain:"
242 |
243 | $ event resolve "promised" value
244 | Plain: value
245 | Nested: value
246 |
247 | # Subscribers after the resolve are fired immediately:
248 |
249 | $ event on "promised" event on "promised" @1 echo "Nested:"
250 | Nested: value
251 |
252 | $ event on "promised" @1 echo "Plain:"
253 | Plain: value
254 |
255 | # And a resolved event never "has" any subscribers:
256 |
257 | $ event has "promised" || echo nope
258 | nope
259 |
260 | ````
261 |
262 | #### event resolved
263 |
264 | `event resolved` *event* returns truth if `event resolve` *event* has been called.
265 |
266 | ````sh
267 | $ event resolved "promised" && echo "yep"
268 | yep
269 |
270 | $ event resolved "another_promise" || echo "not yet"
271 | not yet
272 | ````
273 |
274 | ### Conditional Operations
275 |
276 | #### event all
277 |
278 | `event all` *event data*... works like [`event emit`](#event-emit), except that execution stops after the first callback that returns false (i.e., a non-zero exit code), and that exit code is returned. Truth is returned if all events return truth.
279 |
280 | ````sh
281 | # Use an event to validate a password
282 |
283 | $ validate() { echo "validating: $1"; [[ $3 =~ $2 ]]; }
284 |
285 | $ event on "password_check" @1 validate "has a number" '[0-9]+'
286 | $ event on "password_check" @1 validate "is 8+ chars" ........
287 | $ event on "password_check" @1 validate "has uppercase" '[A-Z]'
288 | $ event on "password_check" @1 validate "has lowercase" '[a-z]'
289 |
290 | $ event all "password_check" 'foo27' || echo "fail!"
291 | validating: has a number
292 | validating: is 8+ chars
293 | fail!
294 |
295 | $ event all "password_check" 'Blue42Schmoo' && echo "pass!"
296 | validating: has a number
297 | validating: is 8+ chars
298 | validating: has uppercase
299 | validating: has lowercase
300 | pass!
301 |
302 | ````
303 |
304 | #### event any
305 |
306 | `event any` *event data...* also works like [`event emit`](#event-emit), except that execution stops on the first callback to return truth (i.e. a zero exit code). An exit code of 1 is returned if all events return non-zero exit codes.
307 |
308 | ````sh
309 | $ match() { echo "checking for $1"; REPLY=$2; [[ $1 == $3 ]]; }
310 |
311 | $ event on "lookup" @1 match a "got one!"
312 | $ event on "lookup" @1 match b "number two"
313 | $ event on "lookup" @1 match c "third time's the charm"
314 |
315 | $ event any "lookup" b && echo "match: $REPLY"
316 | checking for a
317 | checking for b
318 | match: number two
319 |
320 | $ event any "lookup" q || echo "fail!"
321 | checking for a
322 | checking for b
323 | checking for c
324 | fail!
325 |
326 | ````
327 |
328 | ### Other Operations
329 |
330 | #### event once
331 |
332 | `event once` *event cmd [args...]* is like [`event on`](#event-on), except that the callback is unsubscribed before it's invoked, ensuring it will be called at most once, even if *event* is emitted multiple times in a row:
333 |
334 | ````sh
335 | $ event once "something" @_ echo
336 | $ event emit "something" this that
337 | this that
338 | $ event emit "something" more stuff
339 | ````
340 |
341 | (Note: a callback added by `event once` cannot be removed by `event off`; if you need to be able to remove such a callback you should use `event on` instead and make the callback remove itself with `event off`.)
342 |
343 | #### event encode
344 |
345 | `event encode` *string* sets `$REPLY` to an encoded version of *string* that is safe to use as part of a bash variable name (i.e. ascii alphanumerics and `_`). Underscores and all other non-alphanumerics are encoded as an underscore and two hex digits.
346 |
347 | ````sh
348 | $ event encode "foo" && echo $REPLY
349 | foo
350 | $ event encode "foo.bar" && echo $REPLY
351 | foo_2ebar
352 | $ event encode "foo_bar" && echo $REPLY
353 | foo_5fbar
354 |
355 | $ event encode ' !"#$%'\''()*+,-./:;<=>?@[\]^_`' && echo "$REPLY"
356 | _20_21_22_23_24_25_27_28_29_2a_2b_2c_2d_2e_2f_3a_3b_3c_3d_3e_3f_40_5b_5c_5d_5e_5f_60
357 |
358 | $ event encode $'\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0b\f\r\x0e\x0f\x10' &&
359 | > echo "$REPLY"
360 | _01_02_03_04_05_06_07_08_09_0a_0b_0c_0d_0e_0f_10
361 |
362 | $ event encode $'{|}~\x7f' && echo "$REPLY"
363 | _7b_7c_7d_7e_7f
364 |
365 | ````
366 |
367 | For performance reasons, the function that handles event encoding is JITted. Every time new non-ASCII or non-alphanumeric characters are seen, the function is rewritten to efficiently handle encoding them. This makes encoding extremely fast when a program only ever uses a handful of punctuation characters in event names or strings passed to `event encode`. Encoding arbitrary strings (or using them as event names) is not recommended, however, since this will "train" the encoder to run more slowly for *all* `event` operations from then on.
368 |
369 | #### event decode
370 |
371 | `event decode` *string* sets `$REPLY` to the original event name for *string*, turning the encoded characters back to their original values. If multiple arguments are given, `REPLY` is an array of results.
372 |
373 | ````sh
374 | $ event decode "foo_2ebar_2dbaz" && echo $REPLY
375 | foo.bar-baz
376 |
377 | $ event decode "_2fspim" "_2bspam" && printf '%s\n' "${REPLY[@]}"
378 | /spim
379 | +spam
380 | ````
381 |
382 | #### event get
383 |
384 | `event get` *event* sets `$REPLY` to a string that can be `eval`'d to do the equivalent of `event emit "event" "${@:2}"`. This can be used for debugging, or to allow callbacks like `local` to be run in a specific calling context. An error 70 is returned if *event* is resolved.
385 |
386 | ~~~sh
387 | $ event get lookup && echo "$REPLY"
388 | match a got\ one\! "${@:2:1}"
389 | match b number\ two "${@:2:1}"
390 | match c third\ time\'s\ the\ charm "${@:2:1}"
391 |
392 | $ event get "promised" && echo "$REPLY"
393 | event "promised" already resolved
394 | [70]
395 | ~~~
396 |
397 | (Notice that the encoded commands in `$REPLY` reference parameters beginning with `"$2"`, which means that if you want to eval them with `"$@"` you'll need to unshift a dummy argument.)
398 |
399 | #### event list
400 |
401 | `event list` *prefix* sets `REPLY` to an array of currently-defined event names beginning with *prefix*. events that currently have listeners are returned, as are resolved events.
402 |
403 | ````sh
404 | # event1 and event2 no longer have subscribers:
405 |
406 | $ event list "event" && echo "${#REPLY[@]}"
407 | 0
408 |
409 | # But there are some events starting with "p"
410 |
411 | $ event list "p" && printf '%s\n' "${REPLY[@]}"
412 | password_check
413 | promised
414 |
415 | $ event list "lookup" && printf '%s\n' "${REPLY[@]}"
416 | lookup
417 | ````
418 |
419 | #### event quote
420 |
421 | `event quote` *args* sets `$REPLY` to a space-separated list of the given arguments in shell-quoted form (using `printf %q`). The resulting string is safe to `eval`, in the sense that the arguments are guaranteed to expand to the same values (and number of values) as were originally given.
422 |
423 | ````sh
424 | $ event quote a b c && echo "$REPLY"
425 | a b c
426 | $ event quote "a b c" && echo "$REPLY"
427 | a\ b\ c
428 | $ event quote x "" y && echo "$REPLY"
429 | x '' y
430 | $ event quote && echo "'$REPLY'"
431 | ''
432 | ````
433 |
434 | #### event error
435 |
436 | `event error` *message [exitlevel]* prints *message* to stderr and returns *exitlevel*, or 64 (`EX_USAGE`) if no *exitlevel* is given. (You will still need to `return` or `exit` for this to have any further effect, unless `set -e` is in effect.)
437 |
438 | ````sh
439 | $ event error "This is an error" 127 >/dev/null
440 | This is an error
441 | [127]
442 | ````
443 |
444 | ### Event Handlers and Subshells
445 |
446 | Just as with bash variables and functions, the current event handlers and the state of promises are inherited by subshells (e.g. in command substitutions), but changes made by a subshell do not affect the state of the calling shell. (As should be expected given that subshells are forked processes without shared memory or other interprocess communication by default.)
447 |
448 | Note that this means adding or removing handlers in a subshell (even _implicitly_, via `fire`, `resolve`, `once`, etc.) has **no effect** on the calling shell. As is normally the case for subshells, you’ll need to read their output and act on it, if you need to apply side-effects in the calling process.
449 |
450 | ### License
451 |
452 |
453 | 
454 | To the extent possible under law, PJ Eby
455 | has waived all copyright and related or neighboring rights to bashup/events.
456 | This work is published from: United States.
457 |
--------------------------------------------------------------------------------
/bashup.events:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | event(){ case $1 in error|quote|encode|decode);; *)
3 | __ev.encode "${2-}";local f n='' e=bashup_event_$REPLY'[1]';f=${e/event/flag}
4 | case $1 in emit) shift;${!f-};eval "${!e-}"; return ;;on|once|off|has)
5 | case "${3-}" in @_) n='$#';; @*[^0-9]*);; @[0-9]*) n=$((${3#@}));; esac; ${n:+
6 | set -- "$1" "$2" "${@:4}" }
7 | case $1/$# in
8 | on*/[12]) set -- error "${2-}: missing callback";; */[12]) REPLY=;;
9 | *) __ev.quote "${@:3}";((${n/\$#/1}))&&REPLY+=' "${@:2:'"$n"'}"';REPLY+=$'\n'
10 | esac
11 | esac
12 | esac ;__ev."$@";}
13 | __ev.error(){ echo "$1">&2;return "${2:-64}";}
14 | __ev.quote(){ REPLY=; ${@+printf -v REPLY ' %q' "$@"}; REPLY=${REPLY# };}
15 | __ev.has(){ [[ ${!e-} && $'\n'"${!e}" == *$'\n'"$REPLY"* && ! ${!f-} ]];}
16 | __ev.get(){ ${!f-};REPLY=${!e-};}
17 | __ev.on(){ __ev.has && return;if [[ ! ${!f-} ]];then eval "$e"+='$REPLY';else eval "${!e-};$REPLY";fi;}
18 | __ev.off(){ __ev.has||return 0; n="${!e}"; n=${REPLY:+"${n#"$REPLY"}"}; eval "$e"=$'"${n//\n"$REPLY"/\n}"';[[ ${!e} ]]||unset "${e%\[1]}";}
19 | __ev.fire(){ ${!f-};set -- "$e" "${@:2}"; while [[ ${!1-} ]];do eval "unset ${1%\[1]};${!1}"; done ;}
20 | __ev.all(){ ${!f-};e=${!e-};eval "${e//$'\n'/||return; }";}
21 | __ev.any(){ ${!f-};e=${!e-};eval "${e//$'\n'/&&return|| } ! :";}
22 | __ev.resolve(){
23 | ${!f-};__ev.fire "$@";__ev.quote "$@"
24 | printf -v n "eval __ev.error 'event \"%s\" already resolved' 70;return" "$1"; eval "${f}"='$n'
25 | printf -v n 'set -- %s' "$REPLY"; eval "${e}"='$n';readonly "${f%\[1]}" "${e%\[1]}"
26 | }
27 | __ev.resolved(){ [[ ${!f-} ]];}
28 | __ev.once(){ n=${n:-0} n=${n/\$#/_}; event on "$1" "@$n" __ev_once $# "@$n" "$@";}
29 | __ev_once(){ event off "$3" "$2" __ev_once "${@:1:$1+2}"; "${@:4}";}
30 | __ev_jit(){
31 | local q r=${__ev_jit-} s=$1;((${#r}<250))||__ev_jit=
32 | while [[ "$s" ]]; do
33 | r=${s::1};s=${s:1};printf -v q %q "$r";eval 's=${s//'"$q}";printf -v r 'REPLY=${REPLY//%s/_%02x};' "${q/#[~]/[~]}" "'$r";eval "$r";__ev_jit+="$r"
34 | done
35 | eval '__ev.encode(){ local LC_ALL=C;REPLY=${1//_/_5f};'\
36 | "${__ev_jit-}"' [[ $REPLY != *[^_[:alnum:]]* ]] || __ev_jit "${REPLY//[_[:alnum:]]/}";}'
37 | };__ev_jit ''
38 | __ev.decode(){ REPLY=();while (($#));do printf -v n %b "${1//_/\\x}";REPLY+=("$n");shift;done;}
39 | __ev.list(){ eval 'set -- "${!'"${e%\[1]}"'@}"';__ev.decode "${@#bashup_event_}";}
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | BINS=bashup.events
2 |
--------------------------------------------------------------------------------
/script/README.md:
--------------------------------------------------------------------------------
1 | ## Scripts To Rule Them All
2 |
3 | The scripts in this directory are a [Scripts To Rule Them All](https://githubengineering.com/scripts-to-rule-them-all/) implementation, powered by [.devkit](https://github.com/bashup/.devkit). They should be run from within the project's root directory, using e.g. `script/test` to run tests, and so on.
4 |
5 | Please check the containing project's documentation for more details, or see the preceding links for more background or reference information.
6 |
7 |
--------------------------------------------------------------------------------
/script/bootstrap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if ! [[ -f .dkrc ]]; then
4 | echo "Please run this script from the project root directory." >&2
5 | exit 64 # EX_USAGE
6 | fi
7 |
8 | if ! [[ -d .devkit ]]; then
9 | # Modify this line if you want to pin a particular .devkit revision:
10 | git clone -q --depth 1 https://github.com/bashup/.devkit
11 | fi
12 |
13 | exec ".devkit/dk" "$(basename "$0")" "$@"
14 |
--------------------------------------------------------------------------------
/script/cibuild:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/script/clean:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/script/console:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/script/server:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/script/setup:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/script/test:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/script/update:
--------------------------------------------------------------------------------
1 | bootstrap
--------------------------------------------------------------------------------
/specs/Misc.cram.md:
--------------------------------------------------------------------------------
1 | ## Miscellaneous Cases and Error Handling
2 |
3 | We want to test all this with `-eu`, to catch undefined variables and dangling fail statuses:
4 |
5 | ````sh
6 | $ source bashup.events; set -eu
7 | ````
8 |
9 | e.g. What happens if you don't pass a callback to event on, .has, or .off?
10 |
11 | ````sh
12 | $ ( event on foo ) || echo [$?]
13 | foo: missing callback
14 | [64]
15 |
16 | $ ( event on ) || echo [$?]
17 | : missing callback
18 | [64]
19 |
20 | $ ( event has x ) || echo [$?]
21 | [1]
22 | $ event on x echo y
23 |
24 | $ ( event has x ); echo [$?]
25 | [0]
26 |
27 | ````
28 |
29 | Or don't give an event to fire or emit? (they're treated as an empty string)
30 |
31 | ````sh
32 | $ ( event has ) || echo [$?]
33 | [1]
34 |
35 | $ event on "" echo empty-string event
36 |
37 | $ ( event has ) && echo yes
38 | yes
39 |
40 | $ ( event emit ) || echo [$?]
41 | empty-string event
42 |
43 | $ ( event fire ) || echo [$?]
44 | empty-string event
45 |
46 | ````
47 |
48 | And what if you try to pass an arg count to something other than has/on/off? (they're treated as arguments)
49 |
50 | ````sh
51 | $ event on foo @_ echo
52 |
53 | $ event emit foo @7 bar baz
54 | @7 bar baz
55 |
56 | $ event fire foo @_ bar baz
57 | @_ bar baz
58 | ````
59 |
60 | Or an invalid arg indicator to has/on/off? (they're considered part of the command)
61 |
62 | ````sh
63 | $ event on bar @9.2 quiz
64 | $ event emit bar || echo [$?]
65 | */bashup.events: line 4: @9.2: command not found (glob)
66 | [127]
67 | ````
68 |
69 | Or try to do something with an already-resolved promise:
70 |
71 | ````sh
72 | # Duplicate emit/fire/resolve/any/all produces code 70 (EX_SOFTWARE):
73 |
74 | $ event resolve "promised" other
75 | $ set +e
76 |
77 | $ event resolve "promised" other
78 | event "promised" already resolved
79 | [70]
80 |
81 | $ event fire "promised" other
82 | event "promised" already resolved
83 | [70]
84 |
85 | $ event emit "promised" other
86 | event "promised" already resolved
87 | [70]
88 |
89 | $ event any "promised" other
90 | event "promised" already resolved
91 | [70]
92 |
93 | $ event all "promised" other
94 | event "promised" already resolved
95 | [70]
96 | ````
97 |
98 | Also, "event once" shouldn't leave anything behind in the event, and should handle various argument counts (the `@_` case is tested in the README):
99 |
100 | ````sh
101 | $ event once foobar echo baz
102 | $ event emit foobar
103 | baz
104 |
105 | $ event has foobar || echo nope
106 | nope
107 |
108 | $ event once foobar @2 echo fish
109 | $ event emit foobar baz spam thingy
110 | fish baz spam
111 |
112 | $ event has foobar || echo nope
113 | nope
114 | ````
--------------------------------------------------------------------------------