-
Notifications
You must be signed in to change notification settings - Fork 0
/
bash++
754 lines (625 loc) · 19.5 KB
/
bash++
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
############################################################
# Bash resources you can 'source' from your Bash script.
#
# John Robertson <[email protected]>
# Initial release: Thu Sep 10 11:58:23 EDT 2020
#
# Fri Sep 11 22:13:16 EDT 2020
# Added malloc, new, delete, call, and fetch
#
# You can check this to see if bash++ has been sourced
BASH_PLUS_PLUS=true
# Global array to use as a return stack
declare -a __RTN_STK
function RTN_empty
############################################################
# Return logical TRUE if stack is empty, FALSE otherwise
# Arguments:
# none
#
{
local rtn=0
[[ -n "${__RTN_STK:+foo}" ]] && rtn=1
return $rtn
}
function RTN_push
############################################################
# Use this to push things onto the global return stack
# Arguments:
# Whatever strings you wish placed on the stack
#
{
local __opts=$(shopt -o -p nounset errexit)
set -ue
# For readability, store array index value here before using
local -i ndx
ndx=${__RTN_STK:+${#__RTN_STK[@]}}
# Push arguments onto the stack
while [[ -n "${1:+foo}" ]]; do
# Place argument onto stack
__RTN_STK[$ndx]="$1"
((++ndx))
# Discard argument
shift
done
# Restore options
eval $__opts
}
function BackTrace
############################################################
# Generate a GDB(ish) style stack dump
{
local i j
1>&2 echo "----------- Stack Dump ------------"
for ((i= 1; i < ${#BASH_SOURCE[@]}; ++i)); do
((j= i-1)) || true
1>&2 echo -e "\ti=$i, j=$j ${BASH_SOURCE[$i]}:${BASH_LINENO[$j]} ${FUNCNAME[$i]} ()"
done
}
function RTN_pop
############################################################
# Use this to pop things off of the return stack
# Arguments:
# Names of global varibles to be loaded by popping
# strings from the stack.
#
{
# Necessary to avoid "unbound variable" exception
local __opts=$(shopt -o -p nounset)
set -u
local -i arg ndx sz
# Get current size of stack
sz=${__RTN_STK:+${#__RTN_STK[@]}}
# Generate correct array index
(( ndx = sz - 1 )) || true
# Walk through supplied references backwards
for (( arg= $#; arg ; --arg )); do
if ((ndx < 0)); then
local line file
# Print out a stack dump
BackTrace
read line file < <(caller)
die "ERROR: ndx= '$ndx' cannot have a negative value; called from $file:$line"
fi
# Copy item on stack to *reference
eval ${!arg}="\${__RTN_STK[\$ndx]}"
# pop item from stack, free memory
unset __RTN_STK[$ndx]
((--ndx)) || true
done
# Restore options
eval $__opts
}
# Global heap counter integer
declare -i __HC=0
function malloc
############################################################
# Allocate a global "address" from the "heap"
# Arguments:
# 0 or more flags which will be passed to the 'declare' builtin.
# e.g. '-A' for associative array
# Returns:
# "address" of allocated memory
#
{
# Increment global heap counter
(( ++__HC ))
# compose unique "address" for this allocation
local addr="__heap_${__HC}"
# "allocate" object gets marked for global visibility,
# and possibly other 'declare' flags
declare -g $@ $addr
# push "address" onto result stack
RTN_push $addr
}
function new ()
############################################################
# 'new' operator to allocate object & call class constructor
# Arguments:
# classname
# constructor_arg1 (optional)
# ...
# Returns:
# "address" of new object
#
{
# Stash the class name with the object
local __class=$1
# Discard class name from argv
shift
# Allocate an associative array from our "heap"
malloc -A
# Retrieve "address" of new object
RTN_pop __R1
# Imprint class name on the object
eval $__R1[__class]=$__class
# Call class constructor
eval $__class::$__class $__R1 \$@
# Push address on return stack
RTN_push $__R1
}
function delete ()
############################################################
# 'delete' operator to call class destructor, free object
# Arguments:
# object_address
# Returns:
# nothing
#
{
# address of object
local this=$1
# class name of object
eval local __class=\${$this[__class]}
# Call class destructor
eval $__class::~$__class $this
# Free object memory
unset $this
}
function show ()
############################################################
# Print object contents
# Arguments:
# object1_address
# object2_address (optional)
# ...
# Returns:
# Nothing
#
{
declare -p $@
}
function fetch ()
############################################################
# Convenient way to access class member values
# Arguments:
# address.member
# Returns:
# value of member on the return stack
#
{
# address of object
local this=${1%.?*} __member=${1#?*.}
eval RTN_push \"\${$this[$__member]}\"
}
function call ()
############################################################
# Call an object member function
# Arguments:
# address.funcName
# func_arg1 (optional)
# ...
# Returns:
# Whatever funcName() returns
#
{
# address of object
local this=${1%.?*} __func=${1#?*.}
# class name of object
eval local __class=\${$this[__class]}
shift
# Call class member function
eval $__class::$__func $this \$@
}
function regex_read ()
############################################################
# Similar to bash 'read' builtin, but parses subsequent
# read buffer using the supplied regular expression.
# Arguments:
# regex pattern
# Returns:
# logical TRUE if read was successful
# or logical FALSE on end-of-file condition.
# Return stack:
# Full string match (if any)
# token1_match (if any)
# ...
# Last argument is _always_ number of matches found. Pop it first.
#
{
# Stash the regular expression
local ndx count regex="$1"
# All other args are for 'read'
shift
# Call read with other supplied args. Fails on EOF
IFS= read $@ || return 1
# Apply regular expression parsing to read buffer
if [[ $REPLY =~ $regex ]]; then
# Place results on return stack
count=${#BASH_REMATCH[@]}
for (( ndx= 0; ndx < count; ++ndx )); do
RTN_push "${BASH_REMATCH[$ndx]}"
done
# Last stack arg is number of match results
RTN_push $count
else
# regex failed to match
RTN_push 0
fi
}
function die ()
############################################################
# Same as 'die' in perl - Print supplied args to stderr
# prefixed with source file name and line number,
# then exit indicating error.
#
{
local line file
read line file < <(caller)
1>&2 echo "$file:$line $@"
exit 1
}
function hkselect
################################################
# Approximate replacement for bash's builtin `select'.
# hkselect() does not require the user to press enter key
# to finalize choice.
# Usage: hkselect arg1 ...
#
# User choices are supplied as args to the function, and
# the hotkey may be indicated by placing an `&' in
# the preceding character, e.g.
# press&kplease
# where `k' is the hotkey. If a hotkey is not indicated,
# then one will be assigned automatically.
#
# Command flags:
# -b 'keystr|funcname|arg1 arg2 ...' Bind escape-based key (F1-F12, Arrows, ...) to a function
# -B global_arr_name Array of -b arguments
# -p left_pad Left margin padding count
# -r 'rebuke string' String to print when user pressed invalid key
# -s 'subtitle string' Subtitle to print below choices
# -w max_width Limit the width of choices layout
#
# Global variables:
# HKSELKEY - key the user pressed when making a successful choice
# REPLY - zero based index of the arg whose hotkey was pressed
#
# RETURN: upon return the value of HKSELKEY will contain the key which
# the user pressed, and REPLY will contain the 0-based index of the
# matching argument passed when hkselect() was called
#
# Sat Mar 27 16:52:53 EDT 2021
# John Robertson <[email protected]>
{
# Stash current shell options
local __opts=$(shopt -o -p nounset errexit)
set -ue +xv
# We cache composed labels and layout information
declare -gA _HKSEL_CACHE
local tmp arg argc dcarg i j len pos lr lcol uc dc warg wlbl wrow wcol argc
local TROWS TWIDTH BLD NRM UND HK subtitle maxwidth pad=2
local -A rv_hk_m hk_m bkfunc_m bkargs_m
local opt OPTIND bkey bfunc bfuncargs is_cached=0
# namerefs for cached information
local -n nr_nrows nr_ncols nr_hk_m nr_wcol_a nr_warg_a nr_lbl_a
TWIDTH=$(tput cols)
TROWS=$(tput lines)
UND=$(tput smul)
BLD=$(tput bold)
NRM=$(tput sgr0)
HK="$(tput setaf 4)$(tput setab 7)$BLD"
local rebuke="${BLD}Please choose one of the highlighted keys$NRM"
while getopts ':b:B:p:r:s:w:' opt; do
case $opt in
b) IFS='|' read bkey bkfunc bkargs <<<"$OPTARG"
# Add binding to the bound-key map
bkfunc_m[$bkey]=$bkfunc
bkargs_m[$bkey]="$bkargs"
;;
B)
local -n _hkselect_bkargs_a=$OPTARG
if [[ -n "${_hkselect_bkargs_a[@]:+foo}" ]]; then
for arg in "${_hkselect_bkargs_a[@]}"; do
IFS='|' read bkey bkfunc bkargs <<<"$arg" || true
# Add binding to the bound-key map
bkfunc_m[$bkey]=$bkfunc
bkargs_m[$bkey]="$bkargs"
done
fi
# unreference supplied array
unset -n _hkselect_bkargs_a
;;
p) pad=$OPTARG;;
r) rebuke="$OPTARG";;
s) subtitle="$OPTARG";;
w) TWIDTH=$OPTARG;;
\?) die "Invalid option: -$OPTARG";;
esac
done
shift $((OPTIND-1))
# Make sure these are bound, and empty
HKSELKEY=
REPLY=
# possibly clip TWIDTH
[[ -n "${maxwidth:+foo}" ]] && ((TWIDTH = TWIDTH > maxwidth ? maxwidth : TWIDTH))
### Determine all hotkey assignments
local arg_ndx argcat sum
local -a pri_char_a dcarg_a
local -A ccount_map
# Determine argc, identify supplied hotkey assignments
argc=${#@}
for ((i=0; i < argc; ++i)); do
# function name is at $0, so we start at $1
(( arg_ndx = i + 1 ))
# copy arg into convenient variable
eval arg=\${$arg_ndx}
argcat+="$arg"
# Stash a downcased version of arg for efficiency
dcarg=$(tr '[:upper:]' '[:lower:]' <<<"$arg")
dcarg_a[$i]="$dcarg"
# Look for an ampersand
tmp=${arg%%&*}
len=${#tmp}
# May not exist
[[ $len = ${#arg} ]] && continue
(( pos = len + 1 )) || true
# Get lowercase hotkey character
dc=${dcarg:pos:1}
# Map to arg index forward & backward
hk_m[$dc]=$i
rv_hk_m[$i]=$dc
done
#### Checksum to uniquely identify this content ###
sum=$(md5sum <<<"$TWIDTH${argcat:+$argcat}")
# Strip junk at end of sum
sum=${sum% -}
# Our cache key is caller information
local ckey=$(caller)
# check for a matching key in the cache
if [[ -n "${_HKSEL_CACHE[$ckey]:+foo}" ]]; then
# Key was found, so compare value against current checksum
if [[ ! ${_HKSEL_CACHE[$ckey]} = $sum ]]; then
# Cache is stale, discard it
unset nrows_${sum} ncols_${sum} warg_a_${sum} wcol_a_${sum} lbl_a_${sum} hk_m_${sum}
else
# Use previously cached results
is_cached=1
fi
else
# Map the checksum on ckey
_HKSEL_CACHE[$ckey]=$sum
fi
# Set our namerefs
nr_nrows=nrows_${sum}
nr_ncols=ncols_${sum}
nr_warg_a=warg_a_${sum}
nr_wcol_a=wcol_a_${sum}
nr_lbl_a=lbl_a_${sum}
nr_hk_m=hk_m_${sum}
if (( ! is_cached )); then
# Create new global variables with unique names
declare -g nrows_${sum} ncols_${sum}
declare -ga warg_a_${sum} wcol_a_${sum} lbl_a_${sum}
declare -gA hk_m_${sum}
# Copy in results obtained above
for dc in "${!hk_m[@]}"; do
nr_hk_m[$dc]=${hk_m[$dc]}
done
fi
# Assign hotkeys and perform layout only if not cached
if (( ! is_cached )); then
# Make character count map as needed
for ((i=0; i < argc; ++i)); do
# Already be mapped?
[[ -n ${rv_hk_m[$i]:+foo} ]] && continue
# Unpack downcased arg
dcarg="${dcarg_a[$i]}"
# Check for usable hotkey characters
for ((j=0; j < ${#dcarg}; ++j)); do
dc=${dcarg:$j:1}
# Skip disallowed charcters
[[ -n ${nr_hk_m[$dc]:+foo} || ! $dc =~ [[:alnum:]] ]] && continue
[[ -n ${ccount_map[$dc]:+foo} ]] || ccount_map[$dc]=0
(( ++ccount_map[$dc] ))
done
done
# Get ccount_map contents into pri_char_a, characters sorted on ascending count
read -d $'\0' -a pri_char_a < <(
cut -f1 < <(
sort -n -k2 < <(
for key in "${!ccount_map[@]}"; do
echo -e "$key\t${ccount_map[$key]}"
done
)
)
) || true
# Assign hotkeys to args
for ((i=0; i < argc; ++i)); do
# Skip existing assignments
[[ -n ${rv_hk_m[$i]:+foo} ]] && continue
# Look for prioritized hotkey
for (( j= 0; j < ${#pri_char_a[@]}; ++j )); do
dc=${pri_char_a[$j]}
dcarg="${dcarg_a[$i]}"
# Already mapped? character in string?
[[ -z ${nr_hk_m[$dc]:+foo} && $dcarg =~ $dc ]] || continue
# map hotkey foward and backward
nr_hk_m[$dc]=$i
rv_hk_m[$i]=$dc
break
done
done
# No longer needed
unset ccount_map pri_char_a
# FIXME: here we *assume* a hotkey was assigned to every arg
# Fix up labels, etc
for ((i=0; i < argc; ++i)); do
# Get a convenient copy of arg
(( arg_ndx = i + 1 ))
eval arg=\${$arg_ndx}
# Handle case of args with no hotkey supplied
if [[ ! $arg =~ \& ]]; then
# Catch common mistake
[[ -z "${rv_hk_m[$i]:+foo}" ]] &&\
die 'ERROR: command flag args must precede choice args here: '$(caller)
# Remember the length of the argument
len=${#arg}
nr_warg_a[$i]=$len
dc=${rv_hk_m[$i]}
dcarg="${dcarg_a[$i]}"
# Identify the position where the character belongs
tmp=${dcarg%%$dc*}
len=${#tmp}
(( pos = len + 1 ))
else # arg was supplied with hotkey pre-assigned
# Remember the length of the argument, adjusted for ampersand
(( len = ${#arg} - 1 ))
nr_warg_a[$i]=$len
dc=${rv_hk_m[$i]}
tmp=${arg%%&*}
len=${#tmp}
(( pos = len + 2 ))
fi
uc=$(tr '[:lower:]' '[:upper:]' <<<$dc)
nr_lbl_a[$i]="$UND${arg:0:$len}$HK$uc$NRM$UND${arg:$pos}$NRM"
done
# Iterate to find optimum layout
nr_nrows=1
nr_ncols=$argc
while true; do
wcol=0
# cycle through supplied arguments
for (( i=0; i < argc; ++i )); do
# On which logical column are we operating?
(( lcol = i % nr_ncols )) || true
# Reset row width at first column
(( wrow = lcol ? wrow : 0 )) || true
# compute width of current label
warg=${nr_warg_a[$i]}
(( wlbl = warg + pad ))
# Go with max of previous column width, or current lbl width
(( wcol = wlbl > wcol ? wlbl : wcol ))
# Accumulate the column width
(( wrow += wcol ))
if (( wrow > TWIDTH )); then
# Try adding a row
(( ++nr_nrows ))
break
fi
# Remember the column width
nr_wcol_a[$lcol]=$wcol
done
(( nr_nrows > TROWS )) && die "$nr_nrows is too many rows, and I can't scroll!"
[[ -z "${wrow:+foo}" ]] && die "No items from which to choose!"
# Success criterion
(( wrow <= TWIDTH )) && break
# adjust ncols to reflect addition of a new row
(( nr_ncols = argc / nr_nrows + (argc % nr_nrows ? 1 : 0) ))
done
fi # is_cached
# Present choices to user
for (( i=0; i < argc; ++i )); do
# On which logical row are we operating?
(( lr = i / nr_ncols )) || true
# On which logical column are we operating?
(( lcol = i % nr_ncols )) || true
# Move down to first line on new row, but not first time
(( i && !lcol )) && echo
# Reset row width at first column
if (( ! lcol )); then
wrow=0
pos=$pad
fi
# Put cursor at beginning of column on this line
(( fwd = pos - wrow ))
tput cuf $fwd
(( wrow += fwd ))
# print the label
echo -n "${nr_lbl_a[$i]}"
warg=${nr_warg_a[$i]}
(( wrow += warg ))
# Update horizontal position
wcol=${nr_wcol_a[$lcol]}
(( pos += wcol ))
done
# Save cursor position
echo && tput sc
[[ -n ${subtitle:+foo} ]] && echo -n "$subtitle"
# Hide cursor
tput civis
# varibles for the loop below
local escbuf= is_invalid=0
# User now makes a choice
while true; do
# read will return when a key is pressed
read -sN 1 HKSELKEY
# We may have a function bound to enter key
if [[ $'\n' = $HKSELKEY ]]; then
bkey=$'\n'
if [[ -n "${bkfunc_m[$bkey]:+foo}" ]]; then
# Get the callback function
bkfunc=${bkfunc_m[$bkey]}
bkargs=${bkargs_m[$bkey]}
# Fire the callback function
eval $bkfunc \$bkargs
# Check return code
(( $? )) || break
else
is_invalid=1
fi
fi
# Handling of bound keys
if (( !is_invalid && ${#escbuf} )); then
# Append key to escbuf
escbuf+=$HKSELKEY
# See if there is more to come
if ! read -st 0; then
if [[ -n "${bkfunc_m[$escbuf]:+foo}" ]]; then
# Get the callback function
bkfunc="${bkfunc_m[$escbuf]}"
bkargs="${bkargs_m[$escbuf]}"
# Clear HKSELKEY
HKSELKEY=
# Fire callback function
eval $bkfunc \$bkargs
# Check return code
(( $? )) || break
else
escbuf=
is_invalid=1
fi
else
# Need more characters
continue
fi
fi
# ESC could mean exit, or that a bound key was pressed
if [[ 0 = $is_invalid && $'\e' = $HKSELKEY ]]; then
# Check for more queued keystrokes
read -st 0 || break
# more queued keystrokes means we buffer this
escbuf=$HKSELKEY
continue
fi
# Check for possible unescaped hotkeys
if [[ 0 = $is_invalid && -n $HKSELKEY ]]; then
# Look for a case insensitive match in our hotkey map
dc=$(tr '[:upper:]' '[:lower:]' <<<$HKSELKEY)
if [[ -n ${nr_hk_m[$dc]:+foo} ]]; then
i=${nr_hk_m[$dc]}
(( REPLY = i )) || true
echo
break
else
is_invalid=1
fi
fi
# Possibly rebuke user
if (( is_invalid )); then
tput rc
echo -n "$rebuke"
sleep 0.5
tput rc
tput el
[[ -n ${subtitle:+foo} ]] && echo -n "$subtitle"
fi
is_invalid=0
done
# Restore the cursor
tput cvvis
# Discard references
unset -n nr_nrows nr_ncols nr_hk_m nr_wcol_a nr_warg_a nr_lbl_a
# Restore options
eval $__opts
}