gloo 6.7.2 → 7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -0
  3. data/docs/application.md +3 -1
  4. data/docs/iterators.md +39 -3
  5. data/docs/language_syntax.md +53 -5
  6. data/docs/objects.md +58 -1
  7. data/docs/operators.md +16 -0
  8. data/docs/plugins.md +9 -1
  9. data/docs/verbs.md +4 -2
  10. data/lib/VERSION +1 -1
  11. data/lib/VERSION_NOTES +20 -0
  12. data/lib/gloo/app/engine.rb +64 -8
  13. data/lib/gloo/app/log.rb +47 -6
  14. data/lib/gloo/convert/converter.rb +63 -2
  15. data/lib/gloo/convert/string_to_date.rb +14 -0
  16. data/lib/gloo/convert/string_to_datetime.rb +16 -0
  17. data/lib/gloo/convert/string_to_decimal.rb +13 -0
  18. data/lib/gloo/convert/string_to_integer.rb +9 -2
  19. data/lib/gloo/convert/string_to_time.rb +28 -0
  20. data/lib/gloo/core/error.rb +11 -2
  21. data/lib/gloo/core/event_manager.rb +7 -4
  22. data/lib/gloo/core/factory.rb +16 -11
  23. data/lib/gloo/core/here.rb +6 -1
  24. data/lib/gloo/core/invoker.rb +2 -3
  25. data/lib/gloo/core/not_found.rb +42 -0
  26. data/lib/gloo/core/obj.rb +28 -2
  27. data/lib/gloo/core/op.rb +2 -0
  28. data/lib/gloo/core/parser.rb +1 -1
  29. data/lib/gloo/core/pn.rb +33 -12
  30. data/lib/gloo/core/tokens.rb +38 -0
  31. data/lib/gloo/core/verb.rb +54 -0
  32. data/lib/gloo/exec/dispatch.rb +5 -6
  33. data/lib/gloo/exec/exec_env.rb +13 -0
  34. data/lib/gloo/exec/runner.rb +10 -2
  35. data/lib/gloo/exec/script.rb +13 -1
  36. data/lib/gloo/expr/expression.rb +39 -1
  37. data/lib/gloo/expr/op_plus.rb +3 -0
  38. data/lib/gloo/objs/basic/boolean.rb +16 -0
  39. data/lib/gloo/objs/basic/container.rb +154 -6
  40. data/lib/gloo/objs/basic/decimal.rb +1 -0
  41. data/lib/gloo/objs/basic/integer.rb +1 -0
  42. data/lib/gloo/objs/basic/string.rb +0 -1
  43. data/lib/gloo/objs/basic/string_msgs.rb +68 -32
  44. data/lib/gloo/objs/ctrl/each.rb +22 -3
  45. data/lib/gloo/objs/ctrl/each_dir.rb +20 -7
  46. data/lib/gloo/objs/ctrl/each_file.rb +78 -9
  47. data/lib/gloo/objs/ctrl/function.rb +6 -5
  48. data/lib/gloo/objs/dt/date.rb +28 -11
  49. data/lib/gloo/objs/dt/datetime.rb +72 -21
  50. data/lib/gloo/objs/dt/dt_tools.rb +46 -2
  51. data/lib/gloo/objs/dt/time.rb +28 -11
  52. data/lib/gloo/objs/str_utils/cipher.rb +17 -4
  53. data/lib/gloo/objs/str_utils/password.rb +6 -2
  54. data/lib/gloo/objs/system/erb.rb +9 -3
  55. data/lib/gloo/objs/system/file_handle.rb +57 -21
  56. data/lib/gloo/objs/system/system.rb +6 -2
  57. data/lib/gloo/objs/web/http_get.rb +12 -2
  58. data/lib/gloo/objs/web/http_post.rb +6 -2
  59. data/lib/gloo/objs/web/json.rb +38 -18
  60. data/lib/gloo/objs/web/uri.rb +36 -16
  61. data/lib/gloo/persist/file_loader.rb +72 -11
  62. data/lib/gloo/persist/file_saver.rb +2 -1
  63. data/lib/gloo/persist/indent_stack.rb +49 -33
  64. data/lib/gloo/persist/line_splitter.rb +40 -3
  65. data/lib/gloo/persist/persist_man.rb +2 -4
  66. data/lib/gloo/plugin/ext_manager.rb +6 -9
  67. data/lib/gloo/plugin/lib_manager.rb +2 -2
  68. data/lib/gloo/verbs/break.rb +1 -0
  69. data/lib/gloo/verbs/check.rb +1 -1
  70. data/lib/gloo/verbs/cls.rb +1 -0
  71. data/lib/gloo/verbs/create.rb +32 -4
  72. data/lib/gloo/verbs/execute.rb +1 -1
  73. data/lib/gloo/verbs/exists.rb +27 -15
  74. data/lib/gloo/verbs/files.rb +2 -1
  75. data/lib/gloo/verbs/help.rb +1 -0
  76. data/lib/gloo/verbs/if.rb +1 -1
  77. data/lib/gloo/verbs/invoke.rb +1 -1
  78. data/lib/gloo/verbs/list.rb +4 -3
  79. data/lib/gloo/verbs/load.rb +3 -3
  80. data/lib/gloo/verbs/move.rb +10 -8
  81. data/lib/gloo/verbs/put.rb +20 -8
  82. data/lib/gloo/verbs/quit.rb +1 -0
  83. data/lib/gloo/verbs/redirect.rb +8 -2
  84. data/lib/gloo/verbs/reload.rb +9 -1
  85. data/lib/gloo/verbs/run.rb +2 -2
  86. data/lib/gloo/verbs/save.rb +4 -2
  87. data/lib/gloo/verbs/tell.rb +1 -1
  88. data/lib/gloo/verbs/unless.rb +1 -1
  89. data/lib/gloo/verbs/unload.rb +8 -0
  90. data/test.gloo/ctrl/each.test.gloo +88 -0
  91. data/test.gloo/lang/declaration.test.gloo +27 -0
  92. data/test.gloo/lang/exceptions.test.gloo +60 -0
  93. data/test.gloo/lang/failures.test.gloo +139 -0
  94. data/test.gloo/lang/it.test.gloo +133 -0
  95. data/test.gloo/lang/naming.test.gloo +11 -0
  96. data/test.gloo/lang/ops.test.gloo +30 -0
  97. data/test.gloo/objs/can.test.gloo +101 -0
  98. data/test.gloo/objs/string.test.gloo +24 -0
  99. data/test.gloo/string/str.test.gloo +28 -0
  100. data/test.gloo/verbs/create.test.gloo +36 -0
  101. data/test.gloo/verbs/exists.test.gloo +33 -1
  102. data/test.gloo/verbs/unload.test.gloo +34 -0
  103. metadata +4 -1
