scout-essentials 1.8.7 → 1.9.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 +26 -12
- data/README.md +83 -112
- data/VERSION +1 -1
- data/doc/Improvements.md +226 -0
- data/doc/StartHere.md +122 -0
- data/doc/developer/AnnotationSystem.md +184 -0
- data/doc/developer/Architecture.md +147 -0
- data/doc/developer/Configuration.md +238 -0
- data/doc/developer/CoreUtilities.md +265 -0
- data/doc/developer/DesignPrinciples.md +129 -0
- data/doc/developer/ErrorHandling.md +203 -0
- data/doc/developer/LockingAndConcurrency.md +157 -0
- data/doc/developer/PathResolution.md +200 -0
- data/doc/developer/PersistenceAndResources.md +119 -0
- data/doc/developer/StreamingModel.md +236 -0
- data/doc/user/AnnotatingData.md +202 -0
- data/doc/user/CachingResults.md +183 -0
- data/doc/user/CommandLineOptions.md +189 -0
- data/doc/user/Cookbook.md +211 -0
- data/doc/user/HandlingStreams.md +236 -0
- data/doc/user/LoggingAndProgress.md +158 -0
- data/doc/user/ProducingResources.md +177 -0
- data/doc/user/RemoteData.md +157 -0
- data/doc/user/RunningCommands.md +218 -0
- data/doc/user/WorkingWithFiles.md +217 -0
- data/lib/scout/cmd.rb +343 -40
- data/lib/scout/concurrent_stream.rb +14 -1
- data/lib/scout/indiferent_hash.rb +1 -1
- data/lib/scout/log/fingerprint.rb +13 -8
- data/lib/scout/log/progress/report.rb +1 -1
- data/lib/scout/log.rb +4 -1
- data/lib/scout/misc/digest.rb +6 -5
- data/lib/scout/misc/format.rb +24 -0
- data/lib/scout/named_array.rb +1 -1
- data/lib/scout/open/stream.rb +2 -2
- data/lib/scout/open/util.rb +8 -4
- data/lib/scout/open.rb +3 -3
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- data/lib/scout/resource/produce.rb +9 -1
- data/research/annotations-data-analysis.md +206 -0
- data/research/behavior-probes.md +1925 -0
- data/research/commands-streaming-analysis.md +272 -0
- data/research/design-philosophy-analysis.md +383 -0
- data/research/doc-audit-findings.md +294 -0
- data/research/ecosystem-attribution.md +118 -0
- data/research/implementation-inventory-core.md +1029 -0
- data/research/implementation-inventory-open.md +417 -0
- data/research/implementation-inventory-path-persist-resource.md +774 -0
- data/research/io-paths-analysis.md +228 -0
- data/research/persistence-resources-analysis.md +244 -0
- data/research/synthesis-report.md +80 -0
- data/scout-essentials.gemspec +37 -15
- data/test/scout/open/test_remote.rb +1 -2
- data/test/scout/test_cmd.rb +411 -0
- metadata +36 -14
- data/doc/Annotation.md +0 -352
- data/doc/CMD.md +0 -363
- data/doc/ConcurrentStream.md +0 -163
- data/doc/IndiferentHash.md +0 -240
- data/doc/Log.md +0 -235
- data/doc/NamedArray.md +0 -174
- data/doc/Open.md +0 -331
- data/doc/Path.md +0 -217
- data/doc/Persist.md +0 -214
- data/doc/Resource.md +0 -229
- data/doc/SimpleOPT.md +0 -236
- data/doc/TmpFile.md +0 -154
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
# Implementation inventory — CORE chunk (audit chunk 1)
|
|
2
|
+
|
|
3
|
+
Evidence-backed inventory of the core utility subsystems of `scout-essentials`
|
|
4
|
+
(v1.8.8). Every claim carries a `file:line` anchor. Disputed semantics were
|
|
5
|
+
settled with `ruby -Ilib` probes (see `research/behavior-probes.md`).
|
|
6
|
+
|
|
7
|
+
Entry point facts that frame everything else:
|
|
8
|
+
|
|
9
|
+
- `lib/scout-essentials.rb:1-10` — the only load entry. Requires in order:
|
|
10
|
+
`scout/exceptions`, `scout/indiferent_hash`, `scout/tmpfile`, `scout/log`,
|
|
11
|
+
`scout/path`, `scout/simple_opt`, `scout/resource`, `scout/resource/scout`,
|
|
12
|
+
`scout/persist`, `scout/config`.
|
|
13
|
+
- `lib/scout.rb` **does not exist** (verified: `ls lib/scout.rb` → No such file).
|
|
14
|
+
There is no `Scout` module file; `module Scout` appears only inside
|
|
15
|
+
`lib/scout/config.rb:5` (as `module Scout::Config`, re-opening `Scout` created
|
|
16
|
+
by `resource/scout.rb`) and `lib/scout/resource/scout.rb:1` (`module Scout;
|
|
17
|
+
extend Resource; self.pkgdir = 'scout'`).
|
|
18
|
+
- Consequently: `NamedArray` and `Hook` are **not** loaded by
|
|
19
|
+
`require 'scout-essentials'` (they need explicit
|
|
20
|
+
`require 'scout/named_array'` / `'scout/misc/hook'`; probe P5).
|
|
21
|
+
`Misc`, `TmpFile`, `Log`, `SOPT`, `IndiferentHash` **are** loaded — and so
|
|
22
|
+
is `CMD` (transitively, via the `open`/`tmpfile` require chain; probe P5
|
|
23
|
+
and follow-up `Object.const_defined?(:CMD) => true`).
|
|
24
|
+
- `lib/scout/tmpfile.rb:1` requires `open` (pulling `ConcurrentStream`, `Open`),
|
|
25
|
+
`misc`, `log` — so `misc.rb` and `open.rb` are load-order ancestors of
|
|
26
|
+
`scout-essentials`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. `lib/scout-essentials.rb` (10 lines)
|
|
31
|
+
|
|
32
|
+
Pure require cascade; no code. Ordering above.
|
|
33
|
+
|
|
34
|
+
What downstream consumes: the `require 'scout-essentials'` idiom itself, plus
|
|
35
|
+
the implicit guarantee that after it `Log`, `Misc`, `TmpFile`, `IndiferentHash`,
|
|
36
|
+
`SOPT`, `Path`, `Scout::Config`, `Persist`, `Scout` are defined.
|
|
37
|
+
|
|
38
|
+
Subtlety worth documenting: after a bare `require 'scout-essentials'` the
|
|
39
|
+
defined constants include `Misc`, `TmpFile`, `Log`, `SOPT`,
|
|
40
|
+
`IndiferentHash`, `Scout::Config` **and `CMD`** (CMD is pulled in
|
|
41
|
+
transitively, see probe P5), while **`NamedArray` and `Hook` are NOT defined**
|
|
42
|
+
(`tmpfile` does not require `named_array`; `misc.rb` does not require
|
|
43
|
+
`misc/hook`). Code assuming either is present must add
|
|
44
|
+
`require 'scout/named_array'` / `require 'scout/misc/hook'`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. `Scout::Config` — `lib/scout/config.rb` (184 lines)
|
|
49
|
+
|
|
50
|
+
### Structure
|
|
51
|
+
- `module Scout::Config` (`config.rb:5`) — class methods only (module with
|
|
52
|
+
`self.` defs, no instances).
|
|
53
|
+
- State: `CACHE ||= IndiferentHash.setup({})` (`:7`) mapping
|
|
54
|
+
`key_string => [[tokens_array, value], ...]`; `GOT_KEYS = []` (`:9`) audit
|
|
55
|
+
trail of `[key, value, tokens]` tuples appended on every `get` (`:136`).
|
|
56
|
+
- Loads at require time: `self.load_config` (`:183`).
|
|
57
|
+
|
|
58
|
+
### Key public methods
|
|
59
|
+
- `add_entry(key, value, tokens)` (`:11-16`) — appends `[tokens, value]` under
|
|
60
|
+
`key.to_s`; always ensures `key:<key>` is among tokens (`:13`).
|
|
61
|
+
- `load_file(file)` (`:18-26`) — line format `key value token token ...` split
|
|
62
|
+
on whitespace; `#`-prefixed lines skipped (`:21`); **empty `key` lines are
|
|
63
|
+
skipped** (`:24` — `if key`). Each line becomes one entry.
|
|
64
|
+
- `load_config` (`:28-32`) — `Path.setup("etc").config.find_all.reverse.each`,
|
|
65
|
+
i.e. all `config` files in the `etc` search path, later files override by
|
|
66
|
+
order of insertion into CACHE (not by load order semantics — ordering matters
|
|
67
|
+
only via `unshift`/`concat` in `match`, see below).
|
|
68
|
+
- `set(values, *tokens)` (`:34-42`) — either a Hash of pairs, or
|
|
69
|
+
`set(key, value, *tokens)` (non-Hash first arg → `{values => tokens.shift}`).
|
|
70
|
+
- `token_priority(token)` (`:44-68`) — token format `name[::N]`:
|
|
71
|
+
explicit `::N` numeric priority wins (`:63-65`); else inferred by prefix:
|
|
72
|
+
`workflow`→4, `task`→3, `file`→2, `line`→1, `key`→20, else 10 (`:49-62`).
|
|
73
|
+
Returns `[token_name_without_priority, priority]`.
|
|
74
|
+
- `match(entries, give_token)` (`:70-87`) — builds `priorities[prio] => [values]`
|
|
75
|
+
for entries whose tokens include `give_token`; **`unshift`s values**
|
|
76
|
+
(`:83`), so later-loaded entries surface first at equal priority.
|
|
77
|
+
- `get(key, *tokens)` (`:90-150`) — resolution algorithm:
|
|
78
|
+
1. pop trailing Hash options; `:default` and `:env` (`:91-95`);
|
|
79
|
+
2. `:env` — first set var among comma-split names becomes `default`
|
|
80
|
+
(`:97-104`);
|
|
81
|
+
3. `tokens = ["key:" + key] if tokens.empty?` (`:106`) — **requires String
|
|
82
|
+
key**; a Symbol key here raises `TypeError` (probe P3a: `no implicit
|
|
83
|
+
conversion of Symbol into String`);
|
|
84
|
+
4. caller inspection adds `file:<path>` and `line:<path>:<lineno>` tokens
|
|
85
|
+
(`:109-120`), filtering frames matching rbbt-era paths
|
|
86
|
+
(`rbbt/(resource.rb|workflow.rb)`, `rbbt/resource/path.rb`,
|
|
87
|
+
`rbbt/util/misc.rb`, `accessor.rb`, `progress-monitor.rb`, `:110-115`) —
|
|
88
|
+
**legacy rbbt regexes, not scout paths**; the caller token logic therefore
|
|
89
|
+
rarely filters anything in scout code today;
|
|
90
|
+
5. all matching tokens collapse into `priorities` (`:122-130`);
|
|
91
|
+
6. `value = priorities.collect{|p| p }.sort_by{|p,v| p}.first.last.first`
|
|
92
|
+
(`:132`) — **lowest priority number wins**, and among a priority bucket
|
|
93
|
+
the **first** value wins (i.e. the last-loaded one, due to unshift);
|
|
94
|
+
7. `'false'` → `false` (`:133`);
|
|
95
|
+
8. `GOT_KEYS << [key, value, tokens]` (`:136`);
|
|
96
|
+
9. value `env:VAR1,VAR2` → resolves to first set ENV var, else `nil`
|
|
97
|
+
(`:138-144`); literal `'nil'` → `nil` (`:145`).
|
|
98
|
+
- `with_config` (`:152-164`) — snapshot `CACHE` (deep-dup of value arrays) and
|
|
99
|
+
`GOT_KEYS`, restore in `ensure`.
|
|
100
|
+
- `process_config(config)` (`:166-180`) — CLI-oriented: if `config` is a
|
|
101
|
+
filename that exists → `load_file`; elsif `Scout.etc.config_profile[config]`
|
|
102
|
+
exists → load that; else parse `key value tok::prio ...` and `set`.
|
|
103
|
+
|
|
104
|
+
### Verified semantics (probes)
|
|
105
|
+
- P3: with entries `key:tk` ("plain"), `file:/some/file.rb` ("filetok"),
|
|
106
|
+
`line:/some/file.rb:1` ("linetok"), `workflow::0` ("wftok") all present,
|
|
107
|
+
`get("tk")` → `"wftok"` (probe P16 adds bare-vs-file). Note `workflow::0` (priority 0) beats
|
|
108
|
+
`line:` (1) and `file:` (2); but the generic `key:` token has priority 20,
|
|
109
|
+
i.e. the **lowest precedence** of all — the implicit key token only ever wins
|
|
110
|
+
when nothing else matches.
|
|
111
|
+
- P3b: `get("k7", "key:k7")` with a `key:k7` entry → `"high"`; explicit tokens
|
|
112
|
+
work. `get("k4", "key:k4")` on missing key → `nil` (no default). `get("k5")
|
|
113
|
+
` with `'false'` value → `false`. `get("k6")` with value `env:NOPE1,NOPE2`
|
|
114
|
+
→ `nil` (env fallback unset).
|
|
115
|
+
- P3b: `Path.setup("etc").config.find_all` → `[]` in a clean checkout (no
|
|
116
|
+
`etc/config` in the search path), so `load_config` is a no-op here — all
|
|
117
|
+
runtime config comes from `set`/`process_config`/ENV.
|
|
118
|
+
|
|
119
|
+
### Error handling / guarantees
|
|
120
|
+
- No exceptions raised on unknown keys (`get` returns default/nil).
|
|
121
|
+
- `get` with Symbol key and no tokens: TypeError from `:106` (documented
|
|
122
|
+
footgun).
|
|
123
|
+
- `with_config` restores both caches even on raise.
|
|
124
|
+
|
|
125
|
+
### Concurrency
|
|
126
|
+
- No mutex anywhere in config.rb; CACHE is shared mutable state.
|
|
127
|
+
|
|
128
|
+
### External integrations / env vars
|
|
129
|
+
- Reads arbitrary ENV vars via `:env` option (`:98-103`) and `env:` value
|
|
130
|
+
prefix (`:138-144`).
|
|
131
|
+
|
|
132
|
+
### Subtle points (docs plausibly wrong)
|
|
133
|
+
- The **priority scheme is inverted from intuition**: lower number = higher
|
|
134
|
+
precedence, and the implicit `key:` token has the **worst** priority (20),
|
|
135
|
+
so a config line with *any* explicit token overrides a bare `key value`
|
|
136
|
+
line. `workflow` (4) beats `task` (3)? No — `task` (3) beats `workflow` (4);
|
|
137
|
+
but explicit `::0` beats everything.
|
|
138
|
+
- Doc claims about "later files override earlier" are only half-true: within a
|
|
139
|
+
priority bucket the *later inserted* (unshifted) value wins
|
|
140
|
+
(`config.rb:83`), and `load_config` reads files in **reverse** find order
|
|
141
|
+
(`:29`) so the last file in the path is loaded first — combined effect is
|
|
142
|
+
that the *first* file in the path search order wins at equal priority.
|
|
143
|
+
- caller-based `file:`/`line:` tokens are derived from the call site of `get`,
|
|
144
|
+
filtered by rbbt-legacy regexes (`:110-115`), so in scout code they point at
|
|
145
|
+
the scout caller line — any doc describing per-workflow config file
|
|
146
|
+
precedence needs this exact rule.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 3. `IndiferentHash` — `lib/scout/indiferent_hash.rb` (177) + submodules
|
|
151
|
+
|
|
152
|
+
### Structure
|
|
153
|
+
- `module IndiferentHash` used as an **extension** (`self.setup(hash)` does
|
|
154
|
+
`hash.extend IndiferentHash`, `indiferent_hash.rb:7-10`). Not a subclass of
|
|
155
|
+
Hash — an extended plain Hash.
|
|
156
|
+
- Submodules: `indiferent_hash/options.rb` (module-level helpers),
|
|
157
|
+
`case_insensitive.rb` (separate `CaseInsensitiveHash` extension),
|
|
158
|
+
`serialize.rb` (`IndiferentHash.serializable`).
|
|
159
|
+
|
|
160
|
+
### Instance protocol (indiferent_hash.rb)
|
|
161
|
+
- `merge(other)` (`:12-19`) — returns new IndiferentHash, other's keys win.
|
|
162
|
+
- `deep_merge(other)` (`:21-32`) — recursive only when both sides are Hash and
|
|
163
|
+
the existing value is already `IndiferentHash` (`:25`).
|
|
164
|
+
- `[]=(key,value)` (`:34-37`) — **deletes any dual-form key first** (`:35`), so
|
|
165
|
+
writing `h[:a]=1` after `h["a"]=2` leaves a single entry.
|
|
166
|
+
- `[](key)` (`:43-61`) — tries the literal key, then the Symbol↔String swap
|
|
167
|
+
(`:51-58`); nested Hash results get `IndiferentHash.setup` (`:47,59`). Guard
|
|
168
|
+
`_default?` (`:39-41`) avoids returning a default-proc value that isn't a
|
|
169
|
+
real key.
|
|
170
|
+
- `values_at(*keys)` (`:63-65`), `include?` (`:67-76`), `delete` (`:78-89`) —
|
|
171
|
+
all dual-form aware.
|
|
172
|
+
- `clean_version` (`:91-97`) — plain Hash with String keys; **first** key form
|
|
173
|
+
wins on collision (`:94`), so `{"a"=>1, :a=>2}.clean_version` keeps 1 (P1).
|
|
174
|
+
- `slice(*list)` (`:99-114`) — expands each Symbol/String into both forms,
|
|
175
|
+
returns IndiferentHash.
|
|
176
|
+
- `keys_to_sym!` (`:116-125`) / `keys_to_sym` (`:127-138`).
|
|
177
|
+
- `pretty_print` (`:140-142`) → `Misc.format_definition_list(self, sep: "\n")`.
|
|
178
|
+
- `except(*list)` (`:144-160`) — dual-form removal.
|
|
179
|
+
- `dig(*keys)` (`:162-171`) — iteratively setups nested hashes.
|
|
180
|
+
- `self.dig(obj, *keys)` (`:173-176`).
|
|
181
|
+
|
|
182
|
+
### Module helpers (options.rb)
|
|
183
|
+
- `add_defaults(options, defaults = {})` (`:2-15`) — String inputs parsed via
|
|
184
|
+
`string2hash`; does **not** overwrite existing keys (`:9`).
|
|
185
|
+
- `process_options(hash, *keys)` (`:17-28`) — **destructive**: pops the
|
|
186
|
+
requested keys out of `hash` (P2/P6: after processing, `h` retains only the
|
|
187
|
+
un-processed keys) and returns one value or an array. Trailing Hash = defaults.
|
|
188
|
+
- `pull_keys(hash, prefix)` (`:30-51`) — extracts `prefix_options` plus every
|
|
189
|
+
`<prefix>_<rest>` key into a new IndiferentHash, deleting from source.
|
|
190
|
+
- `zip2hash(list1, list2)` (`:53-59`), `positional2hash(keys, *values)`
|
|
191
|
+
(`:61-72`) — merges a trailing Hash of extras (defaults), drops nil/"" values
|
|
192
|
+
and unknown keys (`:65-67`).
|
|
193
|
+
- `array2hash(array, default = nil)` (`:74-81`) — `[k,v]` pairs; `default.dup`
|
|
194
|
+
fills nil values (raises if default lacks `dup`).
|
|
195
|
+
- `process_to_hash(list)` (`:83-86`) — `zip2hash(list, yield(list))`.
|
|
196
|
+
- `hash2string(hash)` (`:88-94`) — `k=v` joined with `#`; only
|
|
197
|
+
Symbol/String/Float/Integer/Numeric/True/False/Module/Class/Object values kept
|
|
198
|
+
(`:90`); Symbol keys get `:` prefix (`:91`). Note `Fixnum` in the whitelist
|
|
199
|
+
is a Ruby<2.4 relic.
|
|
200
|
+
- `string2hash(string, sep="#")` (`:96-116`) — `#`-separated `k=v` pairs.
|
|
201
|
+
Value coercion order (`:104-112`): empty→`true`; `:x`→Symbol; `/re/`→Regexp;
|
|
202
|
+
`'x'`/`"x"`→unquoted String; `\d+`→Integer; `\d*\.\d+`→Float; `"true"`→`true`;
|
|
203
|
+
`"false"`→**stays the String `"false"` (false-bug, see below)**; else String.
|
|
204
|
+
- `parse_options(str)` (`:118-154`) — whitespace-separated `k=v`, quoted values
|
|
205
|
+
keep spaces, comma values split into Arrays preserving quotes (`:130-140`).
|
|
206
|
+
Scalar coercions identical to `string2hash`, including the same false-bug.
|
|
207
|
+
List-valued options (`a=1,2`) become Arrays of Strings and skip the scalar
|
|
208
|
+
coercions entirely.
|
|
209
|
+
- `print_options(options)` (`:156-173`) — inverse-ish of parse_options:
|
|
210
|
+
arrays become `k=a,b` with quoted elements containing spaces; scalar values
|
|
211
|
+
quoted when empty or containing spaces.
|
|
212
|
+
|
|
213
|
+
### CaseInsensitiveHash (case_insensitive.rb)
|
|
214
|
+
- `self.setup(hash)` extend-based (`:3-5`); `downcase_keys` memoized map
|
|
215
|
+
downcased→original (`:7-15`); `[](key, *rest)` falls back to the original
|
|
216
|
+
key on miss (`:17-22`); `values_at` (`:24-28`). Read-only convenience — no
|
|
217
|
+
`[]=`/`delete`/`include?` overrides.
|
|
218
|
+
|
|
219
|
+
### serialize.rb
|
|
220
|
+
- `self.serializable(obj)` (`:2-23`) — deep copy of Hashes, Arrays >100
|
|
221
|
+
truncated to first 70 + last 30 + `['...', 'TRUNCATED only 100 out of N
|
|
222
|
+
shown']` (`:13-18`).
|
|
223
|
+
|
|
224
|
+
### Probes
|
|
225
|
+
- P1: symbol/string interchange, slice, merge, clean_version first-wins, dig.
|
|
226
|
+
- P2: `string2hash` vs `parse_options` coercion table (P2's `g=false` line
|
|
227
|
+
predates the false-bug investigation; P20 supersedes it).
|
|
228
|
+
- P2: `process_options` destructiveness (`h.keys` shrinks).
|
|
229
|
+
|
|
230
|
+
### Subtle points
|
|
231
|
+
- **`"false"` never coerces to `false`** in either `string2hash` or
|
|
232
|
+
`parse_options` (`options.rb:110-111` and `:148-149`): the idiom
|
|
233
|
+
`options[key] = false and next if value == "false"` assigns `false`, but the
|
|
234
|
+
assignment itself evaluates to falsy `false`, so `and next` does not fire and
|
|
235
|
+
the trailing `options[key] = value` overwrites it with the String `"false"`
|
|
236
|
+
(probe P20; verified against the installed gem copy too). `"true"` coerces
|
|
237
|
+
correctly only by luck. `Scout::Config.get` *does* coerce the String
|
|
238
|
+
`'false'` (config.rb:133), so the gem's two boolean-from-string mechanisms
|
|
239
|
+
disagree.
|
|
240
|
+
- `clean_version` keeps the **first** key encountered, not "string wins" —
|
|
241
|
+
with `{:a=>1,"a"=>2}` the Symbol survives (P1: `["a","b"]` after clean on
|
|
242
|
+
`{"a"=>1,:b=>2}` was a different case; the general rule is `each` order,
|
|
243
|
+
`:91-96`).
|
|
244
|
+
- `process_options` **mutates** its argument; docs claiming a pure accessor
|
|
245
|
+
would be wrong.
|
|
246
|
+
- (superseded by the bullet above — the two parsers actually agree, both fail
|
|
247
|
+
to produce `false`; the real asymmetry is with `Scout::Config.get`.)
|
|
248
|
+
- `IndiferentHash` is an extension, so `IndiferentHash === h` is false for a
|
|
249
|
+
plain Hash and `h.class` stays `Hash`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 4. `NamedArray` — `lib/scout/named_array.rb` (165 lines)
|
|
254
|
+
|
|
255
|
+
### Structure
|
|
256
|
+
- `module NamedArray; extend Annotation; annotation :fields, :key` (`:2-4`) —
|
|
257
|
+
an Annotation module applied to Arrays. Requires `annotation` (`:1`).
|
|
258
|
+
- Loaded **only** via explicit `require 'scout/named_array'` (P5: NameError
|
|
259
|
+
under bare `scout-essentials`).
|
|
260
|
+
|
|
261
|
+
### Class methods
|
|
262
|
+
- `field_match(field, name)` (`:10-20`) — tolerant equality: exact; `"(name)"`
|
|
263
|
+
inside field or vice-versa; prefix followed by space. Non-Strings compared
|
|
264
|
+
with `==`.
|
|
265
|
+
- `identify_name(names, selected, strict: false)` (`:22-57`) — maps a field
|
|
266
|
+
selector to an index (or Range/Integer passthrough, `:27-30`); `Symbol :key`
|
|
267
|
+
→ `:key` (`:32`); `Symbol` otherwise recurses via `to_s` (`:32`);
|
|
268
|
+
Strings: exact index (`:42-43`), numeric-string → Integer (`:44-46`),
|
|
269
|
+
`strict` stops (`:47`), else `field_match` scan (`:48-49`), else nil. Note
|
|
270
|
+
dead branch `when (names.nil? and String)` (`:33-38`): if `names` is nil
|
|
271
|
+
and the selector is a String, the code calls
|
|
272
|
+
`identify_field(key_field, fields, ...)` on **undefined locals**, so the
|
|
273
|
+
branch would raise NameError if ever reached. Latent bug, noted for docs.
|
|
274
|
+
- `_zip_fields(array, max = nil)` (`:113-125`) / `zip_fields(array)`
|
|
275
|
+
(`:127-144`, slices at 10000) / `add_zipped(source, new)` (`:146-152`).
|
|
276
|
+
**Line `:119` is dead code** (P15/P15b): `v.length == 1 & max > 1` parses
|
|
277
|
+
as `v.length == ((1 & max) > 1)` — `1 & max` is always 0/1 so
|
|
278
|
+
`(1 & max) > 1` is always false, and singleton columns other than the
|
|
279
|
+
first are NEVER repeated (observed: nil padding). Only the first column
|
|
280
|
+
(`:126`, `first.length == 1 and max > 1`) can be broadcast — asymmetric
|
|
281
|
+
behavior docs must not "fix" into symmetry.
|
|
282
|
+
|
|
283
|
+
### Instance protocol
|
|
284
|
+
- `all_fields` (`:6-8`), `identify_name(selected)` (`:59-61`),
|
|
285
|
+
`positions(fields)` (`:63-71`), `[](key)`/`[]=(key,value)` by field name
|
|
286
|
+
(`:73-83`, returning nil silently when the field is unknown),
|
|
287
|
+
`concat(other)` handling Hash or NamedArray (`:86-99`), `to_hash`
|
|
288
|
+
(`:101-107`, IndiferentHash), `values_at(*positions)` (`:109-111`),
|
|
289
|
+
`method_missing` for field-name accessors (`:154-160`), `pretty_print`
|
|
290
|
+
(`:162-164`).
|
|
291
|
+
|
|
292
|
+
### Probes (P15)
|
|
293
|
+
- `NamedArray.setup([1,2], [:a,:b])` → `.a`→1, `.b`→2, `.to_hash`→
|
|
294
|
+
`{:a=>1,:b=>2}`; `identify_name([:a,:b], "b")`→1, `"zzz"`→nil;
|
|
295
|
+
`positions("b")`→1, `[:a,:b]`→[0,1]; `field_match` tolerance confirmed
|
|
296
|
+
(0 for exact, nil for unrelated).
|
|
297
|
+
|
|
298
|
+
### Subtle points
|
|
299
|
+
- `[]=` silently no-ops on unknown field (`:81-82`).
|
|
300
|
+
- Not auto-loaded (footgun for docs).
|
|
301
|
+
- `_zip_fields` boolean/bitwise expression at `:119`.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 5. `Misc` — `lib/scout/misc.rb` (11 lines) + 10 submodules
|
|
306
|
+
|
|
307
|
+
`misc.rb:1-9` requires format, insist, digest, filesystem, monitor, system,
|
|
308
|
+
helper, matching, math — **note `hook.rb` is NOT required** by `misc.rb`
|
|
309
|
+
(verified: `misc.rb:1-9` list). `Hook` is a separate top-level module.
|
|
310
|
+
|
|
311
|
+
### 5.1 misc/format.rb (310 lines) — string/number formatting
|
|
312
|
+
- `COLOR_LIST` (`:2`), `colors_for(list)` (`:4-19`) assigns colors in order,
|
|
313
|
+
memoizing per distinct element, returns `[colors, used]`.
|
|
314
|
+
- `format_seconds(time, extended = false)` (`:21-26`) → `HH:MM:SS` (+`.cs`).
|
|
315
|
+
- `CHAR_SENCONDS` (`:28`, typo for SECONDS; `"″"` unless `SCOUT_NOCOLOR`) and
|
|
316
|
+
`format_seconds_short` (`:29-37`).
|
|
317
|
+
- `MAX_TTY_LINE_WIDTH = 120` (`:39`).
|
|
318
|
+
- `format_paragraph(text, size, indent, offset)` (`:40-73`) — wraps text at
|
|
319
|
+
`size` (default `Log.tty_size || 120`, capped at 120), preserving
|
|
320
|
+
paragraph-like separators (blank lines, markdown `*`/`-`/```/leading spaces)
|
|
321
|
+
via the regex at `:48`; long words truncated with `...` (`:57`).
|
|
322
|
+
- `format_definition_list_item(dt, dd, indent, size, color: :yellow)`
|
|
323
|
+
(`:75-100`) and `format_definition_list(defs, ...)` (`:102-111`) — used by
|
|
324
|
+
`IndiferentHash#pretty_print`, `NamedArray#pretty_print`, SOPT docs.
|
|
325
|
+
- `camel_case` (`:113-118`, note the misleading `return` inside a guard:
|
|
326
|
+
`return string if string !~ /_/ && string =~ /[A-Z]+.*/`),
|
|
327
|
+
`camel_case_lower` (`:120-124`), `snake_case` (`:126-134`).
|
|
328
|
+
Probes: `snake_case("SomeCamelCase")`→"some_camel_case";
|
|
329
|
+
`camel_case("some_snake_case")`→"SomeSnakeCase";
|
|
330
|
+
`camel_case("ABCdef")`→"ABCdef" (acronym preserved);
|
|
331
|
+
`camel_case_lower`→"someSnakeCase".
|
|
332
|
+
- `humanize(value, options = {})` (`:138-178`) — formats `:sentence` (default),
|
|
333
|
+
`:allcaps`, `:class`, `:nocaps`; acronym-aware (Miguel Vazquez edit note at
|
|
334
|
+
`:146`). Probes: "some_field_name"→"Some field name";
|
|
335
|
+
`format: :class`→"SomeFieldName"; "ABC_acronym_x"→"ABC acronym x".
|
|
336
|
+
- `fixascii`/`to_utf8`/`fixutf8` (`:180-204`) — encoding repair;
|
|
337
|
+
`fixutf8` has an operator-precedence-laden guard at `:194-195`.
|
|
338
|
+
- `humanize_list(list)` (`:206-213`).
|
|
339
|
+
- `human_number(n)` (`:215-237`) — K/M/B/T units. Probes: 0→"0", 950→"950",
|
|
340
|
+
1234→"1.2K", -2500000→"-2.5M".
|
|
341
|
+
- `parse_sql_values(txt)` (`:239-277`) — minimal INSERT-values parser.
|
|
342
|
+
Probe: `('a','b,c'),(1,2)` → `[["a","b,c"],["1","2"]]` (all values Strings).
|
|
343
|
+
- `timespan(str, default = "s")` (`:279-309`) — leading `-` negates
|
|
344
|
+
(`:281`); `HH:MM:SS`/`MM:SS` handled (`:283-286`, note the
|
|
345
|
+
`seconds, minutes, hours` naming of the reversed split — a 2-part
|
|
346
|
+
`"01:02"` string is minutes:seconds); token table `:288-300` (`s,sec,m,min,
|
|
347
|
+
','',h,d,w,mo,y`); `tokens[nil]`/`tokens[""]` default (`:302-303`);
|
|
348
|
+
aggregates `(\d+)(\w*)` pairs (`:305-307`).
|
|
349
|
+
- **Compound timespans and unknown units raise `TypeError`** (P7/P15,
|
|
350
|
+
confirmed against the library itself):
|
|
351
|
+
`Misc.timespan("1h30m")` and `Misc.timespan("1x")` both raise
|
|
352
|
+
`TypeError: nil can't be coerced into Integer`, because the scan yields the
|
|
353
|
+
unit string `"h30m"` (or `"x"`) which is absent from the token table, and
|
|
354
|
+
`amount.to_i * nil` blows up. Working forms: `"10"`→10, `"10s"`→10,
|
|
355
|
+
`"01:02:03"`→3723, `"01:02"`→62, `"1,30"`→31, `"1'30''"`→31,
|
|
356
|
+
`"-10s"`→-10, `"1d"`→86400, `"1mo"`→2678400, `"1y"`→31536000.
|
|
357
|
+
Docs claiming `"1h30m"`-style compound parsing or lenient unknown-unit
|
|
358
|
+
handling are wrong.
|
|
359
|
+
|
|
360
|
+
### 5.2 misc/digest.rb (93 lines) — content digests
|
|
361
|
+
- `MAX_ARRAY_DIGEST_LENGTH = 100_000` (`:2`).
|
|
362
|
+
- `digest_str(obj)` (`:4-51`) — recursive stable string; Strings that look
|
|
363
|
+
like existing files are digested as paths with a memo cache (`:10-14`,
|
|
364
|
+
`@@digest_str_cache`); Integers/Symbols `to_s`; huge Arrays (>100k) sampled
|
|
365
|
+
at positions 1,2,mid,len-2,len-1 (`:19-26`); Floats formatted by magnitude
|
|
366
|
+
(`:32-40`); Procs → digest of `source_location` (`:45-46`); else `inspect`.
|
|
367
|
+
- `digest(obj)` (`:53-56`) — MD5 hex of `digest_str` (Strings used verbatim).
|
|
368
|
+
- `file_md5(file)` (`:58-66`) — `Digest::MD5.file`; on failure falls back to
|
|
369
|
+
hashing the *path string* (`:63-65`) — intentional but surprising.
|
|
370
|
+
- `fast_file_md5(file, sample = 3_000_000)` (`:68-79`) — MD5 of
|
|
371
|
+
`size:` + first/middle/last 3MB samples; note `f.seek(size - sample - 1)`
|
|
372
|
+
(`:75`) can go negative for small files (seek to negative offset) — in
|
|
373
|
+
practice guarded by callers comparing `File.size(file) > 10_000_000`
|
|
374
|
+
(`:87`).
|
|
375
|
+
- `digest_file(file)` (`:81-92`) — honours a sibling `<file>.md5` file
|
|
376
|
+
(`:84-86`); >10MB → fast_file_md5 else file_md5 (`:87-91`).
|
|
377
|
+
|
|
378
|
+
### 5.3 misc/filesystem.rb (86 lines)
|
|
379
|
+
- `in_dir(dir)` (`:2-11`) — mkdir_p + chdir with ensure-restore.
|
|
380
|
+
- `path_relative_to(basedir, path)` (`:13-24`).
|
|
381
|
+
- `tarize_cmd(path, dest)` (`:26-34`) — shells out to `tar cvfz` via CMD.
|
|
382
|
+
- `tarize(source_dir, archive_path)` (`:36-73`) — pure-Ruby tar.gz writer
|
|
383
|
+
using `Gem::Package::TarWriter` + `Zlib::GzipWriter`.
|
|
384
|
+
- `untar(file, target)` (`:75-85`) — `tar xvfz` via CMD.
|
|
385
|
+
|
|
386
|
+
### 5.4 misc/helper.rb (78 lines) — array helpers
|
|
387
|
+
- `intersect_sorted_arrays(a1, a2)` (`:2-19`) — **destructive on inputs**
|
|
388
|
+
(uses `shift`).
|
|
389
|
+
- `counts(array)` (`:21-29`) — Hash tally. Duplicated verbatim in math.rb:75-83.
|
|
390
|
+
- `chunk(array, size)` (`:34-49`), `divide(array, num)` (`:53-62`, round-robin
|
|
391
|
+
distribution), `ordered_divide(array, num)` (`:66-76`, contiguous slices).
|
|
392
|
+
Probes/test: `chunk(%w(1..9),2)[0]`==%w(1 2); `ordered_divide(...,2).length`==5.
|
|
393
|
+
|
|
394
|
+
### 5.5 misc/hook.rb (50 lines) — top-level `Hook`
|
|
395
|
+
- `Hook.extended(hook_class)` (`:2-4`), `Hook.apply(hook_class, base_class)`
|
|
396
|
+
(`:6-43`) — wraps singleton and instance methods of `base_class` so each
|
|
397
|
+
registered hook gets a chance (`claim` predicate opt-in), falling back to
|
|
398
|
+
`orig_<method>`; `Hook.hook_method` (`:45-49`).
|
|
399
|
+
- Not required by `misc.rb`; loaded only when explicitly required
|
|
400
|
+
(`test/scout/misc/test_hook.rb` does `require 'scout/util/misc'`-style load
|
|
401
|
+
or direct).
|
|
402
|
+
|
|
403
|
+
### 5.6 misc/insist.rb (56 lines) — retry block
|
|
404
|
+
- `insist(times = 4, sleep = nil, msg = nil)` (`:2-55`):
|
|
405
|
+
- `TryAgain` → sleep+retry unconditionally (no counter) (`:22-24`);
|
|
406
|
+
- `StopInsist` → re-raise the wrapped exception (`:25-26`);
|
|
407
|
+
- `Aborted`/`Interrupt` → warn and re-raise, no retry (`:27-33`);
|
|
408
|
+
- other `Exception` → warn (unless `msg == false`), backoff using either a
|
|
409
|
+
caller-supplied sleep, an Array of sleeps (`times` as Array, `:10-14`),
|
|
410
|
+
or a synthesized ladder `[0, 0.001, 0.01, 0.1, 0.5]` (`:17`), retry while
|
|
411
|
+
`try < times` (`:51-52`), else re-raise (`:53`).
|
|
412
|
+
- `SCOUT_LOG_INSIST=true` triggers `Log.exception` on each failure (`:35`).
|
|
413
|
+
|
|
414
|
+
### 5.7 misc/matching.rb (47 lines)
|
|
415
|
+
- `_convert_match_condition(condition)` (`:2-11`) — `'true'`→true,
|
|
416
|
+
`'false'`→false, `/re/`→Regexp, `<=x`/`>=x`/`<x`/`>x`→`[:cmp, op, x.to_f]`,
|
|
417
|
+
`!x`→`[:invert, ...]`, else the raw String.
|
|
418
|
+
- `match_value(value, condition)` (`:13-42`) — nil/nil→true (`:16`), nil
|
|
419
|
+
value→false (`:17`); Regexp; TrueClass/FalseClass conditions accept the
|
|
420
|
+
class objects or the strings "true"/"false" (`:22-25`); String condition
|
|
421
|
+
numeric-compares when value is Numeric (`:27`); Array conditions handle
|
|
422
|
+
`:cmp`, `:invert`, and OR-of-conditions (`:30-38`); unknown → raise
|
|
423
|
+
(`:39-41`).
|
|
424
|
+
- `tokenize(str)` (`:44-46`) — `"`/`'`/bare tokens.
|
|
425
|
+
|
|
426
|
+
### 5.8 misc/math.rb (121 lines)
|
|
427
|
+
- `log2`/`log10` with precomputed multipliers (`:3-11`), `max`/`min`
|
|
428
|
+
nil-skipping (`:13-29`), `std_num_vector` (`:31-38`), `sum` (`:40-42`),
|
|
429
|
+
`mean` (`:44-46`), `median` (`:48-52`), `variance` (sample, n-1, `:54-67`),
|
|
430
|
+
`sd` (`:69-73`), `counts` (duplicate of helper.rb's, `:75-83`),
|
|
431
|
+
`proportions(array)` (`:85-101`, with a singleton `to_s` override that
|
|
432
|
+
prints a sorted tally), `zscore` (`:103-107`), `softmax` (`:109-120`).
|
|
433
|
+
|
|
434
|
+
### 5.9 misc/monitor.rb (67 lines)
|
|
435
|
+
- `pid_alive?(pid)` (`:2-5`), `benchmark(repeats, message)` (`:7-27`),
|
|
436
|
+
`profile(options)` (`:29-45`, requires `ruby-prof` at call time),
|
|
437
|
+
`exec_time(&block)` (`:47-56`), `wait_for_interrupt` (`:58-66`, sleeps until
|
|
438
|
+
Interrupt).
|
|
439
|
+
|
|
440
|
+
### 5.10 misc/system.rb (118 lines)
|
|
441
|
+
- `add_libdir(dir)` (`:3-6`), `hostname` (`:8-12`, `ENV["HOSTNAME"]` or
|
|
442
|
+
backticks), `children(ppid)` (`:14-19`, `sys/proctable`), `wait_child(pid)`
|
|
443
|
+
(`:21-26`), `abort_child(pid, wait)` (`:28-35`, TERM),
|
|
444
|
+
`env_add(var, value, sep = ":", prepend = true)` (`:37-49`, idempotent;
|
|
445
|
+
test: appending `test_value1:test_value2`),
|
|
446
|
+
`with_env_hash`/`with_env`/`with_envs` (`:51-82`, ENV snapshot/restore),
|
|
447
|
+
`update_git(gem_name = 'scout-essentials')` (`:85-113`, git pull +
|
|
448
|
+
submodule update + rake install via `CMD.cmd_log`), `processors`
|
|
449
|
+
(`:115-117`, `Etc.nprocessors`).
|
|
450
|
+
|
|
451
|
+
### Env vars read by Misc files
|
|
452
|
+
`SCOUT_NOCOLOR` (format.rb:28), `HOSTNAME` (system.rb:10),
|
|
453
|
+
`SCOUT_LOG_INSIST` (insist.rb:35).
|
|
454
|
+
|
|
455
|
+
### Subtle points
|
|
456
|
+
- `Misc.counts` defined twice (helper.rb:21, math.rb:75) — identical.
|
|
457
|
+
- `intersect_sorted_arrays` mutates its arguments.
|
|
458
|
+
- `file_md5` fallback hashes the path string, not the content.
|
|
459
|
+
- `Hook` is top-level and not auto-required.
|
|
460
|
+
- `timespan` raises TypeError for unknown units *and* for compound strings
|
|
461
|
+
like `"1h30m"` (nil multiplier, P7/P15) — it only accepts a single
|
|
462
|
+
`(<digits>)(<unit>)` pair.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 6. `TmpFile` — `lib/scout/tmpfile.rb` (131 lines)
|
|
467
|
+
|
|
468
|
+
### Structure / conventions
|
|
469
|
+
- `MAX_FILE_LENGTH = 150` (`:7`).
|
|
470
|
+
- `user_tmp(subdir = nil)` (`:9-15`) — **`ENV["HOME"]/tmp/scout[/<subdir>]`**
|
|
471
|
+
(home-relative, not `/tmp`!). `tmpdir` defaults to
|
|
472
|
+
`user_tmp('tmpfiles')` (`:21-23`), overridable via `TmpFile.tmpdir=`.
|
|
473
|
+
- `random_name(prefix = 'tmp-', max = 1_000_000_000)` (`:27-30`),
|
|
474
|
+
`tmp_file(prefix, max, dir)` (`:33-37`, Path-aware).
|
|
475
|
+
- `with_file(content = nil, erase = true, options = {})` (`:39-75`) — flexible
|
|
476
|
+
arg shifts (Hash-only call, Hash-as-second-arg); options `:prefix`,
|
|
477
|
+
`:tmpdir`, `:max`, `:extension`; IO content streamed in 1024-byte chunks
|
|
478
|
+
(`:57-65`); file removed in all cases after the block when `erase`
|
|
479
|
+
(`:72`).
|
|
480
|
+
- `with_dir(erase = true, options = {})` (`:77-88`), `in_dir(*args)`
|
|
481
|
+
(`:90-96`, wraps `Misc.in_dir`).
|
|
482
|
+
- `SLASH_REPLACE = '·'` (`:98`) — **MIDDLE DOT U+00B7**, not a hyphen.
|
|
483
|
+
- `tmp_for_file(file, tmp_options = {}, other_options = {})` (`:99-130`):
|
|
484
|
+
- explicit `:file` in tmp_options short-circuits (`:100-101`);
|
|
485
|
+
- base name = `prefix + ":" + file` (or bare file), `.gz`/`.bgz` stripped
|
|
486
|
+
(`:103-107`), `[key]` appended for `:key` (`:109`);
|
|
487
|
+
- `other_options[:filters]` adds `&F[match=<digest>]` segments (`:111-115`);
|
|
488
|
+
- whitespace → `_`, `/` → `·` (`:120`);
|
|
489
|
+
- names longer than `MAX_FILE_LENGTH + 10` are truncated at 150 with the
|
|
490
|
+
MD5 of the tail appended (`:125`);
|
|
491
|
+
- non-empty remaining `other_options` (minus `:unnamed`, `:122-123`) append
|
|
492
|
+
`:<md5 of options>` (`:127`);
|
|
493
|
+
- result is a `Path` inside `persistence_dir` (`:117-129`).
|
|
494
|
+
|
|
495
|
+
### Probes (P4)
|
|
496
|
+
- `user_tmp` → `/home/mvazque2/tmp/scout`; `user_tmp('foo')` →
|
|
497
|
+
`.../tmp/scout/foo`; `tmpdir` → `.../tmp/scout/tmpfiles`.
|
|
498
|
+
- `tmp_file('pfx-')` → `.../tmpfiles/pfx-332333330` (random).
|
|
499
|
+
- `random_name` → `"tmp-57604039"`.
|
|
500
|
+
- `tmp_for_file("/a/b/c.tsv")` →
|
|
501
|
+
`.../tmpfiles/·a·b·c.tsv` (slashes → middle dots).
|
|
502
|
+
- `tmp_for_file("/a/b/c.tsv", {}, {filters: {...}})` → 32-char md5 suffix
|
|
503
|
+
(P4/P19).
|
|
504
|
+
- `tmp_for_file("/a/b/c.tsv", {prefix: "P", key: "K"}, {unnamed: 1})` →
|
|
505
|
+
`.../tmpfiles/P:·a·b·c.tsv[K]` — note `:unnamed` excluded from the digest
|
|
506
|
+
suffix, and with only `:unnamed` present no `:`-digest is appended.
|
|
507
|
+
|
|
508
|
+
### Conventions summary (doc-relevant)
|
|
509
|
+
- Temp root is `$HOME/tmp/scout`, subdir `tmpfiles`.
|
|
510
|
+
- Deterministic cache names use `·` for `/`, optional `PREFIX:` and `[key]`
|
|
511
|
+
decorations, optional `&F[...]` filter markers, and a trailing
|
|
512
|
+
`:md5` when extra options survive filtering.
|
|
513
|
+
|
|
514
|
+
### Error handling
|
|
515
|
+
`with_file`/`with_dir` attempt removal **only in the success path** — an
|
|
516
|
+
exception propagating out of the block skips cleanup (`:70-74`: no `ensure`
|
|
517
|
+
around the `yield`; verified by probe P12, which observed the temp file
|
|
518
|
+
surviving a raised block). Docs claiming "always cleaned up" would be wrong.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## 7. Exceptions — `lib/scout/exceptions.rb` (39 lines)
|
|
523
|
+
|
|
524
|
+
All top-level constants (no `Scout::` namespace). Verified hierarchy (probe
|
|
525
|
+
P9):
|
|
526
|
+
|
|
527
|
+
| Constant | Superclass | Notes |
|
|
528
|
+
|---|---|---|
|
|
529
|
+
| `ScoutDeprecated` | StandardError | `:1` |
|
|
530
|
+
| `ScoutException` | StandardError | `:2` |
|
|
531
|
+
| `FieldNotFoundError` | StandardError | `:3` |
|
|
532
|
+
| `TryAgain` | StandardError | `:5` |
|
|
533
|
+
| `StopInsist` | **Exception** | `:6-11`, wraps `#exception` accessor |
|
|
534
|
+
| `Aborted` | StandardError | `:13` |
|
|
535
|
+
| `ParameterException` | ScoutException | `:15` |
|
|
536
|
+
| `MissingParameterException` | ParameterException | `:16-20`, msg "Missing parameter 'x'" |
|
|
537
|
+
| `ProcessFailed` | StandardError | `:21-36`, `pid`,`msg` accessors; `new(nil,msg)` → "Failed to run msg" |
|
|
538
|
+
| `ConcurrentStreamProcessFailed` | ProcessFailed | `:37-43`, `#concurrent_stream` = filename if available |
|
|
539
|
+
| `OpenURLError` | StandardError | `:45` |
|
|
540
|
+
| `DontClose` | **Exception** | `:47-53`, `#payload` |
|
|
541
|
+
| `DontPersist` | **Exception** | `:55` |
|
|
542
|
+
| `KeepLocked` | DontPersist | `:56-61`, `#payload` |
|
|
543
|
+
| `KeepBar` | **Exception** | `:63-68`, `#payload` |
|
|
544
|
+
| `LockInterrupted` | TryAgain | `:70` |
|
|
545
|
+
| `ClosedStream` | StandardError | `:72` |
|
|
546
|
+
| `ResourceNotFound` | ScoutException | `:74` |
|
|
547
|
+
| `CMD::Timeout` | ProcessFailed | cmd.rb:22-29 (see §11) |
|
|
548
|
+
|
|
549
|
+
Probes: `ProcessFailed.new(123,"msg").message` → "Process 123 failed - msg";
|
|
550
|
+
`ProcessFailed.new(nil,"cmd").message` → "Failed to run cmd";
|
|
551
|
+
`StopInsist.new(ArgumentError.new("x")).exception` → the ArgumentError.
|
|
552
|
+
|
|
553
|
+
Key design fact: several control-flow signals (`StopInsist`, `DontClose`,
|
|
554
|
+
`DontPersist`, `KeepLocked`, `KeepBar`) derive from **Exception, not
|
|
555
|
+
StandardError**, so a blanket `rescue => e` (which catches StandardError)
|
|
556
|
+
will *not* intercept them. This is deliberate and a classic doc-trap.
|
|
557
|
+
|
|
558
|
+
`ScoutDeprecated` and `FieldNotFoundError` appear unused within this chunk
|
|
559
|
+
(search of lib/ found `module Scout` only in config.rb / resource; no other
|
|
560
|
+
references inspected here) — flag for the doc-writer to verify usage in
|
|
561
|
+
downstream repos.
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## 8. `Log` — `lib/scout/log.rb` (453) + `log/{color,color_class,fingerprint,trap}.rb`
|
|
566
|
+
|
|
567
|
+
### 8.1 log.rb core
|
|
568
|
+
- Severity constants and names (`:14-20`): `DEBUG LOW MEDIUM HIGH INFO WARN
|
|
569
|
+
ERROR NONE` = 0..7. `SEVERITY_NAMES` frozen via `||=`.
|
|
570
|
+
- `default_severity` (`:21-33`) — reads `$HOME/.scout/etc/log_severity`, else
|
|
571
|
+
`INFO` (4). Memoized in `@@default_severity`.
|
|
572
|
+
- ENV `SCOUT_LOG` maps names to severities; unset or unrecognized →
|
|
573
|
+
default_severity (`:35-54`).
|
|
574
|
+
- `tty_size` (`:56-72`) — `IO.console.winsize.last`, fallback `tput cols`,
|
|
575
|
+
fallback `ENV["TTY_SIZE"]` or 80; wrapped in `ignore_stderr`; memoized.
|
|
576
|
+
- `last_caller(stack)` (`:75-83`) — first frame not from `scout/log.rb`.
|
|
577
|
+
- `get_level(level)` (`:85-98`) — Numeric → int, String/Symbol → const, else
|
|
578
|
+
0; on bad name calls `Log.exception` (which returns nil → `|| 0`).
|
|
579
|
+
- `with_severity(level)` (`:100-108`).
|
|
580
|
+
- `logfile(file=nil)` (`:110-124`) — nil resets, String opens append+sync,
|
|
581
|
+
IO accepted, else raise. Note `Log.logfile` with no args *resets* rather
|
|
582
|
+
than reads — trap.rb:7 relies on this returning the old value? No: `:7`
|
|
583
|
+
calls `Log.logfile` (no args) then restores with `Log.logfile = old_logfile`
|
|
584
|
+
via the attr_writer — actually `trap.rb:7,36` reads then re-assigns; the
|
|
585
|
+
attr_writer is declared at `:11` (`attr_writer :tty_size, :logfile`).
|
|
586
|
+
- `up_lines/down_lines/return_line/clear_line` (`:125-139`) — ANSI cursor
|
|
587
|
+
movement, all no-ops under `nocolor`.
|
|
588
|
+
- `MUTEX`-synchronized `log_write`/`log_puts` (`:141-166`) — write to
|
|
589
|
+
`@@logfile` if set else STDERR; IOError swallowed.
|
|
590
|
+
- `logn(message, severity = MEDIUM)` (`:169-190`) — prefix
|
|
591
|
+
`MM/DD/YY-HH:MM:SS.mmm[SEVERITY]`; `[pid]` included when
|
|
592
|
+
`SCOUT_DEBUG_PID=true` (`:178-182`); messages at severity >= INFO are
|
|
593
|
+
highlighted (`:183`); updates `Log::LAST` to "log" (`:188`) — the shared
|
|
594
|
+
string used for interleaving control with progress bars.
|
|
595
|
+
- `log(message, severity = MEDIUM, &block)` (`:192-198`) — appends "\n",
|
|
596
|
+
lazy block evaluation.
|
|
597
|
+
- `log_obj_inspect` / `log_obj_fingerprint` (`:200-224`).
|
|
598
|
+
- Severity helpers `debug/low/medium/high/info/warn/error` (`:226-252`).
|
|
599
|
+
- `exception(e)` (`:254-268`) — messages containing "NOLOG" are dropped
|
|
600
|
+
(`:255`); "NOSTACK" suppresses backtrace (`:260`); default prints reversed
|
|
601
|
+
(innermost-first) backtrace unless `SCOUT_ORIGINAL_STACK=true` (`:261-267`).
|
|
602
|
+
- `deprecated(m)` (`:270-274`), `color_stack` (`:276-288`, colorizes
|
|
603
|
+
workflow/scout-/rbbt- frames), `tsv(tsv, example)` (`:290-316`),
|
|
604
|
+
`stack(stack)` (`:318-330`), `count_stack`/`with_stack_counts`
|
|
605
|
+
(`:332-355`).
|
|
606
|
+
- Kernel-level debug helpers defined at top level (`:358-451`): `ppp`, `fff`,
|
|
607
|
+
`ddd/lll/mmm/iii/wwww/eee` (inspect at each severity), `ddf/llf/mmf/iif/
|
|
608
|
+
wwwf/eef` (fingerprint variants), `sss(level,&block)` (severity switch),
|
|
609
|
+
`ccc(obj,&block)` (conditional on `$scout_debug_log`).
|
|
610
|
+
|
|
611
|
+
### 8.2 log/color.rb (228 lines)
|
|
612
|
+
- `module Colorize` (`:6-128`): `colors` (name→hex IndiferentHash, `:11-19`),
|
|
613
|
+
`diverging_colors` (`:25-40`, 12 hex values), `from_name(color)`
|
|
614
|
+
(`:42-62`, hex passthrough, name lookup, special white/black/green/red/
|
|
615
|
+
yellow/blue remaps), `continuous(array, start, eend, percent)`
|
|
616
|
+
(`:64-80`), `gradient`/`rank_gradient` (`:82-94`), `distinct(array)`
|
|
617
|
+
(`:97-112`, cycles diverging colors darkened by 0.3/times), `tsv(tsv,
|
|
618
|
+
options)` (`:114-127`).
|
|
619
|
+
- `module Log` extends `Term::ANSIColor` (`:130-131`); `nocolor` accessor
|
|
620
|
+
initialized from `ENV["SCOUT_NOCOLOR"] == 'true'` (`:137`).
|
|
621
|
+
- `WHITE, DARK, GREEN, YELLOW, RED = Color::SOLARIZED.values_at :base0,
|
|
622
|
+
:base00, :green, :yellow, :magenta` (`:139`) — note GREEN maps to
|
|
623
|
+
SOLARIZED[:green] but YELLOW maps to :magenta and RED to :magenta too
|
|
624
|
+
(last assignment wins for the tuple order base0, base00, green, yellow,
|
|
625
|
+
magenta → WHITE, DARK, GREEN, YELLOW, RED). Actually `values_at` returns 5
|
|
626
|
+
values; YELLOW←:yellow, RED←:magenta per the source order `:base0, :base00,
|
|
627
|
+
:green, :yellow, :magenta`.
|
|
628
|
+
- `SEVERITY_COLOR` (`:141`) = `[reset, cyan, green, magenta, blue, yellow,
|
|
629
|
+
red]` indexed by severity int.
|
|
630
|
+
- `CONCEPT_COLORS` (`:142-162`) — named concepts (title, path, value, error,
|
|
631
|
+
done, started, ...).
|
|
632
|
+
- `HIGHLIGHT = "\033[1m"` (`:163`).
|
|
633
|
+
- `uncolor(str)` (`:165-167`), `reset_color` (`:169-171`).
|
|
634
|
+
- `color(color, str = nil, reset = false)` (`:173-216`) — returns plain dup
|
|
635
|
+
under nocolor; special-cases `:integer`/`:float` (sign/magnitude coloring,
|
|
636
|
+
`:176-184`) and `:status` (`:186-201`); Integer color indexes
|
|
637
|
+
SEVERITY_COLOR; concept names resolve via CONCEPT_COLORS; other Symbols via
|
|
638
|
+
`Term::ANSIColor`; nil str → just the color string.
|
|
639
|
+
- `highlight(str = nil)` (`:218-226`).
|
|
640
|
+
|
|
641
|
+
### 8.3 log/color_class.rb (269 lines)
|
|
642
|
+
- Vendored `class Color` (McClain Looney, 2007, MIT) with `SOLARIZED` palette
|
|
643
|
+
(`:33-52`), rgba accessors (`:90-93`), `Color.parse` (`:96`),
|
|
644
|
+
`lighten/darken/blend` (`:161-234`), hex conversion helpers, and a
|
|
645
|
+
top-level `rgb(*args)` convenience (`:265`).
|
|
646
|
+
|
|
647
|
+
### 8.4 log/fingerprint.rb (82 lines)
|
|
648
|
+
- `FP_MAX_STRING = 150`, `FP_MAX_ARRAY = 20`, `FP_MAX_HASH = 10` (`:3-5`).
|
|
649
|
+
- `truncate_string(string, max)` (`:7-16`) — keeps head/tail around
|
|
650
|
+
`<...length - md5[0..4]...>`.
|
|
651
|
+
- `fingerprint(obj)` (`:18-81`) — dispatches to `obj.fingerprint` when
|
|
652
|
+
available (`:19`); nil/true/false/Symbol; Strings quoted with `'` and
|
|
653
|
+
newlines escaped (`:30-32`); ConcurrentStream (`:33-36`); IO/File
|
|
654
|
+
(`:37-40`); Arrays truncated to first/2nd/mid/last-2/last with length
|
|
655
|
+
prefix (`:41-46`); Hashes >10 entries collapse to `H:{keys;values}`
|
|
656
|
+
(`:47-61`); Floats by magnitude — `>10`→`%.1f`, `>1`→`%.3f`, else
|
|
657
|
+
`%.6f` (`:62-69`); Thread → `thread["name"]`; Set → array; else `to_s`.
|
|
658
|
+
- Probes (P8): `Log.fingerprint("a"*250)` → `'aaa...<...250 -
|
|
659
|
+
63c7c...>...aaa'`; `[1,2]`→`"[1, 2]"`; `{:a=>1}`→`"{:a=>1}"`;
|
|
660
|
+
`3.5`→`"3.500"`; `100.0`→`"100.0"`; `2.5`→`"2.500"`; `0.000123`→
|
|
661
|
+
`"0.000123"`; nil/true/:sym as expected.
|
|
662
|
+
|
|
663
|
+
### 8.5 log/trap.rb (107 lines)
|
|
664
|
+
- `trap_std(msg = "STDOUT", msge = "STDERR", severity = 0, severity_err =
|
|
665
|
+
nil)` (`:2-38`) — replaces STDOUT/STDERR with pipes, spawns two reader
|
|
666
|
+
threads that `Log.logn` each line, redirects Log's own output to the saved
|
|
667
|
+
STDERR dup, restores in ensure and joins threads.
|
|
668
|
+
- `trap_stderr(msg = "STDERR", severity = 0)` (`:40-62`).
|
|
669
|
+
- `_ignore_stderr`/`ignore_stderr` (`:64-84`) and
|
|
670
|
+
`_ignore_stdout`/`ignore_stdout` (`:86-106`) — reopen to /dev/null with
|
|
671
|
+
restore; fall back to plain yield if /dev/null is missing.
|
|
672
|
+
|
|
673
|
+
### Env vars read
|
|
674
|
+
`HOME` (log.rb:23), `SCOUT_LOG` (:35), `TTY_SIZE` (:64,66), `SCOUT_DEBUG_PID`
|
|
675
|
+
(:178), `SCOUT_ORIGINAL_STACK` (:261,319), `SCOUT_NOCOLOR` (color.rb:137).
|
|
676
|
+
|
|
677
|
+
### Subtle points
|
|
678
|
+
- `Log.logfile` with no argument **resets** the logfile (log.rb:111-113); it
|
|
679
|
+
is not a reader. `trap.rb:7,43` therefore capture/restore semantics rely on
|
|
680
|
+
the writer (`Log.logfile = ...`).
|
|
681
|
+
- Default severity is INFO (4): DEBUG/LOW/MEDIUM/HIGH messages are **not**
|
|
682
|
+
printed by default (P11).
|
|
683
|
+
- The reversed-backtrace default in `Log.exception` and `Log.stack`.
|
|
684
|
+
- `Log::LAST` is a single shared mutable String used as a protocol flag
|
|
685
|
+
between `logn` and progress-bar printing.
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## 9. `Log::ProgressBar` — `log/progress.rb` (106) + `progress/{util,report}.rb`
|
|
690
|
+
|
|
691
|
+
### 9.1 progress.rb
|
|
692
|
+
- `Log.no_bar=`/`no_bar` (`:5-12`) — class var plus
|
|
693
|
+
`ENV["SCOUT_NO_PROGRESS"] == "true"`.
|
|
694
|
+
- `ProgressBar` (`:14-105`): class attrs `default_file`, `default_severity`
|
|
695
|
+
(`:16-19`); instance attrs `max ticks frequency depth desc file bytes
|
|
696
|
+
process callback severity` (`:21`).
|
|
697
|
+
- `initialize(max = nil, options = {})` (`:23-43`) — options pulled with
|
|
698
|
+
`IndiferentHash.process_options` (destructive), defaults
|
|
699
|
+
`:depth => 0, :frequency => 2, :severity => default_severity`; `max = nil`
|
|
700
|
+
when `TrueClass === max` (`:28`); desc newlines stripped (`:38`).
|
|
701
|
+
- `percent` (`:45-49`) — 0 when no ticks, **100 when max == 0** (`:47`).
|
|
702
|
+
- `init` (`:55-62`), `tick(step = 1)` (`:64-86`) — no-op under `no_bar`;
|
|
703
|
+
reports when `diff >= frequency` or when the percent advanced and
|
|
704
|
+
`diff > 0.3` (`:77,85`).
|
|
705
|
+
- `pos(pos)` (`:88-91`), `process(elem)` (`:93-104`) — the `process` callback
|
|
706
|
+
may return false (ignore), true (tick), Integer (pos) or Float (fraction
|
|
707
|
+
of max).
|
|
708
|
+
|
|
709
|
+
### 9.2 progress/util.rb (173 lines)
|
|
710
|
+
- `BAR_MUTEX`, `BARS`, `REMOVE`, `SILENCED` (`:4-7`).
|
|
711
|
+
- `add_offset`/`remove_offset`/`offset` (`:9-28`) — nesting offset for bars
|
|
712
|
+
created by threads.
|
|
713
|
+
- `new_bar(max, options = {})` (`:30-40`) — Hash-only call supported
|
|
714
|
+
(`:31-32`); `cleanup_bars` first; depth default = `BARS.length + offset`.
|
|
715
|
+
- `cleanup_bars` (`:42-63`), `remove_bar(bar, error = false)` (`:65-79`,
|
|
716
|
+
calls `bar.error`/`bar.done`), `remove(error)` instance wrapper (`:81-83`).
|
|
717
|
+
- `with_bar(max = nil, options = {})` (`:85-99`) — honors `options[:bar]`;
|
|
718
|
+
rescues `KeepBar` to keep the bar, any other exception marks error and
|
|
719
|
+
re-raises.
|
|
720
|
+
- `guess_obj_max(obj)` (`:101-137`) — `wc -l` via CMD for Step paths and
|
|
721
|
+
files (nil for gzip/bgzip/remote), `length`/`size` for TSV/Array/Hash.
|
|
722
|
+
- `get_obj_bar(obj, bar = nil)` (`:139-165`) — String→desc, true→auto max,
|
|
723
|
+
Numeric→explicit max, Hash→options with `:max`, ProgressBar→reused (max
|
|
724
|
+
filled in), Step→desc+file.
|
|
725
|
+
- `with_obj_bar(obj, bar = true)` (`:167-170`).
|
|
726
|
+
|
|
727
|
+
### 9.3 progress/report.rb (244 lines)
|
|
728
|
+
- `print(io, str)` (`:4-9`) — gated by bar severity vs `Log.severity` and
|
|
729
|
+
`Log.no_bar`; writes via `Log.log_write`; sets `Log::LAST` = "progress".
|
|
730
|
+
- `thr_msg` (`:12-85`) — throughput estimate from a bounded history window
|
|
731
|
+
(max 30 samples, growth heuristics `:18-35`); mean/mean_max tracked
|
|
732
|
+
(`:41-53`); formats "N per sec." or "X secs each".
|
|
733
|
+
- `eta_msg` (`:88-117`) — 10-dot indicator, percent, `HH:MM:SS` ETA and used
|
|
734
|
+
time, ticks of max, bytes/items.
|
|
735
|
+
- `report_msg` (`:119-136`), `load(info)` (`:138-159`), `save` (`:161-166`,
|
|
736
|
+
YAML to `file`), `report(io = STDERR)` (`:168-197`) — redraws the stack of
|
|
737
|
+
active bars using cursor up/down lines, appends itself to BARS, saves to
|
|
738
|
+
file.
|
|
739
|
+
- `done(io = STDERR)` (`:199-218`) — prints summary, removes the YAML file,
|
|
740
|
+
invokes `callback`.
|
|
741
|
+
- `error(io = STDERR)` (`:220-242`) — same but red, and callback failures are
|
|
742
|
+
swallowed with a debug log.
|
|
743
|
+
|
|
744
|
+
### Concurrency
|
|
745
|
+
Single `BAR_MUTEX` guards BARS/REMOVE/SILENCED; per-bar state (`ticks`,
|
|
746
|
+
history) is **not** synchronized — `tick` from multiple threads races by
|
|
747
|
+
design. `with_bar` handles KeepBar.
|
|
748
|
+
|
|
749
|
+
### Env vars
|
|
750
|
+
`SCOUT_NO_PROGRESS` (progress.rb:11).
|
|
751
|
+
|
|
752
|
+
### Subtle points
|
|
753
|
+
- Bars persist state to YAML in `file` (progress report.rb:161-166) and are
|
|
754
|
+
removed on done/error — a doc claiming progress state is ephemeral would be
|
|
755
|
+
wrong for filed bars.
|
|
756
|
+
- `percent` returns 100 when `max == 0` regardless of ticks.
|
|
757
|
+
- `Log.no_bar` short-circuits `tick` entirely (no bookkeeping at all).
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 10. `SOPT` — `lib/scout/simple_opt.rb` (5 lines) + 5 submodules
|
|
762
|
+
|
|
763
|
+
### 10.1 accessor.rb (54 lines)
|
|
764
|
+
- Module-level state: `all`, `shortcuts`, `inputs`, `input_shortcuts`,
|
|
765
|
+
`input_types`, `input_descriptions`, `input_defaults` (`:6-32`), writers
|
|
766
|
+
(`:3`), `reset` (`:34-37`), `delete_inputs(inputs)` (`:39-48`),
|
|
767
|
+
`usage` (`:50-53`, prints doc and `exit 0`).
|
|
768
|
+
|
|
769
|
+
### 10.2 parse.rb (69 lines)
|
|
770
|
+
- `fix_shortcut(short, long)` (`:2-35`) — collision resolution: keep the
|
|
771
|
+
requested short unless taken (`:3`); if the long name already owns a
|
|
772
|
+
shortcut, reuse it (`:5-6`); else derive from initials (`:13-16`) or digit
|
|
773
|
+
(`:17-20`), then extend letter by letter skipping `.-_` (`:24-30`); returns
|
|
774
|
+
nil when unresolvable.
|
|
775
|
+
- `register(short, long, asterisk, description)` (`:37-46`) — the presence of
|
|
776
|
+
an asterisk means `:string`, otherwise `:boolean` (`:45`).
|
|
777
|
+
- `parse(opt_str)` (`:48-68`) — splits on newlines **or colons** (`:51-55`);
|
|
778
|
+
each entry `"-s--long[*] description"` parsed with the regex at `:61`.
|
|
779
|
+
Returns the list of long names.
|
|
780
|
+
|
|
781
|
+
### 10.3 get.rb (59 lines)
|
|
782
|
+
- `GOT_OPTIONS` module constant IndiferentHash (`:2`).
|
|
783
|
+
- `consume(args = ARGV)` (`:6-47`) — walks args; stops at `--` (`:11`);
|
|
784
|
+
matches `--?(key)(=value)?`; unknown options are **skipped in place**
|
|
785
|
+
(`:18-20`); recognized ones are deleted from the array (`:22`);
|
|
786
|
+
`:string` inputs take the next arg as value (`:29-31`); booleans accept
|
|
787
|
+
`=false`/`F`/`FALSE`/`no` (with a warning when passed as a separate token,
|
|
788
|
+
`:33-36`) and otherwise evaluate true (`:37`); result keys are symbolized
|
|
789
|
+
with `keys_to_sym!` (`:42`) and merged into `GOT_OPTIONS` (`:44`).
|
|
790
|
+
- `get(opt_str)` (`:49-52`) = parse + consume(ARGV).
|
|
791
|
+
- `require(options, *parameters)` (`:54-58`) — raises `ParameterException`
|
|
792
|
+
for nil values (note: a `false` value passes).
|
|
793
|
+
|
|
794
|
+
### 10.4 doc.rb (126 lines)
|
|
795
|
+
- `command` (`:8-10`, `File.basename($0)`), `summary`, `synopsys`
|
|
796
|
+
(deliberate misspelling of synopsis, `:16-23`), `description`.
|
|
797
|
+
- `input_format(name, type, default, short)` (`:29-47`) — renders
|
|
798
|
+
`-s,--name` plus type-specific suffixes (`[=false]`, `=<file|->` for
|
|
799
|
+
:tsv/:text, `=<list|file|->` for :array, `=<type>` otherwise) and
|
|
800
|
+
`(default: ...)`.
|
|
801
|
+
- `input_array_doc(input_array)` (`:49-70`), `input_doc(...)` (`:72-101`)
|
|
802
|
+
both auto-register options via `register`.
|
|
803
|
+
- `doc` (`:104-125`) — man-page-ish rendering with SYNOPSYS/DESCRIPTION/
|
|
804
|
+
OPTIONS sections.
|
|
805
|
+
|
|
806
|
+
### 10.5 setup.rb (26 lines)
|
|
807
|
+
- `setup(str)` (`:3-25`) — splits the doc string on blank lines; first part
|
|
808
|
+
= summary (unless it starts with `$-`), next `$`-prefixed part = synopsys
|
|
809
|
+
(`$ ` prefix stripped, `:17`), following non-`-` parts = description, the
|
|
810
|
+
rest of `-` lines = options; then `SOPT.parse` and `SOPT.consume`.
|
|
811
|
+
|
|
812
|
+
### Probes (P10)
|
|
813
|
+
- `SOPT.parse("-f--first* first arg:-f--fun")` → inputs `["first","fun"]`,
|
|
814
|
+
shortcuts `{"f"=>"first","fu"=>"fun"}` (note `f` collision resolved to
|
|
815
|
+
`fu`), types `{"first"=>:string,"fun"=>:boolean}`, descriptions
|
|
816
|
+
`{"first"=>"first arg","fun"=>""}`.
|
|
817
|
+
- `SOPT.consume(["-f","myfile","--fun"])` → `{:first=>"myfile",
|
|
818
|
+
:fun=>true}`; consuming again with extra unknown options
|
|
819
|
+
(`-x --other val`) leaves them in the args and merges
|
|
820
|
+
`{:flag=>true,:other=>"val"}` when registered.
|
|
821
|
+
- `SOPT.consume` stops at `--` and leaves `["--","positional"]` untouched
|
|
822
|
+
(P10).
|
|
823
|
+
- `fix_shortcut("f","fun")` → "fu".
|
|
824
|
+
- `parse` with `:` vs `\n` separators both work (P10: newline-separated
|
|
825
|
+
entries parsed identically).
|
|
826
|
+
|
|
827
|
+
### Subtle points
|
|
828
|
+
- Boolean false values are recognized only via `=F`/`=false`/`=FALSE`/`=no`
|
|
829
|
+
(get.rb:37); a *separate* following token from that same set is consumed
|
|
830
|
+
with a warning (get.rb:33-36) — but note the truthiness shortcut means
|
|
831
|
+
`--flag anything` (a non-false next token) is NOT consumed, so a stray
|
|
832
|
+
positional after a boolean flag silently stays in args.
|
|
833
|
+
- `SOPT.require` treats `false` as present.
|
|
834
|
+
- `synopsys` is the actual method name (misspelled) — docs must use it.
|
|
835
|
+
- `consume` mutates the passed array (deletes recognized args).
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
839
|
+
## 11. `CMD` — `lib/scout/cmd.rb` (666 lines)
|
|
840
|
+
|
|
841
|
+
### Structure
|
|
842
|
+
- `require`s: indiferent_hash, concurrent_stream, log, exceptions,
|
|
843
|
+
open/stream, stringio, open3, fileutils (`:1-8`).
|
|
844
|
+
- `TIMEOUT_KILL_GRACE = 1.0` (`:14`).
|
|
845
|
+
- `CMD::Timeout < ProcessFailed` (`:22-29`) — carries `command`, `timeout`;
|
|
846
|
+
message "command 'X' exceeded timeout of N seconds".
|
|
847
|
+
- `TOOLS` IndiferentHash (`:31`).
|
|
848
|
+
|
|
849
|
+
### Tool management
|
|
850
|
+
- `tool(tool, claim = nil, test = nil, cmd = nil, &block)` (`:32-34`) —
|
|
851
|
+
registers `[claim, test, block, cmd]`.
|
|
852
|
+
- `conda(tool, env, channel = 'bioconda')` (`:36-42`).
|
|
853
|
+
- `get_tool(tool)` (`:45-89`) — if a test or `command -v` fails, installs via
|
|
854
|
+
`claim.produce`, a block returning a Hash (→ `Resource.install`), then
|
|
855
|
+
probes `--version`/`-version`/`--help`/no-flag to cache a version string in
|
|
856
|
+
`@@init_cmd_tool`.
|
|
857
|
+
- `scan_version_text(text, cmd)` (`:91-109`) — several regex heuristics for
|
|
858
|
+
version strings.
|
|
859
|
+
- `versions` (`:110-113`).
|
|
860
|
+
- `bash(cmd)` (`:115-118`) — wraps in `bash -l` heredoc with `:autojoin`.
|
|
861
|
+
|
|
862
|
+
### Option processing
|
|
863
|
+
- `process_cmd_options(options = {})` (`:120-146`) — validates option keys
|
|
864
|
+
against `^[a-z_0-9\-=.]+$/i` (raises otherwise, `:125`), escapes single
|
|
865
|
+
quotes in values (`:127`), `:add_option_dashes` prefixes `--`, booleans
|
|
866
|
+
render as bare flags, nil/false are dropped, `x=` keys render `x='value'`.
|
|
867
|
+
Probe: `{"o1"=>"v1",:o2=>true,:o3=>false,"o4="=>"v"}` →
|
|
868
|
+
`"o1 'v1' o2 o4='v'"`; with dashes → `"--a '1' --long_opt 'x'"`. A key with
|
|
869
|
+
an apostrophe raises RuntimeError.
|
|
870
|
+
- `process_cmd_options_array(options)` (`:151-176`) — same semantics
|
|
871
|
+
returning an argv array (no quoting), used for the no-shell array mode.
|
|
872
|
+
|
|
873
|
+
### `cmd(tool, cmd = nil, options = {}, &block)` (`:178-611`)
|
|
874
|
+
- Normalizes the `(Hash)` second-arg form (`:179`).
|
|
875
|
+
- Defaults `:stderr => Log::DEBUG` (`:181`); pops `:in :stderr :sudo :post
|
|
876
|
+
:pipe :log :no_fail/:nofail :no_wait :xvfb :progress_bar :save_stderr
|
|
877
|
+
:autojoin :timeout :dont_close_in` (`:182-222`).
|
|
878
|
+
- `:save_stderr` accepts true (accumulate in `out.std_err`), a path
|
|
879
|
+
(String/Pathname/Path — opened 'w', mkdir_p of dirname, closed by CMD) or
|
|
880
|
+
any `write`/`<<` object (never closed) (`:195-216`).
|
|
881
|
+
- `array_mode = Array === tool` (`:226`) — argv execution via
|
|
882
|
+
`Open3.popen3(ENV, *cmd_array)` (`:288-293`), no shell; otherwise a
|
|
883
|
+
single shell string via `get_tool` resolution and `process_cmd_options`
|
|
884
|
+
(`:249-284`). `'{opt}'` placeholder substitution (`:275-279`).
|
|
885
|
+
- `:xvfb` wraps with `xvfb-run` (`:232-237, 262-268`).
|
|
886
|
+
- `:sudo` prefixes sudo (`:245, 281-283`).
|
|
887
|
+
- Spawn failure → warn, close any opened stderr file, `raise ProcessFailed,
|
|
888
|
+
nil, cmd unless no_fail` (or `return`) (`:294-301`).
|
|
889
|
+
- `:timeout` (Numeric > 0) → watchdog thread (`:310-408`): waits
|
|
890
|
+
`wait_thr.join(timeout)`, on expiry sets `timed_out`, `Log.low`, then for
|
|
891
|
+
pipe mode `sout.abort(timeout_exception)` + reap, else
|
|
892
|
+
`caller_thread.raise`; reaping escalates INT → KILL after
|
|
893
|
+
`TIMEOUT_KILL_GRACE` and always waits the pid (`:341-370`). Watchdog is
|
|
894
|
+
not registered in `sout.threads`.
|
|
895
|
+
- stdin handling thread when `:in` responds to `read` (`:410-440`), else
|
|
896
|
+
`sin.close`.
|
|
897
|
+
- **pipe mode** (`:444-497`): `ConcurrentStream.setup sout` with
|
|
898
|
+
`:pids/:autojoin/:no_fail`; `:post` becomes `sout.callback`; an err thread
|
|
899
|
+
feeds `bar.process(line)` (progress), `sout.log` (last line),
|
|
900
|
+
`sout.std_err` accumulation, the save_stderr destination, and `Log.log` at
|
|
901
|
+
the configured severity; threads `[in_thread, err_thread, wait_thr]`
|
|
902
|
+
attached; returns the stream.
|
|
903
|
+
- **non-pipe mode** (`:498-610`): reads stdout into a StringIO, waits the
|
|
904
|
+
status, joins the watchdog, raises CMD::Timeout if timed out, annotates the
|
|
905
|
+
output with `exit_status`, fills `std_err`, writes the accumulated stderr
|
|
906
|
+
to the destination, raises `ProcessFailed` (message includes the captured
|
|
907
|
+
stderr) on non-success unless `no_fail`, else logs stderr at the given
|
|
908
|
+
severity; Timeout path aborts the internal stream and re-raises; ensure
|
|
909
|
+
closes the destination when CMD owns it and runs `post`.
|
|
910
|
+
|
|
911
|
+
### Convenience wrappers
|
|
912
|
+
- `cmd_pid(*args)` (`:613-659`) — forces `:pipe`, streams chars to STDERR
|
|
913
|
+
honouring a log level, feeds `progress_bar`, joins and removes the bar.
|
|
914
|
+
- `cmd_log(*args)` (`:661-664`) — `cmd_pid` + nil.
|
|
915
|
+
|
|
916
|
+
### External integrations
|
|
917
|
+
`Open3.popen3`, `xvfb-run`, `sudo`, `tar` (from misc), `bash -l`, `command -v`,
|
|
918
|
+
conda (bioconda), Resource installation. Gems: none beyond stdlib here
|
|
919
|
+
(`term-ansicolor` arrives via log).
|
|
920
|
+
|
|
921
|
+
### Concurrency
|
|
922
|
+
Threads: watchdog (timeout), stdin feeder, stderr drainer, `wait_thr`; all
|
|
923
|
+
named; `report_on_exception = false` on helpers; ConcurrentStream carries
|
|
924
|
+
`pids` and `threads` for join semantics.
|
|
925
|
+
|
|
926
|
+
### Probes
|
|
927
|
+
- P14: `CMD.cmd(["echo","a","b"]).read` → `"a b\n"`; save_stderr to a path
|
|
928
|
+
captures `"E1\n"`; `process_cmd_options` quoting as above; invalid key
|
|
929
|
+
raises.
|
|
930
|
+
- `CMD.tool("echo", nil, nil, "echo")` + `get_tool` → `"echo"`.
|
|
931
|
+
|
|
932
|
+
### Subtle points
|
|
933
|
+
- The **default** stderr severity is `Log::DEBUG` (`:181`), so stderr is only
|
|
934
|
+
echoed at DEBUG or when explicitly given a severity; `:stderr => true`
|
|
935
|
+
maps to `Log::HIGH` (`:239-241, 270-272`).
|
|
936
|
+
- `no_fail` also suppresses the spawn-time ProcessFailed (`:299-300`).
|
|
937
|
+
- `{opt}` placeholder must be quoted in the command string (`:275-276`).
|
|
938
|
+
- `:save_stderr` with a path **truncates** ('w') and creates parent dirs.
|
|
939
|
+
- Timeout in pipe mode surfaces through `ConcurrentStream#abort` semantics,
|
|
940
|
+
not a direct raise (documented in the comment block `:310-325`).
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
## 12. Packaging / root files (read for context)
|
|
945
|
+
|
|
946
|
+
- `VERSION` → `1.8.8` (single line).
|
|
947
|
+
- `Gemfile` → no runtime deps; dev group only (`shoulda`, `rdoc ~> 3.12`,
|
|
948
|
+
`bundler ~> 1.0`, `juwelier ~> 2.1.0`, `simplecov`).
|
|
949
|
+
- `scout-essentials.gemspec` → juwelier-generated (`:1-2`), `s.name`/:8,
|
|
950
|
+
`s.version = "1.8.8"`/:9; no `add_dependency` lines (runtime deps live in
|
|
951
|
+
the Rakefile block instead).
|
|
952
|
+
- `Rakefile` → `ENV["BRANCH"] = 'main'`/:3; Juwelier::Tasks block
|
|
953
|
+
`gem.add_runtime_dependency 'term-ansicolor' | 'yaml' | 'rake' | 'listen'`
|
|
954
|
+
(:18-21); Rake::TestTask pattern `test/**/test_*.rb` (:26-28) with
|
|
955
|
+
`test.libs << 'lib' << 'test'`.
|
|
956
|
+
- `lib/scout.rb` does **not exist** (only `lib/scout-essentials.rb` and the
|
|
957
|
+
`lib/scout/` tree), so `require 'scout'` is not a supported entry point.
|
|
958
|
+
- Runtime gem dependencies actually observable from the audited files:
|
|
959
|
+
`term-ansicolor` (lib/scout/log/color.rb:4), stdlib `io/console`
|
|
960
|
+
(lib/scout/log.rb:6),
|
|
961
|
+
`open3`, `stringio`, `fileutils`, `yaml`
|
|
962
|
+
(lib/scout/log/progress/report.rb:1).
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## Files covered checklist (CORE chunk)
|
|
967
|
+
|
|
968
|
+
Read in full (line-anchored above):
|
|
969
|
+
|
|
970
|
+
- [x] lib/scout-essentials.rb
|
|
971
|
+
- [x] lib/scout/indiferent_hash.rb
|
|
972
|
+
- [x] lib/scout/indiferent_hash/case_insensitive.rb
|
|
973
|
+
- [x] lib/scout/indiferent_hash/options.rb
|
|
974
|
+
- [x] lib/scout/indiferent_hash/serialize.rb
|
|
975
|
+
- [x] lib/scout/named_array.rb
|
|
976
|
+
- [x] lib/scout/misc.rb
|
|
977
|
+
- [x] lib/scout/misc/digest.rb
|
|
978
|
+
- [x] lib/scout/misc/filesystem.rb
|
|
979
|
+
- [x] lib/scout/misc/format.rb
|
|
980
|
+
- [x] lib/scout/misc/helper.rb
|
|
981
|
+
- [x] lib/scout/misc/hook.rb
|
|
982
|
+
- [x] lib/scout/misc/insist.rb
|
|
983
|
+
- [x] lib/scout/misc/matching.rb
|
|
984
|
+
- [x] lib/scout/misc/math.rb
|
|
985
|
+
- [x] lib/scout/misc/monitor.rb
|
|
986
|
+
- [x] lib/scout/misc/system.rb
|
|
987
|
+
- [x] lib/scout/tmpfile.rb
|
|
988
|
+
- [x] lib/scout/exceptions.rb
|
|
989
|
+
- [x] lib/scout/config.rb
|
|
990
|
+
- [x] lib/scout/log.rb
|
|
991
|
+
- [x] lib/scout/log/color.rb
|
|
992
|
+
- [x] lib/scout/log/color_class.rb
|
|
993
|
+
- [x] lib/scout/log/fingerprint.rb
|
|
994
|
+
- [x] lib/scout/log/trap.rb
|
|
995
|
+
- [x] lib/scout/log/progress.rb
|
|
996
|
+
- [x] lib/scout/log/progress/report.rb
|
|
997
|
+
- [x] lib/scout/log/progress/util.rb
|
|
998
|
+
- [x] lib/scout/simple_opt.rb
|
|
999
|
+
- [x] lib/scout/simple_opt/accessor.rb
|
|
1000
|
+
- [x] lib/scout/simple_opt/doc.rb
|
|
1001
|
+
- [x] lib/scout/simple_opt/get.rb
|
|
1002
|
+
- [x] lib/scout/simple_opt/parse.rb
|
|
1003
|
+
- [x] lib/scout/simple_opt/setup.rb
|
|
1004
|
+
- [x] lib/scout/cmd.rb
|
|
1005
|
+
|
|
1006
|
+
Supporting files read:
|
|
1007
|
+
|
|
1008
|
+
- [x] Gemfile
|
|
1009
|
+
- [x] scout-essentials.gemspec
|
|
1010
|
+
- [x] Rakefile
|
|
1011
|
+
- [x] VERSION
|
|
1012
|
+
- [x] (lib/scout.rb — confirmed absent)
|
|
1013
|
+
|
|
1014
|
+
Tests consulted for semantics:
|
|
1015
|
+
|
|
1016
|
+
- [x] test/scout/test_config.rb
|
|
1017
|
+
- [x] test/scout/test_tmpfile.rb
|
|
1018
|
+
- [x] test/scout/test_indiferent_hash.rb
|
|
1019
|
+
- [x] test/scout/indiferent_hash/test_options.rb
|
|
1020
|
+
- [x] test/scout/indiferent_hash/test_case_insensitive.rb (listed, semantics via source)
|
|
1021
|
+
- [x] test/scout/test_named_array.rb
|
|
1022
|
+
- [x] test/scout/test_misc.rb (empty)
|
|
1023
|
+
- [x] test/scout/misc/{test_digest,test_helper,test_system,test_matching,test_math,test_filesystem,test_hook,test_insist}.rb
|
|
1024
|
+
- [x] test/scout/test_log.rb
|
|
1025
|
+
- [x] test/scout/log/{test_color,test_fingerprint,test_progress}.rb
|
|
1026
|
+
- [x] test/scout/simple_opt/{test_parse,test_get,test_doc,test_setup}.rb
|
|
1027
|
+
- [x] test/scout/test_cmd.rb
|
|
1028
|
+
- [x] test/scout/test_cmd_save_stderr.rb (listed)
|
|
1029
|
+
- [x] test/test_helper.rb
|