cinnabar 0.0.0 → 0.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6647629512489671373fd91ffdd165fcdc08306caad030e7ad827adefb070bf5
4
- data.tar.gz: 24e45eca6665b1a715dc9b7cd58547b6e6bb4ffbc1777a6250c8d55f537acaed
3
+ metadata.gz: 7ba73ee68ae237057d44f55990de599fae1290bdb0ad22ed9f4a3cc6688e8a9c
4
+ data.tar.gz: 0ca77910329aa2c50e2ee0f6562ae1873ab31db0926dedc856aafeb9e1b98c03
5
5
  SHA512:
6
- metadata.gz: 9de983869bf035c25dcadaab1f4b803a46e2dabc48e1d5bfe42ca4314006f9b13f1aa7bebb37fbbfb226c266fd0689dec57b15c0957735137349e9b9b0bf2a79
7
- data.tar.gz: 53d3feb704e7004cb4ba194854186a543fe0d42a9fca1abe5a689aba9464bac1ad7d6f30cdd83214710dd6543a1a2be260b3ea6568d459ea5b6bfa77358a6cc4
6
+ metadata.gz: 9123046f853c2533f6adbab221418066af5c351b283c346e31937ecd7a8010b897860cb3fb1365386d3d6abfdf7e822e405aa79ab63420fd1553cb5605f08b9f
7
+ data.tar.gz: 54b6ac22db5f6b5bb7287329d9b74876f03a8e76738968276cdd45b6d2ce22281b084c203ae93e66615bead81a2ce94cebbfdfd518c84956017a6f3a3e51949b
data/.rubocop.yml CHANGED
@@ -36,7 +36,7 @@ Layout/CaseIndentation:
36
36
  EnforcedStyle: end
37
37
  IndentOneStep: true
38
38
  Layout/MultilineMethodCallIndentation:
39
- EnforcedStyle: indented_relative_to_receiver
39
+ EnforcedStyle: indented
40
40
 
41
41
 
42
42
  Naming/AsciiIdentifiers:
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --markup markdown
2
+ --title 'Cinnabar'
3
+ --protected
4
+ --no-private
data/docs/Readme-zh.md CHANGED
@@ -24,41 +24,9 @@
24
24
  2. 朱砂有毒。这个项目是为了 **猛、糙、快** (a.k.a. *Dirty and Quick*) 的目的而开发的,可能会产生意料之外的副作用(它并非完全无害)。
25
25
  3. 朱砂是一种硫化汞 (HgS) 的矿物,呈深红色,而 Ruby 也是一种深红色的宝石。给一个 Ruby 项目取名为 “Cinnabar(朱砂)” 非常贴切。
26
26
 
27
- ## 快速上手
27
+ ## API DOC
28
28
 
29
- Github Actions for cinnabar
29
+ ![ClassDiagram](../misc/assets/svg/ClassDiagram.svg)
30
30
 
31
- ```yaml
32
- env:
33
- # Speeds up script startup by disabling RubyGems
34
- RUBYOPT: "--disable=gems"
35
- default_ci_shell: ruby cinnabar/ci.rb {0}
31
+ - Github Pages: <https://2moe.github.io/cinnabar>
36
32
 
37
- jobs:
38
- build:
39
- runs-on: ubuntu-latest
40
- defaults:
41
- run:
42
- shell: ${{env.default_ci_shell}}
43
- steps:
44
- - uses: actions/checkout@v6
45
-
46
- - name: clone cinnabar
47
- uses: actions/checkout@v6
48
- with:
49
- repository: 2moe/cinnabar
50
- path: cinnabar
51
- ref: v0.0.0
52
-
53
- - name: (example) run cargo command
54
- run: |
55
- {
56
- cargo: (),
57
- build: (),
58
- profile: 'release',
59
- verbose: true,
60
- target: 'x86_64-unknown-linux-musl'
61
- }
62
- .to_argv
63
- .run
64
- ```
data/docs/Readme.md CHANGED
@@ -24,6 +24,12 @@ A:
24
24
  2. Cinnabar is toxic. This project was developed for *Dirty and Quick* purposes and may produce unexpected side effects—in a sense, it is not entirely harmless.
25
25
  3. Cinnabar, a mineral form of mercury sulfide (HgS), is a deep red-colored stone. And ruby is also a deep red stone. Naming a Ruby project "Cinnabar" is particularly fitting.
26
26
 
27
+ ## API DOC
28
+
29
+ ![ClassDiagram](../misc/assets/svg/ClassDiagram.svg)
30
+
31
+ - Github Pages: <https://2moe.github.io/cinnabar>
32
+
27
33
  ## Quick Start
28
34
 
29
35
  Github Actions for cinnabar
@@ -33,6 +39,8 @@ env:
33
39
  # Speeds up script startup by disabling RubyGems
34
40
  RUBYOPT: "--disable=gems"
35
41
  default_ci_shell: ruby cinnabar/ci.rb {0}
42
+ # optional values: debug, info, warn, error, fatal, unknown
43
+ RUBY_LOG: "debug"
36
44
 