@@ -10,6 +10,8 @@ module Gloo
10
10
  module Objs
11
11
  module StringMsgs
12
12
 
13
+ MISSING_PARAM_MSG = 'Missing parameter!'.freeze
14
+
13
15
  #
14
16
  # Get the list of message names this mixin implements, derived
15
17
  # from its own msg_* methods so String/Text don't have to
@@ -39,7 +41,8 @@ module Gloo
39
41
  'starts_with? ({str}) — Check if the string starts with the given string. A parameter is required: the string to look for at the beginning of this string. It will have a boolean.',
40
42
  'ends_with? ({str}) — Check if the string ends with the given string. A parameter is required: the string to look for at the end of this string. It will have a boolean.',
41
43
  'substring? ({str}) — Check if the string includes the given sub-string. A parameter is required: the string to look for in this string. It will have a boolean.',
42
- 'format_for_html — Format this string for HTML output. Tabs, spaces and returns are converted to HTML elements. The value of the string is changed.',
44
+ 'index_of ({needle} {from}) — Find the 0-based position of the first occurrence of {needle} at or after position {from}. The {needle} parameter is required; {from} is optional and defaults to 0. Does not change the value of the string. It will have the position, or -1 if {needle} is not found; an out-of-range {from} (negative, or past the end of the string) is an error, and it will have false.',
45
+ 'format_for_html — Format this string for HTML output. Tabs, spaces and returns are converted to HTML elements. The value of the string is changed. It will have the formatted string.',
43
46
  'encode64 — Base64 encode the string. This message changes the value of the string. It will have the encoded string.',
