toys 0.2.2 → 0.3.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.
@@ -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