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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +200 -47
  3. data/README.md +136 -0
  4. data/Rakefile +1 -0
  5. data/VERSION +1 -1
  6. data/doc/Annotation.md +352 -0
  7. data/doc/CMD.md +203 -0
  8. data/doc/ConcurrentStream.md +163 -0
  9. data/doc/IndiferentHash.md +240 -0
  10. data/doc/Log.md +235 -0
  11. data/doc/NamedArray.md +174 -0
  12. data/doc/Open.md +331 -0
  13. data/doc/Path.md +217 -0
  14. data/doc/Persist.md +214 -0
  15. data/doc/Resource.md +229 -0
  16. data/doc/SimpleOPT.md +236 -0
  17. data/doc/TmpFile.md +154 -0
  18. data/lib/scout/annotation/annotated_object.rb +8 -0
  19. data/lib/scout/annotation/annotation_module.rb +1 -0
  20. data/lib/scout/cmd.rb +19 -12
  21. data/lib/scout/concurrent_stream.rb +3 -1
  22. data/lib/scout/config.rb +2 -2
  23. data/lib/scout/indiferent_hash/options.rb +2 -2
  24. data/lib/scout/indiferent_hash.rb +16 -0
  25. data/lib/scout/log/color.rb +5 -3
  26. data/lib/scout/log/fingerprint.rb +8 -8
  27. data/lib/scout/log/progress/report.rb +6 -6
  28. data/lib/scout/log.rb +7 -7
  29. data/lib/scout/misc/digest.rb +11 -13
  30. data/lib/scout/misc/format.rb +2 -2
  31. data/lib/scout/misc/system.rb +5 -0
  32. data/lib/scout/open/final.rb +16 -1
  33. data/lib/scout/open/remote.rb +0 -1
  34. data/lib/scout/open/stream.rb +30 -5
  35. data/lib/scout/open/util.rb +32 -0
  36. data/lib/scout/path/digest.rb +12 -2
  37. data/lib/scout/path/find.rb +19 -6
  38. data/lib/scout/path/util.rb +37 -1
  39. data/lib/scout/persist/open.rb +2 -0
  40. data/lib/scout/persist.rb +7 -1
  41. data/lib/scout/resource/path.rb +2 -2
  42. data/lib/scout/resource/util.rb +18 -4
  43. data/lib/scout/resource.rb +15 -1
  44. data/lib/scout/simple_opt/parse.rb +2 -0
  45. data/lib/scout/tmpfile.rb +1 -1
  46. data/scout-essentials.gemspec +19 -6
  47. data/test/scout/misc/test_hook.rb +2 -2
  48. data/test/scout/open/test_stream.rb +43 -15
  49. data/test/scout/path/test_find.rb +1 -1
  50. data/test/scout/path/test_util.rb +11 -0
  51. data/test/scout/test_path.rb +4 -4
  52. data/test/scout/test_persist.rb +10 -1
  53. metadata +31 -5
  54. data/README.rdoc +0 -18
