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
@@ -69,14 +69,17 @@ module Gloo
69
69
  # Add the given modifier to the date.
70
70
  #
71
71
  def msg_add
72
- modifier = "1 day"
72
+ data = nil
73
73
  if @params&.token_count&.positive?
74
74
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
75
75
  data = expr.evaluate
76
- modifier = data
77
76
  end
77
+ modifier = DtTools.amount( @engine, data )
78
+ return false unless modifier
78
79
 
79
- dt = Chronic.parse( self.value )
80
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
81
+ return false unless dt
82
+
80
83
  new_value = DtTools.add( dt, modifier )
81
84
  self.set_value( new_value )
82
85
  @engine.heap.it.set_to self.value
@@ -86,14 +89,17 @@ module Gloo
86
89
  # Subtract the given modifier from the date.
87
90
  #
88
91
  def msg_sub
89
- modifier = "1 day"
92
+ data = nil
90
93
  if @params&.token_count&.positive?
91
94
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
92
95
  data = expr.evaluate
93
- modifier = data
94
96
  end
97
+ modifier = DtTools.amount( @engine, data )
98
+ return false unless modifier
99
+
100
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
101
+ return false unless dt
95
102
 
96
- dt = Chronic.parse( self.value )
97
103
  self.set_value( DtTools.sub( dt, modifier ) )
98
104
  @engine.heap.it.set_to self.value
99
105
  end
@@ -102,7 +108,10 @@ module Gloo
102
108
  # Set the value to the beginning of the month.
103
109
  #
104
110
  def msg_begin_month
105
- dt = self.value.to_time.beginning_of_month
111
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
112
+ return false unless dt
113
+
114
+ dt = dt.to_time.beginning_of_month
106
115
  self.set_value dt
107
116
  @engine.heap.it.set_to dt
108
117
  end
@@ -111,7 +120,10 @@ module Gloo
111
120
  # Set the value to the end of the month.
112
121
  #
113
122
  def msg_end_month
114
- dt = self.value.to_time.end_of_month
123
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
124
+ return false unless dt
125
+
126
+ dt = dt.to_time.end_of_month
115
127
  self.set_value dt
116
128
  @engine.heap.it.set_to dt
117
129
  end
@@ -120,7 +132,10 @@ module Gloo
120
132
  # Set the value to the beginning of the year.
121
133
  #
122
134
  def msg_begin_year
123
- dt = self.value.to_time.beginning_of_year
135
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
136
+ return false unless dt
137
+
138
+ dt = dt.to_time.beginning_of_year
124
139
  self.set_value dt
125
140
  @engine.heap.it.set_to dt
126
141
  end
@@ -129,7 +144,10 @@ module Gloo
129
144
  # Set the value to the end of the year.
130
145
  #
131
146
  def msg_end_year
132
- dt = self.value.to_time.end_of_year
147
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
148
+ return false unless dt
149
+
150
+ dt = dt.to_time.end_of_year
133
151
  self.set_value dt
134
152
  @engine.heap.it.set_to dt
135
153
  end
@@ -138,7 +156,10 @@ module Gloo
138
156
  # Set the value to the beginning of the week.
139
157
  #
140
158
  def msg_begin_week
141
- dt = self.value.to_time.beginning_of_week( start_day = :sunday )
159
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
160
+ return false unless dt
161
+
162
+ dt = dt.to_time.beginning_of_week( start_day = :sunday )
142
163
  self.set_value dt
143
164
  @engine.heap.it.set_to dt
144
165
  end
@@ -147,7 +168,10 @@ module Gloo
147
168
  # Set the value to the end of the week.
148
169
  #
149
170
  def msg_end_week
150
- dt = self.value.to_time.end_of_week( start_day = :sunday )
171
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
172
+ return false unless dt
173
+
174
+ dt = dt.to_time.end_of_week( start_day = :sunday )
151
175
  self.set_value dt
152
176
  @engine.heap.it.set_to dt
153
177
  end
@@ -156,7 +180,10 @@ module Gloo
156
180
  # Set the value to the beginning of the day.
157
181
  #