44
47
  'decode64 — Decode the string from Base64. This message changes the value of the string. It will have the decoded string.',
45
48
  'escape — Escape the string to make it URL safe. This message changes the value of the string. It will have the escaped string.',
@@ -65,7 +68,7 @@ module Gloo
65
68
  # Strip whitespace from the beginning and end of the string.
66
69
  #
67
70
  def msg_trim
68
- return '' unless value
71
+ return @engine.heap.it.set_to( '' ) unless value
69
72
 
70
73
  result = value.strip
71
74
  @engine.heap.it.set_to result
@@ -86,7 +89,7 @@ module Gloo
86
89
  return result
87
90
  else
88
91
  # Error
89
- @engine.log.error MISSING_PARAM_MSG
92
+ @engine.syntax_err MISSING_PARAM_MSG
90
93
  @engine.heap.it.set_to false
91
94
  return false
92
95
  end
@@ -105,7 +108,7 @@ module Gloo
105
108
  return result
106
109
  else
107
110
  # Error
108
- @engine.log.error MISSING_PARAM_MSG
111
+ @engine.syntax_err MISSING_PARAM_MSG
109
112
  @engine.heap.it.set_to false
110
113
  return false
111
114
  end
@@ -115,7 +118,7 @@ module Gloo
115
118
  # Substitute the given string with another string.
116
119
  #
117
120
  def msg_sub
118
- return '' unless value
121
+ return @engine.heap.it.set_to( '' ) unless value
119
122
  if @params&.token_count&.positive?
120
123
  expr = Gloo::Expr::Expression.new( @engine, [ @params.tokens.first ] )
121
124
  from = expr.evaluate
@@ -128,7 +131,7 @@ module Gloo
128
131
  return result
129
132
  else
130
133
  # Error
131
- @engine.log.error MISSING_PARAM_MSG
134
+ @engine.syntax_err MISSING_PARAM_MSG
132
135
  @engine.heap.it.set_to false
133
136
  return false
134
137
  end
@@ -139,7 +142,7 @@ module Gloo
139
142
  # Find all occurrences and replace them.
140
143
  #
141
144
  def msg_gsub
142
- return '' unless value
145
+ return @engine.heap.it.set_to( '' ) unless value
143
146
 
144
147
  if @params&.token_count&.positive?
145
148
  expr = Gloo::Expr::Expression.new( @engine, [ @params.tokens.first ] )
@@ -153,7 +156,7 @@ module Gloo
153
156
  return result
154
157
  else
155
158
  # Error
156
- @engine.log.error MISSING_PARAM_MSG
159
+ @engine.syntax_err MISSING_PARAM_MSG
157
160
  @engine.heap.it.set_to false
158
161
  return false
159
162
  end
@@ -165,7 +168,7 @@ module Gloo
165
168
  # beginning or end of the string. Does not change the string's value.
166
169
  #
167
170
  def msg_split
168
- return '' unless value
171
+ return @engine.heap.it.set_to( '' ) unless value
169
172
 
170
173
  if @params&.token_count&.positive?
171
174
  expr = Gloo::Expr::Expression.new( @engine, [ @params.tokens.first ] )
@@ -178,7 +181,7 @@ module Gloo
178
181
  return result
179
182
  else
180
183
  # Error
