cmds 0.0.9 → 0.1.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.
data/lib/cmds/spawn.rb ADDED
@@ -0,0 +1,251 @@
1
+ # stdlib
2
+ require 'open3'
3
+ require 'thread'
4
+
5
+ # deps
6
+ require 'nrser'
7
+ require 'nrser/refinements'
8
+
9
+ # project
10
+ require 'cmds/pipe'
11
+ require 'cmds/io_handler'
12
+
13
+ using NRSER
14
+
15
+ module Cmds
16
+ # internal core function to spawn and stream inputs and/or outputs using
17
+ # threads.
18
+ #
19
+ # originally inspired by
20
+ #
21
+ # https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
22
+ #
23
+ # with major modifications from looking at Ruby's open3 module.
24
+ #
25
+ # @param [String] cmd
26
+ # shell-ready command string.
27
+ #
28
+ # @param [nil | String | #read] input
29
+ # string or readable input. here so that Cmds instances can pass their
30
+ # `@input` instance variable -- `&io_block` overrides it.
31
+ #
32
+ # @param [#call & (#arity ∈ {0, 1})] &io_block
33
+ # optional block to handle io. behavior depends on arity:
34
+ #
35
+ # - arity `0`
36
+ # - block is called and expected to return an object
37
+ # suitable for input (`nil`, `String` or `IO`-like).
38
+ # - arity `1`
39
+ # - block is called with the {Cmds::IOHandler} instance for the
40
+ # execution, which it can use to handle input and outputs.
41
+ #
42
+ # @raise [ArgumentError]
43
+ # if `&io_block` has arity greater than 1.
44
+ #
45
+ def self.spawn cmd, input = nil, &io_block
46
+ Cmds.debug "entering Cmds#really_stream",
47
+ cmd: cmd,
48
+ input: input,
49
+ io_block: io_block
50
+
51
+ # create the handler that will be yielded to the input block
52
+ handler = Cmds::IOHandler.new
53
+
54
+ # handle input
55
+ #
56
+ # if a block was provided it overrides the `input` argument.
57
+ #
58
+ if io_block
59
+ case io_block.arity
60
+ when 0
61
+ # when the input block takes no arguments it returns the input
62
+ input = io_block.call
63
+ when 1
64
+ # when the input block takes one argument, give it the handler and
65
+ # ignore the return value
66
+ io_block.call handler
67
+
68
+ # if input was assigned to the handler in the block, use it as input
69
+ input = handler.in unless handler.in.nil?
70
+ else
71
+ # bad block provided
72
+ raise ArgumentError.new <<-BLOCK.squish
73
+ provided input block must have arity 0 or 1
74
+ BLOCK
75
+ end # case io_block.arity
76
+ end # if io_block
77
+
78
+ # hash of options that will be passed to `spawn`
79
+ spawn_opts = {}
80
+
81
+ Cmds.debug "looking at input...",
82
+ input: input
83
+
84
+ # (possibly) create the input pipe... this will be nil if the provided
85
+ # input is io-like. in this case it will be used directly in the
86
+ # `spawn` options.
87
+ in_pipe = case input
88
+ when nil, String
89
+ Cmds.debug "input is a String or nil, creating pipe..."
90
+
91
+ in_pipe = Cmds::Pipe.new "INPUT", :in
92
+ spawn_opts[:in] = in_pipe.r
93
+
94
+ # don't buffer input
95
+ in_pipe.w.sync = true
96
+ in_pipe
97
+
98
+ else
99
+ Cmds.debug "input should be io-like, setting spawn opt.",
100
+ input: input
101
+ if input == $stdin
102
+ Cmds.debug "input is $stdin."
103
+ end
104
+ spawn_opts[:in] = input
105
+ nil
106
+
107
+ end # case input
108
+
109
+ # (possibly) create the output pipes.
110
+ #
111
+ # `stream` can be told to send it's output to either:
112
+ #
113
+ # 1. a Proc that will invoked with each line.
114
+ # 2. an io-like object that can be provided as `spawn`'s `:out` or
115
+ # `:err` options.
116
+ #
117
+ # in case (1) a `Cmds::Pipe` wrapping read and write piped `IO` instances
118
+ # will be created and assigned to the relevant of `out_pipe` or `err_pipe`.
119
+ #
120
+ # in case (2) the io-like object will be sent directly to `spawn` and
121
+ # the relevant `out_pipe` or `err_pipe` will be `nil`.
122
+ #
123
+ out_pipe, err_pipe = [
124
+ ["ERROR", :err],
125
+ ["OUTPUT", :out],
126
+ ].map do |name, sym|
127
+ Cmds.debug "looking at #{ name }..."
128
+ # see if hanlder.out or hanlder.err is a Proc
129
+ if handler.send(sym).is_a? Proc
130
+ Cmds.debug "#{ name } is a Proc, creating pipe..."
131
+ pipe = Cmds::Pipe.new name, sym
132
+ # the corresponding :out or :err option for spawn needs to be
133
+ # the pipe's write handle
134
+ spawn_opts[sym] = pipe.w
135
+ # return the pipe
136
+ pipe
137
+
138
+ else
139
+ Cmds.debug "#{ name } should be io-like, setting spawn opt.",
140
+ output: handler.send(sym)
141
+ spawn_opts[sym] = handler.send(sym)
142
+ # the pipe is nil!
143
+ nil
144
+ end
145
+ end # map outputs
146
+
147
+ Cmds.debug "spawning...",
148
+ cmd: cmd,
149
+ opts: spawn_opts
150
+
151
+ pid = Process.spawn cmd, spawn_opts
152
+
153
+ Cmds.debug "spawned.",
154
+ pid: pid
155
+
156
+ wait_thread = Process.detach pid
157
+ wait_thread[:name] = "WAIT"
158
+
159
+ Cmds.debug "wait thread created.",
160
+ thread: wait_thread
161
+
162
+ # close child ios if created
163
+ # the spawned process will read from in_pipe.r so we don't need it
164
+ in_pipe.r.close if in_pipe
165
+ # and we don't need to write to the output pipes, that will also happen
166
+ # in the spawned process
167
+ [out_pipe, err_pipe].each {|pipe| pipe.w.close if pipe}
168
+
169
+ # create threads to handle any pipes that were created
170
+
171
+ in_thread = if in_pipe
172
+ Thread.new do
173
+ Thread.current[:name] = in_pipe.name
174
+ Cmds.debug "thread started, writing input..."
175
+
176
+ in_pipe.w.write input unless input.nil?
177
+
178
+ Cmds.debug "write done, closing in_pipe.w..."
179
+ in_pipe.w.close
180
+
181
+ Cmds.debug "thread done."
182
+ end # Thread
183
+ end
184
+
185
+ out_thread, err_thread = [out_pipe, err_pipe].map do |pipe|
186
+ if pipe
187
+ Thread.new do
188
+ Thread.current[:name] = pipe.name
189
+ Cmds.debug "thread started"
190
+
191
+ loop do
192
+ Cmds.debug "blocking on gets..."
193
+ line = pipe.r.gets
194
+ if line.nil?
195
+ Cmds.debug "received nil, output done."
196
+ else
197
+ Cmds.debug <<-BLOCK.squish
198
+ received #{ line.bytesize } bytes, passing to handler.
199
+ BLOCK
200
+ end
201
+ handler.thread_send_line pipe.sym, line
202
+ break if line.nil?
203
+ end
204
+
205
+ Cmds.debug "reading done, closing pipe.r (unless already closed)..."
206
+ pipe.r.close unless pipe.r.closed?
207
+
208
+ Cmds.debug "thread done."
209
+ end # thread
210
+ end # if pipe
211
+ end # map threads
212
+
213
+ Cmds.debug "handing off main thread control to the handler..."
214
+ begin
215
+ handler.start
216
+
217
+ Cmds.debug "handler done."
218
+
219
+ ensure
220
+ # wait for the threads to complete
221
+ Cmds.debug "joining threads..."
222
+
223
+ [in_thread, out_thread, err_thread, wait_thread].each do |thread|
224
+ if thread
225
+ Cmds.debug "joining #{ thread[:name] } thread..."
226
+ thread.join
227
+ end
228
+ end
229
+
230
+ Cmds.debug "all threads done."
231
+ end
232
+
233
+ status = wait_thread.value.exitstatus
234
+ Cmds.debug "exit status: #{ status.inspect }"
235
+
236
+ Cmds.debug "checking @assert and exit status..."
237
+ if @assert && status != 0
238
+ # we don't necessarily have the err output, so we can't include it
239
+ # in the error message
240
+ msg = <<-BLOCK.squish
241
+ streamed command `#{ cmd }` exited with status #{ status }
242
+ BLOCK
243
+
244
+ raise SystemCallError.new msg, status
245
+ end
246
+
247
+ Cmds.debug "streaming completed."
248
+
249
+ return status
250
+ end # .spawn
251
+ end # Cmds
data/lib/cmds/sugar.rb CHANGED
@@ -3,270 +3,193 @@
3
3
  # global methods