158
182
  def msg_begin_day
159
- dt = self.value.to_time.beginning_of_day
183
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
184
+ return false unless dt
185
+
186
+ dt = dt.to_time.beginning_of_day
160
187
  self.set_value dt
161
188
  @engine.heap.it.set_to dt
162
189
  end
@@ -165,7 +192,10 @@ module Gloo
165
192
  # Set the value to the end of the day.
166
193
  #
167
194
  def msg_end_day
168
- dt = self.value.to_time.end_of_day
195
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
196
+ return false unless dt
197
+
198
+ dt = dt.to_time.end_of_day
169
199
  self.set_value dt
170
200
  @engine.heap.it.set_to dt
171
201
  end
@@ -175,7 +205,10 @@ module Gloo
175
205
  # Tell the datetime to check if it is today.
176
206
  #
177
207
  def msg_is_today
178
- today = DtTools.is_today?( self.value )
208
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
209
+ return false unless dt
210
+
211
+ today = DtTools.is_today?( dt )
179
212
  @engine.heap.it.set_to today
180
213
  return today
181
214
  end
@@ -184,7 +217,10 @@ module Gloo
184
217
  # Tell the datetime to check if it is in the future.
185
218
  #
186
219
  def msg_is_future
187
- today = DtTools.is_future?( self.value )
220
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
221
+ return false unless dt
222
+
223
+ today = DtTools.is_future?( dt )
188
224
  @engine.heap.it.set_to today
189
225
  return today
190
226
  end
@@ -193,7 +229,10 @@ module Gloo
193
229
  # Tell the datetime to check if it is in the past.
194
230
  #
195
231
  def msg_is_past
196
- today = DtTools.is_past?( self.value )
232
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
233
+ return false unless dt
234
+
235
+ today = DtTools.is_past?( dt )
197
236
  @engine.heap.it.set_to today
198
237
  return today
199
238
  end
@@ -202,7 +241,10 @@ module Gloo
202
241
  # Tell the datetime to check if it is yesterday.
203
242
  #
204
243
  def msg_is_yesterday
205
- today = DtTools.is_yesterday?( self.value )
244
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
245
+ return false unless dt
246
+
247
+ today = DtTools.is_yesterday?( dt )
206
248
  @engine.heap.it.set_to today
207
249
  return today
208
250
  end
@@ -211,7 +253,10 @@ module Gloo
211
253
  # Tell the datetime to check if it is tomorrow.
212
254
  #
213
255
  def msg_is_tomorrow
214
- today = DtTools.is_tomorrow?( self.value )
256
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
257
+ return false unless dt
258
+
259
+ today = DtTools.is_tomorrow?( dt )
215
260
  @engine.heap.it.set_to today
216
261
  return today
217
262
  end
@@ -220,7 +265,10 @@ module Gloo
220
265
  # Tell the datetime to check if it is this week.
221
266
  #
222
267
  def msg_is_this_week
223
- today = DtTools.is_this_week?( self.value )
268
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
269
+ return false unless dt
270
+
271
+ today = DtTools.is_this_week?( dt )
224
272
  @engine.heap.it.set_to today
225
273
  return today
226
274
  end
@@ -245,7 +293,9 @@ module Gloo
245
293
  data = expr.evaluate
246
294
  format = data
247
295
  end
248
- dt = Chronic.parse( self.value )
296
+ dt = DtTools.parse_value( @engine, self.value, self.class.typename )
297
+ return false unless dt
298
+
249
299
  @engine.heap.it.set_to dt.strftime( format )
250
300
  end
251
301
 
@@ -284,6 +334,7 @@ module Gloo
284
334
  'end_year — Set the datetime to the end of the year.',
285
335
  'format ({fmt}) — Format the date and time using a strftime-style format string. Default if none given: %Y-%m-%d %H:%M:%S. It will have the formatted string.'
286
336
  ],
337
+ :notes => 'A value that isn\'t a date and time is left with no value, with a warning. A clock time out of range (eg. \'25:99\') is used as a best guess, with a warning that shows the value used.',
287
338
  :examples => <<~EXAMPLES.strip