37
45
  jobs:
38
46
  build:
@@ -48,7 +56,7 @@ jobs:
48
56
  with:
49
57
  repository: 2moe/cinnabar
50
58
  path: cinnabar
51
- ref: v0.0.0
59
+ ref: v0.0.1
52
60
 
53
61
  - name: (example) run cargo command
54
62
  run: |
@@ -60,5 +68,87 @@ jobs:
60
68
  target: 'x86_64-unknown-linux-musl'
61
69
  }
62
70
  .to_argv
63
- .run
71
+ .run_cmd
72
+ ```
73
+
74
+ ## Examples
75
+
76
+ ### Command Runner
77
+
78
+ #### `.run` + pass stdin data
79
+
80
+ ```ruby,yaml
81
+ - run: |
82
+ opts = { stdin_data: "Hello", allow_failure: true }
83
+ stdout = %w[wc -m].run(opts:)
84
+
85
+ stdout.to_i == 5 #=> true
86
+ ```
87
+
88
+ #### `.async_run`
89
+
90
+ ```ruby,yaml
91
+ - run: |
92
+ 'building wasi file...'.log_dbg
93
+ task = {
94
+ cargo: (),
95
+ b: (),
96
+ r: true,
97
+ target: 'wasm32-wasip2'
98
+ } .to_argv
99
+ .async_run
100
+
101
+ stdout, status = task.wait_with_output
102
+ stdout.log_info
103
+ raise "wasi" unless status.success?
104
+ ```
105
+
106
+ #### `.async_run` + pass stdin data
107
+
108
+ ```ruby,yaml
109
+ - run: |
110
+ stdin_data = <<~'QMP_JSON'
111
+ { "execute":"qmp_capabilities" }
112
+ { "execute":"query-cpu-model-expansion",
113
+ "arguments":{"type":"full","model":{"name":"host"}} }
114
+ { "execute":"quit" }
115
+ QMP_JSON
116
+
117
+ # opts = { stdin_data:, stdin_binmode: false }
118
+ opts = { stdin_data: }
119
+
120
+ accel = %w[kvm hvf tcg].join ':'
121
+ task = {
122
+ 'qemu-system-x86_64': (),
123
+ machine: "accel=#{accel}",
124
+ cpu: 'host',
125
+ display: 'none',
126
+ nodefaults: true,
127
+ no_user_config: true,
128
+ qmp: 'stdio',
129
+ } .to_argv_bsd
130
+ .async_run(opts:)
131
+
132
+ stdout, status = task.wait_with_output
133
+ stdout.log_info if status.success?
134
+ ```
135
+
136
+ ### Downloader
137
+
138
+ ```ruby,yaml
139
+ - run: |
140
+ url = 'https://docs.ruby-lang.org/en/master'
141
+ url.download
142
+ # OR: url.download({out_dir: "/tmp", file_name: "index.html"})
143
+ ```
144
+
145
+ ### Function Pipe
146
+
147
+ ```ruby,yaml
148
+ - run: |
149
+ upper = ->s { s.upcase }
150
+
151
+ 'Foo'
152
+ .▷(upper)
153
+ .▷ :puts #=> "FOO"
64
154
  ```
@@ -2,9 +2,9 @@
2
2
 
3
3
  # Define Cinnabar first to prevent errors when creating Cinnabar::SubMod
4
4
  # (compact ClassAndModuleChildren)
5
- #
6
5
  # ------------------
7
6
 
7
+ # @see https://github.com/2moe/cinnabar
8
8
  module Cinnabar; end
9
9
 
10
10
  # To ensure compatibility with "--disable=gems" (allowing users to pre-require),
@@ -1,162 +1,511 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module Cinnabar::Command
4
5
  module_function
5
6
 
7
+ require 'open3'
6
8
  using Sinlog::Refin
7
9
 
8
- # This lambda function is capable of running system command.
10
+ # Executes the command synchronously (blocking) and returns its standard output.
9
11
  #
10
- # = Example
12
+ # @raise [RuntimeError] when `allow_failure: false` and the process exits with non-zero status
11
13
  #
12
- # using Cinnabar::FnPipe::Refin
14
+ # @example pass env
13
15
  #
14
16
  # Cmd = Cinnabar::Command
15
- # %w[sleep 3].▷ Cmd.run
16
- def run
17
- ->(array) do
18
- 'Running system cmd'.log_dbg
19
- array.log_info
20
- success = system(*array)
21
- Kernel.raise "Command failed: #{array.join(' ')}" unless success
17
+ # cmd_arr = %w[sh -c] << 'printf $WW'
18
+ # env_hash = {WW: 2}
19
+ # opts = {allow_failure: true}
20
+ # output = Cmd.run(cmd_arr, env_hash, opts:)
21
+ # output.to_i == 2 #=> true
22
+ #
23
+ # @example pass stdin data
24
+ #
25
+ # opts = {stdin_data: "Hello\nWorld\n"}
26
+ # output = Cinnabar::Command.run(%w[wc -l], opts:)
27
+ # output.to_i == 2 #=> true
28
+ #
29
+ # @param cmd_arr [Array<String>] The command and its arguments (e.g., `%w[printf hello]`).
30
+ # @param env_hash [#to_h] Environment variables to pass to the command.
31
+ # @param opts [Hash]
32
+ #
33
+ # - Only the `:allow_failure` is extracted and handled explicitly;
34
+ # - all other keys are passed through to **Open3.capture2** unchanged.
35
+ #
36
+ # @option opts [Boolean] :allow_failure
37
+ # Indicates whether the command is allowed to fail.
38
+ #
39
+ # @return [String, nil] the standard output of the command.
40
+ #
41
+ # @see async_run
42
+ def run(cmd_arr, env_hash = nil, opts: {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
43
+ 'Running and capturing the output of a system command.'.log_dbg
44
+ cmd_arr.log_info
45
+ "opts: #{opts}".log_dbg
46
+
47
+ allow_failure = opts.delete(:allow_failure) || false
48
+
49
+ final_env = normalize_env(env_hash)
50
+
51
+ begin
52
+ stdout, status =
53
+ if final_env.nil?
54
+ Open3.capture2(*cmd_arr, opts)
55
+ else
56
+ Open3.capture2(final_env, *cmd_arr, opts)
57
+ end
58
+ rescue StandardError => e
59
+ Kernel.raise e unless allow_failure
60
+ e.log_err
61
+ return stdout
22
62
  end
63
+
64
+ return stdout if status.success?
65
+
66
+ err_msg = "Command failed: #{cmd_arr.join(' ')}"
67
+ Kernel.raise err_msg unless allow_failure
68
+
69
+ err_msg.log_err
70
+ stdout
23
71
  end
24
72
 
25
- # Lambda that runs system command in the background.
73
+ # Executes a system command using Ruby's `Kernel.system`.
26
74
  #
27
- # @raise [RuntimeError] if the process exits with non-zero status
75
+ # It runs the command synchronously, blocks until completion, and does not capture stdout or stderr.
28
76
  #
29
- # = Example
77
+ # @param cmd_arr [Array<String>] The command and its arguments as an array,
78
+ # e.g., `%w[ls -lh]`.
79
+ #
80
+ # @param env_hash [#to_h] Environment variables to pass to the command.
81
+ # @param opts [Hash]
82
+ #
83
+ # - Only the `:allow_failure` is extracted and handled explicitly;
84
+ # - all other keys are passed through to `Kernel.system` unchanged.
85
+ #
86
+ # @option opts [Boolean] :allow_failure
87
+ # Indicates whether the command is allowed to fail.
88
+ # If true, the method will return false instead of raising an exception when the
89
+ # command exits with a non-zero status.
90
+ #
91
+ # @see https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options
92
+ #
93
+ # @example pwd
30
94
  #
31
95
  # Cmd = Cinnabar::Command
32
- # pid = %w[sleep 5].then(&Cmd.run_in_bg)
33
- def run_in_bg
34
- ->(array) do
35
- 'Running system cmd in the background'.log_dbg
36
- array.log_info
37
- Process.spawn(*array)
96
+ # opts = {chdir: '/tmp', allow_failure: true}
97
+ # Cmd.run_cmd(%w[pwd], opts:)
98
+ #
99
+ # @example pass env
100
+ #
101
+ # Cmd = Cinnabar::Command
102
+ # cmd_arr = %w[sh -c] << 'printf $WW'
103
+ # env_hash = {WW: 2}
104
+ # Cmd.run_cmd(cmd_arr, env_hash)
105
+ #
106
+ # @return [Boolean] Returns true if the command succeeds (exit status 0),
107
+ # or false if it fails and `allow_failure` is true.
108
+ # @raise [RuntimeError] Raises an error if the command fails and `allow_failure` is false.
109
+ def run_cmd(cmd_arr, env_hash = nil, opts: {})
110
+ 'Running system command'.log_dbg
111
+ cmd_arr.log_info
112
+ "opts: #{opts}".log_dbg
113
+
114
+ allow_failure = opts.delete(:allow_failure) || false
115
+ exception = !allow_failure
116
+ "exception: #{exception}".log_dbg
117
+ options = opts.merge({ exception: })
118
+
119
+ final_env = normalize_env(env_hash)
120
+ if final_env.nil?
121
+ Kernel.system(*cmd_arr, options)
122
+ else
123
+ Kernel.system(final_env, *cmd_arr, options)
38
124
  end
39
125
  end
40
126
 
41
- # ---------------
127
+ # @param hash [#to_h]
128
+ # @return [Hash{String => String}, nil] a hash where both keys and values are strings
129
+ def normalize_env(hash)
130
+ return nil if hash.nil?
131
+ return nil if hash.respond_to?(:empty?) && hash.empty?
132
+
133
+ hash.to_h { |k, v| [k.to_s, v.to_s] }
134
+ .tap { "normalized_env:#{_1}".log_dbg }
135
+ end
42
136
 
43
- # Lambda that waits for a child process to complete and checks its status.
137
+ # Launch a command asynchronously (non-blocking) and return its stdout stream and process waiter.
44
138
  #
45
- # This method requires the child process to exit with a successful status;
46
- # otherwise, it will raise an error.
139
+ # This is a sugar over **Open3.popen2**, intended to start a subprocess
140
+ # and **immediately** hand back:
47
141
  #
48
- # = Example
142
+ # 1. an `IO` for reading the command's stdout, and
143
+ # 2. a `Process::Waiter` (a thread-like object) that can be awaited later.
49
144
  #
50
- # using Cinnabar::FnPipe::Refin
145
+ # - If `:stdin_data` is provided, the data will be written to
146
+ # the child's stdin and the stdin will be closed.
147
+ # - When `:stdin_data` is absent, stdin is simply closed and
148
+ # the method returns immediately without blocking on output.
149
+ #
150
+ # @param cmd_arr [Array<String>] The command and its arguments (e.g., `%w[printf hello]`).
151
+ # @param env_hash [#to_h] Optional environment variables;
152
+ # keys/values will be normalized by {#normalize_env} before being passed to the child.
153
+ #
154
+ # @param opts [Hash] Additional options.
155
+ #
156
+ # - Only the following keys are extracted and handled explicitly;
157
+ # - :stdin_data
158
+ # - :binmode
159
+ # - :stdin_binmode
160
+ # - :stdout_binmode
161
+ #
162
+ # all other keys are passed through to **Open3.popen2** unchanged.
163
+ #
164
+ # @option opts [String, #readpartial] :stdin_data
165
+ # Data to write to the child's stdin. If it responds to `#readpartial`,
166
+ # it will be streamed via **IO.copy_stream**;
167
+ #
168
+ # @option opts [Boolean] :binmode
169
+ # When `true`, set both stdin and stdout to binary mode (useful for binary data).
170
+ #
171
+ # @option opts [Boolean] :stdin_binmode
172
+ # Sets only stdin to binary mode.
173
+ #
174
+ # @option opts [Boolean] :stdout_binmode
175
+ # Sets only stdout to binary mode.
176
+ #
177
+ # @return [Array(IO, Process::Waiter)] A pair `[stdout_io, waiter]`:
178
+ #
179
+ # - `stdout_io` is an `IO` for reading **stdout**
180
+ # - `waiter` is a `Process::Waiter`;
181
+ # - call `waiter.value` to get `Process::Status`;
182
+ # - or `waiter.join` to block until the process exits.
183
+ #
184
+ # @raise [StandardError] Reraises any non-`Errno::EPIPE` exception encountered while writing to stdin.
185
+ # `Errno::EPIPE` is logged and swallowed.
186
+ #
187
+ # @example start a process and later wait for it
188
+ #
189
+ # Cmd = Cinnabar::Command
190
+ # stdout_fd, waiter = Cmd.async_run(['sh', '-c', 'echo hello; sleep 1; echo done'])
191
+ #
192
+ # output, status = Cmd.wait_with_output(stdout_fd, waiter)
193
+ #
194
+ # @example pass stdin data
51
195
  #