4
4
  # ==============
5
5
 
6
- # proxies to `Cmds::capture`
7
- def Cmds *args, &block
8
- Cmds.capture *args, &block
6
+ # @see Cmds.capture
7
+ def Cmds template, *args, **kwds, &input_block
8
+ Cmds.capture template, *args, **kwds, &input_block
9
9
  end
10
10
 
11
- # proxies to `Cmds::ok?`
12
- def Cmds? *args, &block
13
- Cmds.ok? *args, &block
14
- end
15
11
 
16
- # proxies to `Cmds::assert`
17
- def Cmds! *args, &block
18
- Cmds.assert *args, &block
12
+ # @see Cmds.ok?
13
+ def Cmds? template, *args, **kwds, &io_block
14
+ Cmds.ok? template, *args, **kwds, &io_block
19
15
  end
20
16
 
21
- class Cmds
22
- # class methods
23
- # =============
24
-
25
- # create a new Cmd from template and subs and call it
26
- # @return [Result]
27
- def self.capture template, *subs, &input_block
28
- new(template, options(subs, input_block)).capture
29
- end
30
-
31
- def self.ok? template, *subs, &input_block
32
- new(template, options(subs, input_block)).ok?
33
- end
34
17
 
35
- def self.error? template, *subs, &input_block
36
- new(template, options(subs, input_block)).error?
37
- end
18
+ # @see Cmds.assert
19
+ def Cmds! template, *args, **kwds, &io_block
20
+ Cmds.assert template, *args, **kwds, &io_block
21
+ end
38
22
 