288
339
  #
289
340
  # Show the current date and time.
@@ -22,6 +22,50 @@ class DtTools
22
22
  end
23
23
 
24
24
 
25
+ UNITS = %w[second minute hour day week fortnight month year].freeze
26
+
27
+ #
28
+ # Get a date/time object's value as a Time, for a message to work
29
+ # with. If it isn't a date or time (Chronic can't read it), report
30
+ # that, put false into it, and return nil.
31
+ #
32
+ def self.parse_value( engine, value, kind )
33
+ unless value.blank?
34
+ dt = is_dt_type?( value ) ? value : Chronic.parse( value.to_s )
35
+ return dt if dt
36
+ end
37
+
38
+ msg = value.blank? ? "There is no #{kind} to work with!" : "'#{value}' is not a #{kind}."
39
+ engine.err msg
40
+ engine.heap.it.set_to false
41
+ return nil
42
+ end
43
+
44
+ #
45
+ # Get the amount for add or sub, in the form "1 day" or "3 months".
46
+ # No amount is 1 of the default unit. A number with no unit is that
47
+ # many of the default unit (a best guess, with a warning). Anything
48
+ # else that isn't a number and a unit is reported; put false into it
49
+ # and return nil.
50
+ #
51
+ def self.amount( engine, data, default_unit = 'day' )
52
+ return "1 #{default_unit}" if data.nil?
53
+
54
+ text = data.to_s.strip
55
+ if text.match?( /\A-?\d+\z/ )
56
+ unit = text == '1' ? default_unit : "#{default_unit}s"
57
+ engine.warn "'#{text}' has no unit; using #{text} #{unit}."
58
+ return "#{text} #{default_unit}"
59
+ end
60
+
61
+ count, unit = text.split( ' ' )
62
+ return text if count&.match?( /\A-?\d+\z/ ) && UNITS.include?( unit.to_s.downcase.chomp( 's' ) )
63
+
64
+ engine.err "'#{text}' is not an amount like '1 day' or '3 months'."
65
+ engine.heap.it.set_to false
66
+ return nil
67
+ end
68
+
25
69
  # ---------------------------------------------------------------------
26
70
  # Date Math
27
71
  # ---------------------------------------------------------------------
@@ -35,7 +79,7 @@ class DtTools
35
79
  amount, unit = modifier.split(' ')
36
80
 
37
81
  # converts "1 day" to 1.day
38
- duration = amount.to_i.send( unit )
82
+ duration = amount.to_i.send( unit.downcase )
39
83
  return date + duration
40
84
  end
41
85
 
@@ -48,7 +92,7 @@ class DtTools
48
92
  amount, unit = modifier.split(' ')
49
93
 
50
94
  # converts "1 day" to 1.day
51
- duration = amount.to_i.send( unit )
95
+ duration = amount.to_i.send( unit.downcase )
52
96
  return dt - duration
53
97
  end
54
98
 
@@ -73,14 +73,17 @@ module Gloo
73
73
  # Add the given modifier to the time.
74
74
  #
75
75
  def msg_add
76
- modifier = "1 hour"
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, 'hour' )
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 time.
91
94
  #
92
95
  def msg_sub
93
- modifier = "1 hour"
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, 'hour' )
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 hour.
107
113
  #
108
114
  def msg_hh
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.hour}".rjust(2, '0')
111
119
  end
112
120
 
@@ -114,7 +122,9 @@ module Gloo
114
122
  # Get the minute.
115
123
  #
116
124
  def msg_mm
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.min}".rjust(2, '0')
119
129
  end
120
130
 
@@ -122,7 +132,9 @@ module Gloo
122
132
  # Get the second.
123
133
  #
124
134
  def msg_ss
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.sec}".rjust(2, '0')
127
139
  end
128
140
 
@@ -130,7 +142,9 @@ module Gloo
130
142
  # Get the AM/PM.
131
143
  #
132
144
  def msg_am
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.strftime( '%p' )
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
  'am — Get the am/pm portion of the time. Put "am" or "pm" into it.',
176
192
  'format ({fmt}) — Format the time using a strftime-style format string. Default if none given: %H:%M:%S. It will have the formatted string.'