181
- @engine.log.error MISSING_PARAM_MSG
184
+ @engine.syntax_err MISSING_PARAM_MSG
182
185
  @engine.heap.it.set_to false
183
186
  return false
184
187
  end
@@ -189,7 +192,7 @@ module Gloo
189
192
  # Same as split( 0, index ). Does not change the string's value.
190
193
  #
191
194
  def msg_splitl
192
- return '' unless value
195
+ return @engine.heap.it.set_to( '' ) unless value
193
196
 
194
197
  if @params&.token_count&.positive?
195
198
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
@@ -200,7 +203,7 @@ module Gloo
200
203
  return result
201
204
  else
202
205
  # Error
203
- @engine.log.error MISSING_PARAM_MSG
206
+ @engine.syntax_err MISSING_PARAM_MSG
204
207
  @engine.heap.it.set_to false
205
208
  return false
206
209
  end
@@ -211,7 +214,7 @@ module Gloo
211
214
  # Same as split( index, size ). Does not change the string's value.
212
215
  #
213
216
  def msg_splitr
214
- return '' unless value
217
+ return @engine.heap.it.set_to( '' ) unless value
215
218
 
216
219
  if @params&.token_count&.positive?
217
220
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
@@ -222,7 +225,7 @@ module Gloo
222
225
  return result
223
226
  else
224
227
  # Error
225
- @engine.log.error MISSING_PARAM_MSG
228
+ @engine.syntax_err MISSING_PARAM_MSG
226
229
  @engine.heap.it.set_to false
227
230
  return false
228
231
  end
@@ -240,10 +243,10 @@ module Gloo
240
243
  # into 'it'.
241
244
  #
242
245
  def msg_split_list
243
- return unless value
246
+ return @engine.heap.it.set_to( 0 ) unless value
244
247
 
245
248
  if @params&.token_count.to_i < 2
246
- @engine.log.error MISSING_PARAM_MSG
249
+ @engine.syntax_err MISSING_PARAM_MSG
247
250
  @engine.heap.it.set_to false
248
251
  return false
249
252
  end
@@ -282,9 +285,45 @@ module Gloo
282
285
  @engine.heap.it.set_to result
283
286
  return result
284
287
  else
288
+ @engine.syntax_err MISSING_PARAM_MSG
289
+ @engine.heap.it.set_to false
290
+ return false
291
+ end
292
+ end
293
+
294
+ #
295
+ # Get the 0-based position of the first occurrence of {needle}
296
+ # at or after the optional {from} position (default 0).
297
+ # Puts -1 into 'it' if not found. An out-of-range {from} (negative,
298
+ # or past the end of the string) logs an error and puts false
299
+ # into 'it'. Does not change the string's value.
300
+ #
301
+ def msg_index_of
302
+ unless @params&.token_count&.positive?
303
+ @engine.syntax_err MISSING_PARAM_MSG
304
+ @engine.heap.it.set_to false
305
+ return false
306
+ end
307
+
308
+ expr = Gloo::Expr::Expression.new( @engine, [ @params.tokens.first ] )
309
+ needle = expr.evaluate.to_s
310
+
311
+ from = 0
312
+ if @params.token_count > 1
313
+ expr = Gloo::Expr::Expression.new( @engine, [ @params.tokens.last ] )
314
+ from = expr.evaluate.to_i
315
+ end
316
+
317
+ str = value.to_s
318
+ if from.negative? || from > str.length
319
+ @engine.err "Start position #{from} is out of range!"
285
320
  @engine.heap.it.set_to false
286
321
  return false
287
322
  end
323
+
324
+ result = str.index( needle, from ) || -1
325
+ @engine.heap.it.set_to result
326
+ return result
288
327
  end
289
328
 
290
329
  #
@@ -328,26 +367,23 @@ module Gloo
328
367
  #
329
368
  def msg_format_for_html
330
369
  text = self.value