39
- def self.assert template, *subs, &input_block
40
- new(
41
- template,
42
- options(subs, input_block).merge!(assert: true)
43
- ).capture
44
- end
45
23
 
24
+ module Cmds
25
+ # create a new {Cmd} instance with the template and parameters and
26
+ # calls {Cmd#prepare}.
27
+ #
28
+ # @param [String] template
29
+ # ERB template parameters are rendered into to create the command string.
30
+ #
31
+ # @param [Array<Object>] *args
32
+ # positional parameters for rendering into the template.
33
+ #
34
+ # @param [Hash{Symbol => Object}] **kwds
35
+ # keyword parameters for rendering into the template.
36
+ #
37
+ # @return [String]
38
+ # rendered and formatted command string ready to be executed.
39
+ #
40
+ def self.prepare template, *args, **kwds
41
+ Cmd.new(template).prepare *args, **kwds
42
+ end
43
+
44
+
45
+ # create a new {Cmd} from template with parameters and call {Cmds#capture}
46
+ # on it.
47
+ #
48
+ # @param template (see .prepare)
49
+ # @param *args (see .prepare)
50
+ # @param **kwds (see .prepare)
51
+ #
52
+ # @param [#call] &input_block
53
+ # optional block that returns a string or IO-like readable object to be
54
+ # used as input for the execution.
55
+ #
56
+ # @return [Result]
57
+ # result with command string, exist status, stdout and stderr.
58
+ #
59
+ def self.capture template, *args, **kwds, &input_block
60
+ Cmd.new(template).capture *args, **kwds, &input_block
61
+ end
62
+
63
+
64
+ # create a new {Cmd} from template with parameters and call {Cmd#ok?}
65
+ # on it.
66
+ #
67
+ # @param template (see .prepare)
68
+ # @param *args (see .prepare)
69
+ # @param **kwds (see .prepare)
70
+ # @param &io_block (see Cmds.spawn)
71
+ #
72
+ # @return [Result]
73
+ # result with command string, exist status, stdout and stderr.
74
+ #
75
+ def self.ok? template, *args, **kwds, &io_block
76
+ Cmd.new(template).ok? *args, **kwds, &io_block
77
+ end
78
+
79
+
80
+ def self.error? template, *args, **kwds, &io_block
81
+ Cmd.new(template).error? *args, **kwds, &io_block
82
+ end
83
+
84
+
85
+ # create a new {Cmd} and
86
+ def self.assert template, *args, **kwds, &io_block
87
+ Cmd.new(template).capture(*args, **kwds, &io_block).assert
88
+ end
89
+
90
+
46
91
  def self.stream template, *subs, &input_block
47
- Cmds.new(template).stream *subs, &input_block
92
+ Cmds::Cmd.new(template).stream *subs, &input_block
48
93
  end
49
-
94
+
95
+
50
96
  def self.stream! template, *subs, &input_block
51
- Cmds.new(template, assert: true).stream *subs, &input_block
97
+ Cmds::Cmd.new(template, assert: true).stream *subs, &input_block
52
98
  end # ::stream!
53
99
 
