-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfunctions.tcl
1213 lines (1100 loc) · 31.9 KB
/
functions.tcl
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
# These are procedures that I think are useful for everyday use.
# Not all procedures in this file may have the most optimized
# implementation, but I guarantee that they work as intended.
# The focus is on functionality first, and improvements can be
# made over time to enhance performance.
# When and why I do this:
# - prioritizing convenience and readability over performance
# - core function is missing in libtcl.a like clock or parray
# - improves code readability like odd? instead of modulo fiddling
# - adds additional functionality to some core function like parray
# - avoids repetitive boilerplate code and keeps things DRY
# - makes code more expressive and self-documenting
# - ensures a consistent interface:
# (predicate functions end with '?',
# utility functions are short and clear)
# - keeps coding fun and Tcl enjoyable 😎
# ---------------------------------------------------------------------
# Procedure: datetime
# Added as a replacement for the missing 'clock' command and for
# convenience. Command is platform-independent, which is especially
# useful when working in environments like jails or on systems where
# external tools (like date) are not available.
# See: https://www.sqlite.org/lang_datefunc.html
#
# Arguments:
# - format: The datetime format (uses SQLite's strftime format)
# Defaults to ISO 8601 (YYYY-MM-DD HH:MM:SS)
# - mods: Modifiers (uses SQLite's date time modifiers)
# Defaults to the current local date and time
#
# Usage examples:
# datetime
# datetime "%Y-%m-%d %H:%M:%S" "'now', '+1 day'"
# datetime "%Y-%m-%d" "'now','start of year','+9 months','weekday 2'"
# datetime "%s"
# ---------------------------------------------------------------------
proc datetime {{fmt "%Y-%m-%d %H:%M:%S"} {mods "'now', 'localtime'"}} {
set sql "SELECT strftime('$fmt', $mods);"
if {[info commands db] eq "db"} {
return [db one $sql]
} else {
sqlite3 __datetime_db :memory:
set result [__datetime_db one $sql]
__datetime_db close
return $result
}
}
# ---------------------------------------------------------------------
# Procedure: parray
# Displays the contents of an array with custom formatting. This command
# allows filtering by key and/or value. It is useful when working with
# Tcl arrays, providing a convenient way to format and display array data
# with optional key and value filters.
#
# Arguments:
# - arrayName: The name of the array (passed by reference).
# - keyfilter: A pattern used to filter the array keys
# default is '*', meaning no filtering
# - valuefilter: A nocase pattern used to filter the array values
# default is '*', meaning no filtering
#
# Usage examples:
# parray myArray
# parray myArray "ba*"
# parray myArray "*" "value*"
# parray myArray "key1" "value1"
# ---------------------------------------------------------------------
proc parray {arrayName {keyfilter *} {valuefilter *}} {
upvar 1 $arrayName __a
if {![array exists __a]} {
return -code error "Variable: '$arrayName' isn't an array"
}
set maxlen 0
set names [lsort [array names __a $keyfilter]]
# Find the maximum name length
foreach name $names {
if {[string length $name] > $maxlen} {
set maxlen [string length $name]
}
}
set maxlen [expr {$maxlen + [string length $arrayName] + 2}]
# Display the array with custom formatting
foreach name $names {
if {[string match -nocase "$valuefilter" $__a($name)]} {
set key_string [format %s(%s) $arrayName $name]
puts stdout [format "%-*s = %s" $maxlen $key_string $__a($name)]
}
}
}
# ---------------------------------------------------------------------
# Procedure: test
# My simple test suite :) It's sufficient for most use cases where
# you don't need advanced features. It's lightweight, easy to use,
# and it can handle basic test scenarios well. The command is flexible
# enough for quick testing of functions, commands, and database
# interactions. If you're looking for something simple and effective,
# this is the way to go!
#
# The test suite supports two types of comparison:
# 1. `regexp`: If the expected value is wrapped in slashes (`/`),
# a regular expression comparison is performed. This allows
# flexibility when you need to match patterns in the result.
# 2. `eq`: If the expected value is not wrapped in slashes,
# a direct string comparison is used, meaning the result must
# match the expected value exactly.
# ---------------------------------------------------------------------
# Usage:
# test testname {
# testbody
# } {expected}
#
# The testbody is a block of Tcl code that will be evaluated, and the
# result is compared to the expected value (expected).
# If the comparison passes, the test is marked as "PASS"; otherwise,
# it will be marked as "FAIL".
# ---------------------------------------------------------------------
proc test {testname testbody {expected {}}} {
set fail_line 0
set result [eval $testbody]
# Check if we want a regexp comparison; the expected value
# is between / and /
if {[regexp {^/.*/$} $expected]} {
# Perform regexp comparison
set expected [string range $expected 1 end-1] ;# remove the slashes
if {[regexp "^${expected}\$" $result]} {
puts "PASS: $testname"
} else {
set fail_line [lindex [info frame -1] 3]
puts "FAIL: $testname, want: $expected, got: $result line: $fail_line"
}
} else {
# Perform simple string comparison
if { $result eq $expected } {
puts "PASS: $testname"
} else {
set fail_line [lindex [info frame -1] 3]
puts "FAIL: $testname, want: $expected, got: $result line: $fail_line"
}
}
}
# ---------------------------------------------------------------------
# Procedure: randhex
# Generates a random hexadecimal string using SQLite if available,
# otherwise falls back to an in-memory SQLite instance.
#
# Arguments:
# length (default: 16) - Number of random bytes to generate.
# Each byte is converted to 2 hex characters,
# so the output length is length * 2 characters.
#
# Behavior:
# - If the global "db" command exists (assumed to be an active SQLite
# connection),
# it is used directly for efficiency. (~35 µs in my case)
# - If "db" does not exist, a temporary SQLite database is
# created in RAM
# to execute the query, which adds some overhead
# (~2500 µs in my case).
#
# Notes:
# - SQLite's randomblob() provides high-quality randomness,
# superior to Tcl's built-in rand().
# - Optimized for web applications where an active database
# connection is usually available to generate dynamic content.
# ---------------------------------------------------------------------
proc randhex {{length 16}} {
set sql "SELECT lower(hex(randomblob($length)))"
if {[cmd? db]} {
return [db one $sql]
} else {
sqlite3 __randhex_db :memory:
set result [__randhex_db one $sql]
__randhex_db close
return $result
}
}
# ---------------------------------------------------------------------
# Procedure: kv (!)
# A minimalist's key-value manager for dict-like lists
# This procedure operates on lists where key-value pairs are stored in
# an even number of elements. The list is modified in-place via
# reference, so no new list is returned and operations are destructive.
#
# Arguments:
# @arg lstVar The list variable name to operate on.
# @arg key The key to get/set/add/delete.
# @arg value The value to set for the given key (if provided).
#
# It allows the following operations on the list:
# - GET (fetch the value for a given key),
# - SET (set a value for a key, or update an existing one),
# - ADD (add a new key-value pair if the key doesn't exist),
# - DEL (delete a key-value pair by removing both the key and value).
#
# Special behavior:
# - If the value is "-" it deletes the key and its value
# - If the key is "-" (minus sign), the entire list is pretty printed.
# - The list must have an even number of elements to work correctly.
#
# Example usage:
# set lst {name Jack age 30}
# kv lst name ;# -> Jack
# kv lst name John ;# Sets new name
# kv lst age ;# -> 30
# kv lst city NYC ;# Adds 'city' -> 'NYC'
# kv lst fullname {John Doe} ;# Adds 'fullname' -> 'John Doe'
# kv lst - ;# Pretty prints the current list
# kv lst city - ;# Deletes the city key and its value
#
# Returns the value associated with the key if GET operation, or an
# empty string if the key is not found. If a modification is made
# (SET/ADD), the list is updated in-place via reference, and the set
# value is returned.
# In the case of DELETE, the removed keyname is returned, or an empty
# string if the key was not found.
# ---------------------------------------------------------------------
proc kv {lstVar key {value {}}} {
upvar 1 $lstVar lst
if {[odd? $lst]} {
return -code error "odd number of elements"
}
set idx [lsearch -exact $lst $key]
# Pretty print when key is "-"
if {$key eq "-"} {
puts "Pretty printing $lstVar:"
set maxlen 0
foreach {k v} $lst {
set maxlen [expr {max($maxlen, [string length $k])}]
}
foreach {k v} $lst {
puts [format "%-*s -> %s" $maxlen $k $v]
}
return
}
if {$value eq {}} {
# get
return [expr {$idx == -1 ? "" : [lindex $lst [expr {$idx + 1}]]}]
} elseif {$value eq "-"} {
# del
if {$idx != -1} {
set lst [lreplace $lst $idx [expr {$idx + 1}]]
}
return $key
} elseif {$idx == -1} {
# add
lappend lst $key $value
} else {
# set
lset lst [expr {$idx + 1}] $value
}
return $value
}
# ---------------------------------------------------------------------
# Procedure: static
# Allows you to create "static" variables that retain their values
# between calls to the procedure.
# It uses a namespace to store the value and ensures that each
# procedure call can access and modify its "static" variable without
# polluting the global scope.
#
# Credits: kruzalex (Alexander Kruzlik)
# See: https://wiki.tcl-lang.org/page/static+variables
#
# Example with a numeric variable:
# proc intgen {} {
# static i 0 ;# Initialize 'i' to 0
# incr i ;# Increment 'i' on each call
# puts $i ;# Print the current value of 'i'
# }
#
# Example with a text (string) variable:
# proc strgen {} {
# static t "" ;# Initialize 't' as an empty string
# append t "-x" ;# Append '-x' to 't' on each call
# puts $t ;# Print the current value of 't'
# }
#
# Call 'intgen' multiple times to see how 'i' persists across calls
# intgen ;# 1
# intgen ;# 2
# intgen ;# 3
#
# Call 'strgen' multiple times to see how 't' persists across calls
# strgen ;# -x
# strgen ;# -x-x
# strgen ;# -x-x-x
# ---------------------------------------------------------------------
proc static {name {value 0}} {
set caller [lindex [info level -1] 0]
set qname ::wappstatic::${caller}
if {![info exists ${qname}::$name]} {
foreach var [list [lrange [info level 0] 1 end]] {
if {[llength $var]==1} {lappend var $value}
namespace eval $qname [linsert $var 0 variable]
}
}
uplevel 1 [list upvar 0 ${qname}::$name $name]
}
# ---------------------------------------------------------------------
# Work in progres
# The following code can be the subject of frequent changes.
# ---------------------------------------------------------------------
# Procedure: timestamp
# Returns current timestamp in seconds, miliseconds, or microseconds
proc timestamp {{unit "s"}} {
set memory 0
if {[cmd? db]} {
set db db
} else {
set memory 1
sqlite3 __timestamp__db :memory:
set db __timestamp__db
}
if {$unit eq "s"} {
set result [$db one {select strftime('%s', 'now')}]
} elseif {$unit eq "ms"} {
set result [$db one {select cast((julianday('now') - 2440587.5) * 86400 * 1000 as integer)}]
} elseif {$unit eq "us"} {
set result [$db one {select cast((julianday('now') - 2440587.5) * 86400 * 1000000 as integer)}]
} else {
return -1 ;# Neplatný parameter
}
if $memory {
db close
}
return $result
}
# Procedure: dbg
# Prints args values to stdout, each on the separate line
proc dbg {args} {
if {[info globals DEBUG] ne ""} {
if {$::DEBUG == 1} {
set m ""
set i 1
foreach arg $args {
append m "arg $i: -> $arg\n"
incr i
}
puts -nonewline $m
}
}
}
# Procedure: listjson
# Converts flat dict-like list into JSON
# Returns:
# - valid JSON string
# - error, if the number of the elements is odd
proc listjson {lst} {
if {[odd? $lst]} {
return -code error "listjson: odd number of elements"
}
set json_str "{"
foreach {key value} $lst {
append json_str "\"$key\": \"$value\","
}
# remove last comma
if {[string length $json_str] > 1} {
set json_str [string range $json_str 0 end-1]
}
append json_str "}"
return $json_str
}
# Procedure: arraylist
# Creates dict-like list from an array
proc arraylist {arrayName} {
upvar $arrayName input
set result {}
set current {}
foreach key [array names input] {
if {$key eq "*"} {
# Prepend record in non-empty current
if {[llength $current] > 0} {
set result "[linsert $result 0 $current] "
}
set current {}
continue
}
set current [linsert $current 0 $key $input($key)]
}
# Prepend last record
if {[llength $current] > 0} {
set result [linsert $result 0 $current]
}
return $result
}
# Predicates
# ---------------------------------------------------------------------
# Procedure: int?
# Returns true if the value is an integer
proc int? {val} {
string is integer -strict $val
}
# Procedure: double?
# Returns true if the value is a double
proc double? {val} {
string is double -strict $val
}
# Procedure: empty?
# Returns true if the value is the empty string or list
proc empty? {val} {
if {[llength $val] == 0 || [string length $val] == 0} {
return 1
}
return 0
}
# Procedure: even?
# Returns true if the value is even
proc even? {val} {
if {[int? $val]} {
if {[expr {$val % 2}] == 0} {
return 1
}
return 0
}
if {[expr {[llength $val] % 2}] == 0} {
return 1
}
return 0
}
# Procedure: odd?
# Returns true if the value is odd
proc odd? {val} {
if {[int? $val]} {
if {[expr {$val % 2}] == 1} {
return 1
}
return 0
}
if {[llength $val] % 2 == 1} {
return 1
}
return 0
}
# Procedure: file?
# Returns true the file exists
proc file? {path} {
if {[file isfile $path]} {
return 1
}
return 0
}
# Procedure: fwrite?
# Returns true if the file is writable
proc fwrite? {path} {
if {[file writable $path]} {
return 1
}
return 0
}
# Procedure: fread?
# Returns true if the file is readable
proc fread? {path} {
if {[file readable $path]} {
return 1
}
return 0
}
# Procedure: dir?
# Returns true if the path is directory
proc dir? {path} {
if {[file isdirectory $path]} {
return 1
}
return 0
}
# Procedure: global?
# Returns true if the varName is global variable
proc global? {varName} {
if {[info globals $varName] ne ""} {
return 1
}
return 0
}
# Procedure: var?
# Checks the existence of the varName in the caller's scope
proc var? {varName} {
return [uplevel 1 [list info exists $varName]]
}
# Procedure: cmd?
# Checks the existence of the command
proc cmd? {command} {
if {[info commands $command] ne ""} {
return 1
}
return 0
}
# Procedure: member?
# Returns true if element is the member of lst
proc member? {lst element} {
expr {[lsearch [flatten $lst] $element] != -1}
}
# Procedure: number?
# Returns true if value is number
proc number? {val} {
if {[int? $val] || [double? $val]} {
return 1
}
return 0
}
# Procedure: boolean?
# Returns true if value is boolean
proc boolean? {val} {
return [expr {
$val == 1 || $val eq on || $val eq true || $val eq yes ||
$val == 0 || $val == off || $val eq false || $val eq no
}]
}
# Procedure: true?
# Returns true if value evaluates to true
proc true? {val} {
return [expr {
$val == 1 || $val eq on || $val eq true || $val eq yes
}]
}
# Procedure: false?
# Returns true if value evaluates to false
proc false? {val} {
return [expr {
$val == 0 || $val eq off || $val eq false || $val eq no
}]
}
# Procedure: zero?
# Returns true if value is number and equals to integer or double
proc zero? {val} {
if {[number? $val]} {
if {[int? $val] || [double? $val]} {
return 1
}
}
return 0
}
# Procedure: starts?
# Returns true if str starts with the prefix
proc starts? {str prefix} {
return [string match "$prefix*" $str]
}
# Procedure: ends?
# Returns true if str ends with the prefix
proc ends? {str suffix} {
return [string match "*$suffix" $str]
}
# Procedure: list?
# If you want to create universal list/string procedures.
# The tax is the shimmering effect.
#
# Tcl does not provide a way to distinguish between list and string
# except for malformed lists. This helper allows universal string/list
# functions to make that distinction. Unfortunately, the shimmering
# effect may occur. It returns true ONLY if the value is a string
# explicitly enclosed in {}. Use when you need procedures that work
# for both strings and lists.
# See: proc count
proc list? {val} {
if {[starts? $val "{"] && [ends? $val "}"]} {
return 1
}
return 0
}
# Procedure: count
# Universal list/string procedure test ... to count number of
# the elements or characters in the input value.
# count $val -> for strings
# count "{$val}" -> for lists
# See: proc list?
proc count {val} {
if {[list? $val]} {
set val {*}$val
return [llength $val]
} else {
return [string length $val]
}
}
# Procedure: trim
# Trims character from the both sides of str
proc trim {str {chars " "}} {
return [string trim $str $chars]
}
# Procedure: ltrim
# Trims character from the left side of str
proc ltrim {str {chars " "}} {
return [string trimleft $str $chars]
}
# Procedure: rtrim
# Trims character from the right side of str
proc rtrim {str {chars " "}} {
return [string trimright $str $chars]
}
# Procedure: lower
# Converts a string to lowercase
proc lower {str} {
return [string tolower $str]
}
# Procedure: upper
# Converts a string to uppercase
proc upper {str} {
return [string toupper $str]
}
# Procedure: if2
# If we don't have if-else.
# Test of a custom if (else) that works and looks as much like
# the original as possible, when called.
proc if2 {cond body1 {else ""} {body2 ""}} {
set cond [uplevel 1 [list expr !!($cond)]]
if {$cond} {
uplevel 1 $body1
} else {
uplevel 1 $body2
}
}
# Procedure: all?
# Checks if all elemnets in the list satisfy the predicate
# all? number? $lst -> 0/1
proc all? {predicate lst} {
if {![cmd? $predicate]} {
return -code error "all?: invalid predicate name '$predicate'"
}
foreach e $lst {
if {![$predicate $e]} {
return 0
}
}
return 1
}
# Procedure: filter
# Filters a list based on a predicate, returning elements that
# satisfy the predicate
# filter odd? {1 2 3} -> 1 3
proc filter {predicate lst} {
if {![cmd? $predicate]} {
return -code error "filter: invalid predicate name '$predicate'"
}
set result {}
foreach e $lst {
if {[$predicate $e]} {
lappend result $e
}
}
return $result
}
# Procedure: reject
# Filters a list based on a predicate, returning elements that
# do NOT satisfy the predicate.
# reject odd? {1 2 3} -> 2
proc reject {predicate lst} {
if {![cmd? $predicate]} {
return -code error "reject: invalid predicate name '$predicate'"
}
set result {}
foreach e $lst {
if {![$predicate $e]} {
lappend result $e
}
}
return $result
}
# Procedure: key
# Returns the value of the key from the dict or dict-like list
proc key {lst key} {
if {[odd? $lst]} {
return -code error "key: odd number of elements"
}
set i [lsearch -exact $lst $key]
if {$i == -1} {
return {}
}
lindex $lst [expr {$i + 1}]
}
# Procedure: idx
#
proc idx {lst index {default ""}} {
if {$index > 0 && $index < [llength $lst]} {
return [lindex $lst $index]
}
return $default
}
# Procedure: trap
# Essentially works like 'if catch then body'.
# Trap stores the latest error in the 'traperror' auto variable
# in the caller's scope.
# Returns 1 on error, 0 on success
# Example usages:
# trap {lassing} -> ignore the error and continue
# trap {lassign} {puts $traperror}
# trap {lassign} {puts "Recovering ..."; lassign {b} a}
# if {[trap {lassign}]} { puts $traperror; do this } else {do that}
proc trap {cmd {body ""}} {
set level [expr {[info level] - 1}]
set caller [lindex [info level $level] 0]
set code [catch {uplevel 1 $cmd} err]
if {$code} {
if {$level == 0} {
set err "Trapped in level 0 in proc: ${caller} -> [vibe '$cmd' lgreen]: $err"
} else {
set err "Trapped in proc: ${caller} -> [vibe '$cmd' lgreen]: $err"
}
uplevel 1 [list set traperror $err]
if {$body ne ""} {
uplevel 1 $body
}
}
return $code
}
# Procedure: reduce
#
proc reduce {func lst {initial {}}} {
if {[empty? $lst]} {
return -code error "reduce: empty list"
}
if {[false? [cmd? $func]]} {
return -code error "reduce: invalid func name '$func'"
}
# If there is no initial, we will use the first element
if {[empty? $initial]} {
set initial [lindex $lst 0]
set lst [lrange $lst 1 end]
}
set acc $initial
foreach element $lst {
set acc [uplevel 1 [list $func $acc $element]]
}
return $acc
}
# Procedure: first
# Returns the first element/word of the lst/str
proc first {lst} {
lindex $lst 0
}
# Procedure: rest
# Returns the lst/str except for the first element/word
proc rest {lst} {
lassign $lst var
}
# Procedure: last
# Returns the last element/word of the lst/str
proc last {lst} {
lindex $lst end
}
# Procedure: replace
# Returns the list or the string where the what was
# replaced with the with :)
proc replace {where what with} {
regsub -all $what $where $with
}
# Procedure: slice
# Returns a slice of the lst from the start index to the end index.
# If end is omitted, returns a slice of the lst from the start index
# to the end of the lst.
proc slice {lst start {end -1}} {
if {$end == -1} {
return [lrange $lst $start end]
}
return [lrange $lst $start $end]
}
# Procedure: range
# Returns a range of the str from the start index to the end index.
# If end is omitted, returns a range of the str from the start index
# to the end of the str.
proc range {str start {end -1}} {
if {$end == -1} {
set end [string length $str]
}
return [string range $str $start $end] ;#[expr {$end - 1}]]
}
# Procedure: parse
# Parse a string by specified separator(s) (which can be a regexp in {})
# This function also handles consecutive separator characters correctly.
# parse "abc:dvd" ":" -> abc dvd
# parse "abc:dvd,xyz" {[:,]} -> abc dvd xyz
# parse "abc::dvd::xyz" "::" -> abc dvd xyz
# parse $s {\n{2,}} -> parse paragraphs :)
proc parse {str {chars " "}} {
set str [regsub -all $chars $str "\x00"]
set result [split $str "\x00"]
return $result
}
# Procedure: setm
# Sets multiple variables at once. The number of variables must match
# the number of values.
# Usage:
# setm v1 v2 v3 yes no "hello world"
proc setm {args} {
set half [expr {[llength $args] / 2}]
set variables [lrange $args 0 $half-1]
set values [lrange $args $half end]
if {[llength $variables] != [llength $values]} {
return -code error "setm: number of variables and values must match"
}
# uplevel 1 "lassign [list $values] $variables"
uplevel 1 [list lassign $values {*}$variables]
}
# Procedure: setl
# Sets multiple variables at once using list. The number of variables
# must match the number of the list elements.
# Usage:
# setl v1 v2 v3 {yes no "hello world"}
proc setl {args} {
set variables [lrange $args 0 end-1]
set values [lindex $args end]
if {[llength $variables] != [llength $values]} {
return -code error "setl: number of variables and values must match"
}
# uplevel 1 "lassign [list $values] $variables"
uplevel 1 [list lassign $values {*}$variables]
}
# Procedure: flatten
# Converts nested lists to the flat lists
# Bye recursive calls and malformed lists
proc flatten {lst} {
replace [replace $lst "{" ""] "}" ""
}
# Procedure: assoc
# Searches for the first sublist in a list whose first element
# matches the given key
# assoc {{a b c} {D E F}} D ;# -> D E F
proc assoc {lst key} {
set idx [lsearch -exact -index 0 $lst $key]
return [lindex $lst $idx]
}
# Procedure: lookup
# Searches for the first sublist in a list whose first element
# matches the given key and returns the value at the relative
# index.
# lookup {{a b c} {D E F}} D 2 ;# -> F
proc lookup {lst key {index 1}} {
set l [assoc $lst $key]
return [lindex $l $index]
}
# Procedure: seek
# Searches for a key in the sublists and returns the value
# at the specified relative index in the sublist, where the first
# element is the key and the second (or more) elements are values.
# If the key is not found, or if the index is out of bounds,
# it returns the specified default value.
# Usage example:
# seek {{name john age 30} {name jack age 55}} jack 2 ;# -> 55
# seek {{abc dvd xyz} {123 456 789}} 456 1 ;# -> 789
# seek {{abc dvd xyz} {123 456 789}} 456 5 "not found" ;# -> not found
proc seek {lst key {index 1} {default ""}} {
foreach row $lst {
set i [lsearch -exact $row $key]
if {$i != -1} {
# if key found
set found [lindex $row [expr {$i + $index}]]
if {$found ne ""} {
return $found
} else {
return -code error "seek: index out of bounds"
}
}
}
return $default ;# key not found
}
# Procedure: fwrite
proc fwrite {path data {translation auto}} {
set fd [open $path w]
chan configure $fd -translation $translation
if {$translation eq "binary"} {
puts -nonewline $fd $data
} else {
puts $fd $data
}
close $fd
}
# Procedure: fappend
proc fappend {path data {translation auto}} {
set fd [open $path a]
chan configure $fd -translation $translation
if {$translation eq "binary"} {
puts -nonewline $fd $data
} else {
puts $fd $data
}
close $fd
}
# Procedure: fread
proc fread {path {translation auto}} {
set fd [open $path r]
chan configure $fd -translation $translation
set data [read $fd]
close $fd
return $data
}
# Procedure: fdel
proc fdel {path} {
file delete $path
}
# Procedure: fdelforce
proc fdelforce {path} {
file delete -force $path
}
# Procedure: sql
# This is essetially [db eval {...}] alias, so you don't forget to set
# the empty values to <null> or something similar automatically
proc sql {sql args} {
array set opt [concat {-db db} $args]
set db $opt(-db)
$db nullvalue <null>
uplevel [list $db eval $sql]
}
# Procedure: sqlget
# Arguments:
# -keys 1/0 add column names into resultset
# -db to set db command name, default is db
# -flat 0/1 flat unstructured list of all returned records and values
# Returns a list or list of lists or list of dict-like lists as a result
# of the SQL SELECT query.
# If 'limit 1' is hardcoded at the end of the query, it returns a single
# list/dict-like list.
proc sqlget {sql args} {
array set opt [concat {-keys 1 -flat 0 -db db} $args]
set ::_keys $opt(-keys)
set flat $opt(-flat)
set db $opt(-db)