cmds 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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