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,218 @@
|
|
|
1
|
+
# Running Commands
|
|
2
|
+
|
|
3
|
+
`CMD` is scout-essentials' subprocess layer: one entry point, `CMD.cmd`, that
|
|
4
|
+
covers shell commands, no-shell command arrays, stdin piping, timeouts, stderr
|
|
5
|
+
handling and external-tool bootstrap. This page documents what the code does
|
|
6
|
+
(`lib/scout/cmd.rb`); for the object `:pipe => true` hands back, see
|
|
7
|
+
[Handling Streams](HandlingStreams.md) and the
|
|
8
|
+
[Streaming Model](../developer/StreamingModel.md).
|
|
9
|
+
|
|
10
|
+
## Command forms
|
|
11
|
+
|
|
12
|
+
`CMD.cmd(tool, cmd, options)` accepts three shapes:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
CMD.cmd('echo hello').read # => "hello\n" (String: run by a shell)
|
|
16
|
+
CMD.cmd(['echo', 'array-form']).read # => "array-form\n"
|
|
17
|
+
CMD.cmd('echo', 'arg2', '-n' => true, '-r' => true).read
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **String** — `tool` alone, or `tool + ' ' + cmd` when both are given. The
|
|
21
|
+
string is handed to `Open3.popen3(ENV, cmd)`, so shell features work.
|
|
22
|
+
- **Array** — no shell is involved; `Open3.popen3(ENV, *cmd_array)` is called
|
|
23
|
+
with the array plus each option as a separate argument. `process_cmd_options_array`
|
|
24
|
+
turns `'n' => 'val'` into `['n', 'val']` (or `['n=val']` for a key ending in
|
|
25
|
+
`=`), so quoting is not an issue here.
|
|
26
|
+
- **Hash-only** (`CMD.cmd({'echo' => 'x'})`) is *not* a command form; the Hash is
|
|
27
|
+
treated as options, `cmd`/`tool` stay nil and the call fails.
|
|
28
|
+
|
|
29
|
+
Verified: probe `tmp/rewrite_B/probe_15_cmd_forms.rb` (string, string+cmd,
|
|
30
|
+
array, array+string, symbol tool with nil cmd, ProcessFailed on hash-only).
|
|
31
|
+
|
|
32
|
+
### The `'{opt}'` placeholder
|
|
33
|
+
|
|
34
|
+
In String form the processed options are substituted for the literal
|
|
35
|
+
placeholder `'{opt}'` **only when it is single-quoted** in the command string:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
CMD.cmd("cut -d' ' -f2 '{opt}'", '-n' => true, in: 'one two three').read
|
|
39
|
+
# => "two\n" options replaced the quoted placeholder
|
|
40
|
+
|
|
41
|
+
CMD.cmd("echo {opt} a 1", 'x' => 1).read # => "{opt} a 1\n"
|
|
42
|
+
CMD.cmd("echo '{opt}' a 1", 'x' => 1).read # => "a 1\n" (note: trailing space)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Unquoted `{opt}` is left untouched — the substitution matches `'\{opt\}'`
|
|
46
|
+
exactly. Without a placeholder the option string is appended to the command.
|
|
47
|
+
Verified: probe `probe_14_cmd_opts.rb` lines 10-11 and the C8 fix in
|
|
48
|
+
`research/doc_audit/RunningCommands.md`.
|
|
49
|
+
|
|
50
|
+
## Option quoting: `process_cmd_options`
|
|
51
|
+
|
|
52
|
+
Every option that is not one of the reserved keys below becomes part of the
|
|
53
|
+
command line. `CMD.process_cmd_options(options)` builds that string:
|
|
54
|
+
|
|
55
|
+
| option/value | result |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `'opt' => true` | `opt` (bare flag, no value) |
|
|
58
|
+
| `'-v' => 'V'` | `-v 'V'` |
|
|
59
|
+
| `'-v=' => 'V'` | `-v='V'` (key ends in `=`: value glued, still quoted) |
|
|
60
|
+
| `'opt' => nil`, `'opt' => false` | dropped |
|
|
61
|
+
| value containing `'` | `\'`-escaped, then wrapped in `'...'` |
|
|
62
|
+
| `:add_option_dashes => true` | prepends `--` to keys not already starting with `-` |
|
|
63
|
+
|
|
64
|
+
The quoting rule is uniform: the value is always wrapped in single quotes, and
|
|
65
|
+
a value containing an apostrophe has it backslash-escaped first. There is no
|
|
66
|
+
"spaces only" special case.
|
|
67
|
+
|
|
68
|
+
**Key validation**: an option key that does not match `/^[a-z_0-9\-=.]+$/i`
|
|
69
|
+
raises `Invalid option key` before anything runs.
|
|
70
|
+
|
|
71
|
+
**Arrays are not expanded.** An Array value is stringified (`to_s`) and quoted
|
|
72
|
+
like any other value — `"#{value}"` produces `n '["a", "b"]'` in String mode
|
|
73
|
+
and `['n', '["a", "b"]']` in array mode. Pass separate calls or build the
|
|
74
|
+
command yourself if you need repeated flags. Verified: `probe_14_cmd_opts.rb`
|
|
75
|
+
lines 4-5 and the cmd-level Array check.
|
|
76
|
+
|
|
77
|
+
Verified examples: `probe_14_cmd_opts.rb` (dashes, `=`, nil/false, true,
|
|
78
|
+
apostrophes, invalid key, arrays).
|
|
79
|
+
|
|
80
|
+
## stderr: severities, capture and files
|
|
81
|
+
|
|
82
|
+
`options[:stderr]` selects how the child's stderr is treated. The default
|
|
83
|
+
(added by `cmd.rb:181`) is **`Log::DEBUG`** — stderr lines are only logged at
|
|
84
|
+
debug severity, i.e. invisible unless you raise `Log.severity` above it.
|
|
85
|
+
|
|
86
|
+
- `:stderr => true` is normalised to `Log::HIGH` (probe `probe_23_bar_log.rb`).
|
|
87
|
+
- Any other Integer is a `Log` severity: the ladder is
|
|
88
|
+
`DEBUG=0, LOW=1, MEDIUM=2, HIGH=3, INFO=4, WARN=5, ERROR=6, NONE=7`
|
|
89
|
+
(`lib/scout/log.rb` `SEVERITY_NAMES`), so `:stderr => Log::MEDIUM` shows
|
|
90
|
+
stderr as warnings while staying quieter than `HIGH`.
|
|
91
|
+
- Log output itself goes to the Log logfile / STDERR (see the
|
|
92
|
+
[Streaming Model](../developer/StreamingModel.md) for what per-stream capture
|
|
93
|
+
means); the per-command stderr *text* is never attached to the stream.
|
|
94
|
+
|
|
95
|
+
### `:save_stderr` — capture stderr instead of logging it
|
|
96
|
+
|
|
97
|
+
`ee24c68` extended `:save_stderr` to three shapes. All of them also fill the
|
|
98
|
+
`std_err` attribute (String on non-pipe results, on the returned stream in pipe
|
|
99
|
+
mode), so you can inspect it after the fact:
|
|
100
|
+
|
|
101
|
+
- **`:save_stderr => true`** — the text is captured into `std_err` and nothing
|
|
102
|
+
is logged.
|
|
103
|
+
- **`:save_stderr => path`** (String, Scout `Path` or `Pathname`) — CMD opens
|
|
104
|
+
the path for writing (truncating), **creates missing parent directories**,
|
|
105
|
+
writes stderr to it **line-buffered** so `tail -f` can follow a running
|
|
106
|
+
command, and **closes it when the command ends**. `std_err` is populated too.
|
|
107
|
+
- **`:save_stderr => io`** (anything responding to `write`/`<<`) — every chunk
|
|
108
|
+
is written and flushed, but CMD **never closes it**; closing is the caller's
|
|
109
|
+
business. `std_err` is populated too.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
res = CMD.cmd('sh -c "echo err >&2; echo out"', :save_stderr => true)
|
|
113
|
+
res2 = CMD.cmd(..., :save_stderr => 'log/cmd.err') # nested dirs created
|
|
114
|
+
dst = File.open('err.txt', 'w')
|
|
115
|
+
CMD.cmd(..., :save_stderr => dst) # dst stays open
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Implementation: `cmd.rb:194-217` (destination setup), the writer thread /
|
|
119
|
+
inline writer, and the `ensure` at `cmd.rb:600-612` that flushes and closes a
|
|
120
|
+
CMD-owned file. Verified by `test/scout/test_cmd_save_stderr.rb` (13 tests,
|
|
121
|
+
incl. live `tail -f` polling) and probes `probe_17_save_stderr.rb`
|
|
122
|
+
(boolean/String/Path/Pathname/IO/StringIO, pipe and non-pipe, truncation) and
|
|
123
|
+
`probe_16_exitstatus.rb` (`std_err` populated in both modes).
|
|
124
|
+
|
|
125
|
+
## Exit status, `no_fail` and failure
|
|
126
|
+
|
|
127
|
+
- Non-pipe mode: `CMD.cmd(...)` waits for the child and raises
|
|
128
|
+
`ProcessFailed` when the exit status is non-zero, unless `:no_fail` (alias
|
|
129
|
+
`:nofail`) is given.
|
|
130
|
+
- `:no_fail => true` **suppresses** `ProcessFailed`/`ConcurrentStreamProcessFailed`
|
|
131
|
+
— and `exit_status` then stays `nil`, in pipe mode too. If you need the code,
|
|
132
|
+
call `join_pids` yourself. Verified: `probe_16_exitstatus.rb`
|
|
133
|
+
(`pipe read+join exit_status: nil`, `explicit join_pids exit_status: 0`,
|
|
134
|
+
`non-pipe exit_status: 0`).
|
|
135
|
+
- `exit_status` is only ever set by `ConcurrentStream#join_pids`
|
|
136
|
+
(`concurrent_stream.rb:127`), which also empties `pids`, so it can only be
|
|
137
|
+
used once. A stream that is read and joined normally has `exit_status == nil`
|
|
138
|
+
(probe `probe_26_join_es.rb`: `read+join: es=nil` with `joined?` true; only
|
|
139
|
+
an explicit early `join_pids` yields `0`). Do not rely on `stream.exit_status`.
|
|
140
|
+
- If the process never starts (bad executable, no such file) `ProcessFailed` is
|
|
141
|
+
raised immediately — also suppressed by `no_fail`, which then returns `nil`.
|
|
142
|
+
- A failed *producer thread* in pipe mode surfaces as
|
|
143
|
+
`ConcurrentStreamProcessFailed` when the consumer closes/joins the stream
|
|
144
|
+
(`probe_13_force_close.rb`).
|
|
145
|
+
|
|
146
|
+
## Timeout
|
|
147
|
+
|
|
148
|
+
`CMD::Timeout < ProcessFailed` carries `command` and `timeout` and is raised by
|
|
149
|
+
a watchdog when `:timeout => seconds` elapses (`cmd.rb:310-408`,
|
|
150
|
+
`TIMEOUT_KILL_GRACE = 1.0`). This is the only way to bound a command's runtime.
|
|
151
|
+
|
|
152
|
+
- **Non-pipe mode**: the watchdog raises in the calling thread.
|
|
153
|
+
- **Pipe mode**: the exception is routed through `ConcurrentStream#abort` — it
|
|
154
|
+
lands in `stream_exception`, the stream is aborted (killing the process,
|
|
155
|
+
clearing pids, unblocking a blocked reader) and is re-raised when the consumer
|
|
156
|
+
reads or joins. It is *not* raised directly in the caller at command start.
|
|
157
|
+
|
|
158
|
+
## Tool management
|
|
159
|
+
|
|
160
|
+
There is **no `CMD.add_tool`**. Registration is:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
CMD.tool(:samtools, claim, test, cmd, &block) # internally stored [claim, test, block, cmd]
|
|
164
|
+
CMD.get_tool(:samtools) # ensures it is usable; returns the command name
|
|
165
|
+
CMD.versions # => {"samtools" => "1.17", ...}
|
|
166
|
+
CMD.conda('samtools', 'env', 'bioconda') # conda install fallback
|
|
167
|
+
CMD.bash(cmd) # bash -l login shell, :autojoin => true
|
|
168
|
+
CMD.scan_version_text(text, 'samtools') # heuristically pull a version string
|
|
169
|
+
CMD.cmd_log('...') # run + echo STDOUT/STDERR, returns nil
|
|
170
|
+
CMD.cmd_pid('...') # same implementation, also returns nil
|
|
171
|
+
# (both force :pipe/:log; the pid only shows up
|
|
172
|
+
# inside the 'STDOUT [pid]:' header)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`get_tool` runs `test` (or `command -v cmd`), and if that fails produces the
|
|
176
|
+
`claim` Resource (or calls `block`; a Hash result is passed to
|
|
177
|
+
`Resource.install`). It then records a version from `--version`/`-version`/
|
|
178
|
+
`--help` output. Tools are stored in the `TOOLS` IndiferentHash; `versions`
|
|
179
|
+
returns only entries matching `/\d+\./`.
|
|
180
|
+
|
|
181
|
+
## Options reference (consumed by `CMD.cmd`)
|
|
182
|
+
|
|
183
|
+
| key | effect |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `:in` | stdin: a String is written by a thread; an IO/StringIO is read; a ConcurrentStream is streamed (and closed unless `:dont_close_in`). Also `:in_pipe` for a pipe-backed writer. Verified: `probe_25_keeping_in.rb` (String, IO, stream, `in_pipe` returns an IO). |
|
|
186
|
+
| `:pipe` | return a ConcurrentStream instead of the text/StringIO |
|
|
187
|
+
| `:stderr` | severity for stderr logging (default `Log::DEBUG`); `true` → `Log::HIGH` |
|
|
188
|
+
| `:save_stderr` | `true` / path / IO — see above |
|
|
189
|
+
| `:no_fail`, `:nofail` | suppress failure raising (both spellings) |
|
|
190
|
+
| `:autojoin` | join the stream when it is closed/read; **`CMD.cmd` sets `:autojoin => no_fail`** |
|
|
191
|
+
| `:no_wait` | alias used to default `autojoin` (`autojoin = no_wait if autojoin.nil?`) |
|
|
192
|
+
| `:timeout` | seconds; watchdog; only runtime bound |
|
|
193
|
+
| `:post` | proc run after the command/stream finishes (teardown, forcing upstream closes) |
|
|
194
|
+
| `:progress_bar` | a `Log::ProgressBar`; stderr lines tick it (`probe_24_bar.rb`: 2 ticks in both pipe and non-pipe mode) |
|
|
195
|
+
| `:log` | defaults to `true` (`log = true if log.nil?`, cmd.rb:224): pipe-mode stderr lines are `Log.log`ged at the chosen `:stderr` severity. `:log => false` silences that logging. Not the same as `CMD.cmd_log` (a separate helper that forces `:pipe`/`:log`). |
|
|
196
|
+
| `:sudo`, `:xvfb` | prefix the command |
|
|
197
|
+
| `:dont_close_in` | keep the `:in` stream open |
|
|
198
|
+
| `:add_option_dashes` | passed through to `process_cmd_options*` |
|
|
199
|
+
|
|
200
|
+
`:wait`, `:canfail`, `:empty_inputs` and `:separator` are **not** options of
|
|
201
|
+
`CMD.cmd` in this repo — `cmd.rb` deletes none of them, so they would be
|
|
202
|
+
forwarded to `process_cmd_options` and end up as command-line text. `:canfail`
|
|
203
|
+
exists on `Persist` (`persist.rb:133`) and on `Resource` claims, not here.
|
|
204
|
+
Verified by grepping `lib/` for the four names (`probe_25_keeping_in.rb` tail).
|
|
205
|
+
|
|
206
|
+
## The block is ignored
|
|
207
|
+
|
|
208
|
+
`CMD.cmd(...) { ... }` accepts a block but **never calls it** — the only block
|
|
209
|
+
invocation inside `cmd.rb` is in `CMD.tool`. Verified live by
|
|
210
|
+
`tmp/docaudit/probe_hs_c15_block.rb`: in both pipe and non-pipe mode the block
|
|
211
|
+
body never runs, while `:post` and `stream.add_callback` do. Use
|
|
212
|
+
`:post => proc{}` or stream callbacks for post-join work.
|
|
213
|
+
|
|
214
|
+
## Related
|
|
215
|
+
|
|
216
|
+
- [Handling Streams](HandlingStreams.md) — the returned object's lifecycle.
|
|
217
|
+
- [Streaming Model](../developer/StreamingModel.md) — internals, error paths.
|
|
218
|
+
- [Working with Files](WorkingWithFiles.md) — `Open.grep` on top of `CMD`.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Working with Files
|
|
2
|
+
|
|
3
|
+
This guide explains how to read, write, and manage files in scout-essentials:
|
|
4
|
+
the `Open` module for I/O, compression, grepping, atomic writes, locking, and
|
|
5
|
+
remote file handling, plus `TmpFile` for scratch space.
|
|
6
|
+
|
|
7
|
+
## Reading: `Open.read` and `Open.open`
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'scout-essentials'
|
|
11
|
+
|
|
12
|
+
Open.read('VERSION') # => "1.8.8" (UTF-8 fixed by default)
|
|
13
|
+
Open.read('VERSION', nofix: true) # => raw bytes as read
|
|
14
|
+
|
|
15
|
+
Open.open('data.txt') do |f|
|
|
16
|
+
f.each_line { |line| ... }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Every stream returned by `Open.open` is extended with `Open::NamedStream`,
|
|
20
|
+
whose `filename` attribute records the file it was opened from (useful when
|
|
21
|
+
the stream flows through pipelines):
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
io = Open.open('data.txt')
|
|
25
|
+
io.filename # => "data.txt"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `Open.read`/`Open.open` accept a `Path` (or a `String`); a `Path` is
|
|
29
|
+
resolved with `find` before opening.
|
|
30
|
+
- Both accept options. Notable ones:
|
|
31
|
+
- `:mode` — open mode (`'r'` default, `'rb'` for binary reads).
|
|
32
|
+
- `:grep` / `:invert_grep` — filter lines while reading (below).
|
|
33
|
+
- `:zip` / `:gzip` / `:bgzip` — force a decompressor regardless of
|
|
34
|
+
extension.
|
|
35
|
+
- `:noz` — disable automatic decompression and read the raw file.
|
|
36
|
+
|
|
37
|
+
## Compression
|
|
38
|
+
|
|
39
|
+
Detection is **extension-only and case-sensitive**. A file is decompressed
|
|
40
|
+
only when its name ends (lower-case) with `.gz`, `.bgz`, `.zip`:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Open.read('file.gz') # decompressed
|
|
44
|
+
Open.read('file.GZ') # raw gzip bytes — NOT detected
|
|
45
|
+
Open.read('file.tgz') # raw gzip bytes — NOT detected
|
|
46
|
+
Open.read('file.tar.gz')# decompressed (ends in .gz)
|
|
47
|
+
Open.read('file.gz.bak')# raw gzip bytes — NOT detected
|
|
48
|
+
Open.gzip?('file.tgz') # => false
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Force decompression explicitly when the name does not cooperate:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
Open.open('file.tgz', :gzip => true).read
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Grepping lines
|
|
58
|
+
|
|
59
|
+
`:grep` and `:invert_grep` select lines using the system `grep`. **With a
|
|
60
|
+
block**, `Open.read` collects the transformed lines into an Array; **without
|
|
61
|
+
a block** it returns the matched lines as a String (and `Open.open` returns
|
|
62
|
+
a grep pipe):
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# file f contains: apple / banana / cherry
|
|
66
|
+
Open.read(f, grep: 'an') # => "banana\n" (String)
|
|
67
|
+
Open.read(f, grep: 'an') { |l| l.strip.upcase } # => ["BANANA"]
|
|
68
|
+
Open.read(f, grep: 'an', invert_grep: true) # => "apple\ncherry\n"
|
|
69
|
+
Open.read(f, grep: %w(apple banana)) { |l| l.strip } # => ["apple", "banana"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`invert_grep` has no effect on its own.** `Open.read`/`Open.open` only
|
|
73
|
+
grep when `grep` is truthy (`Open#file_open(file, grep = false, ...)`
|
|
74
|
+
extracts `:grep`/`:invert_grep` but only greps for `grep`); passing
|
|
75
|
+
`invert_grep:` without `grep:` returns the whole file unchanged:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Open.read(f, invert_grep: 'an') # => "apple\nbanana\ncherry\n" (no-op)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
To invert without going through `:grep`, use the low-level helper with its
|
|
82
|
+
third argument:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
Open.grep(Open.open(f), 'an', true).read # => "apple\ncherry\n"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`:grep` accepts a String (passed to the system `grep` as a pattern) or an
|
|
89
|
+
Array of Strings (written to a pattern file used with `grep -f`). Arrays are
|
|
90
|
+
matched with `grep -w -F` (whole-word, fixed strings) unless you pass
|
|
91
|
+
`fixed_grep: false`, which switches to plain `grep -f` semantics.
|
|
92
|
+
|
|
93
|
+
## Writing: `Open.write` and appends
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
Open.write('out.txt', "content\n") # creates parent dirs
|
|
97
|
+
Open.write('out.txt') { |f| f.puts "x" } # block form
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`Open.write` always overwrites by default (`mode: 'w'`). **To append, pass
|
|
101
|
+
`mode: 'a'`** — there is no public `Open.append` class method:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Open.write('log.txt', "one\n")
|
|
105
|
+
Open.write('log.txt', "two\n", mode: 'a')
|
|
106
|
+
Open.read('log.txt') # => "one\ntwo\n"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
On error, `Open.write` removes the partially written file and re-raises.
|
|
110
|
+
|
|
111
|
+
### Atomic writes: `sensible_write`
|
|
112
|
+
|
|
113
|
+
`Open.sensible_write(file, content = nil, options = {})` refuses to overwrite
|
|
114
|
+
an existing file unless `:force => true`; it takes a String or a block and
|
|
115
|
+
removes the target when the block raises (`Aborted` is swallowed silently):
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
Open.sensible_write(path, "v1")
|
|
119
|
+
Open.sensible_write(path, "v2") # keeps "v1"
|
|
120
|
+
Open.sensible_write(path, "v2", force: true) # overwrites
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## File primitives
|
|
124
|
+
|
|
125
|
+
All of these accept `Path` or `String` and resolve `Path`s via `find`; most
|
|
126
|
+
create parent directories as needed:
|
|
127
|
+
|
|
128
|
+
| Call | Effect |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `Open.cp(src, dst)` | `cp_r`, creates parent dirs, replaces `dst` |
|
|
131
|
+
| `Open.mv(src, dst)` | two-step move (`.tmp_mv.` intermediate), creates parent dirs |
|
|
132
|
+
| `Open.rm(file)` | removes a file or broken link |
|
|
133
|
+
| `Open.rm_rf(file)` | recursive remove |
|
|
134
|
+
| `Open.mkdir(dir)` | `mkdir -p` if missing |
|
|
135
|
+
| `Open.mkfiledir(file)` | `mkdir -p` the parent of `file` |
|
|
136
|
+
| `Open.touch(file)` | `touch`, creating parent dirs |
|
|
137
|
+
| `Open.ln(src, dst)` | hard link (falls back to `ln_s` via `Open.link`) |
|
|
138
|
+
| `Open.ln_s(src, dst)` | symbolic link |
|
|
139
|
+
| `Open.ln_h(src, dst)` | `ln -L` with `cp -L` fallback |
|
|
140
|
+
| `Open.link(src, dst)` | `Open.ln`, falling back to `Open.ln_s` |
|
|
141
|
+
| `Open.link_dir(src, dst)` | `cp -lr` — copy a directory tree of hard links |
|
|
142
|
+
| `Open.same_file(a, b)` | `File.identical?` |
|
|
143
|
+
| `Open.exists?(f)`, `Open.directory?(f)`, `Open.size(f)`, `Open.ctime(f)`, `Open.mtime(f)` | stats |
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
src = 'data/src.txt'
|
|
147
|
+
Open.write(src, 'S')
|
|
148
|
+
Open.cp(src, 'data/sub/dst.txt') # parents created
|
|
149
|
+
Open.mv('data/sub/dst.txt', 'data/m.txt')
|
|
150
|
+
Open.touch('data/t/t.txt')
|
|
151
|
+
Open.ln(src, 'data/hard.txt') # nlink > 1
|
|
152
|
+
Open.link_dir('data/a', 'data/a2')
|
|
153
|
+
Open.same_file(src, 'data/hard.txt') # => true
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`Open.mtime` has one special case: for a symlink or a file with multiple
|
|
157
|
+
hard links it consults a sibling `.info` file when `Step` is defined (a
|
|
158
|
+
scout-gear concept) and falls back to `Pathname#realpath`; otherwise it is a
|
|
159
|
+
plain `File.mtime`.
|
|
160
|
+
|
|
161
|
+
## Locking: `Open.lock`
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
Open.lock('var/cache/persistence/file') do
|
|
165
|
+
# exclusive access while the block runs
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`Open.lock(file, unlock = true, options = {})` uses the vendored `Lockfile`
|
|
170
|
+
implementation. The lock file lives next to the target by default; pass a
|
|
171
|
+
`Lockfile` instance via `options[:lock]` to reuse an existing one. See
|
|
172
|
+
[Caching Results](CachingResults.md) for how `Persist` uses it.
|
|
173
|
+
|
|
174
|
+
## Streams and `consume_stream`
|
|
175
|
+
|
|
176
|
+
`Open.open_pipe` returns a `ConcurrentStream` that you can consume lazily;
|
|
177
|
+
`Open.consume_stream(stream)` reads it to the end (and joins it), and
|
|
178
|
+
`Open.sensible_write` accepts a stream, writing it asynchronously. When a
|
|
179
|
+
stream carries a `filename`, `NamedStream` records it so tools can recover
|
|
180
|
+
the original name. See
|
|
181
|
+
[Streaming Model](../developer/StreamingModel.md).
|
|
182
|
+
|
|
183
|
+
## Remote files
|
|
184
|
+
|
|
185
|
+
`Open.read`/`Open.open` transparently handle `http(s)://` and `ssh:` URLs by
|
|
186
|
+
shelling out (`Open.wget`, `Open.ssh`), and `Open.sync` / `Open.rsync` copy
|
|
187
|
+
remote trees into the local cache (`Open.remote_cache_dir`, default
|
|
188
|
+
`$HOME/.scout/var/cache/open-remote`). Remote fetching is covered in
|
|
189
|
+
[RemoteData.md](RemoteData.md).
|
|
190
|
+
|
|
191
|
+
## Scratch files: `TmpFile`
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
TmpFile.tmp_file('prefix') # => ".../tmpfiles/tmp-<n>" (a fresh path)
|
|
195
|
+
TmpFile.user_tmp # => $HOME/tmp/scout
|
|
196
|
+
TmpFile.tmp_for_file('a/b/c') # => "·a·b·c" (flat, separators replaced with ·)
|
|
197
|
+
TmpFile.with_file(content) do |file| ... end # writes, yields the path, removes it
|
|
198
|
+
TmpFile.with_dir do |dir| ... end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`TmpFile.tmp_for_file` flattens the whole path into a single file name
|
|
202
|
+
inside the tmpfiles directory (`/` → `·`), it does **not** create a nested
|
|
203
|
+
directory structure; `TmpFile.tmp_for_dir` does not exist. Options may
|
|
204
|
+
append a `[key]` and an `&F[...]` fingerprint, and names are truncated to
|
|
205
|
+
`MAX_FILE_LENGTH` (150).
|
|
206
|
+
|
|
207
|
+
## Related
|
|
208
|
+
|
|
209
|
+
- [Caching Results](CachingResults.md) — `Persist` uses `Open.lock` and
|
|
210
|
+
`sensible_write`.
|
|
211
|
+
- [Path Resolution](../developer/PathResolution.md) — `find`/`follow`, path
|
|
212
|
+
maps.
|
|
213
|
+
- [Producing Resources](ProducingResources.md) — Resource uses Path for
|
|
214
|
+
resolution.
|
|
215
|
+
- For streaming internals, see
|
|
216
|
+
[Streaming Model](../developer/StreamingModel.md).
|
|
217
|
+
- For remote data, see [RemoteData.md](RemoteData.md).
|