177
193
  ],
194
+ :notes => 'A value that isn\'t a time is left with no value, with a warning. A clock time out of range (eg. \'25:99\') is used as a best guess, with a warning that shows the time used.',
178
195
  :examples => <<~EXAMPLES.strip
179
196
  #
180
197
  # Show the current time.
@@ -14,6 +14,10 @@ module Gloo
14
14
  KEYWORD_SHORT = 'crypt'.freeze
15
15
 
16
16
  CIPHER_TYPE = 'AES-256-CBC'.freeze
17
+
18
+ # A key or vector of the wrong size, or data encrypted with a
19
+ # different key.
20
+ CIPHER_ERRORS = [ ArgumentError, OpenSSL::Cipher::CipherError ].freeze
17
21
  KEY = 'key'.freeze
18
22
  INIT_VECTOR = 'init_vector'.freeze
19
23
  DATA = 'data'.freeze
@@ -81,6 +85,7 @@ module Gloo
81
85
  # Update the data value of the object.
82
86
  #
83
87
  def update_data( new_val )
88
+ @engine.heap.it.set_to new_val
84
89
  o = find_child_resolve_alias DATA
85
90
  return unless o
86
91
 
@@ -143,14 +148,22 @@ module Gloo
143
148
  # Decrypt the encrypted child object.
144
149
  #
145
150
  def msg_decrypt
146
- update_data Cipher.decrypt( data, key, init_vector )
151
+ result = nil
152
+ return false unless attempt( "decrypt #{pn}", *CIPHER_ERRORS ) do
153
+ result = Cipher.decrypt( data, key, init_vector )
154
+ end
155
+ update_data result
147
156
  end
148
157
 
149
158
  #
150
159
  # Encrypt the decrypted child object.
151
160
  #
152
161
  def msg_encrypt
153
- update_data Cipher.encrypt( data, key, init_vector )
162
+ result = nil
163
+ return false unless attempt( "encrypt #{pn}", *CIPHER_ERRORS ) do
164
+ result = Cipher.encrypt( data, key, init_vector )
165
+ end
166
+ update_data result
154
167
  end
155
168
 
156
169
  # ---------------------------------------------------------------------
@@ -204,8 +217,8 @@ module Gloo
204
217
  ],
205
218
  :messages => [
206
219
  'generate_keys — Generate an encryption key and initialization vector. The keys are base64 encoded.',
207
- 'encrypt — Encrypt the data.',
208
- 'decrypt — Decrypt the data.'
220
+ 'encrypt — Encrypt the data. The data child is changed. It will have the encrypted data.',
221
+ 'decrypt — Decrypt the data. The data child is changed. It will have the decrypted data.'
209
222
  ],
210
223
  :examples => <<~EXAMPLES.strip
211
224
  #
@@ -79,6 +79,7 @@ module Gloo
79
79
  # Update the hashed password value.
80
80
  #
81
81
  def update_hash( new_hash )
82
+ @engine.heap.it.set_to new_hash
82
83
  o = find_child_resolve_alias HASH
83
84
  return unless o
84
85
 
@@ -154,7 +155,10 @@ module Gloo
154
155
  # Uses the salt and the hash to check the password.
155
156
  #
156
157
  def msg_check
157
- hashed_pwd = BCrypt::Password.new( hash )
158
+ hashed_pwd = nil
159
+ return false unless attempt( "check the password against the hash in #{pn}", BCrypt::Errors::InvalidHash ) do
160
+ hashed_pwd = BCrypt::Password.new( hash )
161
+ end
158
162
  result = ( hashed_pwd == salt_pwd )
159
163
  @engine.heap.it.set_to result
160
164
  end
@@ -178,7 +182,7 @@ module Gloo
178
182
  ],
179
183
  :messages => [
180
184
  'generate ({len}) — Generate a new password, put in the password child object. The {len} parameter is optional; the length is 7 if not specified.',
181
- 'hash — Hash the password in the child object. Put it in the hash child object.',
185
+ 'hash — Hash the password in the child object. Put it in the hash child object. It will have the hash.',
182
186
  'check — Check the password in the child object. Is it the same as the hash?'
183
187
  ],