data/doc/Annotation.md ADDED
@@ -0,0 +1,352 @@
1
+ # Annotation
2
+
3
+ The Annotation module provides a lightweight system for adding typed, named "annotations" (simple instance variables with accessors) to arbitrary Ruby objects and arrays. It's used by defining annotation modules (modules that `extend Annotation` and declare attributes via `annotation`) and then applying those annotation modules to objects at runtime.
4
+
5
+ Key features:
6
+ - Define annotation modules with named attributes.
7
+ - Attach annotation modules to objects or arrays (including Strings, Arrays, Hashes, Procs, etc.).
8
+ - Annotated arrays (`AnnotatedArray`) propagate annotations to their items and provide annotation-aware iteration and collection operations.
9
+ - Support for serialization (Marshal) and a `purge` operation that strips annotations from objects (recursively for arrays and hashes).
10
+
11
+ ---
12
+
13
+ ## Creating an annotation module
14
+
15
+ Define a module and `extend Annotation`. Declare annotation attributes using `annotation :attr1, :attr2`.
16
+
17
+ Example:
18
+
19
+ ```ruby
20
+ module MyAnnotation
21
+ extend Annotation
22
+ annotation :code, :note
23
+ end
24
+ ```
25
+
26
+ When a module is created this way, it gets:
27
+ - an internal `@annotations` list (names of attributes),
28
+ - accessors for declared attributes (e.g. `code`, `code=`),
29
+ - the ability to be used to annotate objects (`MyAnnotation.setup(obj, ...)` or by `obj.extend MyAnnotation`).
30
+
31
+ ---
32
+
33
+ ## Applying annotations
34
+
35
+ Use `Annotation.setup` or the annotation module's `setup` to attach annotations to objects.
36
+
37
+ Basic usage:
38
+
39
+ - Annotation.setup with a single annotation module:
40
+ ```ruby
41
+ Annotation.setup(obj, MyAnnotation, code: "X")
42
+ ```
43
+ or
44
+ ```ruby
45
+ MyAnnotation.setup(obj, code: "X")
46
+ ```
47
+
48
+ - Annotation.setup accepts annotation types as:
49
+ - a Module constant (e.g. `MyAnnotation`),
50
+ - an Array of modules (`[A, B]`),
51
+ - a String of module names separated by `|` (will be constantized).
52
+
53
+ - The `annotation_hash` (values for attributes) can be:
54
+ - a Hash mapping attribute name => value,
55
+ - a list of positional values that are zipped with the declared attribute names,
56
+ - a Hash with string keys (converted to symbols).
57
+ Examples:
58
+ ```ruby
59
+ MyAnnotation.setup(obj, :code) # sets first attribute to :code (positional)
60
+ MyAnnotation.setup(obj, code: "some text") # sets code => "some text"
61
+ MyAnnotation.setup(obj, "code" => "v") # string keys are accepted
62
+ MyAnnotation.setup(obj, code2: :code) # remap behavior (see examples below)
63
+ ```
64
+
65
+ - `Annotation.setup` convenience:
66
+ ```ruby
67
+ # Apply one or more annotation modules
68
+ Annotation.setup(obj, [MyAnnotation, OtherAnnotation], code: "c", code3: "d")
69
+ ```
70
+
71
+ Notes on calling `AnnotationModule.setup` (the module-level setup method used by both `Annotation.setup` and `AnnotationModule.setup`):
72
+ - If the target `obj` is frozen, it will be duplicated before extending.
73
+ - If a block is passed or `obj` is `nil` and a block is provided, the block (Proc) itself will be annotated (useful to attach annotations to callbacks).
74
+ - When multiple modules are applied to the same object, their annotations are merged; accessors are available for all annotated attributes.
75
+
76
+ Examples from tests:
77
+ ```ruby
78
+ str = "String"
79
+ MyAnnotation.setup(str, :code)
80
+ # now str responds to `code` and `code` == :code
81
+ ```
82
+
83
+ You can annotate other objects using an annotated object:
84
+ ```ruby
85
+ a = MyAnnotation.setup("a", code: "c")
86
+ b = "b"
87
+ a.annotate(b) # copies the annotation values and types from a to b
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Annotated object API
93
+
94
+ When an object is annotated (extended with annotation modules), it gains these helpers (provided by `Annotation::AnnotatedObject`):
95
+
96
+ - `annotation_types` -> Array of annotation modules applied to the object.
97
+ - `annotation_hash` -> Hash mapping annotation attribute names (symbols) to values stored on the object.
98
+ - `annotation_info` -> combines `annotation_hash` plus:
99
+ - `annotation_types` (modules list),
100
+ - `annotated_array` boolean (true when object is an `AnnotatedArray`).
101
+ - `.serialize` (instance) and `AnnotatedObject.serialize(obj)` (class method) -> returns a purged `annotation_info` merged with a `literal: obj` entry (useful when creating stable representations).
102
+ - `annotation_id` / `id` -> deterministic digest built from the object and its annotation info (uses `Misc.digest` in the framework).
103
+ - `annotate(other)` -> applies all annotation types and attribute values of `self` onto `other`.
104
+ - `purge` -> returns a duplicate of the object with all annotation-related instance variables removed (`@annotations`, `@annotation_types`, `@container`).
105
+ - `make_array` -> returns a new array containing the object, annotated with the same annotation types/values, and extended as an `AnnotatedArray`.
106
+
107
+ Example:
108
+ ```ruby
109
+ s = MyAnnotation.setup("s", code: "C")
110
+ s.annotation_hash # => { code: "C" }
111
+ s.id # => some digest
112
+ s2 = "other"
113
+ s.annotate(s2)
114
+ s2.code # => "C"
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Annotated arrays (AnnotatedArray)
120
+
121
+ `AnnotatedArray` is an array mixin that:
122
+ - stores annotation types/values at the array level,
123
+ - automatically annotates elements when they are accessed or iterated,
124
+ - provides container tracking on items: annotated items get `container` and `container_index` attributes (via `AnnotatedArrayItem`).
125
+
126
+ To make an array annotation-aware:
127
+ ```ruby
128
+ AnnotationModule.setup(ary, code: "C")
129
+ ary.extend AnnotatedArray
130
+ ```
131
+
132
+ Behavior and methods:
133
+ - Element access ([], first, last) returns annotated elements (unless `clean = true` passed to `[]`).
134
+ - `each`, `each_with_index`, `select`, `inject`, `collect` iterate over annotated items (so blocks receive annotated items).
135
+ - `compact`, `uniq`, `flatten`, `reverse`, `sort_by` are overridden to return annotated arrays (the result is annotated and extended with `AnnotatedArray`).
136
+ - `subset(list)` and `remove(list)` return new annotated arrays representing the set intersection/difference.
137
+ - Annotated array items receive `container` (the array) and `container_index` (the index position when produced via iteration/access).
138
+
139
+ Examples:
140
+ ```ruby
141
+ ary = ["x"]
142
+ MyAnnotation.setup(ary, "C")
143
+ ary.extend AnnotatedArray
144
+
145
+ ary.code # => "C" (array-level)
146
+ ary[0].code # => "C" (element annotated)
147
+ ary.first.code # => "C"
148
+ ary.each { |e| puts e.code } # iterates annotated elements
149
+ ```
150
+
151
+ `AnnotatedArrayItem` helpers:
152
+ - `container` -> reference to the array that annotated the item.
153
+ - `container_index` -> index position supplied by the array when returning that item.
154
+
155
+ Utility:
156
+ - `AnnotatedArray.is_contained?(obj)` -> true if obj is annotated as an `AnnotatedArrayItem`.
157
+
158
+ ---
159
+
160
+ ## Serialization and Marshal support
161
+
162
+ Annotations are stored as instance variables on the annotated objects; thus, `Marshal.dump` / `Marshal.load` preserve annotations and attribute values.
163
+
164
+ Example (from tests):
165
+ ```ruby
166
+ a = MyAnnotation.setup("a", code: 'test1', code2: 'test2')
167
+ serialized = Marshal.dump(a)
168
+ a2 = Marshal.load(serialized)
169
+ a2.code # => 'test1'
170
+ ```
171
+
172
+ Arrays extended with `AnnotatedArray` and annotated likewise survive Marshal roundtrip; loaded arrays still annotate their elements.
173
+
174
+ ---
175
+
176
+ ## Purging annotations
177
+
178
+ - AnnotatedObject#purge removes annotation instance variables from the object and returns a dup without annotations (`@annotations`, `@annotation_types`, `@container`).
179
+ - Annotation.purge(obj) is recursive:
180
+ - If obj is nil => returns nil.
181
+ - If obj is an annotated array => calls the object's purge and then purges each element recursively.
182
+ - If obj is an Array => returns an Array where each element is purged.
183
+ - If obj is a Hash => returns a new Hash with purged keys and values.
184
+ - Otherwise, if object is annotated (`Annotation.is_annotated?(obj)`), returns `obj.purge`, else returns the object itself.
185
+
186
+ Example:
187
+ ```ruby
188
+ ary = ["string"]
189
+ MyAnnotation.setup(ary, "C")
190
+ ary.extend AnnotatedArray
191
+
192
+ Annotation.is_annotated?(ary) # => true
193
+ Annotation.is_annotated?(ary.first) # => true
194
+
195
+ purged = Annotation.purge(ary)
196
+ Annotation.is_annotated?(purged) # => false
197
+ Annotation.is_annotated?(purged.first) # => false
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Detection helpers
203
+
204
+ - `Annotation.is_annotated?(obj)` -> true if the object has been annotated (the object has an `@annotation_types` instance variable).
205
+ - `AnnotatedArray.is_contained?(obj)` -> true if object is an `AnnotatedArrayItem`.
206
+
207
+ ---
208
+
209
+ ## Extending and composing annotations
210
+
211
+ - Multiple annotation modules can be applied to the same object. Their attributes and values are merged on the object.
212
+ - Annotation modules may `include` other annotation modules. When a module including another annotation module is itself extended into an object, the included module's declared attributes are propagated.
213
+ - When a module `extend Annotation`, the `Annotation.extended` hook ensures:
214
+ - `@annotations` is initialized,
215
+ - the module includes `Annotation::AnnotatedObject` (so annotated objects get object helpers),
216
+ - the module extends `Annotation::AnnotationModule` (which implements `annotation`, `setup`, and include/extend integration code).
217
+
218
+ Example of composing annotations:
219
+ ```ruby
220
+ module A
221
+ extend Annotation
222
+ annotation :a1
223
+ end
224
+
225
+ module B
226
+ extend Annotation
227
+ annotation :b1
228
+ end
229
+
230
+ obj = "s"
231
+ Annotation.setup(obj, [A, B], a1: 'one', b1: 'two')
232
+ # obj now responds to a1, b1
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Notes and edge cases
238
+
239
+ - `Annotation.setup(obj, ...)` returns `nil` immediately if `obj.nil?`.
240
+ - If the target object is frozen, the setup will duplicate it before extending.
241
+ - `AnnotationModule.setup` can accept positional values (zipped with the declared attributes) or a hash mapping attribute names to values.
242
+ - Example: `MyModule.setup(obj, :val_for_first)` sets the first declared attribute to `:val_for_first`.
243
+ - Example: `MyModule.setup(obj, :a => 1, :b => 2)` sets attributes by name.
244
+ - You can annotate a block/proc by passing a block to `setup` (or passing `nil` as the object and supplying a block). The block (Proc) will be extended with the annotation module.
245
+ - `Annotation.setup` accepts a third argument (`annotation_hash`) or positional values similar to the module-level `setup`. It also accepts `annotation_types` as a string with `|` separated module names, an Array of modules, or a single module.
246
+
247
+ ---
248
+
249
+ ## API quick reference
250
+
251
+ Annotation module-level:
252
+ - Annotation.setup(obj, annotation_types, annotation_hash_or_positional_values)
253
+ - obj: object to annotate (String, Array, Array instance, Proc, etc.)
254
+ - annotation_types: Module, String (module names separated by `|`), or Array of modules
255
+ - annotation_hash_or_positional_values: Hash or positional values mapped to declared attributes
256
+ - returns the annotated object (or nil if obj.nil?)
257
+
258
+ - Annotation.extended(base) (internal hook) — prepares modules that `extend Annotation`.
259
+ - Annotation.is_annotated?(obj) -> boolean
260
+ - Annotation.purge(obj) -> returns object or a purged (annotation-free) copy/structure
261
+
262
+ Annotation::AnnotationModule (module methods available on modules that `extend Annotation`):
263
+ - annotation(*attrs) -> declare attributes and create accessors
264
+ - annotations -> declared attributes list
265
+ - included(mod) — when the annotation module is included in another module, merges declared attributes
266
+ - extended(obj) — when the annotation module is extended into an object, sets up `@annotations` and registers this module into the object's `annotation_types`
267
+ - setup(obj, *values_or_hash, &block) -> annotate `obj` (or block) with this module and set attribute values
268
+
269
+ Annotation::AnnotatedObject (instance methods added to annotated objects):
270
+ - annotation_types -> array of modules applied
271
+ - annotation_hash -> Hash of attribute names => values
272
+ - annotation_info -> combines annotation_hash + metadata
273
+ - serialize / AnnotatedObject.serialize(obj) -> purged annotation_info merged with literal
274
+ - annotation_id / id -> digest based id
275
+ - annotate(other) -> copy annotations onto `other`
276
+ - purge -> duplicate object and remove annotation instance variables
277
+ - make_array -> wrap object into annotated array
278
+
279
+ AnnotatedArray (array-level helpers):
280
+ - extend AnnotatedArray to annotate arrays and propagate annotations to their items
281
+ - annotate_item(obj, position = nil) -> annotate an item and set container/container_index
282
+ - [] (overridden), first, last, each, each_with_index, select, inject, collect, compact, uniq, flatten, reverse, sort_by, subset, remove
283
+
284
+ ---
285
+
286
+ ## Examples (from tests)
287
+
288
+ Define annotation modules:
289
+
290
+ ```ruby
291
+ module AnnotationClass
292
+ extend Annotation
293
+ annotation :code, :code2
294
+ end
295
+
296
+ module AnnotationClass2
297
+ extend Annotation
298
+ annotation :code3, :code4
299
+ end
300
+ ```
301
+
302
+ Annotate a string:
303
+
304
+ ```ruby
305
+ str = "String"
306
+ AnnotationClass.setup(str, :code) # sets str.code == :code
307
+ AnnotationClass2.setup(str, :c3, :c4)
308
+ # str now includes both annotation modules and has code/code2/code3/code4
309
+ ```
310
+
311
+ Annotate arrays and propagate to elements:
312
+
313
+ ```ruby
314
+ ary = ["string"]
315
+ AnnotationClass.setup(ary, "Annotation String")
316
+ ary.extend AnnotatedArray
317
+ ary.code # => "Annotation String"
318
+ ary[0].code # => "Annotation String"
319
+ ary.first.code # => "Annotation String"
320
+ ```
321
+
322
+ Purge annotations:
323
+
324
+ ```ruby
325
+ ary = ["string"]
326
+ AnnotationClass.setup(ary, "C")
327
+ ary.extend AnnotatedArray
328
+
329
+ purged = Annotation.purge(ary)
330
+ # purged and purged.first are not annotated anymore
331
+ ```
332
+
333
+ Marshal roundtrip preserves annotations:
334
+
335
+ ```ruby
336
+ a = AnnotationClass.setup("a", code: 'test1', code2: 'test2')
337
+ d = Marshal.dump(a)
338
+ a2 = Marshal.load(d)
339
+ a2.code # => 'test1'
340
+ ```
341
+
342
+ Annotating a block:
343
+
344
+ ```ruby
345
+ # annotate the block (proc) itself
346
+ proc_obj = AnnotationClass.setup(nil, code: :c) do
347
+ puts "hello"
348
+ end
349
+ proc_obj.code # => :c
350
+ ```
351
+
352
+ This document covers the primary use and behaviors of Annotation, the annotation modules that extend it, the AnnotatedObject helpers added to annotated objects, and the AnnotatedArray behaviors. Use the examples above as templates to create, combine, and apply annotations to objects and collections.
data/doc/CMD.md ADDED
@@ -0,0 +1,203 @@
1
+ # CMD
2
+
3
+ CMD provides a convenience layer for running external commands, capturing/streaming their IO, integrating with the framework's ConcurrentStream and Open helpers, and for tool discovery/installation helpers. It wraps Open3.popen3 and adds standard patterns for piping, feeding stdin, logging stderr, auto-joining producer threads/processes, and error handling.
4
+
5
+ Key features:
6
+ - Run commands (synchronously or as streams) with flexible options.
7
+ - Pipe command output as ConcurrentStream-enabled IO so consumers can read and then join/wait for producers.
8
+ - Feed data into command stdin from String/IO.
9
+ - Collect and log stderr, optionally saving it.
10
+ - Auto-join producer threads/PIDs and surface process failures as exceptions.
11
+ - Tool discovery/installation helpers (TOOLS registry, get_tool, conda, scan version).
12
+ - Convenience helpers: bash, cmd_pid, cmd_log.
13
+
14
+ ---
15
+
16
+ ## Basic usage
17
+
18
+ - CMD.cmd(command_or_tool, cmd_fragment_or_options = nil, options = {}) -> returns:
19
+ - When run with `:pipe => true` returns an IO-like stream (ConcurrentStream-enabled) that you can read from; caller should join or let autojoin close/join.
20
+ - When `:pipe => false` (default) returns a StringIO containing stdout (collected), after waiting for process completion.
21
+
22
+ Examples:
23
+ ```ruby
24
+ # simple capture
25
+ out = CMD.cmd("echo '{opt}' test").read # => "test\n"
26
+ # with options processed into the command
27
+ out = CMD.cmd("cut", "-f" => 2, "-d" => ' ', :in => "a b").read # => "b\n"
28
+
29
+ # pipe mode (stream returned)
30
+ stream = CMD.cmd("tail -f /var/log/syslog", :pipe => true)
31
+ puts stream.read # streaming consumption
32
+ stream.join # wait for producers and check exit status
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Important options
38
+
39
+ All options are passed as an options Hash (converted with IndiferentHash), and many are special keys:
40
+
41
+ - :pipe (boolean) — if true, return a stream you can read from; otherwise CMD returns a StringIO after the process completes.
42
+ - :in — input to feed to the command:
43
+ - String will be wrapped by StringIO and streamed to process stdin.
44
+ - IO/StringIO passed will be consumed using `readpartial` in a background thread.
45
+ - :stderr — controls stderr logging/handling:
46
+ - Integer severity → Log.log writes at that severity.
47
+ - true → maps to Log::HIGH.
48
+ - If stderr is enabled, stderr lines are logged as they arrive.
49
+ - :post — callable (proc) run after command finishes (attached as stream callback in pipe mode).
50
+ - :log — boolean to enable logging of stderr to Log (default true in many paths). Passing true/false toggles logging.
51
+ - :no_fail (or :nofail) — if true do not raise on non-zero exit in pipe-mode setup; if omitted errors raise ProcessFailed or ConcurrentStreamProcessFailed.
52
+ - :autojoin — when true, the returned stream will auto-join producers on EOF/close (defaults in many calls to match :no_wait).
53
+ - :no_wait — don't wait for process to finish (used to set autojoin).
54
+ - :xvfb — if true or string, wrap command in xvfb-run with server args (helper for GUI/CMD).
55
+ - :progress_bar / :bar — pass a ProgressBar object to process stderr lines via bar.process.
56
+ - :save_stderr — if true, collect stderr lines into stream.std_err.
57
+ - :dont_close_in — when feeding :in IO, do not close the source IO after streaming to stdin.
58
+ - :log, :autojoin, :no_fail, :pipe, :in etc. are all processed and removed from the command string.
59
+
60
+ Command option helpers:
61
+ - CMD.process_cmd_options(options_hash) → returns CLI options string:
62
+ - If `:add_option_dashes` key set, keys without leading dashes are prefixed with `--`.
63
+ - Values are quoted and single quotes escaped.
64
+ - Handles boolean flags (true/false/nil).
65
+
66
+ Examples:
67
+ ```ruby
68
+ CMD.process_cmd_options("--user-agent" => "firefox")
69
+ # => "--user-agent 'firefox'"
70
+
71
+ CMD.process_cmd_options("--user-agent=" => "firefox")
72
+ # => "--user-agent='firefox'"
73
+
74
+ CMD.process_cmd_options("-q" => true)
75
+ # => "-q"
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Streaming mode internals
81
+
82
+ When `:pipe => true`:
83
+ - CMD uses Open3.popen3 to spawn the process and receives sin (stdin), sout (stdout), serr (stderr), wait_thr.
84
+ - If `:in` is provided and is an IO/StringIO, a background thread writes it into process stdin (unless `dont_close_in`).
85
+ - Stderr is consumed in a background thread (either logged via Log at the provided severity, or passed to ProgressBar if provided, or collected if `save_stderr`).
86
+ - `ConcurrentStream.setup` is called on the returned `sout` with threads and pids plus options like `autojoin` and `no_fail`.
87
+ - That allows consumers to call `sout.read`, `sout.join`, or rely on `autojoin` to close/join automatically.
88
+ - `sout.callback` can be set to `post` callable to run after successful join.
89
+
90
+ Error handling:
91
+ - For pipe mode the library will detect non-zero process exit and raise `ConcurrentStreamProcessFailed` on join unless `no_fail` is true.
92
+ - If `:no_fail` is passed true, failures are logged but not raised.
93
+
94
+ ---
95
+
96
+ ## Non-pipe mode internals
97
+
98
+ When `:pipe` is false (default):
99
+ - CMD still uses Open3.popen3, but it reads all stdout into a StringIO and waits for process completion before returning.
100
+ - Stderr is read in a background thread and optionally logged/collected; after process completion, if process exit is non-zero, a `ProcessFailed` exception is raised (unless `no_fail`).
101
+ - This mode is convenient for quick synchronous captures.
102
+
103
+ ---
104
+
105
+ ## Tool discovery & installation helpers
106
+
107
+ - CMD.tool(name, claim = nil, test = nil, cmd = nil, &block)
108
+ - Register tools with metadata: claim (Resource or Path), a test command, install block/command and optional fallback cmd string.
109
+
110
+ - CMD.get_tool(tool)
111
+ - Check if tool is available (runs `test` or `cmd --help`); if not, attempts to produce claim or run registered block to install.
112
+ - Caches result in @@init_cmd_tool to avoid repeated checks.
113
+ - Attempts to read version by trying `--version`, `-version`, `--help`, etc., and parsing text via `CMD.scan_version_text`.
114
+
115
+ - CMD.scan_version_text(text, cmd = nil) → returns matched version string or nil.
116
+ - Heuristics to find version substrings related to the command name.
117
+
118
+ - CMD.conda(tool, env = nil, channel = 'bioconda')
119
+ - Convenience to install with conda in either a given env or the login shell.
120
+
121
+ ---
122
+
123
+ ## Convenience wrappers
124
+
125
+ - CMD.bash(command_string)
126
+ - Runs the given commands inside `bash -l` (login shell) and returns the resulting stream (pipe) — helpful when you need shell initialization (e.g., conda).
127
+
128
+ - CMD.cmd_pid(...) / CMD.cmd_log(...)
129
+ - `cmd_pid` runs a pipe command while streaming stdout to STDERR (or logs) and returns nil; it handles progress bars and returns after join.
130
+ - `cmd_log` is a thin wrapper around `cmd_pid` that simply returns nil.
131
+
132
+ ---
133
+
134
+ ## Error types
135
+
136
+ - ProcessFailed — raised for non-zero exit in synchronous mode or when explicitly checked.
137
+ - ConcurrentStreamProcessFailed — raised when pipe-mode join detects failing producer subprocess (non-zero exit) and `no_fail` is not set.
138
+
139
+ ---
140
+
141
+ ## Examples (from tests)
142
+
143
+ - Basic command capture:
144
+ ```ruby
145
+ CMD.cmd("echo '{opt}' test").read # -> "test\n"
146
+ CMD.cmd("cut", "-f" => 2, "-d" => ' ', :in => "one two").read # -> "two\n"
147
+ ```
148
+
149
+ - Pipe usage:
150
+ ```ruby
151
+ stream = CMD.cmd("echo test", :pipe => true)
152
+ puts stream.read # "test\n"
153
+ stream.join
154
+ ```
155
+
156
+ - Piped pipeline:
157
+ ```ruby
158
+ f = Open.open(file)
159
+ io = CMD.cmd('tail -n 10', :in => f, :pipe => true)
160
+ io2 = CMD.cmd('head -n 10', :in => io, :pipe => true)
161
+ io3 = CMD.cmd('head -n 10', :in => io2, :pipe => true)
162
+ puts io3.read.split("\n").length # => 10
163
+ ```
164
+
165
+ - Handling errors:
166
+ ```ruby
167
+ # Raises ProcessFailed for missing command
168
+ CMD.cmd('fake-command')
169
+
170
+ # In pipe mode you may get ConcurrentStreamProcessFailed on join or read/join
171
+ CMD.cmd('grep . NONEXISTINGFILE', :pipe => true).join
172
+ ```
173
+
174
+ - Use `:no_fail => true` to suppress exceptions on failure and just log.
175
+
176
+ ---
177
+
178
+ ## Recommendations & patterns
179
+
180
+ - Prefer `:pipe => true` + ConcurrentStream when you want streaming processing without waiting for full output in memory.
181
+ - Provide `:in` as an IO to stream large inputs into a subprocess.
182
+ - Use `:autojoin => true` to automatically join producers on EOF/close (useful for simple consumers).
183
+ - Register tools via `CMD.tool` and use `CMD.get_tool` to locate or auto-install/produce required tools.
184
+ - Always check or propagate exceptions from `join` for pipe-mode streams to detect failing subprocesses.
185
+
186
+ ---
187
+
188
+ ## Quick API reference
189
+
190
+ - CMD.cmd(tool_or_cmd, cmd_fragment_or_options = nil, options = {}) => StringIO or ConcurrentStream (when pipe)
191
+ - CMD.process_cmd_options(options_hash) => option string appended to command
192
+ - CMD.setup tool registry:
193
+ - CMD.tool(name, claim=nil, test=nil, cmd=nil, &block)
194
+ - CMD.get_tool(name)
195
+ - CMD.scan_version_text(text, cmd = nil)
196
+ - CMD.versions -> hash of detected versions
197
+ - CMD.bash(cmd_string) — run in bash -l
198
+ - CMD.cmd_pid / CMD.cmd_log — helpers for logging and running commands that stream stdout to logs
199
+ - CMD.conda(tool, env=nil, channel='bioconda') — convenience installer wrapper
200
+
201
+ ---
202
+
203
+ CMD centralizes robust process execution patterns needed throughout the framework: streaming, joining, logging, error detection and tool bootstrap. Use its options to control behavior for production-grade command invocation.