d0b1675c9d9eb4b2214eed8d3e67992f8520c79e
[shellsnippets/shellsnippets.git] / mksh / assockit.ksh
1 # $MirOS: contrib/hosted/tg/assockit.ksh,v 1.6 2014/11/02 18:05:19 tg Exp $
2 # -*- mode: sh -*-
3 #-
4 # Copyright © 2011, 2013
5 #       Thorsten “mirabilos” Glaser <tg@mirbsd.org>
6 # Copyright © 2013
7 #       Thorsten Glaser <t.glaser@tarent.de>
8 #
9 # Provided that these terms and disclaimer and all copyright notices
10 # are retained or reproduced in an accompanying document, permission
11 # is granted to deal in this work without restriction, including un‐
12 # limited rights to use, publicly perform, distribute, sell, modify,
13 # merge, give away, or sublicence.
14 #
15 # This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
16 # the utmost extent permitted by applicable law, neither express nor
17 # implied; without malicious intent or gross negligence. In no event
18 # may a licensor, author or contributor be held liable for indirect,
19 # direct, other damage, loss, or other issues arising in any way out
20 # of dealing in the work, even if advised of the possibility of such
21 # damage or existence of a defect, except proven that it results out
22 # of said person’s immediate fault when using the work as intended.
23 #-
24 # Associative, multi-dimensional arrays in Pure mksh™ (no exec!)
25 #-
26 # An item in an assockit array has the following properties:
27 # – the base-identifier of the shell array it’s in
28 # – the index into the shell array it’s in
29 # – an entry called flags
30 #   • data type: ASSO_{VAL,STR,INT,REAL,BOOL,NULL,AIDX,AASS}
31 # – an entry called key
32 # – an entry called value, unless NULL/AIDX/AASS
33 # Shell array paths are constructed like this:
34 # { 'foo': [ { 'baz': 123 } ] } named 'test' becomes:
35 # ‣ root-level lookup
36 #   – Asso__f[16#AE0C1A48] = ASSO_AASS | ASSO_ISSET|ASSO_ALLOC
37 #   – Asso__k[16#AE0C1A48] = 'test' (hash: AE0C1A48)
38 # ‣ Asso_AE0C1A48 = top-level
39 #   – Asso_AE0C1A48_f[16#BF959A6E] = ASSO_AIDX | ASSO_ISSET|ASSO_ALLOC
40 #   – Asso_AE0C1A48_k[16#BF959A6E] = 'foo' (hash: BF959A6E)
41 # ‣ Asso_AE0C1A48BF959A6E = next-level
42 #   – Asso_AE0C1A48BF959A6E_f[0] = ASSO_AASS | ASSO_ISSET|ASSO_ALLOC
43 #   – Asso_AE0C1A48BF959A6E_k[0] = 0
44 # ‣ Asso_AE0C1A48BF959A6E00000000 = last-level (below FOO)
45 #   – FOO_f[16#57F1BA9A] = ASSO_INT | ASSO_ISSET|ASSO_ALLOC
46 #   – FOO_k[16#57F1BA9A] = 'baz' (hash: 57F1BA9A)
47 #   – FOO_v[16#57F1BA9A] = 123
48 # When assigning a value, by default, the type of the
49 # intermediates is set to ASSO_AASS unless it already
50 # is ASSO_AIDX; the type of the terminals is ASSO_VAL
51 # unless it’s ASSO_{STR,INT,REAL,BOOL,NULL} before.
52
53 # check prerequisites
54 asso_x=${KSH_VERSION#????MIRBSD KSH R}
55 asso_x=${asso_x%% *}
56 if [[ $asso_x != +([0-9]) ]] || (( asso_x < 40 )); then
57         print -u2 'assockit.ksh: need at least mksh R40'
58         exit 1
59 fi
60
61 # set up variables
62 typeset -Uui16 -Z11 asso_h=0 asso_f=0 asso_k=0
63 typeset asso_b=""
64 set -A asso_y
65 set -A Asso__f
66 set -A Asso__k
67
68 # define constants
69 typeset -Uui16 -Z11 -r ASSO_VAL=2#000           # type: any Korn Shell scalar
70 typeset -Uui16 -Z11 -r ASSO_STR=2#001           # type: string
71 typeset -Uui16 -Z11 -r ASSO_INT=2#010           # type: integral
72 typeset -Uui16 -Z11 -r ASSO_REAL=2#011          # type: JSON float (string)
73 typeset -Uui16 -Z11 -r ASSO_BOOL=2#100          # type: JSON "true" / "false"
74 typeset -Uui16 -Z11 -r ASSO_NULL=2#101          # type: JSON "null"
75 typeset -Uui16 -Z11 -r ASSO_AIDX=2#110          # type: indexed array
76 typeset -Uui16 -Z11 -r ASSO_AASS=2#111          # type: associative array
77 typeset -Uui16 -Z11 -r ASSO_MASK_ARR=2#110      # bitmask for array type
78 typeset -Uui16 -Z11 -r ASSO_MASK_TYPE=2#111     # bitmask for type
79 typeset -Uui16 -Z11 -r ASSO_ISSET=16#40000000   # element is set
80 typeset -Uui16 -Z11 -r ASSO_ALLOC=16#80000000   # ksh element is set
81
82 # notes:
83 # – the code assumes ASSO_VAL=0 < all scalar types with value \
84 #   < ASSO_NULL < all array types
85
86 # public functions
87
88 # set a value
89 # example: asso_setv 123 'test' 'foo' 0 'baz'
90 function asso_setv {
91         if (( $# < 2 )); then
92                 print -u2 'assockit.ksh: syntax: asso_setv value key [key ...]'
93                 return 2
94         fi
95         local _v=$1 _f _i
96         shift
97
98         # look up the item, creating paths as needed
99         asso__lookup 1 "$@"
100         # if it’s an array, free that recursively
101         if (( (_f = asso_f) & ASSO_MASK_ARR )); then
102                 asso__r_free 1
103                 (( _f &= ~ASSO_MASK_TYPE ))
104         fi
105         # if it’s got a type, check for a match
106         if (( _i = (_f & ASSO_MASK_TYPE) )); then
107                 asso__typeck $_i "$_v" || (( _f &= ~ASSO_MASK_TYPE ))
108         fi
109         # set the new flags and value
110         asso__r_setfv $_f "$_v"
111 }
112
113 # get the flags of an item, or return 1 if not set
114 # result is in the global variable asso_f
115 function asso_isset {
116         if (( $# < 1 )); then
117                 print -u2 'assockit.ksh: syntax: asso_isset key [key ...]'
118                 return 2
119         fi
120
121         asso__lookup 0 "$@"
122 }
123
124 # get the type of an item (return 1 if unset, 2 if error)
125 # example: x=$(asso_gett 'test' 'foo' 0 'baz') => $((ASSO_VAL))
126 function asso_gett {
127         asso_isset "$@" || return
128         print -n -- $((asso_f & ASSO_MASK_TYPE))
129 }
130
131 # get the value of an item (return 1 if unset, 2 if error)
132 # example: x=$(asso_getv 'test' 'foo' 0 'baz') => 123
133 function asso_getv {
134         asso_loadv "$@" || return
135         print -nr -- "$asso_x"
136 }
137
138 # get the value of an item, but result is in the global variable asso_x
139 function asso_loadv {
140         if (( $# < 1 )); then
141                 print -u2 'assockit.ksh: syntax: asso_loadv key [key ...]'
142                 return 2
143         fi
144
145         asso__lookup 0 "$@" || return 1
146         if (( (asso_f & ASSO_MASK_TYPE) < ASSO_NULL )); then
147                 nameref _Av=${asso_b}_v
148                 asso_x=${_Av[asso_k]}
149         else
150                 asso_x=""
151         fi
152 }
153
154 # get all set keys of an item of array type (return 1 if no array)
155 # result is in the global variable asso_y
156 function asso_loadk {
157         if (( $# < 1 )); then
158                 print -u2 'assockit.ksh: syntax: asso_loadk key [key ...]'
159                 return 2
160         fi
161
162         set -A asso_y
163         asso__lookup 0 "$@" || return 1
164         (( asso_f & ASSO_MASK_ARR )) || return 1
165         nameref _keys=${asso_b}${asso_k#16#}_k
166         set -A asso_y -- "${_keys[@]}"
167 }
168
169 # set a string value
170 # example: asso_sets 'abc' 'test' 'foo' 0 'baz'
171 function asso_sets {
172         if (( $# < 2 )); then
173                 print -u2 'assockit.ksh: syntax: asso_sets value key [key ...]'
174                 return 2
175         fi
176
177         asso__settv $ASSO_STR "$@"
178 }
179
180 # set an integral value
181 # example: asso_seti 123 'test' 'foo' 0 'baz'
182 function asso_seti {
183         if (( $# < 2 )); then
184                 print -u2 'assockit.ksh: syntax: asso_seti value key [key ...]'
185                 return 2
186         fi
187
188         if ! asso__typeck $ASSO_INT "$1"; then
189                 print -u2 "assockit.ksh: not an integer: '$1'"
190                 return 1
191         fi
192         asso__settv $ASSO_INT "$@"
193 }
194
195 # set a floating point (real) value
196 # example: asso_setr -123.45e+67 'test' 'foo' 0 'baz'
197 function asso_setr {
198         if (( $# < 2 )); then
199                 print -u2 'assockit.ksh: syntax: asso_setr value key [key ...]'
200                 return 2
201         fi
202
203         if ! asso__typeck $ASSO_REAL "$1"; then
204                 print -u2 "assockit.ksh: not a real: '$1'"
205                 return 1
206         fi
207         asso__settv $ASSO_REAL "$@"
208 }
209
210 # set a boolean value
211 # example: asso_setb t 'test' 'foo' 0 'baz'
212 function asso_setb {
213         if (( $# < 2 )); then
214                 print -u2 'assockit.ksh: syntax: asso_setb value key [key ...]'
215                 return 2
216         fi
217
218         if ! asso__typeck $ASSO_BOOL "$1"; then
219                 print -u2 "assockit.ksh: not a truth value: '$1'"
220                 return 1
221         fi
222         asso__settv $ASSO_BOOL "$@"
223 }
224
225 # set value to null
226 # example: asso_setnull 'test' 'foo' 0 'baz'
227 function asso_setnull {
228         if (( $# < 1 )); then
229                 print -u2 'assockit.ksh: syntax: asso_setnull key [key ...]'
230                 return 2
231         fi
232
233         asso__settv $ASSO_NULL 0 "$@"
234 }
235
236 # set type and scalar value
237 # example: asso_settv $ASSO_INT 123 'test' 'foo' 0 'baz'
238 function asso_settv {
239         if (( $# < 3 )) || ! asso__intck "$1" || \
240             (( $1 != ($1 & ASSO_MASK_TYPE) )); then
241                 print -u2 'assockit.ksh: syntax: asso_settv type value key...'
242                 return 2
243         fi
244
245         if ! asso__typeck $1 "$2"; then
246                 print -u2 "assockit.ksh: wrong type scalar: '$2'"
247                 return 1
248         fi
249         asso__settv "$@"
250 }
251
252 # unset value
253 # example: asso_unset 'test' 'foo' 0 'baz'
254 function asso_unset {
255         if (( $# < 1 )); then
256                 print -u2 'assockit.ksh: syntax: asso_unset key [key ...]'
257                 return 2
258         fi
259
260         # look up the item, not creating paths
261         if asso__lookup 0 "$@"; then
262                 # free the item recursively
263                 asso__r_free 0
264         fi
265         return 0
266 }
267
268 # make an entry into an indexed array
269 # from scalar => data into [0]
270 # from associative array => data lost
271 function asso_setidx {
272         if (( $# < 1 )); then
273                 print -u2 'assockit.ksh: syntax: asso_setidx key [key ...]'
274                 return 2
275         fi
276
277         local _f _v
278
279         asso__lookup 1 "$@"
280         if (( !((_f = asso_f) & ASSO_MASK_ARR) )); then
281                 nameref _Av=${asso_b}_v
282                 _v=${_Av[asso_k]}
283         elif (( (_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
284                 return 0
285         fi
286         asso__r_free 1
287         asso__r_setf $ASSO_AIDX
288         if (( !(_f & ASSO_MASK_ARR) )); then
289                 asso__lookup 1 "$@" 0
290                 asso__r_setfv $_f "$_v"
291         fi
292 }
293
294 # make an entry into an associative array
295 # from scalar => data lost
296 # from indexed array => data converted
297 function asso_setasso {
298         if (( $# < 1 )); then
299                 print -u2 'assockit.ksh: syntax: asso_setasso key [key ...]'
300                 return 2
301         fi
302
303         local _f
304
305         asso__lookup 1 "$@"
306         if (( !((_f = asso_f) & ASSO_MASK_ARR) )); then
307                 asso__r_free 1
308                 asso__r_setf $ASSO_AASS
309         elif (( (_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
310                 asso__r_idx2ass
311         fi
312         return 0
313 }
314
315 # private functions
316
317 # set type and scalar value, unchecked
318 function asso__settv {
319         local _t=$1 _v=$2 _f
320         shift; shift
321
322         # look up the item, creating paths as needed
323         asso__lookup 1 "$@"
324         # if it’s an array, free that recursively
325         if (( (_f = asso_f) & ASSO_MASK_ARR )); then
326                 asso__r_free 1
327         fi
328         (( _f = (_f & ~ASSO_MASK_TYPE) | _t ))
329         # set the new flags and value
330         asso__r_setfv $_f "$_v"
331 }
332
333 # check if this is a numeric (integral) value (0=ok 1=error)
334 function asso__intck {
335         local _v=$1
336
337         [[ $_v = ?(+([0-9])'#')+([0-9a-zA-Z]) ]] || return 2
338         { : $((_v)) ; } 2>&-
339 }
340
341 # map a boolean value (0=false 1=true 2=error)
342 function asso__boolmap {
343         local _v=$1
344
345         if asso__intck "$_v"; then
346                 (( _v == 0 ))
347                 return
348         fi
349         case $_v {
350         ([Tt]?([Rr][Uu][Ee])|[Yy]?([Ee][Ss])|[Oo][NnKk])
351                 return 1 ;;
352         ([Ff]?([Aa][Ll][Ss][Ee])|[Nn]?([Oo])|[Oo][Ff][Ff])
353                 return 0 ;;
354         }
355         return 2
356 }
357
358 # check if the type matches the value (0=ok 1=error)
359 function asso__typeck {
360         if (( $# != 2 )); then
361                 print -u2 'assockit.ksh: syntax: asso__typeck type value'
362                 return 2
363         fi
364         local _t=$1 _v=$2
365         (( _t == ASSO_VAL || _t == ASSO_STR || _t == ASSO_NULL )) && return 0
366         if (( _t == ASSO_INT )); then
367                 asso__intck "$_v"
368                 return
369         fi
370         if (( _t == ASSO_BOOL )); then
371                 asso__boolmap "$_v"
372                 (( $? < 2 ))
373                 return
374         fi
375         (( _t & ASSO_MASK_ARR )) && return 1
376         # ASSO_REAL
377         [[ $_v = ?(-)@(0|[1-9]*([0-9]))?(.+([0-9]))?([Ee]?([+-])+([0-9])) ]]
378 }
379
380 # look up an item ($1=1: create paths as necessary)
381 function asso__lookup {
382         local _c=$1 _k _n _r
383         shift
384
385         _n=Asso_
386         _r=0
387         asso_f=$ASSO_AASS
388         for _k in "$@"; do
389                 if (( _r || !(asso_f & ASSO_MASK_ARR) )); then
390                         (( _r )) || asso__r_free 1
391                         asso__r_setf $ASSO_AASS
392                 elif (( (asso_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
393                         asso__intck "$_k" || asso__r_idx2ass
394                 fi
395                 asso_b=$_n
396                 asso__lookup_once "$_k"
397                 if (( _r = $? )); then
398                         # not found. not create?
399                         (( _c )) || return 1
400                         asso__r_setk "$_k"
401                 fi
402                 _n=$_n${asso_k#16#}
403         done
404         return 0
405 }
406
407 # set flags for asso_b[asso_k] and update asso_f
408 function asso__r_setf {
409         nameref _Af=${asso_b}_f
410
411         asso_f=$(($1 | ASSO_ISSET | ASSO_ALLOC))
412         _Af[asso_k]=$asso_f
413 }
414
415 # set flags and value for asso_b[asso_k] and update asso_f
416 function asso__r_setfv {
417         nameref _Af=${asso_b}_f
418         nameref _Av=${asso_b}_v
419
420         _Av[asso_k]=$2
421         asso_f=$(($1 | ASSO_ISSET | ASSO_ALLOC))
422         _Af[asso_k]=$asso_f
423 }
424
425 # set key for not yet existing asso_b[asso_k] and update asso_f
426 function asso__r_setk {
427         nameref _Af=${asso_b}_f
428         nameref _Ak=${asso_b}_k
429
430         _Ak[asso_k]=$1
431         asso_f=$((ASSO_ALLOC))
432         _Af[asso_k]=$asso_f
433 }
434
435 # in asso_b of type asso_f look up element $1
436 # set its asso_f and asso_k or return 1 when not found
437 function asso__lookup_once {
438         local _e=$1 _seth=0
439         nameref _Af=${asso_b}_f
440         nameref _Ak=${asso_b}_k
441
442         if (( (asso_f & ASSO_MASK_TYPE) == ASSO_AIDX )); then
443                 asso_k=$((_e))
444         else
445                 asso_k=16#${_e@#}
446 #               asso_k=$(somehash "$_e")
447                 while :; do
448                         asso_f=${_Af[asso_k]}
449                         (( asso_f & ASSO_ALLOC )) || break
450                         if (( !(asso_f & ASSO_ISSET) )); then
451                                 if (( !_seth )); then
452                                         # save index
453                                         asso_h=$asso_k
454                                         _seth=1
455                                 fi
456                                 (( --asso_k ))
457                                 continue
458                         fi
459                         [[ ${_Ak[asso_k]} = "$_e" ]] && break
460                         # iterate
461                         (( --asso_k ))
462                 done
463         fi
464         asso_f=${_Af[asso_k]}
465         # found?
466         (( asso_f & ASSO_ISSET )) && return 0
467         # not found.
468         if (( _seth )); then
469                 # when allocating, use this one instead
470                 asso_k=$asso_h
471         fi
472         return 1
473 }
474
475 # free the currently selected asso_b[asso_k] recursively
476 function asso__r_free {
477         local _keepkey=$1
478         nameref _Af=${asso_b}_f
479
480         asso_f=${_Af[asso_k]}
481         (( asso_f & ASSO_ALLOC )) || return
482         if (( asso_f & ASSO_ISSET )); then
483                 if (( asso_f & ASSO_MASK_ARR )); then
484                         local _ob=$asso_b _ok=$asso_k
485                         asso_b=$asso_b${asso_k#16#}
486                         nameref _s=${asso_b}_f
487                         for asso_k in "${!_s[@]}"; do
488                                 asso__r_free
489                         done
490                         eval unset ${asso_b}_f ${asso_b}_k ${asso_b}_v
491                         asso_b=$_ob asso_k=$_ok
492                 fi
493                 eval unset $asso_b'_v[asso_k]'
494                 (( _keepkey )) || eval unset $asso_b'_k[asso_k]'
495         fi
496         asso_f=$((ASSO_ALLOC))
497         _Af[asso_k]=$asso_f
498 }
499
500 # make indexed asso_b[asso_k] into associative array
501 function asso__r_idx2ass {
502         print -u2 'assockit.ksh: warning: asso__r_idx2ass not implemented'
503         print -u2 'assockit.ksh: warning: data will be lost'
504         asso__r_free
505         asso__r_setf $ASSO_AASS
506 }