331
- out = ""
332
- return out unless text
370
+ return @engine.heap.it.set_to( '' ) unless text
333
371
 
334
- # indentation
372
+ # Indentation: each leading space is one &nbsp;, each leading
373
+ # tab four, in whatever order they come.
374
+ out = ""
335
375
  text.each_line do |line|
336
376
  i = 0
337
- while line[i] == ' '
377
+ while [ ' ', "\t" ].include?( line[ i ] )
378
+ out << ( line[ i ] == ' ' ? '&nbsp;' : '&nbsp;&nbsp;&nbsp;&nbsp;' )
338
379
  i += 1
339
- out << "&nbsp;"
340
380
  end
341
-
342
- i = 0
343
- while line[i] == "\t"
344
- i += 1
345
- out << "&nbsp;&nbsp;&nbsp;&nbsp;"
346
- end
347
- out << line
381
+ out << line[ i..]
348
382
  end
349
-
383
+
350
384
  self.value = out.gsub( "\n", "<br/>" )
385
+ @engine.heap.it.set_to self.value
386
+ return self.value
351
387
  end
352
388
 
353
389
  #
@@ -515,13 +551,13 @@ module Gloo
515
551
  #
516
552
  # Resolve the target container path for split_list. The path may
517
553
  # point directly at a container, or at an alias that points to
518
- # one. Returns nil (and logs an error, setting 'it' to false)
554
+ # one. Returns nil (and reports an error, setting 'it' to false)
519
555
  # if the path doesn't exist or doesn't resolve to a container.
520
556
  #
521
557
  def split_list_target( token )
522
558
  pn = Gloo::Core::Pn.new( @engine, token )
523
559
  unless pn&.exists?
524
- @engine.log.error 'Target container path does not exist!'
560
+ @engine.err Gloo::Core::NotFound.object( token )
525
561
  @engine.heap.it.set_to false
526
562
  return nil
527
563
  end
@@ -529,7 +565,7 @@ module Gloo
529
565
  target = pn.resolve
530
566
  target = Gloo::Objs::Alias.resolve_alias( @engine, target )
531
567
  unless target&.is_container?
532
- @engine.log.error 'Target for split_list must be a container!'
568
+ @engine.err 'Target for split_list must be a container!'
533
569
  @engine.heap.it.set_to false
534
570
  return nil
535
571
  end
@@ -133,8 +133,8 @@ module Gloo
133
133
  'child (alias) — iterate a container\'s children.',
134
134
  'word (string) — iterate the words in a string.',
135
135
  'line (string) — iterate the lines in a block of text.',
136
- 'file (file) — iterate the files in a folder; an optional ext (string) child limits to files of that extension.',
137
- 'dir (file) — iterate the directories in a folder.'
136
+ 'file (file) — iterate the files in a folder. Optional children: ext (string) limits to files of that extension, ignoring case; recursive (bool) walks the subfolders too; include_dirs (bool) lists folders as well as files.',
137
+ 'dir (file) — iterate the directories in a folder. Optional child: recursive (bool) walks the subfolders too.'
138
138
  ],
139
139
  :messages => [
140
140
  'run — Run the loop for each item in the collection.'
@@ -144,7 +144,16 @@ module Gloo
144
144
  '(string) child naming a property of each child, plus ' \
145
145
  'on_group_start / on_group_end (script) children — those ' \
146
146
  'scripts run whenever the group_by value changes, letting ' \
147
- 'you aggregate results in groups.',
147
+ 'you aggregate results in groups. ' \
148
+ 'When iterating files or directories, the in folder may be ' \
149
+ 'given with or without a trailing slash, may start with ~ ' \
150
+ 'for the home folder, and may include ' \
151
+ 'wildcards: in: /notes/**/ walks every subfolder, and ' \
152
+ 'in: /projects/*/docs/ looks in each project\'s docs folder. ' \
153
+ 'Wildcards combine with ext, recursive and include_dirs. ' \
154
+ 'Paths are full paths, sorted, with each folder before ' \
155
+ 'what is in it. Hidden files and folders are skipped, and ' \
156
+ 'a recursive walk does not go into symlinked folders.',
148
157
  :examples => <<~EXAMPLES.strip
149
158
  #
150
159
  # Show each child in a container.
@@ -167,6 +176,16 @@ module Gloo
167
176
  on_load [script] :
168
177
  show 'showing children in container' (white)
169
178
  tell each_child.for to run
179
+
180
+ #
181
+ # Show every markdown file in a folder tree.
182
+ #
183
+ notes [each] :
184
+ file [file] :
185
+ in [file] : /my/notes/
186
+ ext [string] : md
187
+ recursive [bool] : true
188
+ do [script] : show ^.file
170
189
  EXAMPLES
171
190
  }
