gloo 7.1 → 7.1.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +0 -2
  3. data/docs/application.md +9 -0
  4. data/docs/development.md +22 -2
  5. data/docs/iterators.md +25 -1
  6. data/docs/language_syntax.md +1 -1
  7. data/docs/plugins.md +1 -1
  8. data/docs/verbs.md +2 -1
  9. data/lib/VERSION +1 -1
  10. data/lib/VERSION_NOTES +16 -0
  11. data/lib/gloo/app/args.rb +3 -5
  12. data/lib/gloo/app/log.rb +2 -2
  13. data/lib/gloo/core/invoker.rb +20 -7
  14. data/lib/gloo/core/pn.rb +5 -0
  15. data/lib/gloo/core/verb.rb +14 -3
  16. data/lib/gloo/exec/exec_env.rb +6 -3
  17. data/lib/gloo/exec/script.rb +7 -0
  18. data/lib/gloo/objs/basic/script.rb +9 -0
  19. data/lib/gloo/objs/ctrl/each.rb +5 -3
  20. data/lib/gloo/objs/ctrl/each_child.rb +6 -2
  21. data/lib/gloo/objs/ctrl/each_dir.rb +1 -1
  22. data/lib/gloo/objs/ctrl/each_file.rb +1 -1
  23. data/lib/gloo/objs/ctrl/each_line.rb +4 -3
  24. data/lib/gloo/objs/ctrl/each_word.rb +1 -1
  25. data/lib/gloo/objs/ctrl/repeat.rb +5 -3
  26. data/lib/gloo/objs/str_utils/cipher.rb +10 -2
  27. data/lib/gloo/persist/declaration_ledger.rb +6 -6
  28. data/lib/gloo/persist/disc_mech.rb +38 -20
  29. data/lib/gloo/persist/file_loader.rb +1 -1
  30. data/lib/gloo/persist/persist_man.rb +1 -1
  31. data/lib/gloo/verbs/break.rb +7 -2
  32. data/lib/gloo/verbs/create.rb +4 -2
  33. data/lib/gloo/verbs/eval.rb +5 -4
  34. data/lib/gloo/verbs/invoke.rb +7 -6
  35. data/lib/gloo/verbs/load.rb +7 -1
  36. data/lib/gloo/verbs/log.rb +8 -4
  37. data/lib/gloo/verbs/show.rb +15 -10
  38. data/lib/gloo.rb +18 -4
  39. data/lib/run.rb +3 -9
  40. data/test.gloo/app/embed.test.gloo +31 -0
  41. data/test.gloo/app/fixtures/embed_harness.rb +15 -0
  42. data/test.gloo/ctrl/each.test.gloo +72 -0
  43. data/test.gloo/lang/exceptions.test.gloo +22 -0
  44. data/test.gloo/objs/cipher.test.gloo +27 -0
  45. data/test.gloo/objs/int.test.gloo +6 -3
  46. data/test.gloo/verbs/break.test.gloo +66 -0
  47. data/test.gloo/verbs/create.test.gloo +25 -0
  48. data/test.gloo/verbs/eval.test.gloo +8 -0
  49. data/test.gloo/verbs/invoke.test.gloo +50 -0
  50. data/test.gloo/verbs/load.test.gloo +34 -0
  51. data/test.gloo/verbs/log.test.gloo +16 -0
  52. data/test.gloo/verbs/show.test.gloo +32 -0
  53. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 211368f27a7bbea86aed449d545c28f05a62857e92e8a3942394261a336e597b
4
- data.tar.gz: 98ea31342225ac55d1ae499727fe2e71bf1679a8c4f1523651e57ac50829a714
3
+ metadata.gz: e3e9f9ec23fd38faee18f95a521fe1d21a8e572b2306c814233c3fc67ae6e5f3
4
+ data.tar.gz: a1f2f961d1860473c14faefb6c5f9ffe0556bae36479cf3755b8333a1b0db68c
5
5
  SHA512:
