toys 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -34,31 +34,116 @@ module Toys
34
34
  # The object context in effect during the execution of a tool.
35
35
  #
36
36
  class Context
37
- def initialize(context_base, tool_name, args, options)
38
- @context_base = context_base
39
- @tool_name = tool_name
40
- @args = args
41
- @options = options
37
+ ##
38
+ # Context key for the verbosity value. Verbosity is an integer defaulting
39
+ # to 0, with higher values meaning more verbose and lower meaning quieter.
40
+ # @return [Symbol]
41
+ #
42
+ VERBOSITY = :__verbosity
43
+
44
+ ##
45
+ # Context key for the `Toys::Tool` object being executed.
46
+ # @return [Symbol]
47
+ #
48
+ TOOL = :__tool
49
+
50
+ ##
51
+ # Context key for the full name of the tool being executed. Value is an
52
+ # array of strings.
53
+ # @return [Symbol]
54
+ #
55
+ TOOL_NAME = :__tool_name
56
+
57
+ ##
58
+ # Context key for the active `Toys::Loader` object.
59
+ # @return [Symbol]
60
+ #
61
+ LOADER = :__loader
62
+
63
+ ##
64
+ # Context key for the active `Logger` object.
65
+ # @return [Symbol]
66
+ #
67
+ LOGGER = :__logger
68
+
69
+ ##
70
+ # Context key for the name of the toys binary. Value is a string.
71
+ # @return [Symbol]
72
+ #
73
+ BINARY_NAME = :__binary_name
74
+
75
+ ##
76
+ # Context key for the argument list passed to the current tool. Value is
77
+ # an array of strings.
78
+ # @return [Symbol]
79
+ #
80
+ ARGS = :__args
81
+
82
+ ##
83
+ # Context key for the usage error raised. Value is a string if there was
84
+ # an error, or nil if there was no error.
85
+ # @return [Symbol]
86
+ #
87
+ USAGE_ERROR = :__usage_error
88
+
89
+ def initialize(context_base, data)
90
+ @_context_base = context_base
91
+ @_data = data
92
+ @_data[LOADER] = context_base.loader
93
+ @_data[BINARY_NAME] = context_base.binary_name
94
+ @_data[LOGGER] = context_base.logger
42
95
  end
43
96
 
44
- attr_reader :tool_name
45
- attr_reader :args
46
- attr_reader :options
97
+ def verbosity
98
+ @_data[VERBOSITY]
99
+ end
47
100
 
48
- def [](key)
49
- @options[key]
101
+ def tool
102
+ @_data[TOOL]
103
+ end
104
+
105
+ def tool_name
106
+ @_data[TOOL_NAME]
107
+ end
108
+
109
+ def args
110
+ @_data[ARGS]
111
+ end
112
+
113
+ def usage_error
114
+ @_data[USAGE_ERROR]
50
115
  end
51
116
 
52
117
  def logger
53
- @context_base.logger
118
+ @_data[LOGGER]
119
+ end
120
+
121
+ def loader
122
+ @_data[LOADER]
54
123
  end
55
124
 
56
125
  def binary_name
57
- @context_base.binary_name
126
+ @_data[BINARY_NAME]
127
+ end
128
+
129
+ def [](key)
130
+ @_data[key]
131
+ end
132
+
133
+ def []=(key, value)
134
+ @_data[key] = value
135
+ end
136
+
137
+ def options
138
+ @_data.select do |k, _v|
139
+ !k.is_a?(::Symbol) || !k.to_s.start_with?("__")
140
+ end
58
141
  end
59
142
 
60
- def run(*args)
61
- @context_base.run(*args)
143
+ def run(*args, exit_on_nonzero_status: false)
144
+ code = @_context_base.run(args.flatten, verbosity: @_data[VERBOSITY])
145
+ exit(code) if exit_on_nonzero_status && !code.zero?
146
+ code
62
147
  end
63
148
 
64
149
  def exit(code)
@@ -70,21 +155,24 @@ module Toys
70
155
  # @private