54
-
55
- # @api sugar
100
+
101
+ # creates a new {Cmd}, captures and returns stdout
102
+ # (sugar for `Cmds.capture(template, *args, **kwds, &input_block).out`).
56
103
  #
57
- # captures and returns stdout
58
- # (sugar for `Cmds.capture(*template, *subs, &input_block).out`).
59
- #
60
- # @see .capture
61
- # @see Result#out
104
+ # @see Cmd.out
62
105
  #
63
- # @param template [String] see {.capture}.
64
- # @param subs [Array] see {.capture}.
65
- # @param input_block [Proc] see {.capture}.
106
+ # @param template (see .prepare)
107
+ # @param *args (see .prepare)
108
+ # @param **kwds (see .prepare)
109
+ # @param &input_block (see .capture)
66
110
  #
67
- # @return [String] the command's stdout.
111
+ # @return [String]
112
+ # the command's stdout.
68
113
  #
69
- def self.out template, *subs, &input_block
70
- capture(template, *subs, &input_block).out
114
+ def self.out template, *args, **kwds, &input_block
115
+ Cmd.new(template).out *args, **kwds, &input_block
71
116
  end
72
-
73
-
74
- # @api sugar
117
+
118
+
119
+ # creates a new {Cmd}, captures and returns stdout. raises an error if the
120
+ # command fails.
75
121
  #
76
- # captures and returns stdout, raising an error if the command fails.
122
+ # @see Cmd.out!
77
123
  #
78
- # @see .capture
79
- # @see Result#out
124
+ # @param template (see .prepare)
125
+ # @param *args (see .prepare)
126
+ # @param **kwds (see .prepare)
127
+ # @param &input_block (see .capture)
80
128
  #
81
- # @param template [String] see {.capture}.
82
- # @param subs [Array] see {.capture}.
83
- # @param input_block [Proc] see {.capture}.
129
+ # @return [String]
130
+ # the command's stdout.
84
131
  #
85
- # @return [String] the command's stdout.
132
+ # @raise [SystemCallError]
133
+ # if the command fails (non-zero exit status).
86
134
  #
87
- # @raise [SystemCallError] if the command fails (non-zero exit status).
88
- #
89
- def self.out! template, *subs, &input_block
90
- Cmds.new(
91
- template,
92
- options(subs, input_block).merge!(assert: true),
93
- ).capture.out
135
+ def self.out! template, *args, **kwds, &input_block
136
+ Cmd.new(template).out! *args, **kwds, &input_block
94
137
  end
95
-
96
-
97
- # @api sugar
98
- #
99
- # captures and chomps stdout
100
- # (sugar for `Cmds.out(*template, *subs, &input_block).chomp`).
138
+
139
+
140
+ # captures a new {Cmd}, captures and chomps stdout
141
+ # (sugar for `Cmds.out(template, *args, **kwds, &input_block).chomp`).
101
142
  #
102
143
  # @see .out
103
144
  #
104
- # @param template [String] see {.capture}.
105
- # @param subs [Array] see {.capture}.
106
- # @param input_block [Proc] see {.capture}.
145
+ # @param template (see .prepare)
146
+ # @param *args (see .prepare)
147
+ # @param **kwds (see .prepare)
148
+ # @param &input_block (see .capture)
107
149
  #
108
- # @return [String] the command's chomped stdout.
150
+ # @return [String]
151
+ # the command's chomped stdout.
109
152
  #
110
- def self.chomp template, *subs, &input_block
111
- out(template, *subs, &input_block).chomp
153
+ def self.chomp template, *args, **kwds, &input_block
154
+ out(template, *args, **kwds, &input_block).chomp
112
155
  end
113
-
114
-
115
- # @api sugar
116
- #
156
+
157
+
117
158
  # captures and chomps stdout, raising an error if the command fails.
118
- # (sugar for `Cmds.out!(*template, *subs, &input_block).chomp`).
159
+ # (sugar for `Cmds.out!(template, *args, **kwds, &input_block).chomp`).
119
160
  #
120
161
  # @see .out!
121
162
  #
122
- # @param template [String] see {.capture}.
123
- # @param subs [Array] see {.capture}.
124
- # @param input_block [Proc] see {.capture}.
163
+ # @param template (see .prepare)
164
+ # @param *args (see .prepare)
165
+ # @param **kwds (see .prepare)
166
+ # @param &input_block (see .capture)
125
167
  #
126
- # @return [String] the command's chomped stdout.
168
+ # @return [String]
169
+ # the command's chomped stdout.
127
170
  #
128
- # @raise [SystemCallError] if the command fails (non-zero exit status).
171
+ # @raise [SystemCallError]
172
+ # if the command fails (non-zero exit status).
129
173
  #