184
188
  :examples => <<~EXAMPLES.strip
@@ -42,6 +42,7 @@ module Gloo
42
42
  # Set the result of the ERB template conversion.
43
43
  #
44
44
  def set_result( data )
45
+ @engine.heap.it.set_to data
45
46
  r = find_child_resolve_alias RESULT
46
47
  return unless r
47
48
 
@@ -106,8 +107,13 @@ module Gloo
106
107
  tmpl = template_value
107
108
  return unless tmpl
108
109
 
109
- render = ERB.new( tmpl )
110
- set_result render.result_with_hash( param_hash )
110
+ # The template is Ruby code the script wrote, so anything it
111
+ # raises (including a syntax error) is the script's problem.
112
+ result = nil
113
+ return false unless attempt( "render the template in #{pn}", StandardError, ScriptError ) do
114
+ result = ERB.new( tmpl ).result_with_hash( param_hash )
115
+ end
116
+ set_result result
111
117
  end
112
118
 
113
119
  # ---------------------------------------------------------------------
@@ -130,7 +136,7 @@ module Gloo
130
136
  'result (text) — The result of the template rendering.'
131
137
  ],
132
138
  :messages => [
133
- 'run — Render the result based on the template and the parameter values.'
139
+ 'run — Render the result based on the template and the parameter values. It will have the rendered result.'
134
140
  ],
135
141
  :examples => <<~EXAMPLES.strip
136
142
  e [can] :
@@ -12,7 +12,6 @@ module Gloo
12
12
  KEYWORD_SHORT = 'dir'.freeze
13
13
 
14
14
  FILE_NAME_ERR = 'file and path name expected'.freeze
15
- FILE_MISSING_ERR = 'file not found'.freeze
16
15
 
17
16
 
18
17
  #
@@ -59,7 +58,7 @@ module Gloo
59
58
  # Show the contents of the file, paginated.
60
59
  #
61
60
  def msg_page
62
- return unless value && File.file?( value )
61
+ return unless check_is_file?
63
62
 
64
63
  system "less #{value}"
65
64
  end
@@ -68,25 +67,32 @@ module Gloo
68
67
  # Show the contents of the file.
69
68
  #
70
69
  def msg_show
71
- return unless value && File.file?( value )
70
+ return unless check_is_file?
72
71
 
73
- puts File.read( value )
72
+ data = file_op( 'read' ) { File.read( value ) }
73
+ puts data if data
74
74
  end
75
75
 
76
76
  #
77
77
  # Read the contents of the file into the object.
78
78
  #
79
79
  def msg_read
80
- return unless check_file_exists?
80
+ return @engine.heap.it.set_to( false ) unless check_file_exists?
81
+
82
+ data = file_op( 'read' ) { File.read( value ) }
83
+ return @engine.heap.it.set_to( false ) unless data
81
84
 
82
- data = File.read( value )
83
85
  if @params&.token_count&.positive?
84
86
  pn = Gloo::Core::Pn.new( @engine, @params.first )
85
87
  o = pn.resolve
88
+ unless o
89
+ @engine.err Gloo::Core::NotFound.object( @params.first )
90
+ return @engine.heap.it.set_to( false )
91
+ end
92
+
86
93
  o.set_value data
87
- else
88
- @engine.heap.it.set_to data
89
94
  end
95
+ @engine.heap.it.set_to data
90
96
  end
91
97
 
92
98
  #
@@ -101,9 +107,11 @@ module Gloo
101
107
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
102
108
  data = expr.evaluate
103
109
  end
104
- existing = File.exist?( value ) ? File.read( value ) : ''
105
- prefix = existing.empty? || existing.end_with?( "\n" ) ? '' : "\n"
106
- File.open( value, 'a' ) { |f| f.puts "#{prefix}#{data}" }
110
+ file_op( 'append to' ) do
111
+ existing = File.exist?( value ) ? File.read( value ) : ''
112
+ prefix = existing.empty? || existing.end_with?( "\n" ) ? '' : "\n"
113
+ File.open( value, 'a' ) { |f| f.puts "#{prefix}#{data}" }
114
+ end
107
115
  end