71
156
  #
72
157
  class Base
73
- def initialize(lookup, binary_name, logger)
74
- @lookup = lookup
75
- @binary_name = binary_name
158
+ def initialize(loader, binary_name, logger)
159
+ @loader = loader
160
+ @binary_name = binary_name || ::File.basename($PROGRAM_NAME)
76
161
  @logger = logger || ::Logger.new(::STDERR)
162
+ @base_level = @logger.level
77
163
  end
78
164
 
165
+ attr_reader :loader
79
166
  attr_reader :binary_name
80
167
  attr_reader :logger
168
+ attr_reader :base_level
81
169
 
82
- def run(*args)
83
- @lookup.execute(self, args.flatten)
170
+ def run(args, verbosity: 0)
171
+ @loader.execute(self, args, verbosity: verbosity)
84
172
  end
85
173
 
86
- def create_context(tool_name, args, options)
87
- Context.new(self, tool_name, args, options)
174
+ def create_context(data)
175
+ Context.new(self, data)
88
176
  end
89
177
  end
90
178
  end
@@ -0,0 +1,41 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ require "toys/utils/module_lookup"
31
+
32
+ module Toys
33
+ ##
34
+ # Namespace for common helper modules
35
+ #
36
+ module Helpers
37
+ def self.lookup(name)
38
+ Utils::ModuleLookup.lookup(:helpers, name)
39
+ end
40
+ end
41
+ end
@@ -29,250 +29,260 @@
29
29
 
30
30
  require "logger"
31
31
 
32
- module Toys::Helpers
33
- ##
34
- # A set of helper methods for invoking subcommands
35
- #
36
- module Exec
37
- def configure_exec(opts = {})
38
- @exec_config ||= {}
39
- @exec_config.merge!(opts)
40
- end
41
-
42
- def exec(cmd, opts = {}, &block)
43
- exec_opts = ExecOpts.new(self)
44
- exec_opts.add(@exec_config) if defined? @exec_config
45
- exec_opts.add(opts)
46
- executor = Executor.new(exec_opts, cmd)
47
- executor.execute(&block)
48
- end
49
-
50
- def ruby(args, opts = {}, &block)
51
- cmd =
52
- if args.is_a?(Array)
53
- [[Exec.ruby_binary, "ruby"]] + args
54
- else
55
- "#{Exec.ruby_binary} #{args}"
56
- end
57
- exec(cmd, opts, &block)
58
- end
59
-
60
- def sh(cmd, opts = {})
61
- exec(cmd, opts).exit_code
62
- end
63
-
64
- def capture(cmd, opts = {})
65
- exec(cmd, opts.merge(out_to: :capture)).captured_out
66
- end
67
-
68
- def self.ruby_binary
69
- ::File.join(::RbConfig::CONFIG["bindir"], ::RbConfig::CONFIG["ruby_install_name"])
70
- end
71
-
32
+ module Toys
33
+ module Helpers
72
34
  ##
73
- # The object passed to a subcommand control block
35
+ # A set of helper methods for invoking subcommands
74
36
  #
75
- class Controller
76
- def initialize(ins, out, err, out_err, pid)
77
- @in = ins
78
- @out = out
79
- @err = err
80
- @out_err = out_err
81
- @pid = pid
37
+ module Exec
38
+ def configure_exec(opts = {})
39
+ @exec_config ||= {}
40
+ @exec_config.merge!(opts)
82
41
  end
83
42
 
84
- attr_reader :in
85
- attr_reader :out
86
- attr_reader :err
87
- attr_reader :out_err
88
- attr_reader :pid
43
+ def exec(cmd, opts = {}, &block)
44
+ exec_opts = ExecOpts.new(self)
45
+ exec_opts.add(@exec_config) if defined? @exec_config
46
+ exec_opts.add(opts)
47
+ executor = Executor.new(exec_opts, cmd)
48
+ executor.execute(&block)
49
+ end
89
50
 