172
191
  end
@@ -9,6 +9,7 @@ module Gloo
9
9
  class EachDir
10
10
 
11
11
  DIR = 'dir'.freeze
12
+ RECURSIVE = 'recursive'.freeze
12
13
 
13
14
  # ---------------------------------------------------------------------
14
15
  # Create Iterator
@@ -42,21 +43,33 @@ module Gloo
42
43
  # Run for each directory.
43
44
  #
44
45
  def run
45
- folder = @iterator_obj.in_value
46
+ folder = EachFile.expand_home( @iterator_obj.in_value )
46
47
  return unless folder
47
48
 
48
49
  unless Dir.exist?( folder )
49
- @engine.err "Folder does not exist: #{folder}"
50
+ # A path with a wildcard is a pattern, not a folder.
51
+ unless EachFile.wildcard?( folder )
52
+ @engine.err Gloo::Core::NotFound.folder( folder )
53
+ return
54
+ end
50
55
  end
51
56
 
52
- Dir.glob( "#{folder}*" ).each do |f|
53
- if Dir.exist?( f )
54
- set_dir f
55
- @iterator_obj.run_do
56
- end
57
+ Dir.glob( EachFile.pattern( folder, recursive? ) ).each do |f|
58
+ next unless File.directory?( f )
59
+
60
+ set_dir f
61
+ @iterator_obj.run_do
57
62
  end
58
63
  end
59
64
 
65
+ #
66
+ # Walk the subfolders too?
67
+ #
68
+ def recursive?
69
+ value = @iterator_obj.find_child_value RECURSIVE
70
+ return Gloo::Objs::Boolean.coerse_to_bool( value )
71
+ end
72
+
60
73
  #
61
74
  # Set the value of the dir.
62
75
  #
@@ -10,7 +10,10 @@ module Gloo
10
10
 
11
11
  FILE = 'file'.freeze
12
12
  EXT = 'ext'.freeze
13
+ RECURSIVE = 'recursive'.freeze
14
+ INCLUDE_DIRS = 'include_dirs'.freeze
13
15
  WILD = '*'.freeze
16
+ RECURSE = '**'.freeze
14
17
 
15
18
 
16
19
  # ---------------------------------------------------------------------
@@ -45,7 +48,7 @@ module Gloo
45
48
  # Run for each file.
46
49
  #
47
50
  def run
48
- folder = @iterator_obj.in_value
51
+ folder = EachFile.expand_home( @iterator_obj.in_value )
49
52
  return unless folder
50
53
 
51
54
  unless Dir.exist?( folder )
@@ -53,20 +56,86 @@ module Gloo
53
56
  @engine.log.info "Folder does not exist: #{folder}"
54
57
  end
55
58
 
56
- Dir.glob( "#{folder}#{wildcard}" ).each do |f|
59
+ Dir.glob( EachFile.pattern( folder, recursive? ) ).each do |f|
60
+ next unless include?( f )
61
+
57
62
  set_file f
58
63
  @iterator_obj.run_do
59
64
  end
60
65
  end
61
66
 