108
116
 
109
117
  #
@@ -117,7 +125,7 @@ module Gloo
117
125
  expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
118
126
  data = expr.evaluate
119
127
  end
120
- File.write( value, data )
128
+ file_op( 'write' ) { File.write( value, data ) }
121
129
  end
122
130
 
123
131
  #
@@ -125,7 +133,8 @@ module Gloo
125
133
  #
126
134
  def msg_delete
127
135
  return unless value
128
- File.delete( value )
136
+
137
+ file_op( 'delete' ) { File.delete( value ) }
129
138
  end
130
139
 
131
140
  #
@@ -156,7 +165,7 @@ module Gloo
156
165
  # Create a directory.
157
166
  #
158
167
  def msg_mkdir
159
- FileUtils.mkdir_p(value) unless Dir.exist?(value)
168
+ file_op( 'make folder' ) { FileUtils.mkdir_p( value ) } unless Dir.exist?( value )
160
169
  end
161
170
 
162
171
  #
@@ -205,10 +214,10 @@ module Gloo
205
214
  # Get the SHA256 hash of the file contents.
206
215
  #
207
216
  def msg_get_sha256
208
- return unless check_file_exists?
217
+ return @engine.heap.it.set_to( false ) unless check_file_exists?
209
218
 
210
- file_hash = FileHandle.hash_for_file( value )
211
- @engine.heap.it.set_to file_hash
219
+ file_hash = file_op( 'read' ) { FileHandle.hash_for_file( value ) }
220
+ @engine.heap.it.set_to( file_hash || false )
212
221
  end
213
222
 
214
223
  #
@@ -221,18 +230,45 @@ module Gloo
221
230
  return file_hash
222
231
  end
223
232
 
233
+ #
234
+ # Run a file system operation. A failure the file system reports
235
+ # (no such file, a folder where a file was expected, no permission,
236
+ # ...) is a runtime error, and the result is nil.
237
+ #
238
+ def file_op( action )
239
+ return yield
240
+ rescue Errno::ENOENT
241
+ @engine.err Gloo::Core::NotFound.file( value )
242
+ return nil
243
+ rescue SystemCallError => e
244
+ @engine.err "Could not #{action} '#{value}': #{e.message}"
245
+ return nil
246
+ end
247
+
248
+ #
249
+ # Check that the file exists and is a file, not a folder.
250
+ # Report an error if not.
251
+ #
252
+ def check_is_file?
253
+ return false unless check_file_exists?
254
+ return true if File.file?( value )
255
+
256
+ @engine.err "'#{value}' is a folder, not a file."
257
+ return false
258
+ end
259
+
224
260
  #
225
261
  # Check to see if the file exists.
226
- # Show error if not.
262
+ # Report an error if not.
227
263
  #
228
264
  def check_file_exists?
229
265
  if value.blank?
230
- @engine.log.error FILE_NAME_ERR
266
+ @engine.err FILE_NAME_ERR
231
267
  return false
232
268
  end
233
269
 
234
270
  unless File.exist?( value )
235
- @engine.log.error FILE_MISSING_ERR
271
+ @engine.err Gloo::Core::NotFound.file( value )
236
272
  return false
237
273
  end
238
274
 
@@ -254,7 +290,7 @@ module Gloo
254
290
  'on disk. The string value of the file object is the path ' \
255
291
  'and name of the file.',
256
292
  :messages => [
257
- 'read ({into.obj}) — Read the file and put the data in the specified object. If {into.obj} is not specified, the data will be in it.',
293
+ 'read ({into.obj}) — Read the file and put the data in the specified object. If {into.obj} is not specified, the data will be in it. If {into.obj} does not exist, that is an error.',
258
294
  'write ({from.obj}) — Write the data in {from.obj} into the file.',
259
295
  'append ({data}) — Append the given data to the file as a new line. Adds a leading newline first if the file doesn\'t already end with one.',
260
296
  'delete — Delete the file.',