90
- def kill(signal)
91
- ::Process.kill(signal, pid)
51
+ def ruby(args, opts = {}, &block)
52
+ cmd =
53
+ if args.is_a?(Array)
54
+ [[Exec.ruby_binary, "ruby"]] + args
55
+ else
56
+ "#{Exec.ruby_binary} #{args}"
57
+ end
58
+ exec(cmd, opts, &block)
92
59
  end
93
- end
94
60
 
95
- ##
96
- # The return result from a subcommand
97
- #
98
- class Result
99
- def initialize(out, err, out_err, status)
100
- @captured_out = out
101
- @captured_err = err
102
- @captured_out_err = out_err
103
- @status = status
61
+ def sh(cmd, opts = {})
62
+ exec(cmd, opts).exit_code
104
63
  end
105
64
 
106
- attr_reader :captured_out
107
- attr_reader :captured_err
108
- attr_reader :captured_out_err
109
- attr_reader :status
65
+ def capture(cmd, opts = {})
66
+ exec(cmd, opts.merge(out_to: :capture)).captured_out
67
+ end
110
68
 
111
- def exit_code
112
- status.exitstatus
69
+ def self.ruby_binary
70
+ ::File.join(::RbConfig::CONFIG["bindir"], ::RbConfig::CONFIG["ruby_install_name"])
113
71
  end
114
- end
115
72
 
116
- ##
117
- # An internal helper class storing the configuration of a subcommand invocation
118
- # @private
119
- #
120
- class ExecOpts
121
73
  ##
122
- # Option keys that belong to exec configuration rather than spawn
123
- # @private
74
+ # The object passed to a subcommand control block
124
75
  #
125
- CONFIG_KEYS = %i[
126
- exit_on_nonzero_status
127
- env
128
- log_level
129
- in_from
130
- out_to
131
- err_to
132
- out_err_to
133
- ].freeze
76
+ class Controller
77
+ def initialize(ins, out, err, out_err, pid)
78
+ @in = ins
79
+ @out = out
80
+ @err = err
81
+ @out_err = out_err
82
+ @pid = pid
83
+ end
134
84
 
135
- def initialize(context)
136
- @context = context
137
- @config = {}
138
- @spawn_opts = {}
139
- end
85
+ attr_reader :in
86
+ attr_reader :out
87
+ attr_reader :err
88
+ attr_reader :out_err
89
+ attr_reader :pid
140
90
 
141
- def add(config)
142
- config.each do |k, v|
143
- if CONFIG_KEYS.include?(k)
144
- @config[k] = v
145
- else
146
- @spawn_opts[k] = v
147
- end
91
+ def kill(signal)
92
+ ::Process.kill(signal, pid)
148
93
  end
149
94
  end
150
95
 
151
- attr_reader :config
152
- attr_reader :spawn_opts
153
- attr_reader :context
154
- end
96
+ ##
97
+ # The return result from a subcommand
98
+ #
99
+ class Result
100
+ def initialize(out, err, out_err, status)
101
+ @captured_out = out
102
+ @captured_err = err
103
+ @captured_out_err = out_err
104
+ @status = status
105
+ end
155
106
 
156
- ##
157
- # An object that manages the execution of a subcommand
158
- # @private
159
- #
160
- class Executor
161
- def initialize(exec_opts, cmd)
162
- @cmd = Array(cmd)
163
- @config = exec_opts.config
164
- @context = exec_opts.context
165
- @spawn_opts = exec_opts.spawn_opts.dup
166
- @captures = {}
167
- @controller_streams = {}
168
- @join_threads = []
169
- @child_streams = []
170
- end
107
+ attr_reader :captured_out
108
+ attr_reader :captured_err
109
+ attr_reader :captured_out_err
110
+ attr_reader :status
171
111
 
