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
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.
|