6
- metadata.gz: c2457159e21684fbdb72367753c52204de25393f74ee715622ad9879a6b228477f580440e516fee5c0d47ea6c198f8568bd109912abb2cbb01704e1e84d94d79
7
- data.tar.gz: 0ba0bd0264f57f0334312c897a07b3023bd2019e835c8ec88e6e9bd827aa6af80c4a7610c1f1198958bb8948da8e2ed4981c56b725b371086df2e8950661ab07
6
+ metadata.gz: d1665486b345b607c64f98519ac7219ac720ccad75d88d135a88225e3ea527871e6305acc95db9b31a89b7ba5373a6016b7fbcb0a354dfc82ecd04a3f5733bc9
7
+ data.tar.gz: 35f9a3c3013d394ed4be04106f682ef92b14328279b12524d345f5a5a04f4ce31d7c8645d38a48484ac699a0d9d8ccc231340004a90cb0b5acdb98f890809b58
data/Rakefile CHANGED
@@ -2,8 +2,6 @@ require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
- ENV[ 'GLOO_ENV' ] = 'TEST'
6
-
7
5
  t.warning = false
8
6
  t.libs << 'test'
9
7
  t.libs << 'lib'
data/docs/application.md CHANGED
@@ -31,6 +31,8 @@ gloo [global option] [file]
31
31
 
32
32
  Running gloo with a file specified will run that file. Once that file is done, gloo will quit. However, by specifying `--cli`, once the file has finished, gloo will remain open in CLI mode.
33
33
 
34
+ Gloo can also be started from another Ruby program; see Development, Running Gloo from Ruby.
35
+
34
36
  If a file named on the command line can't be found (a typo, the wrong folder, a missing `.gloo` extension on a full path), gloo prints `File '{name}' was not found.` to stderr and exits with a non-zero status. Any other load or run error also exits non-zero — including syntax errors in a file that otherwise loaded (gloo keeps loading past them, reporting each with its file and line) — so a shell script calling gloo can tell a failed run from a clean one.
35
37
 
36
38
  When specifying a file there are a couple ways to reference the gloo file to open:
@@ -76,6 +78,11 @@ When specifying a file there are a couple ways to reference the gloo file to ope
76
78
  - Exits with status 1 if any test failed, or errors or warnings
77
79
  were logged that no test expected; 0 otherwise.
78
80
  - See Testing.
81
+ --embed - Run in Embedded mode
82
+ - Start the engine and return, for a Ruby program that drives
83
+ gloo itself.
84
+ - This is the default when gloo is created in code with no file
85
+ or mode. See Development, Running Gloo from Ruby.
79
86
  --version - Show application version
80
87
  --help - Show the help screen
81
88
  ```
@@ -158,6 +165,8 @@ Gloo writes to the `gloo.log` and `error.log` files as well as to the console.
158
165
 
159
166
  Debug messages are written to the log only, but other messages are also written to the console unless the application is running in quiet mode.
160
167
 
168
+ On the console, errors and warnings go to stderr, while a script's own output (`show`) goes to stdout. So the two can be redirected separately: `gloo my_script.gloo 2>/dev/null` shows just the script's output.
169
+
161
170
  Only error and warning level messages are written to the error log.
162
171
 
163
172
  In test mode (`--test`), errors and warnings go to the log files only, not the console, so the test output is just the test results. An error a test expects (or one from a test file that deliberately contains a problem) is still in `error.log`. The summary at the end of a test run says how many errors and warnings were logged, and by which tests, leaving out those a test expects (`expect_errors`). See Testing.
data/docs/development.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Development
2
2
 
3
- How the interpreter's source is laid out, how to run its tests, and the conventions to follow when changing it. This page is for working on gloo itself; for using gloo, start with Getting Started.
3
+ How the interpreter's source is laid out, how to run its tests, the conventions to follow when changing it, and how to run gloo from your own Ruby code. This page is for working on gloo itself or driving it from Ruby; for using gloo, start with Getting Started.
4
4
 
5
5
  **Contents**
6
6
 
@@ -8,6 +8,7 @@ How the interpreter's source is laid out, how to run its tests, and the conventi
8
8
  - Tests
9
9
  - Documentation
10
10
  - Reporting Errors
11
+ - Running Gloo from Ruby
11
12
 
12
13
  ## Source Layout
13
14
 
@@ -40,7 +41,7 @@ There are two automated suites. Run both when making a change, and add to both:
40
41
 
41
42
  ### Unit tests — `test/`
42
43
 
43
- Standard Ruby minitest tests. The layout mirrors `lib/gloo/`, one test file per class or verb: `test/<area>/<name>_test.rb`, for example `test/verbs/show_test.rb` for `lib/gloo/verbs/show.rb`. Test classes inherit from `GlooTest` (in `test/base_test.rb`).
44
+ Standard Ruby minitest tests. The layout mirrors `lib/gloo/`, one test file per class or verb: `test/<area>/<name>_test.rb`, for example `test/verbs/show_test.rb` for `lib/gloo/verbs/show.rb`. Test classes inherit from `BaseTest` (in `test/base_test.rb`), or from `BaseEngineTest` (in `test/base_engine_test.rb`) for a test that needs a started engine in `@engine`.
44
45
 
45
46
  ```
