1 # $MirOS: contrib/hosted/tg/assockit.ksh,v 1.4 2013/04/26 17:20:33 tg Exp $
3 # Copyright © 2011, 2013
4 # Thorsten “mirabilos” Glaser <tg@mirbsd.org>
6 # Thorsten Glaser <t.glaser@tarent.de>
8 # Provided that these terms and disclaimer and all copyright notices
9 # are retained or reproduced in an accompanying document, permission
10 # is granted to deal in this work without restriction, including un‐
11 # limited rights to use, publicly perform, distribute, sell, modify,
12 # merge, give away, or sublicence.
14 # This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
15 # the utmost extent permitted by applicable law, neither express nor
16 # implied; without malicious intent or gross negligence. In no event
17 # may a licensor, author or contributor be held liable for indirect,
18 # direct, other damage, loss, or other issues arising in any way out
19 # of dealing in the work, even if advised of the possibility of such
20 # damage or existence of a defect, except proven that it results out
21 # of said person’s immediate fault when using the work as intended.
23 # Associative, multi-dimensional arrays in Pure mksh™ (no exec!)
25 # An item in an assockit array has the following properties:
26 # – the base-identifier of the shell array it’s in
27 # – the index into the shell array it’s in
28 # – an entry called flags
29 # • data type: ASSO_{VAL,STR,INT,REAL,BOOL,NULL,AIDX,AASS}
30 # – an entry called key
31 # – an entry called value, unless NULL/AIDX/AASS
32 # Shell array paths are constructed like this:
33 # { 'foo': [ { 'baz': 123 } ] } named 'test' becomes:
35 # – Asso__f[16#AE0C1A48] = ASSO_AASS | ASSO_ISSET|ASSO_ALLOC
36 # – Asso__k[16#AE0C1A48] = 'test' (hash: AE0C1A48)
37 # ‣ Asso_AE0C1A48 = top-level
38 # – Asso_AE0C1A48_f[16#BF959A6E] = ASSO_AIDX | ASSO_ISSET|ASSO_ALLOC
39 # – Asso_AE0C1A48_k[16#BF959A6E] = 'foo' (hash: BF959A6E)
40 # ‣ Asso_AE0C1A48BF959A6E = next-level
41 # – Asso_AE0C1A48BF959A6E_f[0] = ASSO_AASS | ASSO_ISSET|ASSO_ALLOC
42 # – Asso_AE0C1A48BF959A6E_k[0] = 0
43 # ‣ Asso_AE0C1A48BF959A6E00000000 = last-level (below FOO)
44 # – FOO_f[16#57F1BA9A] = ASSO_INT | ASSO_ISSET|ASSO_ALLOC
45 # – FOO_k[16#57F1BA9A] = 'baz' (hash: 57F1BA9A)
46 # – FOO_v[16#57F1BA9A] = 123
47 # When assigning a value, by default, the type of the
48 # intermediates is set to ASSO_AASS unless it already
49 # is ASSO_AIDX; the type of the terminals is ASSO_VAL
50 # unless it’s ASSO_{STR,INT,REAL,BOOL,NULL} before.
53 asso_x=${KSH_VERSION#????MIRBSD KSH R}
55 if [[ $asso_x != +([0-9]) ]] || (( asso_x < 40 )); then
56 print -u2 'assockit.ksh: need at least mksh R40'
61 typeset -Uui16 -Z11 asso_h=0 asso_f=0 asso_k=0
68 typeset -Uui16 -Z11 -r ASSO_VAL=2#000 # type: any Korn Shell scalar
69 typeset -Uui16 -Z11 -r ASSO_STR=2#001 # type: string
70 typeset -Uui16 -Z11 -r ASSO_INT=2#010 # type: integral
71 typeset -Uui16 -Z11 -r ASSO_REAL=2#011 # type: JSON float (string)
72 typeset -Uui16 -Z11 -r ASSO_BOOL=2#100 # type: JSON "true" / "false"
73 typeset -Uui16 -Z11 -r ASSO_NULL=2#101 # type: JSON "null"
74 typeset -Uui16 -Z11 -r ASSO_AIDX=2#110 # type: indexed array
75 typeset -Uui16 -Z11 -r ASSO_AASS=2#111 # type: associative array
76 typeset -Uui16 -Z11 -r ASSO_MASK_ARR=2#110 # bitmask for array type
77 typeset -Uui16 -Z11 -r ASSO_MASK_TYPE=2#111 # bitmask for type
78 typeset -Uui16 -Z11 -r ASSO_ISSET=16#40000000 # element is set
79 typeset -Uui16 -Z11 -r ASSO_ALLOC=16#80000000 # ksh element is set
82 # – the code assumes ASSO_VAL=0 < all scalar types with value \
83 # < ASSO_NULL < all array types
88 # example: asso_setv 123 'test' 'foo' 0 'baz'
91 print -u2 'assockit.ksh: syntax: asso_setv value key [key ...]'
97 # look up the item, creating paths as needed
99 # if it’s an array, free that recursively
100 if (( (_f = asso_f) & ASSO_MASK_ARR )); then
102 (( _f &= ~ASSO_MASK_TYPE ))
104 # if it’s got a type, check for a match
105 if (( _i = (_f & ASSO_MASK_TYPE) )); then
106 asso__typeck $_i "$_v" || (( _f &= ~ASSO_MASK_TYPE ))
108 # set the new flags and value
109 asso__r_setfv $_f "$_v"
112 # get the flags of an item, or return 1 if not set
113 # result is in the global variable asso_f
114 function asso_isset {
115 if (( $# < 1 )); then
116 print -u2 'assockit.ksh: syntax: asso_isset key [key ...]'
123 # get the type of an item (return 1 if unset, 2 if error)
124 # example: x=$(asso_gett 'test' 'foo' 0 'baz') => $((ASSO_VAL))
126 asso_isset "$@" || return
127 print -n -- $((asso_f & ASSO_MASK_TYPE))
130 # get the value of an item (return 1 if unset, 2 if error)
131 # example: x=$(asso_getv 'test' 'foo' 0 'baz') => 123
133 asso_loadv "$@" || return
134 print -nr -- "$asso_x"
137 # get the value of an item, but result is in the global variable asso_x
138 function asso_loadv {
139 if (( $# < 1 )); then
140 print -u2 'assockit.ksh: syntax: asso_loadv key [key ...]'
144 asso__lookup 0 "$@" || return 1
145 if (( (asso_f & ASSO_MASK_TYPE) < ASSO_NULL )); then
146 nameref _Av=${asso_b}_v
147 asso_x=${_Av[asso_k]}
153 # get all set keys of an item of array type (return 1 if no array)
154 # result is in the global variable asso_y
155 function asso_loadk {
156 if (( $# < 1 )); then
157 print -u2 'assockit.ksh: syntax: asso_loadk key [key ...]'
161 asso__lookup 0 "$@" || return 1
162 (( asso_f & ASSO_MASK_ARR )) || return 1
163 nameref _keys=${asso_b}${asso_k#16#}_k
164 set -A asso_y -- "${_keys[@]}"
168 # example: asso_sets 'abc' 'test' 'foo' 0 'baz'
170 if (( $# < 2 )); then
171 print -u2 'assockit.ksh: syntax: asso_sets value key [key ...]'
175 asso__settv $ASSO_STR "$@"
178 # set an integral value
179 # example: asso_seti 123 'test' 'foo' 0 'baz'
181 if (( $# < 2 )); then
182 print -u2 'assockit.ksh: syntax: asso_seti value key [key ...]'
186 if ! asso__typeck $ASSO_INT "$1"; then
187 print -u2 "assockit.ksh: not an integer: '$1'"
190 asso__settv $ASSO_INT "$@"
193 # set a floating point (real) value
194 # example: asso_setr -123.45e+67 'test' 'foo' 0 'baz'
196 if (( $# < 2 )); then
197 print -u2 'assockit.ksh: syntax: asso_setr value key [key ...]'
201 if ! asso__typeck $ASSO_REAL "$1"; then
202 print -u2 "assockit.ksh: not a real: '$1'"
205 asso__settv $ASSO_REAL "$@"
208 # set a boolean value
209 # example: asso_setb t 'test' 'foo' 0 'baz'
211 if (( $# < 2 )); then
212 print -u2 'assockit.ksh: syntax: asso_setb value key [key ...]'
216 if ! asso__typeck $ASSO_BOOL "$1"; then
217 print -u2 "assockit.ksh: not a truth value: '$1'"
220 asso__settv $ASSO_BOOL "$@"
224 # example: asso_setnull 'test' 'foo' 0 'baz'
225 function asso_setnull {
226 if (( $# < 1 )); then
227 print -u2 'assockit.ksh: syntax: asso_setnull key [key ...]'
231 asso__settv $ASSO_NULL 0 "$@"
234 # set type and scalar value
235 # example: asso_settv $ASSO_INT 123 'test' 'foo' 0 'baz'
236 function asso_settv {
237 if (( $# < 3 )) || ! asso__intck "$1" || \
238 (( $1 != ($1 & ASSO_MASK_TYPE) )); then
239 print -u2 'assockit.ksh: syntax: asso_settv type value key...'
243 if ! asso__typeck $1 "$2"; then
244 print -u2 "assockit.ksh: wrong type scalar: '$2'"
251 # example: asso_unset 'test' 'foo' 0 'baz'
252 function asso_unset {
253 if (( $# < 1 )); then
254 print -u2 'assockit.ksh: syntax: asso_unset key [key ...]'
258 # look up the item, not creating paths
259 if asso__lookup 0 "$@"; then
260 # free the item recursively
266 # make an entry into an indexed array
267 # from scalar => data into [0]
268 # from associative array => data lost
269 function asso_setidx {
270 if (( $# < 1 )); then
271 print -u2 'assockit.ksh: syntax: asso_setidx key [key ...]'
278 if (( !((_f = asso_f) & ASSO_MASK_ARR) )); then
279 nameref _Av=${asso_b}_v
281 elif (( (_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
285 asso__r_setf $ASSO_AIDX
286 if (( !(_f & ASSO_MASK_ARR) )); then
287 asso__lookup 1 "$@" 0
288 asso__r_setfv $_f "$_v"
292 # make an entry into an associative array
293 # from scalar => data lost
294 # from indexed array => data converted
295 function asso_setasso {
296 if (( $# < 1 )); then
297 print -u2 'assockit.ksh: syntax: asso_setasso key [key ...]'
304 if (( !((_f = asso_f) & ASSO_MASK_ARR) )); then
306 asso__r_setf $ASSO_AASS
307 elif (( (_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
315 # set type and scalar value, unchecked
316 function asso__settv {
320 # look up the item, creating paths as needed
322 # if it’s an array, free that recursively
323 if (( (_f = asso_f) & ASSO_MASK_ARR )); then
326 (( _f = (_f & ~ASSO_MASK_TYPE) | _t ))
327 # set the new flags and value
328 asso__r_setfv $_f "$_v"
331 # check if this is a numeric (integral) value (0=ok 1=error)
332 function asso__intck {
335 [[ $_v = ?(+([0-9])'#')+([0-9a-zA-Z]) ]] || return 2
339 # map a boolean value (0=false 1=true 2=error)
340 function asso__boolmap {
343 if asso__intck "$_v"; then
348 ([Tt]?([Rr][Uu][Ee])|[Yy]?([Ee][Ss])|[Oo][NnKk])
350 ([Ff]?([Aa][Ll][Ss][Ee])|[Nn]?([Oo])|[Oo][Ff][Ff])
356 # check if the type matches the value (0=ok 1=error)
357 function asso__typeck {
358 if (( $# != 2 )); then
359 print -u2 'assockit.ksh: syntax: asso__typeck type value'
363 (( _t == ASSO_VAL || _t == ASSO_STR || _t == ASSO_NULL )) && return 0
364 if (( _t == ASSO_INT )); then
368 if (( _t == ASSO_BOOL )); then
373 (( _t & ASSO_MASK_ARR )) && return 1
375 [[ $_v = ?(-)@(0|[1-9]*([0-9]))?(.+([0-9]))?([Ee]?([+-])+([0-9])) ]]
378 # look up an item ($1=1: create paths as necessary)
379 function asso__lookup {
387 if (( _r || !(asso_f & ASSO_MASK_ARR) )); then
388 (( _r )) || asso__r_free 1
389 asso__r_setf $ASSO_AASS
390 elif (( (asso_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
391 asso__intck "$_k" || asso__r_idx2ass
394 asso__lookup_once "$_k"
395 if (( _r = $? )); then
396 # not found. not create?
405 # set flags for asso_b[asso_k] and update asso_f
406 function asso__r_setf {
407 nameref _Af=${asso_b}_f
409 asso_f=$(($1 | ASSO_ISSET | ASSO_ALLOC))
413 # set flags and value for asso_b[asso_k] and update asso_f
414 function asso__r_setfv {
415 nameref _Af=${asso_b}_f
416 nameref _Av=${asso_b}_v
419 asso_f=$(($1 | ASSO_ISSET | ASSO_ALLOC))
423 # set key for not yet existing asso_b[asso_k] and update asso_f
424 function asso__r_setk {
425 nameref _Af=${asso_b}_f
426 nameref _Ak=${asso_b}_k
429 asso_f=$((ASSO_ALLOC))
433 # in asso_b of type asso_f look up element $1
434 # set its asso_f and asso_k or return 1 when not found
435 function asso__lookup_once {
437 nameref _Af=${asso_b}_f
438 nameref _Ak=${asso_b}_k
440 if (( (asso_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
444 # asso_k=$(somehash "$_e")
446 asso_f=${_Af[asso_k]}
447 (( asso_f & ASSO_ALLOC )) || break
448 if (( !(asso_f & ASSO_ISSET) )); then
449 if (( !_seth )); then
457 [[ ${_Ak[asso_k]} = "$_e" ]] && break
462 asso_f=${_Af[asso_k]}
464 (( asso_f & ASSO_ISSET )) && return 0
467 # when allocating, use this one instead
473 # free the currently selected asso_b[asso_k] recursively
474 function asso__r_free {
476 nameref _Af=${asso_b}_f
478 asso_f=${_Af[asso_k]}
479 (( asso_f & ASSO_ALLOC )) || return
480 if (( asso_f & ASSO_ISSET )); then
481 if (( asso_f & ASSO_MASK_ARR )); then
482 local _ob=$asso_b _ok=$asso_k
483 asso_b=$asso_b${asso_k#16#}
484 nameref _s=${asso_b}_f
485 for asso_k in "${!_s[@]}"; do
488 eval unset ${asso_b}_f ${asso_b}_k ${asso_b}_v
489 asso_b=$_ob asso_k=$_ok
491 eval unset $asso_b'_v[asso_k]'
492 (( _keepkey )) || eval unset $asso_b'_k[asso_k]'
494 asso_f=$((ASSO_ALLOC))
498 # make indexed asso_b[asso_k] into associative array
499 function asso__r_idx2ass {
500 print -u2 'assockit.ksh: warning: asso__r_idx2ass not implemented'
501 print -u2 'assockit.ksh: warning: data will be lost'
503 asso__r_setf $ASSO_AASS