172
- def execute(&block)
173
- setup_in_stream
174
- setup_out_stream(:out, :out_to, :out)
175
- setup_out_stream(:err, :err_to, :err)
176
- setup_out_stream(:out_err, :out_err_to, [:out, :err])
177
- log_command
178
- wait_thread = start_process
179
- status = control_process(wait_thread, &block)
180
- create_result(status)
181
- end
112
+ def exit_code
113
+ status.exitstatus
114
+ end
182
115
 
183
- private
116
+ def success?
117
+ exit_code.zero?
118
+ end
184
119
 
185
- def log_command
186
- unless @config[:log_level] == false
187
- cmd_str = @cmd.size == 1 ? @cmd.first : @cmd.inspect
188
- @context.logger.add(@config[:log_level] || ::Logger::INFO, cmd_str)
120
+ def error?
121
+ !exit_code.zero?
189
122
  end
190
123
  end
191
124
 
192
- def start_process
193
- args = []
194
- args << @config[:env] if @config[:env]
195
- args.concat(@cmd)
196
- pid = ::Process.spawn(*args, @spawn_opts)
197
- @child_streams.each(&:close)
198
- ::Process.detach(pid)
199
- end
125
+ ##
126
+ # An internal helper class storing the configuration of a subcommand invocation
127
+ # @private
128
+ #
129
+ class ExecOpts
130
+ ##
131
+ # Option keys that belong to exec configuration rather than spawn
132
+ # @private
133
+ #
134
+ CONFIG_KEYS = %i[
135
+ exit_on_nonzero_status
136
+ env
137
+ log_level
138
+ in_from
139
+ out_to
140
+ err_to
141
+ out_err_to
142
+ ].freeze
143
+
144
+ def initialize(context)
145
+ @context = context
146
+ @config = {}
147
+ @spawn_opts = {}
148
+ end
200
149
 
201
- def control_process(wait_thread)
202
- begin
203
- if block_given?
204
- controller = Controller.new(
205
- @controller_streams[:in], @controller_streams[:out], @controller_streams[:err],
206
- @controller_streams[:out_err], wait_thread.pid
207
- )
208
- yield controller
150
+ def add(config)
151
+ config.each do |k, v|
152
+ if CONFIG_KEYS.include?(k)
153
+ @config[k] = v
154
+ else
155
+ @spawn_opts[k] = v
156
+ end
209
157
  end
210
- ensure
211
- @controller_streams.each_value(&:close)
212
158
  end
213
- @join_threads.each(&:join)
214
- wait_thread.value
159
+
160
+ attr_reader :config
161
+ attr_reader :spawn_opts
162
+ attr_reader :context
215
163
  end
216
164
 
217
- def create_result(status)
218
- if @config[:exit_on_nonzero_status]
219
- exit_status = status.exitstatus
220
- @context.exit(exit_status) if exit_status != 0
165
+ ##
166
+ # An object that manages the execution of a subcommand
167
+ # @private
168
+ #
169
+ class Executor
170
+ def initialize(exec_opts, cmd)
171
+ @cmd = Array(cmd)
172
+ @config = exec_opts.config
173
+ @context = exec_opts.context
174
+ @spawn_opts = exec_opts.spawn_opts.dup
175
+ @captures = {}
176
+ @controller_streams = {}
177
+ @join_threads = []
178
+ @child_streams = []
221
179
  end
222
- Result.new(@captures[:out], @captures[:err], @captures[:out_err], status)
223
- end
224
180
 
225
- def setup_in_stream
226
- setting = @config[:in_from]
227
- if setting
228
- r, w = ::IO.pipe
229
- @spawn_opts[:in] = r
230
- w.sync = true
231
- @child_streams << r
232
- case setting
233
- when :controller
234
- @controller_streams[:in] = w
235
- when String
236
- write_string_thread(w, setting)
237
- else
238
- raise "Unknown type for in_from"
239
- end
181
+ def execute(&block)
182
+ setup_in_stream
183
+ setup_out_stream(:out, :out_to, :out)
184
+ setup_out_stream(:err, :err_to, :err)
185
+ setup_out_stream(:out_err, :out_err_to, [:out, :err])
186
+ log_command
187
+ wait_thread = start_process
188
+ status = control_process(wait_thread, &block)
189
+ create_result(status)
240
190
  end