46
47
  rake test
@@ -98,3 +99,22 @@ Gloo reports every problem in a way scripts can see (see Language, Syntax, Error
98
99
  - A question whose answer is "no" is just a result, not an error.
99
100
 
100
101
  Don't use `@engine.log.error` or `@engine.log.warn` for these; they only write to the log, and scripts can't see them. Build "not found" messages with `Gloo::Core::NotFound` (for example `NotFound.object( name )`) so they always read the same way.
102
+
103
+ ## Running Gloo from Ruby
104
+
105
+ Gloo can be started from another Ruby program, such as a test harness or an app that runs gloo commands. Create the engine with the params gloo should see — the same words you would type after `gloo` on the command line:
106
+
107
+ ```ruby
108
+ require 'gloo'
109
+
110
+ context = Gloo::App::EngineContext.new( [] )
111
+ engine = Gloo::App::Engine.new( context )
112
+ engine.start
113
+
114
+ engine.process_cmd "create greeting as string : 'hello'"
115
+ engine.process_cmd 'show greeting'
116
+ ```
117
+
118
+ Only those params are used. The Ruby program's own command line (`ARGV`) is never read, so running `ruby harness.rb some_file.gloo` doesn't make gloo load `some_file.gloo`. Pass anything gloo should see explicitly. Only the `gloo` command itself (`Gloo.run`) reads the command line.
119
+
120
+ With no file and no mode switch, the engine runs in Embedded mode (`--embed`): `start` sets the engine up and returns without loading or running anything, and your code drives it from there, for example with `process_cmd`.
data/docs/iterators.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - Each Line
10
10
  - Each Word
11
11
  - Repeat
12
+ - Ending a Loop Early
12
13
 
13
14
  ## Each
14
15
 
@@ -214,7 +215,7 @@ Iterate for each line in a text block (string).
214
215
  Children:
215
216
 
216
217
  - `line` (string)
217
- - The single line of text.
218
+ - The single line of text, without its line ending (`\n` or `\r\n`). A blank line is still visited, as an empty string.
218
219
  - `in` (text)
219
220
  - The block of text or string.
220
221
  - `do` (script)
@@ -328,3 +329,26 @@ repeat [can] :
328
329
  ```
329
330
 
330
331
  See also: Object Base.
332
+
333
+ ## Ending a Loop Early
334
+
335
+ `break` in a loop's `do` script ends the whole loop, not just that pass. This works for `repeat` and every kind of `each`. The script that ran the loop carries on with its next line:
336
+
337
+ ```gloo
338
+ early [can] :
339
+ on_load [script] :
340
+ tell early.r to run
341
+ show 'after the loop'
342
+ r [repeat] :
343
+ times [int] : 5
344
+ index [int] :
345
+ do [script] :
346
+ if early.r.index = 2 then break
347
+ show early.r.index
348
+ ```
349
+
350
+ This shows `0`, `1`, then `after the loop`.
351
+
352
+ `break` ends the script it's written in. So in nested loops, a `break` in the inner loop's `do` ends only the inner loop, and a `break` in a script the `do` script runs (with `run`, or a function it invokes) ends only that script, not the loop. An `each` with `group_by` still runs `on_group_end` for the group that was open when the loop ended.
353
+
354
+ See also: Break.
@@ -75,7 +75,7 @@ Gloo makes a best guess and keeps going when it can, but it never does so silent
75
75
  - **Warning** — the command was done, but probably not the way it was meant: a value that isn't really of its type, used as a best guess (`'x' is not an integer; using 0.`), or indentation that doesn't line up. A warning is only logged; it isn't an error.
76
76
  - **Just a result** — a question whose answer is "no" is not an error at all: `exists?`, `contains?`, `substring?`, and `index_of` (which gives -1) answer in `it`.
77
77
 
78
- Errors are logged with where they happened in front of the message: the file and line while a file is loading (`app.gloo:12: Unknown type 'strng'; using untyped.`), or the script and its line (`app.on_load, line 3: Object 'app.nme' was not found.`). "Not found" errors always read the same way: `Object 'x' was not found.`, `File 'x' was not found.`, `Folder 'x' was not found.`, `Verb 'x' was not found.`
78
+ Errors and warnings are logged with where they happened in front of the message: the file and line while a file is loading (`app.gloo:12: Unknown type 'strng'; using untyped.`), even when a script started the load; otherwise the script and its line (`app.on_load, line 3: Object 'app.nme' was not found.`). On the console both go to stderr, apart from a script's own output. "Not found" errors always read the same way: `Object 'x' was not found.`, `File 'x' was not found.`, `Folder 'x' was not found.`, `Verb 'x' was not found.`
79
79
 
80
80
  What happens after an error:
81
81
 
data/docs/plugins.md CHANGED
@@ -372,7 +372,7 @@ Register it from `stats_ext.rb` with `callback.register_obj( Stats )`.
372
372
 
373
373
  ### Custom `each` iterators
374
374
 
375
- An extension's object can also plug into the built-in `each` verb by providing its own iterator. `extensions/git/src/each_repo.rb` shows the shape: a plain Ruby class (not a `Gloo::Core::Obj`) with `self.use_for?( iterator_obj )` — which returns `true` when the `each` loop's iterator object looks like a match (here, when it has a `repo` child) — and a `run` method that walks whatever it's iterating over, setting the loop's child value and calling `@iterator_obj.run_do` for each item. See the git extension for the full pattern; this hook only makes sense for extensions that also register a matching object type (here, `git_repo`).
375
+ An extension's object can also plug into the built-in `each` verb by providing its own iterator. `extensions/git/src/each_repo.rb` shows the shape: a plain Ruby class (not a `Gloo::Core::Obj`) with `self.use_for?( iterator_obj )` — which returns `true` when the `each` loop's iterator object looks like a match (here, when it has a `repo` child) — and a `run` method that walks whatever it's iterating over, setting the loop's child value and calling `@iterator_obj.run_do` for each item. `run_do` returns true when a `break` in the `do` script ended it, so stop the walk then: `break if @iterator_obj.run_do`. See the git extension for the full pattern; this hook only makes sense for extensions that also register a matching object type (here, `git_repo`).
376
376
 
377
377
  ### Documenting the extension
378
378
 
data/docs/verbs.md CHANGED
@@ -86,12 +86,13 @@ If the destination doesn't exist, or the expression can't be worked out (eg. it
86
86
 
87
87
  ## Load & Save
88
88
 
89
- `load` reads a `.gloo` file into the heap and runs its `on_load` script. Give a path relative to the project folder (no extension needed) or a full path (extension required); `*` in place of a file name loads every `.gloo` file in a folder.
89
+ `load` reads a `.gloo` file into the heap and runs its `on_load` script. Give a path relative to the project folder, a full path, or one starting with `~` for your home folder; the `.gloo` extension is optional. `*` in place of a file name loads every `.gloo` file in a folder, wherever the folder is. Anything before the `*` is the start of the file names, so `my/app/a*` loads just the files whose names start with `a`.
90
90
 
91
91
  ```gloo
92
92
  > load my/project/config
93
93
  > load my/app/*
94
94
  > load ~/.my_app/settings.gloo
95
+ > load ~/.my_app/plugins/*
95
96
  ```
96
97
 
97
98
  If the name can't be resolved to a file, `load` reports `File '{name}' was not found.` — it fires `on_error` and, when the file was named on the `gloo` command line, prints the message to stderr and exits non-zero. A bad or missing file never fails silently.
data/lib/VERSION CHANGED
@@ -1 +1 @@
1
- 7.1
1
+ 7.1.2
data/lib/VERSION_NOTES CHANGED
@@ -1,3 +1,19 @@
1
+ 7.1.2 - 2026.10.28
2
+ - Fixes bug with create verb and ^.here reference.
3
+ - Behaviour change: an each over lines gives each line without its line ending.
4
+ - Load with * now works for a full path or a ~ folder, not just a folder in the project.
5
+ - Load no longer needs the .gloo extension for a full or ~ path.
6
+ - Clean run of gloo from a different ruby process (or test).
7
+ - Fixes looking for each child with a group by.
8
+
9
+
10
+ 7.1.1 - 2026.10.26
11
+ - Fixes issues with the value in it after a failed command.
12
+ - Warnings go to stderr, like errors.
13
+ - Break in a loop's do script now ends the whole loop.
14
+ - Cipher doc and test updated.
15
+
16
+
1
17
  7.1 - 2026.10.23
2
18
  - Loading an empty gloo file is now allowed and only gives a warning.
3
19
  - Adds configuration setting for tmp directory.
data/lib/gloo/app/args.rb CHANGED
@@ -12,7 +12,6 @@ module Gloo
12
12
  class Args
13
13
 
14
14
  QUIET = 'quiet'.freeze
15
- GLOO_ENV = 'GLOO_ENV'.freeze
16
15
 
17
16
  attr_reader :switches, :files, :app_path, :command_tokens
18
17
 
@@ -26,8 +25,9 @@ module Gloo
26
25
  @app_path = nil
27
26
  @command_tokens = []
28
27
 
28
+ # Only the params given; the host process's ARGV is read by
29
+ # Gloo.run, when gloo is run as the command.
29
30
  params.each { |o| process_one_arg( o ) }
30
- ARGV.each { |o| process_one_arg( o ) }
31
31
  end
32
32
 
33
33
  #
@@ -117,9 +117,7 @@ module Gloo
117
117
  # Then finally use the default: embedded mode.
118
118
  #
119
119
  def detect_mode
120
- mode = if ENV[ GLOO_ENV ] == Gloo::App::Mode::TEST.to_s
121
- Mode::TEST
122
- elsif app?
120
+ mode = if app?
123
121
  Mode::APP
124
122
  elsif version?
125
123
  Mode::VERSION
data/lib/gloo/app/log.rb CHANGED
@@ -168,13 +168,13 @@ module Gloo
168
168
 
169
169
  #
170
170
  # Write a warning message to the log.
171
- # Also write to the console unless quiet.
171
+ # Also write to the console (on stderr, like errors) unless quiet.
172
172
  #
173
173
  def warn( msg )
174
174
  @warning_count += 1
175
175
  @logger.warn msg
176
176
  @error.warn msg
177
- puts @theme.warn( msg ) if errors_to_console?
177
+ $stderr.puts @theme.warn( msg ) if errors_to_console?
178
178
  end
179
179
 
180
180
  #
@@ -18,26 +18,39 @@ module Gloo
18
18
  #
19
19
  # Resolve, validate and invoke the function at the given
20
20
  # target path, with the given raw (unevaluated) arg tokens.
21
- # Reports an error and returns nil for any failure - including
22
- # a failure inside the function itself (see Function#invoke,
23
- # which leaves `it` alone rather than setting it to an
24
- # unreliable result when that happens).
21
+ # For any failure, reports an error, puts false in `it` and
22
+ # returns nil - before the call (see failed), or inside the
23
+ # function itself (see Function#invoke).
25
24
  #
26
25
  def self.invoke( engine, target, arg_tokens )
27
26
  if target.nil? || target.to_s.strip.empty?
28
27
  engine.syntax_err NO_TARGET_ERR
29
- return nil
28
+ return failed( engine )
30
29
  end
31
30
 
32
31
  func = resolve_function( engine, target )
33
- return nil unless func
32
+ return failed( engine ) unless func
34
33
 
34
+ # An arg that couldn't be worked out was already reported, but
35
+ # the function's own error tracking would clear it if the call
36
+ # went ahead.
37
+ errors_before = engine.heap.error.error_count
35
38
  args = evaluate_arg_tokens( engine, arg_tokens )
36
- return nil unless params_count_ok?( engine, func, args )
39
+ return failed( engine ) if engine.heap.error.error_count > errors_before
40
+ return failed( engine ) unless params_count_ok?( engine, func, args )
37
41
 
38
42
  return invoke_function( engine, func, args )
39
43
  end
40
44
 
45
+ #
46
+ # The function couldn't be called (the problem was already
47
+ # reported): put false in `it`, and return nil.
48
+ #
49
+ def self.failed( engine )
50
+ engine.heap.it.set_to false
51
+ return nil
52
+ end
53
+
41
54
  #
42
55
  # Resolve the function object at the target path. Reports an
43
56
  # error and returns nil if the path doesn't resolve to
data/lib/gloo/core/pn.rb CHANGED
@@ -172,7 +172,12 @@ module Gloo
172
172
  # returns nil without reporting an error. A caller that needs the
173
173
  # object reports its own error.
174
174
  #
175
+ # A here (^) or context (@) reference is expanded first, as resolve
176
+ # does, so a new object's parent can be given the same way as any
177
+ # other path (eg. create ^.x inside a script).
178
+ #
175
179
  def get_parent
180
+ expand_refs
176
181
  o = @engine.heap.root
177
182
 
178
183
  if self.includes_path?
@@ -94,15 +94,26 @@ module Gloo
94
94
  def take_evaluated_arg
95
95
  return true unless @params
96
96
 
97
- errors_before = @engine.heap.error.error_count
98
- value = Gloo::Expr::Expression.new( @engine, @params.tokens ).evaluate
99
- return false if @engine.heap.error.error_count > errors_before
97
+ value, ok = evaluate_expr( @params.tokens )
98
+ return false unless ok
100
99
 
101
100
  @tokens = @tokens.with_token( value.to_s )
102
101
  @params = nil
103
102
  return true
104
103
  end
105
104
 
105
+ #
106
+ # Work out an expression from the given tokens. Returns the value
107
+ # and whether it worked: false if the expression reported a
108
+ # problem (already reported), in which case the value is whatever
109
+ # it had worked out so far.
110
+ #
111
+ def evaluate_expr( tokens )
112
+ errors_before = @engine.heap.error.error_count
113
+ value = Gloo::Expr::Expression.new( @engine, tokens ).evaluate
114
+ return value, @engine.heap.error.error_count == errors_before
115
+ end
116
+
106
117
  #
107
118
  # Run the verb.
108
119
  #
@@ -35,13 +35,16 @@ module Gloo
35
35
  end
36
36
 
37
37
  #
38
- # Where the line now running is, for error messages: the running
39
- # script and its line, or the file and line being loaded.
38
+ # Where the line now running is, for error messages: the file and
39
+ # line being loaded, or the running script and its line. While a
40
+ # file is being read, a problem is in that file, even when a
41
+ # script started the load.
40
42
  #
41
43
  def current_location
44
+ return @load_location if @load_location
42
45
  return @running_script.location if @running_script
43
46
 
44
- return @load_location
47
+ return nil
45
48
  end
46
49
 
47
50
  #
@@ -65,6 +65,13 @@ module Gloo
65
65
  @break_out = true
66
66
  end
67
67
 
68
+ #
69
+ # Did a break end this script's run?
70
+ #
71
+ def broke_out?
72
+ return @break_out
73
+ end
74
+
68
75
  end
69
76
  end
70
77
  end
@@ -115,6 +115,15 @@ module Gloo
115
115
  def msg_run
116
116
  s = Gloo::Exec::Script.new( @engine, self )
117
117
  s.run
118
+ @broke_out = s.broke_out?
119
+ end
120
+
121
+ #
122
+ # Did a break end this script's last run? A loop uses this to
123
+ # stop when its do script breaks.
124
+ #
125
+ def broke_out?
126
+ return @broke_out == true
118
127
  end
119
128
 
120
129
  # ---------------------------------------------------------------------
@@ -43,13 +43,15 @@ module Gloo
43
43
  end
44
44
 
45
45
  #
46
- # Run the do script once.
46
+ # Run the do script once. Returns true if a break in it ended the
47
+ # run, so the loop should stop.
47
48
  #
48
49
  def run_do
49
50
  o = find_child DO
50
- return unless o
51
+ return false unless o
51
52
 
52
53
  Gloo::Exec::Dispatch.message( @engine, 'run', o )
54
+ return o.respond_to?( :broke_out? ) && o.broke_out?
53
55
  end
54
56
 
55
57
 
@@ -132,7 +134,7 @@ module Gloo
132
134
  'One of the following, whose presence determines which kind of loop this is:',
133
135
  'child (alias) — iterate a container\'s children.',
134
136
  'word (string) — iterate the words in a string.',
135
- 'line (string) — iterate the lines in a block of text.',
137
+ 'line (string) — iterate the lines in a block of text. Each line comes without its line ending; a blank line is visited as an empty string.',
136
138
  '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
139
  'dir (file) — iterate the directories in a folder. Optional child: recursive (bool) walks the subfolders too.'
138
140
  ],
@@ -116,21 +116,25 @@ module Gloo
116
116
 
117
117
  o = Gloo::Objs::Alias.resolve_alias( @engine, o )
118
118
  o.children.each do |child|
119
+ new_group = false
119
120
  if group_mode
120
121
  group_value = group_by_value( child )
121
122
  if last_group != group_value
122
123
  last_group = group_value
124
+ new_group = true
125
+ # The ending group's end runs while child is still its last child.
123
126
  if first_time
124
127
  first_time = false
125
128
  else
126
129
  run_on_group_end
127
130
  end
128
- run_on_group_start
129
131
  end
130
132
  end
131
133
 
132
134
  set_child child
133
- @iterator_obj.run_do
135
+ # The new group's start runs once child is its first child.
136
+ run_on_group_start if new_group
137
+ break if @iterator_obj.run_do
134
138
  end
135
139
 
136
140
  if group_mode && !first_time
@@ -58,7 +58,7 @@ module Gloo
58
58
  next unless File.directory?( f )
59
59
 
60
60
  set_dir f
61
- @iterator_obj.run_do
61
+ break if @iterator_obj.run_do
62
62
  end
63
63
  end
64
64
 
@@ -60,7 +60,7 @@ module Gloo
60
60
  next unless include?( f )
61
61
 
62
62
  set_file f
63
- @iterator_obj.run_do
63
+ break if @iterator_obj.run_do
64
64
  end
65
65
  end
66
66
 
@@ -40,15 +40,16 @@ module Gloo
40
40
  # ---------------------------------------------------------------------
41
41
 
42
42
  #
43
- # Run for each line.
43
+ # Run for each line, without its line ending (\n or \r\n). A blank
44
+ # line is still visited, as an empty string.
44
45
  #
45
46
  def run
46
47
  str = @iterator_obj.in_value
47
48
  return unless str
48
49
 
49
- str.each_line do |line|
50
+ str.each_line( chomp: true ) do |line|
50
51
  set_line line
51
- @iterator_obj.run_do
52
+ break if @iterator_obj.run_do
52
53
  end
53
54
  end
54
55
 
@@ -48,7 +48,7 @@ module Gloo
48
48
 
49
49
  str.split( ' ' ).each do |word|
50
50
  set_word word
51
- @iterator_obj.run_do
51
+ break if @iterator_obj.run_do
52
52
  end
53
53
  end
54
54
 
@@ -37,13 +37,15 @@ module Gloo
37
37
  end
38
38
 
39
39
  #
40
- # Run the do script once.
40
+ # Run the do script once. Returns true if a break in it ended the
41
+ # run, so the loop should stop.
41
42
  #
42
43
  def run_do
43
44
  o = find_child DO
44
- return unless o
45
+ return false unless o
45
46
 
46
47
  Gloo::Exec::Dispatch.message( @engine, 'run', o )
48
+ return o.respond_to?( :broke_out? ) && o.broke_out?
47
49
  end
48
50
 
49
51
  #
@@ -98,7 +100,7 @@ module Gloo
98
100
  def msg_run
99
101
  times.times do |index|
100
102
  set_index index
101
- run_do
103
+ break if run_do
102
104
  end
103
105
  end
104
106
 
@@ -209,7 +209,8 @@ module Gloo
209
209
  {
210
210
  :name => KEYWORD,
211
211
  :shortcut => KEYWORD_SHORT,
212
- :description => 'Tool to encrypt and decrypt data.',
212
+ :description => 'Tool to encrypt and decrypt data, with ' \
213
+ 'AES-256-CBC.',
213
214
  :children => [
214
215
  'key (string) — The encryption key. Base64 encoded.',
215
216
  'init_vector (string) — The initialization vector. Base64 encoded.',
@@ -218,8 +219,15 @@ module Gloo
218
219
  :messages => [
219
220
  'generate_keys — Generate an encryption key and initialization vector. The keys are base64 encoded.',
220
221
  '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.'
222
+ 'decrypt — Decrypt the data. The data child is changed. It will have the decrypted data. With the wrong key or init_vector, decrypt usually reports an error and it is false; but not always (see notes).'
222
223
  ],
224
+ :notes => 'AES-CBC can\'t always tell that a key is wrong. A ' \
225
+ 'wrong key usually leaves the decrypted data with padding ' \
226
+ 'that doesn\'t check out, which is reported as an error. But ' \
227
+ 'about 1 time in 256 the padding happens to look right: then ' \
228
+ 'decrypt reports nothing, and the data is garbage rather than ' \
229
+ 'the original. To be sure the key is right, keep a value you ' \
230
+ 'can check, such as encrypting a known word alongside the data.',
223
231
  :examples => <<~EXAMPLES.strip
224
232
  #
225
233
  # Encrypt and Decrypt a string.
@@ -14,11 +14,10 @@ module Gloo
14
14
  attr_reader :roots
15
15
 
16
16
  #
17
- # Set up a ledger for the file at pn.
17
+ # Set up a ledger for one file's load.
18
18
  #
19
- def initialize( engine, pn )
19
+ def initialize( engine )
20
20
  @engine = engine
21
- @pn = pn
22
21
  @roots = []
23
22
  @mine = {}.compare_by_identity
24
23
  end
@@ -43,15 +42,16 @@ module Gloo
43
42
  # `value`. If another file created it and the values differ, the
44
43
  # new value is being ignored -- warn so it isn't a surprise. A
45
44
  # duplicate name inside one file is left alone (a documented
46
- # "first one wins").
45
+ # "first one wins"). The warning's location (this file and line)
46
+ # comes from the load.
47
47
  #
48
48
  def clash( prior, name, value )
49
49
  return if @mine.key?( prior )
50
50
  return if value.nil? || value.to_s.strip.empty?
51
51
  return if prior.value.to_s == value.to_s
52
52
 
53
- @engine.log.warn(
54
- "#{@pn}: '#{name}' is already declared in another loaded file with a " \
53
+ @engine.warn(
54
+ "'#{name}' is already declared in another loaded file with a " \
55
55
  "different value (#{prior.value.inspect} kept, #{value.inspect} ignored)."
56
56
  )
57
57
  end