130
- def self.chomp! template, *subs, &input_block
131
- out!(template, *subs, &input_block).chomp
174
+ def self.chomp! template, *args, **kwds, &input_block
175
+ out!(template, *args, **kwds, &input_block).chomp
132
176
  end
133
-
134
-
135
- # @api sugar
136
- #
177
+
178
+
137
179
  # captures and returns stderr
138
- # (sugar for `Cmds.capture(template, *subs, &input_block).err`).
180
+ # (sugar for `Cmds.capture(template, *args, **kwds, &input_block).err`).
139
181
  #
140
182
  # @see .capture
141
- # @see Result#err
142
- #
143
- # @param template [String] see {.capture}.
144
- # @param subs [Array] see {.capture}.
145
- # @param input_block [Proc] see {.capture}.
146
- #
147
- # @return [String] the command's stderr.
148
- #
149
- def self.err template, *subs, &input_block
150
- capture(template, *subs, &input_block).err
151
- end
152
-
153
- # instance methods
154
- # ================
155
-
156
- alias_method :call, :capture
157
-
158
- def ok?
159
- stream == 0
160
- end
161
-
162
- def error?
163
- stream != 0
164
- end
165
-
166
- # def assert
167
- # capture.raise_error
168
- # end
169
-
170
- def proxy
171
- stream do |io|
172
- io.in = $stdin
173
- end
174
- end
175
-
176
-
177
- # @api sugar
178
- #
179
- # captures and returns stdout
180
- # (sugar for `#capture(*subs, &input_block).out`).
181
- #
182
- # @see #capture
183
- # @see Result#out
184
- #
185
- # @param subs [Array] see {.capture}.
186
- # @param input_block [Proc] see {.capture}.
187
- #
188
- # @return [String] the command's stdout.
189
- #
190
- def out *subs, &input_block
191
- capture(*subs, &input_block).out
192
- end
193
-
194
-
195
- # @api sugar
196
- #
197
- # captures and returns stdout
198
- # (sugar for `#capture(*subs, &input_block).out`).
199
- #
200
- # @see #capture
201
- # @see Result#out
202
- #
203
- # @param subs [Array] see {.capture}.
204
- # @param input_block [Proc] see {.capture}.
205
- #
206
- # @return [String] the command's stdout.
207
- #
208
- # @raise [SystemCallError] if the command fails (non-zero exit status).
209
- #
210
- def out! *subs, &input_block
211
- self.class.new(
212
- @template,
213
- merge_options(subs, input_block).merge!(assert: true),
214
- ).capture.out
215
- end
216
-
217
-
218
- # @api sugar
219
183
  #
220
- # captures and chomps stdout
221
- # (sugar for `#out(*subs, &input_block).chomp`).
184
+ # @param template (see .prepare)
185
+ # @param *args (see .prepare)
186
+ # @param **kwds (see .prepare)
187
+ # @param &input_block (see .capture)
222
188
  #
223
- # @see #out
189
+ # @return [String]
190
+ # the command's stderr.
224
191
  #
225
- # @param subs [Array] see {.capture}.
226
- # @param input_block [Proc] see {.capture}.
227
- #
228
- # @return [String] the command's chomped stdout.
229
- #
230
- def chomp *subs, &input_block
231
- out(*subs, &input_block).chomp
232
- end
233
-
234
-
235
- # @api sugar
236
- #
237
- # captures and chomps stdout, raising an error if the command failed.
238
- # (sugar for `#out!(*subs, &input_block).chomp`).
239
- #
240
- # @see #capture
241
- # @see Result#out
242
- #
243
- # @param subs [Array] see {.capture}.
244
- # @param input_block [Proc] see {.capture}.
245
- #
246
- # @return [String] the command's chomped stdout.
247
- #
248
- # @raise [SystemCallError] if the command fails (non-zero exit status).
249
- #
250
- def chomp! *subs, &input_block
251
- out!(*subs, &input_block).chomp
252
- end
253
-
254
-
255
- # @api sugar
256
- #
257
- # captures and returns stdout
258
- # (sugar for `#capture(*subs, &input_block).err`).
259
- #
260
- # @param subs [Array] see {.capture}.
261
- # @param input_block [Proc] see {.capture}.
262
- #
263
- # @see #capture
264
- # @see Result#err
265
- #
266
- # @return [String] the command's stderr.
267
- #
268
- def err *subs, &input_block
269
- capture(*subs, &input_block).err
192
+ def self.err template, *args, **kwds, &input_block
193
+ capture(template, *args, **kwds, &input_block).err
270
194
  end
271
-
272
- end # class Cmds
195
+ end # Cmds