-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathblib
More file actions
executable file
·1317 lines (1146 loc) · 47.8 KB
/
blib
File metadata and controls
executable file
·1317 lines (1146 loc) · 47.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
#
#+blib - a bash library
#+
#+The basic functions which are imported by default.
#+
#+Copyright (C) 2026 David Hobach LGPLv3
#+version: Execute `blib version` or use [b_version](#b_version).
#+
#+### Disclaimer ###
#+This program is free software: you can redistribute it and/or modify
#+it under the terms of the Lesser GNU General Public License as published by
#+the Free Software Foundation, either version 3 of the License, or
#+(at your option) any later version.
#+
#+This program is distributed in the hope that it will be useful,
#+but WITHOUT ANY WARRANTY; without even the implied warranty of
#+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#+Lesser GNU General Public License for more details.
#+
#+You should have received a copy of the Lesser GNU General Public License
#+along with this program. If not, see <https://www.gnu.org/licenses/>.
#+
#+**The above statements apply to all modules of blib if not mentioned otherwise.**
#+
#+### Coding Conventions ###
#+ #. embrace the KISS principle
#+ #. some general guidelines: <http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming/>
#+ #. use b\_\[module\]\_\[camel case function name\] to denote functions meant to be used by users of the library, B_\[module\]\_\[upper case var name\] for global variables
#+ #. use BLIB\_\[module\]\_\[upper case var name\] for global variables (to be avoided whenever possible) not meant to be used by library users ("private" variables); library users can use setters or getters
#+ #. use the BLIB\_STORE with the above naming conventions for "private" variables whenever possible
#+ #. use blib\_\[module\]\_\[camel case function name\] to denote private functions not meant to be used by library users; the function name should not contain any underscores
#+ #. the blib module itself is the only exception which can use b_, B_, blib_, BLIB_ without module name
#+ #. prefixes such as t_, T_ and UTD_ are exclusively related to test code
#+ #. set exit codes (!= 0 --> issue) wherever it makes sense
#+ #. keep the global namespace clean whenever possible
#+ #. declare -g must be used in order to allow sourcing from a function ([b_import](#b_import))
#+ #. modules must check their dependencies via [b_deps](#b_deps)
#+ #. modules should provide a header similar to that of the blib source file in order to make the documentation generation work
#+ #. modules may be placed in subfolders of arbitrary depth
#+ #. use 0 to indicate true and 1 to indicate false for variables; for exit codes use a non-zero exit code to indicate the number of errors
#+ #. functions should be tagged with the following tags, if applicable:
#+ *@StateChanging* - the function changes the internal state of the script in a way that will not propagate to supershells (e.g. global variables) and should thus not be called from subshells (unless the user wants the state to only change in that shell)
#+ *@B_E* - the function uses [B_E](#B_E) for error handling and may thus behave differently depending on the implemented error handler
#+
#+### Library Usage ###
#+with the default bash options:
#+```bash
#+ source blib
#+ b_checkVersion 1 0 || { >&2 echo "This script depends on blib (https://github.com/3hhh/blib) version 1.0 or higher. Please install a supported version." ; exit 1 ; }
#+
#+ b_import [module]
#+```
#+### Global Variables ###
#+B_LIB_DIR
#+Path of the blib installation directory.
B_LIB_DIR="$(readlink -f "${BASH_SOURCE[0]}")" || { >&2 echo "ERROR: Failed to compute the blib path." ; exit 3 ; }
B_LIB_DIR="$(dirname "$B_LIB_DIR")" || { >&2 echo "ERROR: Failed to compute the blib path." ; exit 4 ; }
#map to store global blib data (we use it to hide global variables from the namespace)
#meant for data that may cause stateless testing with [runSL](#runSL) to fail (i.e. state changes here are monitored)
#most module data should go here
declare -gA BLIB_STORE=(
["BLIB_SCRIPT_DIR"]="$B_LIB_DIR"
["BLIB_SCRIPT_NAME"]="blib"
["BLIB_LIB_DIR"]="$B_LIB_DIR/lib"
["BLIB_DOC_DIR"]="$B_LIB_DIR/doc"
["BLIB_STATIC_DOC_DIR"]="$B_LIB_DIR/doc_static"
["BLIB_TESTS_DIR"]="$B_LIB_DIR/tests"
["BLIB_UTIL_DIR"]="$B_LIB_DIR/util"
["BLIB_CDOC"]="$B_LIB_DIR/util/blib-cdoc"
["BLIB_IMPORTED_MODULES"]=""
["BLIB_STANDALONE_MODE"]="1"
["BLIB_DEFAULT_MSG_HANDLER_INTERMEDIATE"]=" "
)
#BLIB_STANDALONE_MODE: set to 0 if this script is a standalone version generated by b_generateStandalone
#another map to store global blib data
#meant for data that changes quite frequently, but shouldn't cause stateless testing with [runSL](#runSL) to fail (i.e. state changes here are _not_ monitored)
declare -gA BLIB_STORE_VOLATILE=(
["BLIB_MODULE_LIST_CACHE"]=""
["BLIB_ERR_ESET"]=""
["BLIB_ERR_HANDLER"]="b_defaultErrorHandler"
["BLIB_MSG_HANDLER"]="b_defaultMessageHandler"
)
#BLIB_ERR_*: used by B_E
#last element = event handler in use before the last call to [b_setErrorHandler](#b_setErrorHandler)
declare -ga BLIB_ERR_HANDLER_LAST=()
#message type --> prefix for the b_defaultMessageHandler
declare -gA BLIB_DEFAULT_MSG_HANDLER_PREFIXES=(
["0"]="INFO: "
["1"]="ERROR: "
)
#message type --> cache for [b_cachingMessageHandler](#b_cachingMessageHandler)
declare -gA BLIB_CACHING_MSG_HANDLER_CACHES=()
#+B_TEST_MODE
#+blib will set this variable to 0, if blib is running in test mode.
#+This variable may be used to bypass code during testing, if bats cannot test that code due to its limitations (e.g. for EXIT traps which bats uses for itself).
B_TEST_MODE=1
#+B_CALLER_NAME
#+Name of the executable or script as String which called blib and any child libraries.
B_CALLER_NAME="${0##*/}"
#+B_ERR
#+Global variable used for error handling throughout blib, cf. [B_E](#B_E).
#+
#+It is recommended to always set an at least partially static error message on confirmed errors as variables may be empty (which would indicate "no error" for [B_E](#B_E)).
B_ERR=""
#+B_RC
#+Can be used to set the return code for [B_E](#B_E) in the case of an error. It defaults to 1.
#+
#+This should be set to an integer value between 1 and 255. Any other value may cause undefined behaviour.
B_RC=1
#+B_CONF_DIR
#+Path to the blib configuration directory. Modules may create subfolders named by their module name there. May not exist.
B_CONF_DIR="/etc/blib"
#+B_SCRIPT
#+Global variable which can be used to obtain the two global variables [B_SCRIPT_DIR](#B_SCRIPT_DIR) and [B_SCRIPT_NAME](#B_SCRIPT_NAME) as follows:
#+```bash
#+eval "$B_SCRIPT"
#+```
B_SCRIPT=""
read -r -d '' B_SCRIPT << 'EOF'
B_SCRIPT_DIR="$(readlink -f "${BASH_SOURCE[0]}")" || { B_ERR="Failed to compute B_SCRIPT_DIR." ; B_E ; }
B_SCRIPT_DIR="$(dirname "$B_SCRIPT_DIR")" || { B_ERR="Failed to compute B_SCRIPT_DIR." ; B_E ; }
B_SCRIPT_NAME="${BASH_SOURCE[0]##*/}"
EOF
#+B_SCRIPT_NAME
#+Path of the sourced or executed bash script executing the eval (symlinks are resolved) of [B_SCRIPT](#B_SCRIPT).
#+B_SCRIPT_DIR
#+Name of the sourced or executed bash script executing the eval (symlinks are resolved) of [B_SCRIPT](#B_SCRIPT).
#+B_LINE
#+Default output variable of [b_readLine](#b_readLine).
#+### Global Aliases ###
#+Alias expansion is automatically enabled by blib as it is required for its core functionality. So if you have strange aliases defined in your shell environment, this may cause undefined blib behaviour.
#aliases should be avoided wherever possible, but sometimes they provide more power than functions
#NOTE: aliases are passed to sourced modules as long as they were defined before
shopt -s expand_aliases
#+B_E
#+*The* blib error handler: All blib modules use it whenever execution errors require special handling that the currently executing code cannot achieve.
#+
#+Syntax:
#+```bash
#+B_ERR="This is an error message." ; B_E ;
#+```
#+If you need to set the return/exit code, you can do it with [B_RC](#B_RC):
#+```bash
#+B_ERR="This is another error message." ; B_RC=6 ; B_E ;
#+```
#+
#+Calling [B_E](#B_E) means:
#+Check [B_ERR](#B_ERR) for an error message and if there is one, handle it. It can be placed at the end of a line or on its own line. [B_E](#B_E) will then process the error message in the way defined by the error handler (cf. [b_defaultErrorHandler](#b_defaultErrorHandler)) and stop any further execution of code in the current context (function, script, ...) returning a non-zero exit code (1) unless the described error was fixed. In the latter unlikely case it'll let execution proceed.
#+
#+More examples:
#+```bash
#+#handle a potentially failing command:
#+cmd || { B_ERR="cmd failed." ; B_E ; }
#+
#+#capture stdout of a potentially failing command:
#+local out=
#+out="$(cmd)" || { B_ERR="cmd failed." ; B_E ; }
#+
#+#handle multiple potentially failing commands in a try/catch like manner:
#+( set -e
#+ cmd1
#+ cmd2
#+ cmd3
#+) #NOTE: || doesn't work here!
#+if [ $? -ne 0 ] ; then
#+ B_ERR="Some commands failed."
#+ B_E
#+fi
#+```
#+The error handler can be re-defined at runtime with [b_setErrorHandler](#b_setErrorHandler).
alias B_E='{
#save exit code
BLIB_STORE_VOLATILE["BLIB_ERR_ESET"]=$?
#error?
if [ -n "$B_ERR" ] ; then
#call error message handler
${BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]}
#handle according to spec
case $? in
0)
B_ERR=""
;;
1)
return $B_RC &> /dev/null || exit $B_RC
;;
*)
exit $B_RC
esac
fi
#restore exit code (the && construct is required to bypass the shopt -e mode from erroring out in e.g. bats)
blib_setExitCode "${BLIB_STORE_VOLATILE["BLIB_ERR_ESET"]}" && blib_setExitCode "${BLIB_STORE_VOLATILE["BLIB_ERR_ESET"]}"
}'
#Note: {} groups must always end with a newline or ;
#+### Functions ###
#blib_setExitCode [status]
function blib_setExitCode {
return $1
}
#+b_printStackTrace [skip level]
#+[skip level]: skip that many levels of the stack trace (optional, default: 1 - skip this function call)
#+print the current stack trace in a human readable way
#+returns: stack trace with the first levels skipped as defined
function b_printStackTrace {
local skipLevel="${1:-1}"
local pad=0
local i=
for (( i=$skipLevel; i < $(( ${#FUNCNAME[@]}-1 )); i++ )); do
local s=$(( $i+1 ))
local func="${FUNCNAME[$i]}"
local lineno="${BASH_LINENO[$i]}"
local src="${BASH_SOURCE[$s]}"
printf "%${pad}s(%s:%d) %s\n" "" "$src" "$lineno" "$func"
pad=$(( pad + 2 ))
done
}
#+b_nop
#+Do nothing.
#+returns: nothing; sets a zero exit code
function b_nop {
:
}
#+b_version [part]
#+Get the version of this blib instance.
#+[part]: Optional parameter defining the part of the version to retrieve (0: all as String (default), 1: major as Integer, 2: minor as Integer).
#+returns: blib version as string; always sets a zero exit code
function b_version {
local part="${1:0}"
local major=1
local minor=7
case "$part" in
1)
echo "$major"
;;
2)
echo "$minor"
;;
*)
echo "${major}.${minor}"
;;
esac
return 0
}
#+b_checkVersion [minimum allowed major] [minimum allowed minor] [maximum allowed major] [maximum allowed minor]
#+Check whether the currently running blib instance meets the given blib version requirements.
#+To e.g. make sure you're on blib version 1.1 or higher, use
#+```bash
#+ source blib
#+ b_checkVersion 1 2 || { >&2 echo "This script depends on blib (https://github.com/3hhh/blib) version 1.2 or higher. Please install a supported version." ; exit 1 ; }
#+```
#+[minimum allowed major]: The lowest acceptable major version number (default: 0).
#+[minimum allowed minor]: The lowest acceptable minor version number (default: 0).
#+[maximum allowed major]: The highest acceptable major version number (default: infinite).
#+[maximum allowed minor]: The highest acceptable minor version number (default: infinite).
#+returns: Sets a zero exit code, if an only if the currently running blib instance meets the version requirements.
function b_checkVersion {
local minMajor="${1:-0}"
local minMinor="${2:-0}"
local maxMajor="$3"
local maxMinor="$4"
# shellcheck disable=SC2155
local major="$(b_version 1)"
# shellcheck disable=SC2155
local minor="$(b_version 2)"
if [ $major -gt $minMajor ] ; then
#all good
:
elif [ $major -eq $minMajor ] ; then
[ $minor -ge $minMinor ] || return 1
else
return 2
fi
if [ -n "$maxMajor" ] ; then
if [ $major -lt $maxMajor ] ; then
#all good
:
elif [ $major -eq $maxMajor ] ; then
[ -n "$maxMinor" ] && [ $minor -gt $maxMinor ] && return 3
else
return 4
fi
fi
return 0
}
#+b_defaultErrorHandler [error out] [send err] [send stack trace]
#+The blib default error handler.
#+As any error handler it must
#+
#+ #. handle the error message (if not the error itself) lying in [B_ERR](#B_ERR)
#+ #. not take any non-numeric arguments
#+ #. not error out itself
#+ #. implement the below *\[error out\]* as its first parameter (to make [b_setBE](#b_setBE) work)
#+ #. ideally use [b_error](#b_error) to send error messages to the user
#+ #. return one of the following exit codes:
#+ a) 0: **if and only if** the error was fixed entirely and the caller may ignore the error (i.e. probably never)
#+ b) 1: The error wasn't fixed. Functions should return to their caller indicating an error (non-zero status code). Direct shell calls will exit. [B_ERR](#B_ERR) is **not** reset to blank, i.e. the next call to [B_E](#B_E) in the same context will cause another error. The caller may use this to either *throw* the error further or handle and clear the error.
#+ c) 2: Force a stop of execution in the current shell / error out.
#+
#+[error out]: Whether or not to call exit after the error message handling, if the error couldn't be handled (default: 0 = always error out / call exit). If set to 1, [B_E](#B_E) will allow e.g. functions to return to their callers.
#+[send err]: Whether or not to send the error message to the user (default: 0 = send).
#+[send stack trace]: Whether or not to send a stack trace to the user (default: identical to \[send err\]).
#+returns: see the description above
function b_defaultErrorHandler {
local errorOut=${1:-0}
local sendErr=${2:-0}
local sendStack=${3:-$sendErr}
local firstMsg=0
if [ $sendErr -eq 0 ] ; then
b_error "$B_ERR" $firstMsg 1
firstMsg=1
fi
if [ $sendStack -eq 0 ] ; then
# shellcheck disable=SC2155
local stack="$(b_printStackTrace 1)"
b_error "Stack Trace:"$'\n'"$stack" $firstMsg 1
firstMsg=1
fi
#set the proper exit code
local ret=5
if [ $errorOut -eq 0 ] ; then
[ $sendErr -eq 0 ] && b_error "Aborting..." $firstMsg 1
ret=2
else
ret=1
fi
#send message end, if any message was sent before
[ $firstMsg -ne 0 ] && b_error "" 1 0
return $ret
}
#+b_setBE [error out]
#+Set the [error out] behaviour of the currently configured error handler.
#+
#+Contrary to [b_setErrorHandler](#b_setErrorHandler) this function may be called by blib modules as all error handlers are required to support [error out] as parameter.
#+
#+Example for switching the error out behaviour:
#+```bash
#+ b_setBE 1
#+ funcThatMayCallB_E #without subshell
#+ ret=$?
#+ b_resetErrorHandler
#+```
#+[error out]: see [b_defaultErrorHandler](#b_defaultErrorHandler) (default: 0)
#+returns: nothing, always sets a zero exit code
#+@StateChanging
function b_setBE {
local errOut=${1:-0}
local re='^([^ ]+)[ ]+[0-9]+(.*)$'
if [[ "${BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]}" =~ $re ]] ; then
b_setErrorHandler "${BASH_REMATCH[1]} ${errOut}${BASH_REMATCH[2]}"
else
#special case: no arguments @error handler
b_setErrorHandler "${BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]} $errOut"
fi
return 0
}
#+b_setErrorHandler [handler]
#+Set the error handler for all future exections of [B_E](#B_E) in the current scope.
#+
#+You can do this in e.g. subshells to limit the effect.
#+
#+blib modules should only use this function if absolutely necessary to temporarily modify the error behaviour whilst making sure that [b_resetErrorHandler](#b_resetErrorHandler) is called in the end. Otherwise it will prevent library users from setting the general behaviour in their scripts.
#+
#+Usually you do not want to write an entirely new handler, but modify the [b_defaultErrorHandler](#b_defaultErrorHandler) parameters with this setter or use [b_setBE](#b_setBE) for that.
#+[handler]: Function to handle errors. See [b_defaultErrorHandler](#b_defaultErrorHandler) for details.
#+returns: nothing
#+@StateChanging
function b_setErrorHandler {
BLIB_ERR_HANDLER_LAST+=( "${BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]}" )
BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]="$1"
}
#+b_resetErrorHandler [reset B_ERR]
#+Set the error handler to whatever it was before the last call to [b_setErrorHandler](#b_setErrorHandler) or [b_setBE](#b_setBE).
#+[reset B_ERR]: Whether or not to also reset [B_ERR](#B_ERR) (default: true/0).
#+returns: nothing, always sets a zero exit code
#+@StateChanging
function b_resetErrorHandler {
local resetBerr="${1:-0}"
local lastInd=$(( ${#BLIB_ERR_HANDLER_LAST[@]} -1 ))
local last="b_defaultErrorHandler"
if [ $lastInd -ge 0 ] ; then
last="${BLIB_ERR_HANDLER_LAST[$lastInd]}"
unset 'BLIB_ERR_HANDLER_LAST[$lastInd]'
fi
BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]="$last"
[ $resetBerr -eq 0 ] && B_ERR=""
return 0
}
#+b_getErrorHandler
#+Get the currently for [B_E](#B_E) configured error handler.
#+returns: the error handler function
function b_getErrorHandler {
echo "${BLIB_STORE_VOLATILE["BLIB_ERR_HANDLER"]}"
}
#+b_silence [function] [param 1] .. [param p]
#+Call the given function with its parameters in the current shell context whilst suppressing all of its output to both stdout and stderr. Anything written to [B_ERR](#B_ERR) however is passed to [B_E](#B_E) (which can still write to stderr).
#+
#+This function is useful when you want to keep an error message set with [B_ERR](#B_ERR), but discard everything else.
#+In contrast `yourfunction &> /dev/null`{.bash} may also drop the error message, if you're using an error handler (see [B_E](#B_E)) that writes to stdout or stderr.
#+[function]: The function to execute.
#+[param p]: An arbitrary number of function parameters.
#+returns: Sets the status code of the called function, but doesn't print anything. [B_E](#B_E) is called on errors.
#+@B_E
function b_silence {
#we use b_setBE 1 to retrieve the B_ERR message if there's any
local ret=
local func="$1"
shift
b_setBE 1
b_execFuncInCurrentContext "$func" "-" "$@" &> /dev/null
ret=$?
b_resetErrorHandler 1
B_E
return $ret
}
#+b_readLine [variable] [read arg 1] .. [read arg r]
#+Read a line from stdin to the given variable. It should generally be preferred over bash's `read` function as it should be much more readable and robust.
#+
#+Contrary to the definition of bash's `read` function, lines may end with newlines _or_ EOF (`read` only accepts newlines).
#+
#+Note that bash's `read` and this function are _relatively_ slow, but use little memory. Sometimes it may make sense to use the faster `mapfile -t` instead, which however reads the entire data into memory.
#+[variable]: Name of the variable to read the line to (default: [B_LINE](#B_LINE)). It's content is only valid on a zero exit code. Otherwise it will be in an undefined state - in particular previous content may not be available anymore.
#+[read arg r]: An arbitrary number of arguments to pass to the `read` function, which is used internally (default: `-r`).
#+returns: Sets a zero exit code, if another line could be read. Otherwise a non-zero exit code is set.
function b_readLine {
local ret="${1:-B_LINE}"
shift
if [ $# -le 0 ] ; then
IFS= read -r "${ret?}" && return 0
else
IFS= read "$@" "${ret?}" && return 0
fi
#bash's read returns a non-zero exit code for the last line, if it doesn't end with a newline
[ -n "${!ret}" ]
}
#+b_assertLastPipes [error message]
#+Assert that the last pipe statements all had a zero exit code.
#+An alternative to `set -o pipefail`.
#+[error message]: Optional error message to use for [B_E](#B_E) in case the assertion fails.
#+returns: Nothing. Assertion failures trigger [B_E](#B_E).
#+@B_E
function b_assertLastPipes {
local pipes=("${PIPESTATUS[@]}")
local err="$1"
local i=
for ((i=0; i < ${#pipes[@]}; i++)) ; do
[ ${pipes[$i]} -ne 0 ] && B_ERR="${err:-"One of the previous pipe operations failed (index: $i)."}" && B_E
done
return 0
}
#+b_defaultMessageHandler [message type] [message] [first part] [last part]
#+Handles the given message by printing information to stdout and errors to stderr.
#+This is the default message handler used by blib. It can be changed by [b_setMessageHandler](#b_setMessageHandler).
#+Message handler implementations must support at least the parameters of this function, but may add additional parameters. If an handler implementation does not support partial messages (i.e. the [first part] and [last part] parameters), it should be chained to a [b_cachingMessageHandler](#b_cachingMessageHandler).
#+[message type]: 0=informational message, 1=error message.
#+[message]: String representing the message.
#+[first part]: If set to 0, assume that the given message is the first part of an overall chain of messages (default: 0).
#+[last part]: If set to 0, assume that the given message is the final part of an overall chain of messages (default: 0).
#+returns: Nothing. Never causes errors.
function b_defaultMessageHandler {
local mtype="$1"
local msg="$2"
local first="${3:-0}"
local last="${4:-0}"
local prefix=""
local suffix=""
if [ $first -eq 0 ] ; then
if [ -n "$msg" ] ; then
prefix="${BLIB_DEFAULT_MSG_HANDLER_PREFIXES["$mtype"]}"
fi
else
prefix="${BLIB_STORE["BLIB_DEFAULT_MSG_HANDLER_INTERMEDIATE"]}"
fi
if [ $last -eq 0 ] ; then
suffix=$'\n'
[ -z "$msg" ] && prefix="" #special case: no message, just finalize any previous message parts
fi
if [ $mtype -eq 0 ] ; then
printf '%s%s%s' "$prefix" "$msg" "$suffix"
else
printf '%s%s%s' "$prefix" "$msg" "$suffix" >&2
fi
return 0
}
#+b_cachingMessageHandler [message handler] [message type] [message] [first part] [last part]
#+A message handler implementation which caches partial messages for a receiving message handler that cannot handle them until they are completed.
#+Use `b_initCachingMessageHandler && b_setMessageHandler "b_cachingMessageHandler [your message handler]"`{.bash} to set your receiving message handler as message handler to cache messages for.
#+This implementation uses the [b_getDefaultMessageHandlerIntermediate](#b_getDefaultMessageHandlerIntermediate) as separator between partial messages.
#+[message handler]: The function implementing the receiving message handler. It must handle the [message type] and the [message].
#+[message type]: See [b_defaultMessageHandler](#b_defaultMessageHandler).
#+[message]: See [b_defaultMessageHandler](#b_defaultMessageHandler).
#+[first part]: See [b_defaultMessageHandler](#b_defaultMessageHandler).
#+[last part]: See [b_defaultMessageHandler](#b_defaultMessageHandler).
#+returns: Nothing. Never causes errors.
function b_cachingMessageHandler {
local mhandler="$1"
local mtype="$2"
local msg="$3"
local first="${4:-0}"
local last="${5:-0}"
#NOTE: the below is _not_ identical to shift 5 since a single shift below may fail, but shift 5 only works for exactly 5 parameters in many bash versions
shift
shift
shift
shift
shift
#get the cache
local cfile="${BLIB_CACHING_MSG_HANDLER_CACHES["$mtype"]}"
#special case: the user forgot to call b_initCachingMessageHandler
if [ -z "$cfile" ] ; then
b_defaultMessageHandler 1 "Implementation error: b_cachingMessageHandler $mhandler was called without calling b_initCachingMessageHandler at least once. Dropping the following message:"$'\n'"$msg"
return 0
fi
#handle the message
if [ $last -eq 0 ] ; then
if [ $first -eq 0 ] ; then
#no need for caching
"$mhandler" "$mtype" "$msg" "$@"
else
#read & clear cache (atomically)
#NOTE: we want to perform this operation _atomically_ in order to not lose messages coming from threads running concurrently --> we use mv as it uses the rename syscall which is atomic on most unix systems
local cfileTmp=
cfileTmp="$(mktemp -u)" || { b_defaultMessageHandler 1 "Failed to call mktemp -u." ; return 0 ; }
mv -f "$cfile" "$cfileTmp" &> /dev/null || { b_defaultMessageHandler 1 "Failed to move $cfile to $cfileTmp." ; return 0 ; }
local cmsg=
cmsg="$(<"$cfileTmp")" || { b_defaultMessageHandler 1 "Failed to read $cfileTmp." ; return 0 ; }
rm -f "$cfileTmp" &> /dev/null || { b_defaultMessageHandler 1 "Failed to remove $cfileTmp." ; return 0 ; }
#generate the final message
local inter=""
[ -n "$msg" ] && inter="${BLIB_STORE["BLIB_DEFAULT_MSG_HANDLER_INTERMEDIATE"]}"
printf -v msg "%s%s%s" "$cmsg" "$inter" "$msg"
#pass the final message to the chained handler
"$mhandler" "$mtype" "$msg" "$@"
fi
else
#first is 0 or 1 and last is 1 --> write to cache (non-atomically)
if [ $first -eq 0 ] ; then
echo -n "$msg" > "$cfile" || { b_defaultMessageHandler 1 "Failed to write to the cache file $cfile." ; return 0 ; }
else
echo -n "${BLIB_STORE["BLIB_DEFAULT_MSG_HANDLER_INTERMEDIATE"]}$msg" >> "$cfile" || { b_defaultMessageHandler 1 "Failed to write to the cache file $cfile." ; return 0 ; }
fi
fi
return 0
}
#+b_initCachingMessageHandler [maximum message type]
#+Initializes a new cache for a [b_cachingMessageHandler](#b_cachingMessageHandler). This function *must* be called at least once before using [b_cachingMessageHandler](#b_cachingMessageHandler).
#+[maximum message type]: Maximum message type for which to allocate caches (default: 1 --> allocate caches for 0..1/info..error).
#+returns: Nothing.
#+@B_E
function b_initCachingMessageHandler {
local max="${1:-1}"
b_import "traps" || B_E
local i=
local cfile=
local cmd=
for ((i=0; i <= max; i++)) ; do
#use a temporary file as cache
#NOTE: this is the closest we can get to stdout/stderr as it is inherited/shared to subthreads similarly to stdout/stderr
cfile="$(mktemp)" || { B_ERR="Failed to allocate a temporary file." ; B_E ; }
BLIB_CACHING_MSG_HANDLER_CACHES["$i"]="$cfile"
if [ $B_TEST_MODE -ne 0 ] ; then
#schedule cleanup
#NOTE: bats uses the EXIT signal itself, so that we cannot do this during testing
printf -v cmd 'rm -f %q &> /dev/null' "$cfile"
b_traps_add "$cmd" "EXIT" || B_E
fi
done
}
#+b_setDefaultMessageHandlerIntermediate [intermediate]
#+[intermediate]: String to use as intermediate between two message parts with [b_defaultMessageHandler](#b_defaultMessageHandler), i.e. the resulting message should be [part 1][intermediate][part 2]. Default: Space.
#+returns: Nothing.
function b_setDefaultMessageHandlerIntermediate {
BLIB_STORE["BLIB_DEFAULT_MSG_HANDLER_INTERMEDIATE"]="$1"
}
#+b_getDefaultMessageHandlerIntermediate
#+Get the currently configured intermediate string for the [b_defaultMessageHandler](#b_defaultMessageHandler).
#+returns: The intermediate String.
function b_getDefaultMessageHandlerIntermediate {
echo "${BLIB_STORE["BLIB_DEFAULT_MSG_HANDLER_INTERMEDIATE"]}"
}
#+b_setDefaultMessageHandlerPrefix [message type] [prefix]
#+Set the prefix to use for the default message handler and the given message type.
#+[message type]: 0=informational message, 1=error message.
#+[prefix]: A string to prefix all messages of the given type.
#+returns: Nothing.
function b_setDefaultMessageHandlerPrefix {
BLIB_DEFAULT_MSG_HANDLER_PREFIXES["$1"]="$2"
}
#+b_getDefaultMessageHandlerPrefix [message type]
#+Get the prefix used for the default message handler and the given message type.
#+[message type]: 0=informational message, 1=error message.
#+returns: The currently configured prefix.
function b_getDefaultMessageHandlerPrefix {
echo "${BLIB_DEFAULT_MSG_HANDLER_PREFIXES["$1"]}"
}
#+b_info [message] [first part] [last part] [message handler param 1] ... [message handler param n]
#+Send an informational message to the user. The message is dispatched via the currently configured message handler (default: [b_defaultMessageHandler](#b_defaultMessageHandler)).
#+[message]: to send
#+[first part]: If set to 0, assume that the given message is the first part of an overall chain of messages (default: 0).
#+[last part]: If set to 0, assume that the given message is the final part of an overall chain of messages (default: 0).
#+[message handler param i]: Arbitrary parameters to pass to the currently configured message handler (cf. [b_setMessageHandler](#b_setMessageHandler)).
#+returns: Nothing, always sets a zero exit code.
function b_info {
${BLIB_STORE_VOLATILE["BLIB_MSG_HANDLER"]} 0 "$@"
return 0
}
#+b_error [message] [first part] [last part] [message handler param 1] ... [message handler param n]
#+Send an error message to the user _without_ erroring out. The message is dispatched via the currently configured message handler (default: [b_defaultMessageHandler](#b_defaultMessageHandler)).
#+99.9% of all users will want to use the combination of [B_ERR](#B_ERR) and [B_E](#B_E) for proper error handling instead.
#+[message]: to send
#+[first part]: If set to 0, assume that the given message is the first part of an overall chain of messages (default: 0).
#+[last part]: If set to 0, assume that the given message is the final part of an overall chain of messages (default: 0).
#+[message handler param i]: Arbitrary parameters to pass to the currently configured message handler (cf. [b_setMessageHandler](#b_setMessageHandler)).
#+returns: Nothing, always sets a zero exit code.
function b_error {
${BLIB_STORE_VOLATILE["BLIB_MSG_HANDLER"]} 1 "$@"
return 0
}
#+b_setMessageHandler [handler]
#+Set the handler used to send messages to the user. By default, [b_defaultMessageHandler](#b_defaultMessageHandler) is used.
#+[handler]: Function to handle the messages. See [b_defaultMessageHandler](#b_defaultMessageHandler) for the requirements.
#+returns: Nothing.
function b_setMessageHandler {
BLIB_STORE_VOLATILE["BLIB_MSG_HANDLER"]="$1"
}
#+b_getMessageHandler
#+Get the currently configured message handler.
#+returns: The message handler.
function b_getMessageHandler {
echo "${BLIB_STORE_VOLATILE["BLIB_MSG_HANDLER"]}"
}
#+b_enforceUser [user name]
#+enforce that the user is the given one and if not, exit the script and set a non-zero status code
#+[user name]: user name to check against
#+returns: nothing
#+@B_E
function b_enforceUser {
local user="$1"
[ "$(whoami)" != "$user" ] && B_ERR="This script must be run as ${user}." && B_E
return 0
}
#+b_isFunction [potential function name]
#+check whether the given function is defined
#+returns: zero exit code if the function is defined
function b_isFunction {
declare -Ff "$1" > /dev/null
#the exit code is returned
}
#blib_getBlibModules [path] [prefix]
#[path]: path to the module directory
#[prefix]: optional prefix to remove (default = path) from the output
#recurse the given path for blib modules
#returns: all available blib module names as newline-separated list
function blib_getBlibModules {
local path="$1"
local prefix="${2:-$path}"
[[ "$prefix" != *"/" ]] && prefix="${prefix}/"
local ret=""
for file in "$path"/* ; do
local fname="${file#"$prefix"}"
if [ -f "$file" ] ; then
[ -n "$ret" ] && ret="$ret"$'\n'
ret="$ret$fname"
elif [ -d "$file" ] ; then
[ -n "$ret" ] && ret="$ret"$'\n'
ret="$ret$( blib_getBlibModules "$file" "$prefix" )"
else
continue
fi
done
echo "$ret"
}
#+b_getBlibModules
#+get all available blib module names as a newline-separated list
#+returns: all available blib module names as newline-separated list
function b_getBlibModules {
#only compute if not in cache
if [ -z "${BLIB_STORE_VOLATILE["BLIB_MODULE_LIST_CACHE"]}" ] ; then
local ret="${BLIB_STORE["BLIB_SCRIPT_NAME"]}"
ret="$ret"$'\n'"$(blib_getBlibModules "${BLIB_STORE["BLIB_LIB_DIR"]}")"
BLIB_STORE_VOLATILE["BLIB_MODULE_LIST_CACHE"]="$(echo "$ret" | sort)"
fi
echo "${BLIB_STORE_VOLATILE["BLIB_MODULE_LIST_CACHE"]}"
}
#blib_getModulePath [module]
#get the module path
#[module]: name of the module
#returns: path to the module; sets a non-zero exit code on errors (incl. when the module path does not exist)
#@B_E
function blib_getModulePath {
local module="$1"
local retDir=""
#check for special case where the module is blib itself
if [[ "$module" == "${BLIB_STORE["BLIB_SCRIPT_NAME"]}" ]] ; then
retDir="${BLIB_STORE["BLIB_SCRIPT_DIR"]}"
else
retDir="${BLIB_STORE["BLIB_LIB_DIR"]}"
fi
local ret="$retDir/$module"
[ ! -f "$ret" ] && B_ERR="The module $module cannot be found at $ret or is inaccessible." && B_E
echo "$ret"
}
#+b_listContains [list] [entry]
#+check whether the given list contains the given entry
#+[list]: newline-separated list
#+[entry]: string to be found on a single line within the list (equality check)
#+returns: a zero exit code if the list contains the entry; a non-zero exit code otherwise
function b_listContains {
local list="$1"
local entry="$2"
while b_readLine ; do
[[ "$B_LINE" == "$entry" ]] && return 0
done <<< "$list"
return 1
}
#+b_deps [dependency 1] ... [dependency n]
#+Assert that the given dependencies are met and error out with [B_E](#B_E) otherwise.
#+This function is meant to be used by modules or scripts to declare all of their dependencies.
#+[dependency i]: Command that is absolutely required to run this script.
#+returns: Nothing. Errors out with [B_E](#B_E), if dependencies are not met.
#+@B_E
function b_deps {
local dep=
local unmet=""
#maybe TODO: support more dependency types
for dep in "$@" ; do
[ -z "$dep" ] && continue
command -v "$dep" &> /dev/null || unmet="$dep"$'\n'"$unmet"
done
[ -z "$unmet" ] || { B_ERR="The script requires the following commands, but they were not found:"$'\n'"$unmet" ; B_E ; }
}
#+b_import [module] [double import]
#+Import the given module into the current context.
#+[module]: relative path of the module to import (relative to the blib/lib root directory)
#+[double import]: if set to 1, import the given module regardless of whether it was imported before (default: 0 = don't do duplicate imports)
#+returns: nothing, errors out if the import failed and sets a non-zero status code; if the import was successful or previously done, a zero exit code is set
#+@StateChanging
#+@B_E
function b_import {
local module="$1"
local doubleImport="${2:-0}"
local modulePath=""
#checks
[ $doubleImport -ne 1 ] && b_listContains "${BLIB_STORE["BLIB_IMPORTED_MODULES"]}" "$module" && return 0
[[ "$module" == "${BLIB_STORE["BLIB_SCRIPT_NAME"]}" ]] && B_ERR="Cannot import $module itself." && B_E
#source (in standalone mode everything is in this file here, no need to source)
if [ "${BLIB_STORE["BLIB_STANDALONE_MODE"]}" -ne 0 ] ; then
modulePath="$(blib_getModulePath "$module")" || { B_ERR="Failed to obtain the path for the module $module."; B_E; }
source "$modulePath" || { B_ERR="Failed to import the module $module from ${modulePath}."; B_E; }
fi
#add to list of imported modules
BLIB_STORE["BLIB_IMPORTED_MODULES"]="${BLIB_STORE["BLIB_IMPORTED_MODULES"]}"$'\n'"$module"
#set a reasonable exit code
return 0
}
function blib_usage {
echo "Usage: blib [command] [command parameters]
blib is a collection of bash functions for easier creation of other scripts. In general blib is meant to be included and not executed directly.
This binary however provides some information to library users.
[command] may be one of:
list list all available modules
info [module] print the description of the given module including all functions and global variables made available by the module
gendoc [-t] [format] generate the blib code documentation in the given format at ${BLIB_STORE["BLIB_DOC_DIR"]}
-t : include the test code documentation
[format]: one of raw, md, pdf, html, man (default: raw)
test [module] run unit tests for the given module; if no module is specified, run all available unit tests
help print this help
version print the blib version"
exit 1
}
#+b_generateStandalone [function] [module dep 1] .. [module dep n] - [function dep 1] .. [function dep d] - [function param 1] .. [function param p]
#+Create a standalone variant of blib in a single file running the given function when called (sourcing that file will only make the functions available) and print that file to stdout.
#+
#+The current execution state is not retained.
#+[function]: The function to call when the generated script is executed. All script parameters when calling \[output file\] are passed to this function. The function must be available in the current context.
#+[module dep i]: Names of the modules to include in the standalone file. They do not need to be imported. They are loaded in the specified order.
#+[-]: Dash used as separator between the various types of arguments. If none is provided, all parameters are assumed to be modules.
#+[function dep j]: An arbitrary number of functions that need to be added in order to satisfy the dependencies of the function to call (e.g. if function A is meant to be called, but uses function B internally, you'll have to pass B as one of its dependencies). Dependencies that can be found in added modules should *not* be added.
#+[function param p]: Static parameters to add to the function as single String. Dynamic parameters should be passed to the generated script.
#+returns: Sets a zero exit code and prints the output file to stdout on success. May error out otherwise.
#+@B_E
function b_generateStandalone {
local func="$1"
local dep=""
local mod=""
local modulePath=""
local par=""
shift
#parse module dependencies
declare -a mdeps=()
for par in "$@" ; do
[[ "$par" == "-" ]] && shift && break
mdeps+=("$par")
shift
done
#parse function dependencies
declare -a fdeps=()
for par in "$@" ; do
[[ "$par" == "-" ]] && shift && break
fdeps+=("$par")
shift
done
#parse params (printf '%q' escapes) & generate the function call
local funcCall="$func"
for par in "$@" ; do
[[ "$par" == "-" ]] && shift && break
printf -v par '%q' "$par"
funcCall="$funcCall $par"
shift
done
#parse blib, add the function call instead of blib_main or whatever else there is (we might be calling b_generateStandalone from standalone mode)
local mainCnt=0
local inMain=1
local main=""
local line=
while b_readLine line ; do
if [ $inMain -eq 0 ] ; then
if [[ "$line" == "#BLIB_MAIN_END" ]] ; then
main="$main"$'\n'"$line"
inMain=1
elif [[ "$line" == *'"$@"' ]] ; then
main="$main"$'\n'" $funcCall"' "$@"'
else
main="$main"$'\n'"$line"
fi
elif [[ "$line" == "#BLIB_MAIN_BEGIN" ]] ; then
main="$main"$'\n'"$line"
inMain=0
mainCnt=$(( $mainCnt +1 ))
else
echo "$line"
fi
done < "${BASH_SOURCE[0]}"
[ $mainCnt -ne 1 ] && B_ERR="Failed to find the blib_main call." && B_E
[ $inMain -ne 1 ] && B_ERR="Failed to find the MAIN_END marker." && B_E
#set standalone mode (must be as early as possible as e.g. static imports may happen with the modules below)
echo ""
echo 'BLIB_STORE["BLIB_STANDALONE_MODE"]="0"'
#add modules
for mod in "${mdeps[@]}" ; do
if [ ${BLIB_STORE["BLIB_STANDALONE_MODE"]} -ne 0 ] ; then
#we only add module content in normal mode (in standalone they either were added before or won't be found)
modulePath="$(blib_getModulePath "$mod")" || { B_ERR="Failed to obtain the path for the module $mod."; B_E; }
echo ""
cat "$modulePath" || { B_ERR="Failed to read $modulePath."; B_E; }
fi
done
#add function dependencies
for dep in "${fdeps[@]}" ; do
echo ""
declare -f "$dep" || { B_ERR="Failed to find the declaration for the function $dep."; B_E; }
done
#add function
echo ""
declare -f "$func" || { B_ERR="Failed to find the declaration for the function $func."; B_E; }
#add call to func
echo ""
echo "$main"
return 0
}
#+b_execFuncInCurrentContext [function] [module dep 1] .. [module dep n] - [function param 1] .. [function param p]
#+Execute the given function in the current context.
#+[function]: The function to execute.
#+[module dep i]: Names of the modules required by the function. They do not need to be imported by the function itself.
#+[-]: A dash as separator character between the various parameters.
#+[function param p]: An arbitrary number of function parameters.
#+returns: Whatever the executed function returns.
function b_execFuncInCurrentContext {
local func="$1"
shift
local par=""
local mod=""
for mod in "$@" ; do
[[ "$mod" == "-" ]] && shift && break
b_import "$mod" || B_E
shift
done
#parse params (printf '%q' escapes) & generate the function call
local funcCall="$func"
for par in "$@" ; do
printf -v par '%q' "$par"
funcCall="$funcCall $par"
shift
done
eval "$funcCall"