62
- #
63
- # Get the wildcard for the glob.
64
- #
65
- def wildcard
66
- o = @iterator_obj.find_child EXT
67
- return WILD unless o
67
+ #
68
+ # Expand a leading ~ to the home folder.
69
+ # Other paths are left as they are, so relative paths stay relative.
70
+ #
71
+ def self.expand_home( folder )
72
+ return folder unless folder.is_a?( ::String ) && folder.start_with?( '~' )
73
+
74
+ return File.expand_path( folder )
75
+ end
76
+
77
+ #
78
+ # Get the glob pattern for everything in the folder, and
79
+ # optionally everything in its subfolders.
80
+ # The folder may itself include wildcards.
81
+ #
82
+ def self.pattern( folder, recursive )
83
+ return File.join( folder, RECURSE, WILD ) if recursive
84
+
85
+ return File.join( folder, WILD )
86
+ end
87
+
88
+ #
89
+ # Does the folder path include glob wildcards?
90
+ #
91
+ def self.wildcard?( folder )
92
+ return folder.to_s.match?( /[*?\[{]/ )
93
+ end
94
+
95
+ #
96
+ # Should the loop run for this path?
97
+ # Folders only when include_dirs is set; files only when they
98
+ # match the extension, if there is one.
99
+ #
100
+ def include?( f )
101
+ return include_dirs? if File.directory?( f )
102
+ return false unless File.file?( f )
103
+
104
+ return ext_match?( f )
105
+ end
106
+
107
+ #
108
+ # Does the file have the extension we're looking for?
109
+ # The match ignores case, so md matches .MD on every platform.
110
+ #
111
+ def ext_match?( f )
112
+ ext = @iterator_obj.find_child_value EXT
113
+ return true if ext.nil? || ext.to_s.strip.empty?
68
114
 
69
- return "#{WILD}.#{o.value}"
115
+ ext = ext.to_s.strip.delete_prefix( '.' )
116
+ return File.extname( f ).casecmp?( ".#{ext}" )
117
+ end
118
+
119
+ #
120
+ # Walk the subfolders too?
121
+ #
122
+ def recursive?
123
+ return child_true?( RECURSIVE )
124
+ end
125
+
126
+ #
127
+ # List folders as well as files?
128
+ #
129
+ def include_dirs?
130
+ return child_true?( INCLUDE_DIRS )
131
+ end
132
+
133
+ #
134
+ # Is the named child present and true?
135
+ #
136
+ def child_true?( name )
137
+ value = @iterator_obj.find_child_value name
138
+ return Gloo::Objs::Boolean.coerse_to_bool( value )
70
139
  end
71
140
 
72
141
  #
@@ -164,10 +164,10 @@ module Gloo
164
164
  #
165
165
  # Invoke the function, run the script and return the result.
166
166
  #
167
- # If on_invoke itself hits an error, it is left alone rather
168
- # than being clobbered with an unreliable result - callers can
169
- # tell this happened because the engine is left in an error
170
- # state (@engine.heap.error), same as any other failed verb.
167
+ # If on_invoke itself hits an error, there is no reliable
168
+ # result, so it is set to false (like any other command that
169
+ # would set it and fails), and the engine is left in an error
170
+ # state (@engine.heap.error).
171
171
  #
172
172
  def invoke args
173
173
  @engine.log.debug "Invoking function: #{name}"
@@ -179,6 +179,7 @@ module Gloo
179
179
  failed = @engine.heap.error.error_count.positive?
180
180
 
181
181
  if failed
182
+ @engine.heap.it.set_to false
182
183
  run_after_invoke
183
184
  return nil
184
185
  end
@@ -230,7 +231,7 @@ module Gloo
230
231
  'result (string) — The result of the function to return to the caller.'
231
232
  ],
232
233
  :messages => [
233
- 'invoke — Invoke the function, set it and return the result. The function result is put into it as well as being returned to the caller.'
234
+ 'invoke — Invoke the function, set it and return the result. The function result is put into it as well as being returned to the caller. If on_invoke hits an error, there is no result: it is false, and after_invoke still runs.'
234
235
  ],
235
236
  :examples => <<~EXAMPLES.strip
236
237
  #
@@ -73,14 +73,17 @@ module Gloo
73
73
  # Add the given modifier to the date.
74
74
  #
75
75
  def msg_add
76
- modifier = "1 day"
76
+ data = nil
77
77
  if @params&.token_count&.positive?
78
78
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
79
79
  data = expr.evaluate
80
- modifier = data
81
80
  end
81
+ modifier = DtTools.amount( @engine, data )
82
+ return false unless modifier
82
83
 
83
- dt = Chronic.parse( self.value )
84
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
85
+ return false unless dt
86
+
84
87
  new_value = DtTools.add( dt, modifier )
85
88
  self.set_value( new_value )
86
89
  @engine.heap.it.set_to self.value
@@ -90,14 +93,17 @@ module Gloo
90
93
  # Subtract the given modifier from the date.
91
94
  #
92
95
  def msg_sub
93
- modifier = "1 day"
96
+ data = nil
94
97
  if @params&.token_count&.positive?
95
98
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
96
99
  data = expr.evaluate
97
- modifier = data
98
100
  end
101
+ modifier = DtTools.amount( @engine, data )
102
+ return false unless modifier
103
+
104
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
105
+ return false unless dt
99
106
 
100
- dt = Chronic.parse( self.value )
101
107
  self.set_value( DtTools.sub( dt, modifier ) )
102
108
  @engine.heap.it.set_to self.value
103
109
  end
@@ -106,7 +112,9 @@ module Gloo
106
112
  # Get the month.
107
113
  #
108
114
  def msg_mm
109
- dt = Chronic.parse( self.value )
115
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
116
+ return false unless dt
117
+
110
118
  @engine.heap.it.set_to "#{dt.month}".rjust(2, '0')
111
119
  end
112
120
 
@@ -114,7 +122,9 @@ module Gloo
114
122
  # Get the day.
115
123
  #
116
124
  def msg_dd
117
- dt = Chronic.parse( self.value )
125
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
126
+ return false unless dt
127
+
118
128
  @engine.heap.it.set_to "#{dt.day}".rjust(2, '0')
119
129
  end
120
130
 
@@ -122,7 +132,9 @@ module Gloo
122
132
  # Get the year.
123
133
  #
124
134
  def msg_yyyy
125
- dt = Chronic.parse( self.value )
135
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
136
+ return false unless dt
137
+
126
138
  @engine.heap.it.set_to dt.year.to_s
127
139
  end
128
140
 
@@ -130,7 +142,9 @@ module Gloo
130
142
  # Get the year (2 digit).
131
143
  #
132
144
  def msg_yy
133
- dt = Chronic.parse( self.value )
145
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
146
+ return false unless dt
147
+
134
148
  @engine.heap.it.set_to dt.year.to_s[-2..-1]
135
149
  end
136
150
 
@@ -146,7 +160,9 @@ module Gloo
146
160
  data = expr.evaluate
147
161
  format = data
148
162
  end
149
- dt = Chronic.parse( self.value )
163
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
164
+ return false unless dt
165
+
150
166
  @engine.heap.it.set_to dt.strftime( format )
151
167
  end
152
168
 
@@ -175,6 +191,7 @@ module Gloo
175
191
  'yyyy — Get the 4 digit year portion of the date. Put the 4 digit year into it.',
176
192
  'format ({fmt}) — Format the date using a strftime-style format string. Default if none given: %Y-%m-%d. It will have the formatted string.'
177
193
  ],
194
+ :notes => 'A value that isn\'t a date (eg. \'2026-02-31\') is left with no value, with a warning: "\'2026-02-31\' is not a date; using no value."',
178
195
  :examples => <<~EXAMPLES.strip
179
196
  #
180
197
  # Show the current date.