52
196
  # Cmd = Cinnabar::Command
53
- # %w[sleep 3]
54
- # .▷(Cmd.run_in_bg) #=> pid
55
- # .▷(Cmd.wait_task)
56
- #
57
- def wait_task
58
- ->(pid) do
59
- "wait pid: #{pid}".log_dbg
60
- Process.wait(pid)
61
- status = $? # rubocop:disable Style/SpecialGlobalVars
62
- "child_status: #{status}".log_dbg
63
- Kernel.raise %(Command failed with "#{status}") unless status.success?
197
+ # opts = {stdin_data: "Run in the background" }
198
+ # io_and_waiter = Cmd.async_run(%w[wc -m], opts:)
199
+ #
200
+ # output, status = Cmd.wait_with_output(*io_and_waiter)
201
+ # status.success? #=> true
202
+ # output.to_i == 21 #=> true
203
+ #
204
+ # @see wait_with_output
205
+ # @see run
206
+ def async_run(cmd_arr, env_hash = nil, opts: {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
207
+ "Asynchronously executing system command: #{cmd_arr}".log_dbg
208
+ "opts: #{opts}".log_dbg
209
+
210
+ stdin_data = opts.delete(:stdin_data)
211
+ binmode = opts.delete(:binmode)
212
+ stdin_binmode = opts.delete(:stdin_binmode)
213
+ stdout_binmode = opts.delete(:stdout_binmode)
214
+
215
+ 'async_run() does not support the :allow_failure option.'.log_warn if opts.delete(:allow_failure)
216
+
217
+ final_env = normalize_env(env_hash)
218
+
219
+ stdin, stdout, waiter =
220
+ if final_env.nil?
221
+ Open3.popen2(*cmd_arr, opts)
222
+ else
223
+ Open3.popen2(final_env, *cmd_arr, opts)
224
+ end
225
+
226
+ if binmode
227
+ stdin.binmode
228
+ stdout.binmode
229
+ else
230
+ stdin.binmode if stdin_binmode
231
+ stdout.binmode if stdout_binmode
232
+ end
233
+
234
+ # Non-blocking: no stdin to write; return immediately
235
+ unless stdin_data
236
+ stdin.close
237
+ return [stdout, waiter]
238
+ end
239
+
240
+ begin
241
+ if stdin_data.respond_to? :readpartial
242
+ IO.copy_stream(stdin_data, stdin)
243
+ else
244
+ stdin.write stdin_data
245
+ end
246
+ rescue Errno::EPIPE => e
247
+ e.log_err
248
+ rescue StandardError => e
249
+ "Failed to write stdin data: #{e}".log_err
250
+ Kernel.raise e
251
+ ensure
252
+ stdin.close
64
253
  end
254
+ [stdout, waiter]
255
+ end
256
+
257
+ # Waits for a process to finish and reads all remaining output from its stdout.
258
+ #
259
+ # @param io_fd [IO] The IO object connected to the process's stdout (or combined stdout & stderr).
260
+ # @param waiter [Process::Waiter] The waiter thread returned by **Open3.popen2** or **Open3.popen2e**.
261
+ #
262
+ # @return [Array(String, Process::Status)] A two-element array:
263
+ #
264
+ # - The full output read from `io_fd`.
265
+ # - The **Process::Status** object representing the process's exit status.
266
+ #
267
+ # @example Wait for process and capture output
268
+ #
269
+ # require 'sinlog'
270
+ # using Sinlog::Refin
271
+ #
272
+ # Cmd = Cinnabar::Command
273
+ #
274
+ # fd, waiter = %w[ruby -e].push('sleep 2; puts "OK"')
275
+ # .then { Cmd.async_run(_1) }
276
+ #
277
+ # "You can now do other things without waiting for the process to complete.".log_dbg
278
+ #
279
+ # "blocking wait".log_info
280
+ # output, status = Cmd.wait_with_output(fd, waiter)
281
+ #
282
+ # "Exit code: #{status.exitstatus}".log_warn unless status.success?
283
+ # "Output:\n#{output}".log_info
284
+ #
285
+ # @note This method blocks until the process exits and all output is read.
286
+ # @see async_run
287
+ def wait_with_output(io_fd, waiter)
288
+ status = waiter.value
289
+ output = io_fd.read
290
+ io_fd.close
291
+ [output, status]
65
292
  end
66
- # ---------------
67
293
  end
68
294
 
69
295
  module Cinnabar::Command
70
- module ArrayExt
71
- def run
72
- Cinnabar::Command.run.call(self)
296
+ # The foundation of {ArrRefin} and {ArrMixin}
297
+ # @see Cinnabar::Command.run
298
+ # @see Cinnabar::Command.async_run
299
+ # @see Cinnabar::Command.run_cmd
300
+ module ArrExt
301
+ # Executes the command synchronously (blocking) and returns its standard output.
302
+ #
303
+ # @note self [`Array<String>`]: The command and its arguments (e.g., `%w[printf hello]`).
304
+ #
305
+ # @param env_hash [#to_h] Environment variables to pass to the command.
306
+ # @param opts [Hash]
307
+ #
308
+ # - Only the `:allow_failure` is extracted and handled explicitly;
309
+ # - all other keys are passed through to **Open3.capture2** unchanged.
310
+ #
311
+ # @raise [StandardError] when `allow_failure: false` and the process exits with non-zero status
312
+ #
313
+ # @return [String, nil] the standard output of the command.
314
+ # @see Cinnabar::Command.run
315
+ #
316
+ # @example pass stdin data
317
+ #
318
+ # using Cinnabar::Command::ArrRefin
319
+ # # OR: include Cinnabar::Command::ArrMixin
320
+ #
321
+ # opts = {allow_failure: true, stdin_data: "Hello\nWorld\n"}
322
+ # output = %w[wc -l].run(opts:)
323
+ # output.to_i == 2 unless output.nil? #=> true
324
+ #
325
+ # @note This method blocks until the process completes.
326
+ def run(env_hash = nil, opts: {})
327
+ Cinnabar::Command.run(self, env_hash, opts:)
328
+ end
329
+
330
+ # Starts a command asynchronously using this `Array<String>`.
331
+ #
332
+ # @note self [`Array<String>`]: The command and its arguments (e.g., `%w[printf hello]`).
333
+ #
334
+ # @param env_hash [#to_h] Optional environment variables.
335
+ # @param opts [Hash]
336
+ #
337
+ # @see Cinnabar::Command.async_run
338
+ #
339
+ # @return [Array(IO, Process::Waiter)] a pair `[stdout_io, waiter]`
340
+ #
341
+ # @example pass stdin data
342
+ #
343
+ # using Cinnabar::Command::ArrRefin
344
+ # # OR: include Cinnabar::Command::ArrMixin
345
+ #
346
+ # opts = {stdin_data: "Hello\nWorld\n"}
347
+ # io_and_waiter = %w[wc -l].async_run(opts:)
348
+ # output, status = Cinnabar::Command.wait_with_output *io_and_waiter
349
+ # output.to_i == 2 #=> true
350
+ def async_run(env_hash = nil, opts: {})
351
+ Cinnabar::Command.async_run(self, env_hash, opts:)
73
352
  end
74
353
 
75
- # Spawns system command (runs in the background)
76
- def run_in_bg
77
- Cinnabar::Command.run_in_bg.call(self)
354
+ # @note self [`Array<String>`]: The command and its arguments (e.g., `%w[printf hello]`).
355
+ #
356
+ # @example pwd
357
+ #
358
+ # using Cinnabar::Command::ArrRefin
359
+ # # OR: include Cinnabar::Command::ArrMixin
360
+ #
361
+ # opts = {chdir: '/tmp', allow_failure: true}
362
+ # status = %w[pwd].run_cmd(opts:)
363
+ #
364
+ # @example pass env
365
+ #
366
+ # using Cinnabar::Command::ArrRefin
367
+ #
368
+ # env_hash = {WW: 2}
369
+ # status =
370
+ # %w[sh -c]
371
+ # .push('printf $WW')
372
+ # .run_cmd(env_hash)
373
+ #
374
+ # status == true
375
+ #
376
+ # @return [Boolean]
377
+ # @see Cinnabar::Command.run_cmd
378
+ def run_cmd(env_hash = nil, opts: {}) # rubocop:disable Style/OptionalBooleanParameter
379
+ Cinnabar::Command.run_cmd(self, env_hash, opts:)
78
380
  end
79
381
  end
80
382
 
81
383
  # ---------------------
82
384
 
83
- # monkey patching: Array#run, Array#run_in_bg
385
+ # Monkey patching: Array#run, Array#async_run, Array#run_cmd
386
+ #
387
+ # @example run
388
+ #
389
+ # include Cinnabar::Command::ArrMixin
390
+ #
391
+ # stdout = %w[printf World].run
392
+ # stdout == "World" #=> true
393
+ #
394
+ # @example async_run
395
+ #
396
+ # include Cinnabar::Command::ArrMixin
397
+ #
398
+ # fd, waiter = %w[ruby -e].push('sleep 2; puts "OK"').async_run
399
+ #
400
+ # status = waiter.value
401
+ # status.success? #=> true
402
+ #
403
+ # output = fd.read.chomp
404
+ # fd.close
405
+ # output == 'OK' #=> true
84
406
  #
85
- # == Example
407
+ # @example async_run + wait_with_output
86
408
  #
87
409
  # include Cinnabar::Command::ArrMixin
410
+ # include Cinnabar::Command::TaskArrMixin
88
411
  #
89
- # %w[ls -l].run
412
+ # task = %w[ruby -e].push('sleep 2; puts "OK"').async_run
90
413
  #
91
- # pid = %w[sleep 3].run_in_bg
414
+ # output, status = task.wait_with_output
415
+ #
416
+ # status.success? #=> true
417
+ # output.chomp == 'OK' #=> true
418
+ #
419
+ # @see ArrExt
92
420
  module ArrMixin
93
- def self.included(_host) = ::Array.include ArrayExt
421
+ def self.included(_host) = ::Array.include ArrExt
94
422
  end
95
423
 
96
- # Refinements: Array#run, Array#run_in_bg
424
+ # Refinements: Array#run, Array#async_run, Array#run_cmd
97
425
  #
98
- # = Examples
99
- #
100
- # == Simple
426
+ # @example run
101
427
  #
102
428
  # using Cinnabar::Command::ArrRefin
103
429
  #
104
- # %w[ls -l].run
105
- # #=> execute system('ls', '-l')
430
+ # stdout =
431
+ # %w[ruby -e]
432
+ # .push('print 2')
433
+ # .run
434
+ #
435
+ # stdout.to_i == 2 #=> true
106
436
  #
107
- # == Argvise + run_in_bg
437
+ # @example run(opts:)
108
438
  #
109
439
  # using Cinnabar::Command::ArrRefin
440
+ #
441
+ # opts = { allow_failure: true, stdin_data: "Hello" }
442
+ #
443
+ # stdout = %w[wc -m].run(opts:)
444
+ #
445
+ # stdout.to_i == 5 #=> true
446
+ #
447
+ # @example Argvise + run_async
448
+ #
449
+ # require 'argvise'
450
+ # require 'cinnabar'
451
+ #
110
452
  # using Argvise::HashRefin
453
+ # using Cinnabar::Command::ArrRefin
454
+ # using Cinnabar::Command::TaskArrRefin
111
455
  #
112
- # pid = {
456
+ # task = {
113
457
  # cargo: (),
114
- # run: (),
458
+ # b: (),
459
+ # r: true,
115
460
  # target: "wasm32-wasip2"
116
- # }.to_argv.run_in_bg
117
- # #=> execute Process.spawn('cargo', 'run', '--target', 'wasm32-wasip2')
461
+ # }
462
+ # .to_argv
463
+ # .run_async
464
+ #
465
+ # stdout, status = task.wait_with_output
466
+ # status.success? #=> true
118
467
  #
468
+ # @see ArrExt
119
469
  module ArrRefin
120
470
  refine ::Array do
121
- import_methods ArrayExt
471
+ import_methods ArrExt
122
472
  end
123
473
  end
124
474
  end
125
475
 
126
476
  module Cinnabar::Command
127
- module IntegerExt
128
- def wait_task
129
- Cinnabar::Command.wait_task.call(self)
130
- end
131
- end
132
-
133
- # ---------------------
134
-
135
- # Monkey Patching: Integer#wait_task
477
+ # The foundation of {TaskArrRefin} and {TaskArrMixin}
478
+ # @see Cinnabar::Command.wait_with_output
136
479
  #
137
- # == Example
480
+ # @example simple
138
481
  #
139
- # include Cinnabar::Command::IntMixin
482
+ # using Cinnabar::Command::ArrRefin
483
+ # using Cinnabar::Command::TaskArrRefin
484
+ # # OR: include Cinnabar::Command::TaskArrMixin
485
+ #
486
+ # task = %w[ruby -e]
487
+ # .push('sleep 2; puts "OK"')
488
+ # .async_run
140
489
  #
141
- # pid = %w[sleep 3].run_in_bg
142
- # p "wait 3s"
143
- # pid.wait_task
144
- module IntMixin
145
- def self.included(_host) = ::Integer.include IntegerExt
490
+ # stdout, status = task.wait_with_output
491
+ # status.success? #=> true
492
+ module TaskArrExt
493
+ def wait_with_output
494
+ Cinnabar::Command.wait_with_output(*self)
495
+ end
146
496
  end
147
497
 
148
- # Refinement: Integer#wait_task
149
- #
150
- # == Example
151
- #
152
- # using Cinnabar::Command::IntRefin
153
- #
154
- # pid = %w[sleep 3].run_in_bg
155
- # p "wait 3s"
156
- # pid.wait_task
157
- module IntRefin
158
- refine ::Integer do
159
- import_methods IntegerExt
498
+ # Refinement: Array#wait_with_output
499
+ # @see TaskArrExt
500
+ module TaskArrRefin
501
+ refine ::Array do
502
+ import_methods TaskArrExt
160
503
  end
161
504
  end
505
+
506
+ # Monkey Patching: Array#wait_with_output
507
+ # @see TaskArrExt
508
+ module TaskArrMixin
509
+ def self.included(_host) = ::Array.include TaskArrExt
510
+ end
162
511
  end
data/lib/cinnabar/net.rb CHANGED
@@ -5,19 +5,33 @@ module Cinnabar::Downloader
5
5
 
6
6
  require 'open-uri'
7
7
 
8
- # == Example
8
+ DEFAULT_DL_OPTS = {
9
+ out_dir: 'tmp', file_name: nil,
10
+ }.freeze
11
+
12
+ # @example
9
13
  #
10
14
  # url = 'https://docs.ruby-lang.org/en/3.4/OpenURI.html'
11
15
  # opts = { out_dir: "tmp", file_name: "doc.html" }
12
16
  # DL = Cinnabar::Downloader
13
17
  # DL.download(url, opts)
14
18
  #
15
- # == Params
19
+ # @param url [String] e.g., **https://url.local**
20
+ #
21
+ # @param opts [Hash] Options for customizing the download behavior.
22
+ # e.g., `{out_dir: 'download', file_name: nil, headers: {'User-Agent' => "aria2/1.37.0"}}`
16
23
  #
17
- # - url: [String] e.g., https://url.local
18
- # - opts: [Hash] e.g., `{out_dir: 'download', file_name: nil, headers: {'User-Agent' => "aria2/1.37.0"}}`
19
- def download(url, opts)
24
+ # @option opts [String] :out_dir
25
+ # Directory where the downloaded file will be saved.
26
+ # @option opts [String, nil] :file_name
27
+ # Name of the output file. If nil, it will be inferred from the URL.
28
+ # @option opts [Hash{String => String}] :headers
29
+ # Optional HTTP headers to include in the request.
30
+ # @return [Integer]
31
+ def download(url, opts = {})
32
+ opts = DEFAULT_DL_OPTS.merge(opts)
20
33
  out_dir, file_name, headers = opts.values_at(:out_dir, :file_name, :headers)
34
+ out_dir = '.' if out_dir.nil?
21
35
 
22
36
  headers = build_headers(headers)
23
37
  parsed_url = Kernel.URI(url)
@@ -30,7 +44,7 @@ module Cinnabar::Downloader
30
44
  .then { IO.copy_stream(_1, file_path.to_s) }
31
45
  end
32
46
 
33
- # => ::Hash
47
+ # @return [Hash]
34
48
  def build_headers(headers)
35
49
  base_headers = {
36
50
  'User-Agent' => 'Mozilla/5.0 (Linux; aarch64 Wayland; rv:138.0) Gecko/20100101 Firefox/138.0',
@@ -38,7 +52,7 @@ module Cinnabar::Downloader
38
52
  base_headers.merge(headers || {}).transform_keys(&:to_s)
39
53
  end
40
54
 
41
- # => ::String
55
+ # @return [String]
42
56
  def determine_filename(file_name, parsed_url)
43
57
  filename = file_name || File.basename(parsed_url.path || '')
44
58
  case filename.strip
@@ -47,7 +61,7 @@ module Cinnabar::Downloader
47
61
  end
48
62
  end
49
63
 
50
- # => Kernel.Pathname
64
+ # @return [Pathname]
51
65
  def setup_file_path(out_dir, file_name)
52
66
  Kernel.Pathname(out_dir)
53
67
  .tap(&:mkpath)
@@ -56,37 +70,53 @@ module Cinnabar::Downloader
56
70
  end
57
71
 
58
72
  module Cinnabar::Downloader
59
- module StringExt
60
- def download(out_dir: 'tmp', file_name: nil, headers: {})
61
- Cinnabar::Downloader.download(self, { out_dir:, file_name:, headers: })
73
+ # The foundation of {StrRefin} and {StrMixin}
74
+ module StrExt
75
+ # @see Cinnabar::Downloader.download
76
+ #
77
+ # @param opts [Hash] Options for customizing the download behavior.
78
+ # e.g., `{out_dir: 'download', file_name: nil, headers: {'User-Agent' => "aria2/1.37.0"}}`
79
+ #
80
+ # @option opts [String] :out_dir
81
+ # @option opts [String, nil] :file_name
82
+ # @option opts [Hash{String => String}] :headers
83
+ # @return [Integer]
84
+ def download(opts = {})
85
+ Cinnabar::Downloader.download(self, opts)
62
86
  end
63
87
  end
64
88
 
65
89
  # -------------
66
90
 
67
- # = Example
91
+ # @example
68
92
  #
69
93
  # include Cinnabar::Downloader::StrMixin
70
94
  #
71
- # url = 'https://docs.ruby-lang.org'
95
+ # url = 'https://docs.ruby-lang.org/en/master'
72
96
  #
73
97
  # url.download
74
- # # OR: url.download(out_dir: "tmp", file_name: "custom.html")
98
+ # # OR: url.download({out_dir: "tmp", file_name: "custom.html"})
99
+ #
100
+ # @see Cinnabar::Downloader.download
101
+ # @see StrExt
75
102
  module StrMixin
76
- def self.included(_host) = ::String.include StringExt
103
+ def self.included(_host) = ::String.include StrExt
77
104
  end
78
105
 
79
- # = Example
106
+ # @example
80
107
  #
81
108
  # using Cinnabar::Downloader::StrRefin
82
109
  #
83
- # url = 'https://docs.ruby-lang.org/en/master/OpenURI.html'
110
+ # url = 'https://docs.ruby-lang.org/en/master'
84
111
  #
85
112
  # url.download
86
- # # OR: url.download(out_dir: "/tmp", file_name: "index.html")
113
+ # # OR: url.download({out_dir: "/tmp", file_name: "index.html"})
114
+ #
115
+ # @see Cinnabar::Downloader.download
116
+ # @see StrExt
87
117
  module StrRefin
88
118
  refine ::String do
89
- import_methods StringExt
119
+ import_methods StrExt
90
120
  end
91
121
  end
92
122
  end
data/lib/cinnabar/pipe.rb CHANGED
@@ -17,7 +17,7 @@ module Cinnabar::FnPipe
17
17
  end
18
18
 
19
19
  module Cinnabar::FnPipe
20
- module ObjectExt
20
+ module Ext
21
21
  def ▷(other) # rubocop:disable Naming/MethodName
22
22
  Cinnabar::FnPipe.▷(self, other)
23
23
  end
@@ -29,7 +29,7 @@ module Cinnabar::FnPipe
29
29
  #
30
30
  # Monkey Patching: Object#▷
31
31
  #
32
- # = Example
32
+ # @example
33
33
  #
34
34
  # include Cinnabar::FnPipe::Mixin
35
35
  #
@@ -40,14 +40,14 @@ module Cinnabar::FnPipe
40
40
  # 2.▷ :puts
41
41
  # #=> 2
42
42
  module Mixin
43
- def self.included(_host) = ::Object.include ObjectExt
43
+ def self.included(_host) = ::Object.include Ext
44
44
  end
45
45
 
46
46
  # Function Pipe
47
47
  #
48
48
  # Refinement: Object#▷
49
49
  #
50
- # = Example
50
+ # @example
51
51
  #
52
52
  # using Cinnabar::FnPipe::Refin
53
53
  #
@@ -63,7 +63,7 @@ module Cinnabar::FnPipe
63
63
  #
64
64
  module Refin
65
65
  refine ::Object do
66
- import_methods ObjectExt
66
+ import_methods Ext
67
67
  end
68
68
  end
69
69
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cinnabar
4
- VERSION = '0.0.0'
4
+ VERSION = '0.0.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cinnabar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - 2moe
@@ -29,6 +29,7 @@ extensions: []
29
29
  extra_rdoc_files: []
30
30
  files:
31
31
  - ".rubocop.yml"
32
+ - ".yardopts"
32
33
  - License
33
34
  - docs/Readme-zh.md
34
35
  - docs/Readme.md