scout-essentials 1.7.1 → 1.8.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.
- checksums.yaml +4 -4
- data/.vimproject +200 -47
- data/README.md +136 -0
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/doc/Annotation.md +352 -0
- data/doc/CMD.md +203 -0
- data/doc/ConcurrentStream.md +163 -0
- data/doc/IndiferentHash.md +240 -0
- data/doc/Log.md +235 -0
- data/doc/NamedArray.md +174 -0
- data/doc/Open.md +331 -0
- data/doc/Path.md +217 -0
- data/doc/Persist.md +214 -0
- data/doc/Resource.md +229 -0
- data/doc/SimpleOPT.md +236 -0
- data/doc/TmpFile.md +154 -0
- data/lib/scout/annotation/annotated_object.rb +8 -0
- data/lib/scout/annotation/annotation_module.rb +1 -0
- data/lib/scout/cmd.rb +19 -12
- data/lib/scout/concurrent_stream.rb +3 -1
- data/lib/scout/config.rb +2 -2
- data/lib/scout/indiferent_hash/options.rb +2 -2
- data/lib/scout/indiferent_hash.rb +16 -0
- data/lib/scout/log/color.rb +5 -3
- data/lib/scout/log/fingerprint.rb +8 -8
- data/lib/scout/log/progress/report.rb +6 -6
- data/lib/scout/log.rb +7 -7
- data/lib/scout/misc/digest.rb +11 -13
- data/lib/scout/misc/format.rb +2 -2
- data/lib/scout/misc/system.rb +5 -0
- data/lib/scout/open/final.rb +16 -1
- data/lib/scout/open/remote.rb +0 -1
- data/lib/scout/open/stream.rb +30 -5
- data/lib/scout/open/util.rb +32 -0
- data/lib/scout/path/digest.rb +12 -2
- data/lib/scout/path/find.rb +19 -6
- data/lib/scout/path/util.rb +37 -1
- data/lib/scout/persist/open.rb +2 -0
- data/lib/scout/persist.rb +7 -1
- data/lib/scout/resource/path.rb +2 -2
- data/lib/scout/resource/util.rb +18 -4
- data/lib/scout/resource.rb +15 -1
- data/lib/scout/simple_opt/parse.rb +2 -0
- data/lib/scout/tmpfile.rb +1 -1
- data/scout-essentials.gemspec +19 -6
- data/test/scout/misc/test_hook.rb +2 -2
- data/test/scout/open/test_stream.rb +43 -15
- data/test/scout/path/test_find.rb +1 -1
- data/test/scout/path/test_util.rb +11 -0
- data/test/scout/test_path.rb +4 -4
- data/test/scout/test_persist.rb +10 -1
- metadata +31 -5
- data/README.rdoc +0 -18
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# ConcurrentStream
|
|
2
|
+
|
|
3
|
+
ConcurrentStream is a mixin that augments IO-like stream objects (pipes returned by subprocess wrappers, in-memory streams, etc.) with concurrency-aware lifecycle management: tracking threads and child PIDs that produce/consume the stream, coordinated joining, aborting, callbacks, and safe cleanup. It is used throughout the framework for streams returned by commands (CMD.cmd) and by tee/pipe helpers in Open.
|
|
4
|
+
|
|
5
|
+
There is also a tiny AbortedStream helper to mark a stream as aborted and attach an exception.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
When a stream is set up with ConcurrentStream.setup, the stream is extended with methods and attributes to:
|
|
12
|
+
- record producer threads and child PIDs (threads/pids),
|
|
13
|
+
- automatically join and check producer exit status,
|
|
14
|
+
- attach callbacks to run once production is complete,
|
|
15
|
+
- abort producers/consumers cleanly (raise exceptions in threads, kill PIDs),
|
|
16
|
+
- support autjoining/auto-closing when the consumer finishes reading,
|
|
17
|
+
- attach a lock object that will be unlocked after join,
|
|
18
|
+
- carry metadata: filename, log, paired stream (pair), next stream in pipeline, and more.
|
|
19
|
+
|
|
20
|
+
This lets a consumer read from a stream and then reliably wait for producers to finish and detect failures (non-zero exit), or abort the whole pipeline on errors.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
ConcurrentStream.setup(stream, options = {}, &block)
|
|
27
|
+
|
|
28
|
+
- Extends `stream` with ConcurrentStream methods (unless already extended).
|
|
29
|
+
- Options (recognized):
|
|
30
|
+
- :threads — thread or array of threads that produce or manage this stream.
|
|
31
|
+
- :pids — pid or array of child process ids to wait for.
|
|
32
|
+
- :callback — proc to call after successful join (can also be provided as block).
|
|
33
|
+
- :abort_callback — proc to call on abort.
|
|
34
|
+
- :filename — textual name for logging/error messages.
|
|
35
|
+
- :autojoin — boolean, if true join on close/read EOF and auto-unlock lock.
|
|
36
|
+
- :lock — Lockfile instance to unlock after join.
|
|
37
|
+
- :no_fail — boolean; if true treat non-zero child exit as non-fatal.
|
|
38
|
+
- :pair — paired stream (e.g., the other side of a pipe) so aborts propagate.
|
|
39
|
+
- :next — next stream in pipeline when teeing/forwarding
|
|
40
|
+
- :log, :std_err — metadata captured for error messages
|
|
41
|
+
- If a block is given it is appended to the stream callback.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```ruby
|
|
45
|
+
ConcurrentStream.setup(io, threads: [t], pids: [pid], autojoin: true, filename: "ls-out")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Important attributes & predicates
|
|
51
|
+
|
|
52
|
+
The stream object gets attributes:
|
|
53
|
+
- threads, pids — lists of threads and subprocess PIDs to manage.
|
|
54
|
+
- callback, abort_callback — procs to call on success/abort.
|
|
55
|
+
- filename — friendly name used in logs and error messages.
|
|
56
|
+
- joined? — true after join completed.
|
|
57
|
+
- aborted? — true after abort called.
|
|
58
|
+
- autjoin — whether to auto-join on EOF/close.
|
|
59
|
+
- lock — optional Lockfile to unlock after join.
|
|
60
|
+
- stream_exception — exception captured that should be re-raised by readers/joins.
|
|
61
|
+
- no_fail — allow ignoring nonzero child exit.
|
|
62
|
+
|
|
63
|
+
AbortedStream.setup(obj, exception = nil) can be used to mark a stream aborted and attach exception (helper used internally).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Joining / waiting
|
|
68
|
+
|
|
69
|
+
- join_threads
|
|
70
|
+
- Joins registered threads, and if a thread's return value is a Process::Status checks success (unless no_fail). If a thread represented a subprocess and exit status indicates failure, raises ConcurrentStreamProcessFailed.
|
|
71
|
+
|
|
72
|
+
- join_pids
|
|
73
|
+
- Waits for PIDs via Process.waitpid and raises on non-zero exit unless no_fail.
|
|
74
|
+
|
|
75
|
+
- join_callback
|
|
76
|
+
- Runs `callback` once and clears it.
|
|
77
|
+
|
|
78
|
+
- join
|
|
79
|
+
- Calls join_threads, join_pids, raises stored stream_exception if set, runs callback, closes stream (if not closed) and marks joined. Also unlocks `lock` if present. Any exceptions are propagated after unlocking.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Aborting
|
|
84
|
+
|
|
85
|
+
- abort(exception=nil)
|
|
86
|
+
- Mark stream aborted, store exception in stream_exception, call abort_callback, abort threads (raise into threads) and kill PIDs with SIGINT (best-effort), clear callbacks, close stream, and unlock lock if held. Also propagate abort to `pair` stream if present.
|
|
87
|
+
|
|
88
|
+
- abort_threads(exception=nil)
|
|
89
|
+
- Raises exception (or Aborted) into producer threads and joins them.
|
|
90
|
+
|
|
91
|
+
- abort_pids
|
|
92
|
+
- Kills pids with INT.
|
|
93
|
+
|
|
94
|
+
Use abort to ensure fast cleanup on error and to signal paired streams.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Reading & closing
|
|
99
|
+
|
|
100
|
+
- read(*args)
|
|
101
|
+
- Wraps normal `read` with exception capture: on error stores `stream_exception` and re-raises. If `autojoin` is enabled and EOF reached, `close` is called automatically.
|
|
102
|
+
|
|
103
|
+
- close(*args)
|
|
104
|
+
- If `autojoin` is true, `close` will try to `join` (ensuring producers have finished) and then close; on exceptions it aborts, joins, and re-raises.
|
|
105
|
+
|
|
106
|
+
`joined?` and `aborted?` reflect the stream's lifecycle.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Callbacks
|
|
111
|
+
|
|
112
|
+
- add_callback(&block)
|
|
113
|
+
- Attaches an additional callback executed after producers finish. Multiple callbacks stack and are executed in order at join time.
|
|
114
|
+
|
|
115
|
+
- callback and abort_callback may be set in setup — used by caller to run cleanup or post-processing.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Error propagation
|
|
120
|
+
|
|
121
|
+
- stream_raise_exception(exception)
|
|
122
|
+
- Stores `stream_exception`, raises it into all producer threads (so they can abort), and calls `abort`.
|
|
123
|
+
|
|
124
|
+
- When join discovers a failing child process, it raises a ConcurrentStreamProcessFailed. Tests exercise this by running `grep` on a nonexisting file and asserting the exception is raised.
|
|
125
|
+
|
|
126
|
+
When `no_fail` is set true, non-zero exits or join errors are logged but not raised.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Utilities
|
|
131
|
+
|
|
132
|
+
- annotate(stream) — copy the current stream's threads/pids/callback/etc. onto another stream (useful when creating derived streams).
|
|
133
|
+
- filename — returns stored filename or a fallback derived from stream.inspect.
|
|
134
|
+
- process_stream(stream, close: true, join: true, message: "...") { ... }
|
|
135
|
+
- Class method wrapper that sets up the stream and ensures the block runs, then closes and joins the stream as requested. On exceptions it aborts the stream and re-raises.
|
|
136
|
+
|
|
137
|
+
Example usage (from framework):
|
|
138
|
+
- `CMD.cmd(..., pipe: true, autojoin: true)` returns a ConcurrentStream-enabled IO. Consumer reads from the stream; when EOF reached or read finishes, the stream auto-joins producers and raises errors on non-zero exit.
|
|
139
|
+
|
|
140
|
+
Test examples:
|
|
141
|
+
```ruby
|
|
142
|
+
# success case
|
|
143
|
+
io = CMD.cmd("ls", pipe: true, autojoin: true)
|
|
144
|
+
io.read
|
|
145
|
+
io.close
|
|
146
|
+
|
|
147
|
+
# failure case raises ConcurrentStreamProcessFailed
|
|
148
|
+
io = CMD.cmd("grep . NONEXISTINGFILE", pipe: true, autojoin: true)
|
|
149
|
+
io.read # raises ConcurrentStreamProcessFailed
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Typical patterns and recommendations
|
|
155
|
+
|
|
156
|
+
- When producing streams from background threads or subprocesses, call `ConcurrentStream.setup(stream, threads: t, pids: [pid], autojoin: true, filename: name)` so readers can join/observe failures.
|
|
157
|
+
- When teeing a stream to multiple outputs, register the splitter thread as a producer on each out-stream so each consumer can join independently.
|
|
158
|
+
- Use `abort(exception)` to stop whole pipeline on error and ensure all producers/consumers are signaled and cleaned up.
|
|
159
|
+
- Use `process_stream` helper to wrap a block that processes a stream and ensure it is closed and joined safely.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
ConcurrentStream centralizes safe management of concurrent IO pipelines: shared producers (threads/PIDs), stream cleanup, callback semantics, and error propagation. It is a core primitive used by Open, CMD, and persistence/teeing helpers to make streaming robust in multi-threaded / multi-process contexts.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# IndiferentHash
|
|
2
|
+
|
|
3
|
+
IndiferentHash provides Hash utilities and a mixin that makes hash access indifferent to String vs Symbol keys. It also includes a set of helper utilities for parsing, merging and transforming option hashes used across the framework.
|
|
4
|
+
|
|
5
|
+
Two main pieces:
|
|
6
|
+
- IndiferentHash mixin (extend any Hash instance with IndiferentHash to get indifferent access behavior and extra hash helpers).
|
|
7
|
+
- CaseInsensitiveHash mixin (separate mixin to allow case-insensitive string keys).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick usage
|
|
12
|
+
|
|
13
|
+
Make a Hash indifferent (string/symbol interchangeable):
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
h = { a: 1, "b" => 2 }
|
|
17
|
+
IndiferentHash.setup(h) # returns h extended with IndiferentHash
|
|
18
|
+
|
|
19
|
+
h[:a] # => 1
|
|
20
|
+
h["a"] # => 1
|
|
21
|
+
h[:b] # => 2
|
|
22
|
+
h["b"] # => 2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Make a Hash case-insensitive (string keys compared case-insensitively):
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
h = { a: 1, "b" => 2 }
|
|
29
|
+
CaseInsensitiveHash.setup(h)
|
|
30
|
+
|
|
31
|
+
h[:a] # => 1
|
|
32
|
+
h["A"] # => 1
|
|
33
|
+
h[:A] # => 1
|
|
34
|
+
h["B"] # => 2
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## IndiferentHash mixin (methods added to a Hash)
|
|
40
|
+
|
|
41
|
+
Call IndiferentHash.setup(hash) to extend a hash instance.
|
|
42
|
+
|
|
43
|
+
Behavior highlights:
|
|
44
|
+
- Access by Symbol or String: h[:k] and h["k"] resolve to the same entry when possible.
|
|
45
|
+
- Nested hashes returned from [] are automatically set up with IndiferentHash.
|
|
46
|
+
- Values are stored normally, but deletion and inclusion checks accept Symbol/String interchangeably.
|
|
47
|
+
|
|
48
|
+
Methods and behaviors:
|
|
49
|
+
|
|
50
|
+
- IndiferentHash.setup(hash)
|
|
51
|
+
- Extends the given hash with IndiferentHash and returns it.
|
|
52
|
+
|
|
53
|
+
- merge(other)
|
|
54
|
+
- Returns a new IndiferentHash with keys from self merged with other (other wins). The result is an IndiferentHash.
|
|
55
|
+
|
|
56
|
+
- deep_merge(other)
|
|
57
|
+
- Recursively merges nested hashes: if both have same key and values are hash-like, merges them deeply (preserving IndiferentHash behavior on nested hashes).
|
|
58
|
+
|
|
59
|
+
- [](key)
|
|
60
|
+
- Returns value for key. If not found directly, attempts the alternate form:
|
|
61
|
+
- If given Symbol/Module, will try the String key.
|
|
62
|
+
- If given String, will try the Symbol key.
|
|
63
|
+
- If the value is itself a Hash, it will be extended with IndiferentHash before returning.
|
|
64
|
+
- Note: behavior pays attention to hash default/default_proc. If a default exists and the key isn't explicitly present in keys, it returns the default without attempting alternative forms.
|
|
65
|
+
|
|
66
|
+
- []=(key, value)
|
|
67
|
+
- Deletes any existing matching key (either symbol or string) before setting the new one. This avoids duplicate representations of the same logical key.
|
|
68
|
+
|
|
69
|
+
- values_at(*keys)
|
|
70
|
+
- Returns an array of values for the provided keys (indifferent to symbol/string form).
|
|
71
|
+
|
|
72
|
+
- include?(key)
|
|
73
|
+
- Returns true if either symbol or string form exists.
|
|
74
|
+
|
|
75
|
+
- delete(key)
|
|
76
|
+
- Deletes by key; will try symbol and string form and return deleted value if found.
|
|
77
|
+
|
|
78
|
+
- clean_version
|
|
79
|
+
- Produces a plain Ruby hash where keys are strings (stringified keys), preferring the first occurrence.
|
|
80
|
+
|
|
81
|
+
- slice(*keys)
|
|
82
|
+
- Returns a new IndiferentHash containing only the requested keys. Accepts symbol or string forms; ensures both forms are considered.
|
|
83
|
+
|
|
84
|
+
- keys_to_sym! and keys_to_sym
|
|
85
|
+
- keys_to_sym! converts string keys in-place to symbols (best-effort; rescue on any failed conversion).
|
|
86
|
+
- keys_to_sym returns a new IndiferentHash with keys converted to symbols.
|
|
87
|
+
|
|
88
|
+
- prety_print
|
|
89
|
+
- A convenience wrapper that calls Misc.format_definition_list(self, sep: "\n") (keeps existing behavior/name `prety_print` as in code).
|
|
90
|
+
|
|
91
|
+
- except(*list)
|
|
92
|
+
- Returns a hash copy excluding provided keys. Accepts symbol/string forms; returns a result consistent with Hash#except but extended to be indifferent.
|
|
93
|
+
|
|
94
|
+
Notes:
|
|
95
|
+
- The implementation ensures nested hashes returned from [] or merges are set up with IndiferentHash automatically.
|
|
96
|
+
- Some method names are intentionally spelled as in the implementation (`prety_print`).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## CaseInsensitiveHash
|
|
101
|
+
|
|
102
|
+
A separate mixin to make key lookup case-insensitive (for string keys). Use CaseInsensitiveHash.setup(hash) to extend a hash.
|
|
103
|
+
|
|
104
|
+
Behavior:
|
|
105
|
+
- On lookup, it first tries the provided key directly. If no value, it converts the key to lowercase string and looks up a precomputed map (original_key_by_downcase) to find the actual stored key. This permits "A" and "a" to refer to the same entry.
|
|
106
|
+
- values_at returns values for provided keys using the case-insensitive lookup.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
h = { a: 1, "b" => 2 }
|
|
112
|
+
CaseInsensitiveHash.setup(h)
|
|
113
|
+
|
|
114
|
+
h["A"] # => 1
|
|
115
|
+
h[:A] # => 1
|
|
116
|
+
h["B"] # => 2
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Options helpers (IndiferentHash::Options functions)
|
|
122
|
+
|
|
123
|
+
These utilities are useful for parsing and handling option hashes and strings.
|
|
124
|
+
|
|
125
|
+
- add_defaults(options, defaults = {})
|
|
126
|
+
- Ensures options is an IndiferentHash, accepts defaults as Hash or string (string gets parsed). Adds defaults only for keys not present in options.
|
|
127
|
+
- Returns the options (modified/extended).
|
|
128
|
+
|
|
129
|
+
- process_options(hash, *keys)
|
|
130
|
+
- Sets up IndiferentHash on hash.
|
|
131
|
+
- If the last argument is a Hash, it is used as defaults (added first).
|
|
132
|
+
- If a single key passed, returns and removes that key from hash (prefers symbol then string).
|
|
133
|
+
- If multiple keys passed, returns array of removed values for each key.
|
|
134
|
+
- Example: IndiferentHash.process_options(h, :limit) or IndiferentHash.process_options(h, :a, :b, default: 1)
|
|
135
|
+
|
|
136
|
+
- pull_keys(hash, prefix)
|
|
137
|
+
- Pulls keys with prefix_... from hash and returns a new IndiferentHash with the suffixes as keys.
|
|
138
|
+
- Also consumes "#{prefix}_options" if present and merges into result.
|
|
139
|
+
- Example: given { foo_bar: 1, "foo_x" => 2 }, pull_keys(h, :foo) => { bar: 1, x: 2 } (keys matched as string/symbol appropriately).
|
|
140
|
+
|
|
141
|
+
- zip2hash(list1, list2)
|
|
142
|
+
- Zips two lists into a hash (keys from list1, values from list2) and sets it up as IndiferentHash.
|
|
143
|
+
|
|
144
|
+
- positional2hash(keys, *values)
|
|
145
|
+
- Converts positional values into a hash keyed by keys.
|
|
146
|
+
- Supports the common pattern where the last argument is a Hash of extra/defaults. In that case:
|
|
147
|
+
- Combines given values into a hash, removes nil/empty values, adds defaults from the extras, and prunes keys not in the original keys set.
|
|
148
|
+
- Example: IndiferentHash.positional2hash([:one,:two], 1, two: 2, extra: 4) => { one: 1, two: 2 }
|
|
149
|
+
|
|
150
|
+
- array2hash(array, default = nil)
|
|
151
|
+
- Accepts an array of [key, value] pairs and builds an IndiferentHash.
|
|
152
|
+
- If value is nil and default provided, uses a dup of default for that key.
|
|
153
|
+
|
|
154
|
+
- process_to_hash(list) { |list| ... }
|
|
155
|
+
- Yields list to block, expects a result list; zips original list with returned list into an IndiferentHash.
|
|
156
|
+
|
|
157
|
+
- hash2string(hash)
|
|
158
|
+
- Serializes a simple hash into a string representation (sorted by key). Only handles values of certain simple classes; others are omitted. Uses ":" prefix for symbol keys/values in output.
|
|
159
|
+
- Output format is key=value pairs joined with "#".
|
|
160
|
+
|
|
161
|
+
- string2hash(string, sep = "#")
|
|
162
|
+
- Parses the string produced by hash2string (or similar) and converts back into an IndiferentHash. Supports:
|
|
163
|
+
- :symbol keys/values (leading ":")
|
|
164
|
+
- quoted strings, integers, floats, booleans, regexps (/.../)
|
|
165
|
+
- empty values treated as true
|
|
166
|
+
- Example roundtrip: IndiferentHash.string2hash(IndiferentHash.hash2string(h)) == h (for supported types).
|
|
167
|
+
|
|
168
|
+
- parse_options(str)
|
|
169
|
+
- Parses a shell-like option string of key=value pairs, supporting quoted values and comma-separated lists (preserving quoted items with spaces).
|
|
170
|
+
- Returns an IndiferentHash.
|
|
171
|
+
- Example: IndiferentHash.parse_options('blueberries=true title="This is a title" list=one,two,"and three"')
|
|
172
|
+
|
|
173
|
+
- print_options(options)
|
|
174
|
+
- Serializes an options hash into a space-separated string of key=value pairs; array values become CSV (properly quoted if containing spaces).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Examples (from tests and usage)
|
|
179
|
+
|
|
180
|
+
Indifferent access:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
h = { a: 1, "b" => 2 }
|
|
184
|
+
IndiferentHash.setup(h)
|
|
185
|
+
h[:a] # => 1
|
|
186
|
+
h["a"] # => 1
|
|
187
|
+
h["b"] # => 2
|
|
188
|
+
h[:b] # => 2
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Deep merge:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
o = { h: { a: 1, b: 2 } }
|
|
195
|
+
n = { h: { c: 3 } }
|
|
196
|
+
IndiferentHash.setup(o)
|
|
197
|
+
o2 = o.deep_merge(n)
|
|
198
|
+
o2[:h]["a"] # => 1
|
|
199
|
+
o2[:h]["c"] # => 3
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Options parsing:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
opts = IndiferentHash.parse_options('blueberries=true title="A title" list=one,two,"and three"')
|
|
206
|
+
opts["title"] # => "A title"
|
|
207
|
+
opts["list"] # => ["one", "two", "and three"]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
String <-> hash roundtrip:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
h = { a: 1, b: :sym, c: true }
|
|
214
|
+
s = IndiferentHash.hash2string(h)
|
|
215
|
+
h2 = IndiferentHash.string2hash(s)
|
|
216
|
+
# h2 should equal h for the supported/simple types
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
pull_keys example:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
h = { "foo_bar" => 1, :foo_baz => 2, "other" => 3 }
|
|
223
|
+
IndiferentHash.setup(h)
|
|
224
|
+
prefixed = IndiferentHash.pull_keys(h, :foo)
|
|
225
|
+
# prefixed => { "bar" => 1, :baz => 2 } (returned as IndiferentHash)
|
|
226
|
+
# and h no longer contains those entries
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Implementation notes / caveats
|
|
232
|
+
|
|
233
|
+
- IndiferentHash.setup extends a single hash instance (not the Hash class). Use it on any hash instance you want to treat indifferently.
|
|
234
|
+
- Nested hashes returned from [] or created by zip/positional helpers get extended automatically with IndiferentHash.
|
|
235
|
+
- The [] lookup has special handling with Hash defaults: if the hash has a default or default_proc
|
|
236
|
+
and the requested key is not present in keys, it will return the default without trying the alternate symbol/string form.
|
|
237
|
+
- CaseInsensitiveHash is independent of IndiferentHash; you can mix both if needed, but their behaviors are separate.
|
|
238
|
+
- Some helper names are spelled as in the codebase (e.g., prety_print), used for compatibility.
|
|
239
|
+
|
|
240
|
+
This module covers common patterns required for flexible option handling in the framework: indifferent access, easy merging and defaulting, parsing/printing option strings, and extracting prefixed option subsets.
|
data/doc/Log.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Log
|
|
2
|
+
|
|
3
|
+
The Log module is the framework-wide logging and progress utility. It provides:
|
|
4
|
+
- leveled logging (DEBUG, LOW, MEDIUM, HIGH, INFO, WARN, ERROR, NONE)
|
|
5
|
+
- colored output and utilities for color manipulation and gradients
|
|
6
|
+
- fingerprinting for compact object summaries
|
|
7
|
+
- a rich ProgressBar facility with multi-bar, ETA and history support
|
|
8
|
+
- helpers to trap and ignore stdout/stderr and to direct logs to a logfile
|
|
9
|
+
- convenience debug/inspect helpers
|
|
10
|
+
|
|
11
|
+
Files / components:
|
|
12
|
+
- log.rb — core logging API, level handling, formatting, logfile control
|
|
13
|
+
- log/color.rb — integration with Term::ANSIColor and concept color mapping
|
|
14
|
+
- log/color_class.rb — Color class for hex color parsing, blending, lightening/darkening
|
|
15
|
+
- log/fingerprint.rb — compact fingerprint representations for many object types
|
|
16
|
+
- log/progress{.rb, /util, /report} — ProgressBar implementation and helpers
|
|
17
|
+
- log/trap.rb — utilities to trap/ignore STDOUT/STDERR
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Configuration & Environment
|
|
22
|
+
|
|
23
|
+
- Log.severity (module attribute): current logging level threshold. Messages below this level are ignored.
|
|
24
|
+
- SEVERITY constants: DEBUG, LOW, MEDIUM, HIGH, INFO, WARN, ERROR, NONE (assigned 0..7).
|
|
25
|
+
- Use Log.get_level(value) to convert numeric/string/symbol into numeric level.
|
|
26
|
+
- Default severity:
|
|
27
|
+
- Determined by environment variable `SCOUT_LOG` if set to one of the level names.
|
|
28
|
+
- Otherwise read from `~/.scout/etc/log_severity` if present, else INFO.
|
|
29
|
+
- Color disabled if `ENV["SCOUT_NOCOLOR"] == 'true'` or SOPT.nocolor set.
|
|
30
|
+
- Log.tty_size returns terminal rows (uses IO.console.winsize or `tput li`), falls back to ENV["TTY_SIZE"] or 80.
|
|
31
|
+
- Log.logfile(file_or_io) — set logfile target:
|
|
32
|
+
- Passing a String opens the file in append mode, sync=true.
|
|
33
|
+
- Passing an IO or File sets that as the logfile.
|
|
34
|
+
- If not set, logging writes to STDERR.
|
|
35
|
+
- Thread-safety: Log.log_write and Log.log_puts synchronize writes via MUTEX.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Coloring & Color utilities
|
|
40
|
+
|
|
41
|
+
- Log extends Term::ANSIColor and exposes helpers:
|
|
42
|
+
- Log.color(color, str = nil, reset = false)
|
|
43
|
+
- color can be:
|
|
44
|
+
- Symbol naming an ansi color (e.g. :green)
|
|
45
|
+
- Integer index into SEVERITY_COLOR
|
|
46
|
+
- Concept color name (Log.CONCEPT_COLORS map keys like :title, :path, :value)
|
|
47
|
+
- A Color/hex handled by Color class (indirectly via Colorize)
|
|
48
|
+
- If str is nil, returns only the color control string (unless nocolor true).
|
|
49
|
+
- If nocolor is true, returns str unchanged.
|
|
50
|
+
- Log.highlight(str = nil): returns HIGHLIGHT sequence or wraps string.
|
|
51
|
+
- Log.uncolor(str) — strips ANSI color sequences.
|
|
52
|
+
|
|
53
|
+
- Color utilities:
|
|
54
|
+
- Color class (log/color_class.rb) handles hex parsing, lighten/darken/blend and returns hex strings.
|
|
55
|
+
- Colorize module (log/color.rb) provides:
|
|
56
|
+
- Color selection by name (Colorize.from_name),
|
|
57
|
+
- continuous gradient generation (Colorize.continuous),
|
|
58
|
+
- gradient/rank mapping and distinct color mapping for categorical values,
|
|
59
|
+
- TSV coloring helpers.
|
|
60
|
+
|
|
61
|
+
- Concept color map:
|
|
62
|
+
- Log.CONCEPT_COLORS (IndiferentHash) maps logical concepts to ANSI colors (e.g. :title => magenta).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Basic Logging API
|
|
67
|
+
|
|
68
|
+
Main functions:
|
|
69
|
+
|
|
70
|
+
- Log.log(message = nil, severity = MEDIUM, &block)
|
|
71
|
+
- Adds newline (if missing) and delegates to Log.logn.
|
|
72
|
+
- Skips if severity < Log.severity.
|
|
73
|
+
|
|
74
|
+
- Log.logn(message = nil, severity = MEDIUM, &block)
|
|
75
|
+
- Emits formatted message without appending newline.
|
|
76
|
+
- Prefix includes timestamp and severity tag inside color.
|
|
77
|
+
- Uses Log.color to color the line. Messages with severity >= INFO are wrapped in Log.highlight.
|
|
78
|
+
- Writes via Log.log_write (synchronized).
|
|
79
|
+
|
|
80
|
+
- Convenience wrappers:
|
|
81
|
+
- Log.debug(msg), Log.low(msg), Log.medium(msg), Log.high(msg), Log.info(msg), Log.warn(msg), Log.error(msg)
|
|
82
|
+
- Each calls Log.log with the corresponding severity.
|
|
83
|
+
|
|
84
|
+
- Log.exception(e)
|
|
85
|
+
- Nicely logs an exception: formats message and backtrace using Log.fingerprint for very long messages and Log.color_stack for colored backtrace output. Honors environment `SCOUT_ORIGINAL_STACK` for ordering.
|
|
86
|
+
|
|
87
|
+
- Log.get_level(level)
|
|
88
|
+
- Accepts Numeric, String (case-insensitive), or Symbol and returns numeric level or 0/nil.
|
|
89
|
+
|
|
90
|
+
- Log.with_severity(level) { ... }
|
|
91
|
+
- Temporarily sets Log.severity for the block.
|
|
92
|
+
|
|
93
|
+
- Log.log_obj_inspect(obj, level, file = $stdout)
|
|
94
|
+
- Logs caller location and obj.inspect at given level.
|
|
95
|
+
|
|
96
|
+
- Log.log_obj_fingerprint(obj, level, file = $stdout)
|
|
97
|
+
- Logs caller location and Log.fingerprint(obj) for compact summary.
|
|
98
|
+
|
|
99
|
+
- Line/terminal helpers:
|
|
100
|
+
- Log.up_lines(n), Log.down_lines(n), Log.return_line, Log.clear_line(out = STDOUT)
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Fingerprinting
|
|
105
|
+
|
|
106
|
+
- Log.fingerprint(obj) produces a compact human-readable representation useful in logs:
|
|
107
|
+
- Strings are truncated with an MD5 snippet if longer than FP_MAX_STRING (150).
|
|
108
|
+
- Arrays and Hashes are truncated beyond FP_MAX_ARRAY / FP_MAX_HASH.
|
|
109
|
+
- Special handling for IO/File, Float formatting, Thread names, Symbol, nil/true/false etc.
|
|
110
|
+
- Used by other logging helpers to keep outputs concise.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Progress bars
|
|
115
|
+
|
|
116
|
+
The ProgressBar is a full-featured facility for reporting progress from concurrent tasks.
|
|
117
|
+
|
|
118
|
+
Key pieces:
|
|
119
|
+
- Use Log::ProgressBar.with_bar(max_or_options, options = {}) {|bar| ... } to create a managed bar.
|
|
120
|
+
- new_bar(max, options) creates a bar; with_bar ensures removal on exit unless KeepBar is raised.
|
|
121
|
+
- ProgressBar instance attributes:
|
|
122
|
+
- max, ticks, frequency, depth, desc, file, bytes, process, callback, severity
|
|
123
|
+
- Behavior:
|
|
124
|
+
- bar.init — initialize and print first state.
|
|
125
|
+
- bar.tick(step = 1) — increment ticks and possibly report (depending on frequency and percent progress).
|
|
126
|
+
- bar.pos(position) — set bar to a specific position (pos - ticks).
|
|
127
|
+
- bar.process(elem) — calls `process` callback and interprets return to tick/pos based on type.
|
|
128
|
+
- bar.percent — computes percent (0..100).
|
|
129
|
+
- Bars are managed centrally in ProgressBar::BARS with concurrency via BAR_MUTEX.
|
|
130
|
+
- Bars can be nested (depth management), silenced, removed, persisted via `file` (save/load YAML state).
|
|
131
|
+
- ProgressBar.report and ProgressBar.report_msg produce formatted output lines including per-second rate, ETA, used time and ticks.
|
|
132
|
+
|
|
133
|
+
Helpers:
|
|
134
|
+
- ProgressBar.get_obj_bar(obj, bar) — helper to create a meaningful bar given an object (TSV, File, Array, Path, etc.) — guesses max records by inspecting file/TSV length if possible.
|
|
135
|
+
- ProgressBar.with_obj_bar(obj, bar = true) — convenience wrapper around with_bar using a guessed max.
|
|
136
|
+
|
|
137
|
+
Notes:
|
|
138
|
+
- Progress printing will skip if Log.no_bar is true (set via environment SCOUT_NO_PROGRESS or Log.no_bar=).
|
|
139
|
+
- ProgressBar persistence: if `file` option provided, the bar saves state to YAML.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Trapping / ignoring STDOUT and STDERR
|
|
144
|
+
|
|
145
|
+
- Log.trap_std(msg = "STDOUT", msge = "STDERR", severity = 0, severity_err = nil) { ... }
|
|
146
|
+
- Redirects STDOUT/STDERR into pipes; background threads read and call Log.logn on captured lines with provided severity and prefix.
|
|
147
|
+
- Useful to capture external command output or to consolidate prints into the log.
|
|
148
|
+
|
|
149
|
+
- Log.trap_stderr(msg = "STDERR", severity = 0) { ... }
|
|
150
|
+
- Captures only STDERR and logs it.
|
|
151
|
+
|
|
152
|
+
- Log.ignore_stderr { ... } / Log.ignore_stdout { ... }
|
|
153
|
+
- Redirects respective stream to /dev/null for the block (silences output). Safe fallback if /dev/null missing.
|
|
154
|
+
|
|
155
|
+
These functions restore original streams when the block ends even if exceptions occur.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Convenience debug/inspect helpers
|
|
160
|
+
|
|
161
|
+
Global helper methods (defined outside Log) for quick debugging:
|
|
162
|
+
|
|
163
|
+
- ppp(message) — pretty print (with color) and file/line location
|
|
164
|
+
- fff(object) — debug printing fingerprint (using Log.debug)
|
|
165
|
+
- ddd(obj, file = $stdout) — Log.log_obj_inspect(obj, :debug)
|
|
166
|
+
- lll(obj, file = $stdout) — low-level inspect wrapper
|
|
167
|
+
- mmm, iii, wwww, eee — wrappers for different severities (medium, info, warn, error)
|
|
168
|
+
- ddf/mmf/llf/iif/wwwf/eef — wrappers calling log_obj_fingerprint at different severities
|
|
169
|
+
- sss(level) { } — temporarily set severity or set it if no block
|
|
170
|
+
- ccc(obj=nil) — conditional debug printing based on $scout_debug_log (used as ad-hoc toggle)
|
|
171
|
+
|
|
172
|
+
These are small helpers used in tests (e.g., iif :foo writes INFO lines).
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Examples
|
|
177
|
+
|
|
178
|
+
Basic logging:
|
|
179
|
+
```ruby
|
|
180
|
+
Log.severity = Log::DEBUG
|
|
181
|
+
Log.info "Starting task"
|
|
182
|
+
Log.debug { "Expensive debug only evaluated when level allows" }
|
|
183
|
+
Log.error "Something failed"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Exception handling:
|
|
187
|
+
```ruby
|
|
188
|
+
begin
|
|
189
|
+
raise "boom"
|
|
190
|
+
rescue => e
|
|
191
|
+
Log.exception(e)
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Progress bar:
|
|
196
|
+
```ruby
|
|
197
|
+
Log::ProgressBar.with_bar(100, desc: "Processing") do |bar|
|
|
198
|
+
100.times do
|
|
199
|
+
bar.tick
|
|
200
|
+
# work...
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Trap STDOUT/STDERR block:
|
|
206
|
+
```ruby
|
|
207
|
+
Log.trap_std("OUT", "ERR", Log::INFO, Log::WARN) do
|
|
208
|
+
system("some_command")
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Set logfile:
|
|
213
|
+
```ruby
|
|
214
|
+
Log.logfile("/tmp/mylog.txt")
|
|
215
|
+
Log.info "Wrote to logfile"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Fingerprint:
|
|
219
|
+
```ruby
|
|
220
|
+
s = "a very long string..."
|
|
221
|
+
Log.debug Log.fingerprint(s) # compact representation for logs
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Implementation notes & caveats
|
|
227
|
+
|
|
228
|
+
- Colors: if nocolor is enabled (env or Log.nocolor), color helpers return raw strings.
|
|
229
|
+
- Log.log_write and Log.log_puts are synchronized to avoid interleaved writes from threads.
|
|
230
|
+
- Log.logn uses caller and Log.last_caller to attempt to find meaningful source location for messages in stack traces.
|
|
231
|
+
- Fingerprint logic truncates long strings and large arrays/hashes to keep logs readable.
|
|
232
|
+
- ProgressBar uses a central registry (BARS) and a mutex for concurrency; nested bars are supported.
|
|
233
|
+
- The Log module depends on utility modules (Misc, IndiferentHash, TSV, Path, etc.) for some features — when used in isolation, those parts may not be available.
|
|
234
|
+
|
|
235
|
+
This document summarizes the Log module capabilities and usage. Use Log for all script-level diagnostic output, and use ProgressBar for long running operations where periodic status and ETA are useful.
|