241
- end
242
191
 
243
- def setup_out_stream(stream_name, config_key, spawn_key)
244
- setting = @config[config_key]
245
- if setting
246
- r, w = ::IO.pipe
247
- @spawn_opts[spawn_key] = w
248
- @child_streams << w
249
- case setting
250
- when :controller
251
- @controller_streams[stream_name] = r
252
- when :capture
253
- @join_threads << capture_stream_thread(r, stream_name)
254
- else
255
- raise "Unknown type for #{config_key}"
192
+ private
193
+
194
+ def log_command
195
+ unless @config[:log_level] == false
196
+ cmd_str = @cmd.size == 1 ? @cmd.first : @cmd.inspect
197
+ @context.logger.add(@config[:log_level] || ::Logger::INFO, cmd_str)
256
198
  end
257
199
  end
258
- end
259
200
 
260
- def write_string_thread(stream, string)
261
- ::Thread.new do
201
+ def start_process
202
+ args = []
203
+ args << @config[:env] if @config[:env]
204
+ args.concat(@cmd)
205
+ pid = ::Process.spawn(*args, @spawn_opts)
206
+ @child_streams.each(&:close)
207
+ ::Process.detach(pid)
208
+ end
209
+
210
+ def control_process(wait_thread)
262
211
  begin
263
- stream.write string
212
+ if block_given?
213
+ controller = Controller.new(
214
+ @controller_streams[:in], @controller_streams[:out], @controller_streams[:err],
215
+ @controller_streams[:out_err], wait_thread.pid
216
+ )
217
+ yield controller
218
+ end
264
219
  ensure
265
- stream.close
220
+ @controller_streams.each_value(&:close)
266
221
  end
222
+ @join_threads.each(&:join)
223
+ wait_thread.value
267
224
  end
268
- end
269
225
 
270
- def capture_stream_thread(stream, name)
271
- ::Thread.new do
272
- begin
273
- @captures[name] = stream.read
274
- ensure
275
- stream.close
226
+ def create_result(status)
227
+ if @config[:exit_on_nonzero_status]
228
+ exit_status = status.exitstatus
229
+ @context.exit(exit_status) if exit_status != 0
230
+ end
231
+ Result.new(@captures[:out], @captures[:err], @captures[:out_err], status)
232
+ end
233
+
234
+ def setup_in_stream
235
+ setting = @config[:in_from]
236
+ if setting
237
+ r, w = ::IO.pipe
238
+ @spawn_opts[:in] = r
239
+ w.sync = true
240
+ @child_streams << r
241
+ case setting
242
+ when :controller
243
+ @controller_streams[:in] = w
244
+ when String
245
+ write_string_thread(w, setting)
246
+ else
247
+ raise "Unknown type for in_from"
248
+ end
249
+ end
250
+ end
251
+
252
+ def setup_out_stream(stream_name, config_key, spawn_key)
253
+ setting = @config[config_key]
254
+ if setting
255
+ r, w = ::IO.pipe
256
+ @spawn_opts[spawn_key] = w
257
+ @child_streams << w
258
+ case setting
259
+ when :controller
260
+ @controller_streams[stream_name] = r
261
+ when :capture
262
+ @join_threads << capture_stream_thread(r, stream_name)
263
+ else
264
+ raise "Unknown type for #{config_key}"
265
+ end
266
+ end
267
+ end
268
+
269
+ def write_string_thread(stream, string)
270
+ ::Thread.new do
271
+ begin
272
+ stream.write string
273
+ ensure
274
+ stream.close
275
+ end
276
+ end
277
+ end
278
+
279
+ def capture_stream_thread(stream, name)
280
+ ::Thread.new do
281
+ begin
282
+ @captures[name] = stream.read
283
+ ensure
284
+ stream.close
285
+ end
276
286
  end
277
287
  end
278
288
  end