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.
@@ -27,38 +27,40 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- module Toys::Templates
31
- ##
32
- # A template for tools that clean build artifacts
33
- #
34
- class Clean
35
- include ::Toys::Template
30
+ module Toys
31
+ module Templates
32
+ ##
33
+ # A template for tools that clean build artifacts
34
+ #
35
+ class Clean
36
+ include Template
36
37
 
37
- def initialize(opts = {})
38
- @name = opts[:name] || "clean"
39
- @paths = opts[:paths] || []
40
- end
38
+ def initialize(opts = {})
39
+ @name = opts[:name] || "clean"
40
+ @paths = opts[:paths] || []
41
+ end
41
42
 
42
- attr_accessor :name
43
- attr_accessor :paths
43
+ attr_accessor :name
44
+ attr_accessor :paths
44
45
 
45
- to_expand do |template|
46
- name(template.name) do
47
- short_desc "Clean built files and directories"
46
+ to_expand do |template|
47
+ name(template.name) do
48
+ desc "Clean built files and directories."
48
49
 
49
- use :file_utils
50
+ use :file_utils
50
51
 
51
- execute do
52
- files = []
53
- patterns = Array(template.paths)
54
- patterns = ["lib/**/*.rb"] if patterns.empty?
55
- patterns.each do |pattern|
56
- files.concat(::Dir.glob(pattern))
57
- end
58
- files.uniq!
52
+ execute do
53
+ files = []
54
+ patterns = Array(template.paths)
55
+ patterns = ["lib/**/*.rb"] if patterns.empty?
56
+ patterns.each do |pattern|
57
+ files.concat(::Dir.glob(pattern))
58
+ end
59
+ files.uniq!
59
60
 
60
- files.each do |file|
61
- rm_rf file
61
+ files.each do |file|
62
+ rm_rf file
63
+ end
62
64
  end
63
65
  end
64
66
  end
@@ -29,62 +29,64 @@
29
29
 
30
30
  require "rubygems/package"
31
31
 
32
- module Toys::Templates
33
- ##
34
- # A template for tools that build and release gems
35
- #
36
- class GemBuild
37
- include ::Toys::Template
32
+ module Toys
33
+ module Templates
34
+ ##
35
+ # A template for tools that build and release gems
36
+ #
37
+ class GemBuild
38
+ include Template
38
39
 
39
- def initialize(opts = {})
40
- @name = opts[:name] || "build"
41
- @gem_name = opts[:gem_name]
42
- @push_gem = opts[:push_gem]
43
- @tag = opts[:tag]
44
- @push_tag = opts[:push_tag]
45
- end
40
+ def initialize(opts = {})
41
+ @name = opts[:name] || "build"
42
+ @gem_name = opts[:gem_name]
43
+ @push_gem = opts[:push_gem]
44
+ @tag = opts[:tag]
45
+ @push_tag = opts[:push_tag]
46
+ end
46
47
 
47
- attr_accessor :name
48
- attr_accessor :gem_name
49
- attr_accessor :push_gem
50
- attr_accessor :tag
51
- attr_accessor :push_tag
48
+ attr_accessor :name
49
+ attr_accessor :gem_name
50
+ attr_accessor :push_gem
51
+ attr_accessor :tag
52
+ attr_accessor :push_tag
52
53
 
53
- to_expand do |template|
54
- unless template.gem_name
55
- candidates = ::Dir.glob("*.gemspec")
56
- if candidates.empty?
57
- raise ::Toys::ToolDefinitionError, "Could not find a gemspec"
54
+ to_expand do |template|
55
+ unless template.gem_name
56
+ candidates = ::Dir.glob("*.gemspec")
57
+ if candidates.empty?
58
+ raise ToolDefinitionError, "Could not find a gemspec"
59
+ end
60
+ template.gem_name = candidates.first.sub(/\.gemspec$/, "")
58
61
  end
59
- template.gem_name = candidates.first.sub(/\.gemspec$/, "")
60
- end
61
- task_type = template.push_gem ? "Release" : "Build"
62
+ task_type = template.push_gem ? "Release" : "Build"
62
63
 
63
- name(template.name) do
64
- short_desc "#{task_type} the gem: #{template.gem_name}"
64
+ name(template.name) do
65
+ desc "#{task_type} the gem: #{template.gem_name}"
65
66
 
66
- use :file_utils
67
- use :exec
67
+ use :file_utils
68
+ use :exec
68
69
 
69
- execute do
70
- configure_exec(exit_on_nonzero_status: true)
71
- gemspec = ::Gem::Specification.load "#{template.gem_name}.gemspec"
72
- version = gemspec.version
73
- gemfile = "#{template.gem_name}-#{version}.gem"
74
- ::Gem::Package.build gemspec
75
- mkdir_p "pkg"
76
- mv gemfile, "pkg"
77
- if template.push_gem
78
- if ::File.directory?(".git") && capture("git status -s").strip != ""
79
- logger.error "Cannot push the gem when there are uncommited changes"
80
- exit(1)
81
- end
82
- sh "gem push pkg/#{gemfile}"
83
- if template.tag
84
- sh "git tag v#{version}"
85
- if template.push_tag
86
- template.push_tag = "origin" if template.push_tag == true
87
- sh "git push #{template.push_tag} v#{version}"
70
+ execute do
71
+ configure_exec(exit_on_nonzero_status: true)
72
+ gemspec = ::Gem::Specification.load "#{template.gem_name}.gemspec"
73
+ version = gemspec.version
74
+ gemfile = "#{template.gem_name}-#{version}.gem"
75
+ ::Gem::Package.build gemspec
76
+ mkdir_p "pkg"
77
+ mv gemfile, "pkg"
78
+ if template.push_gem
79
+ if ::File.directory?(".git") && capture("git status -s").strip != ""
80
+ logger.error "Cannot push the gem when there are uncommited changes"
81
+ exit(1)
82
+ end
83
+ sh "gem push pkg/#{gemfile}"
84
+ if template.tag
85
+ sh "git tag v#{version}"
86
+ if template.push_tag
87
+ template.push_tag = "origin" if template.push_tag == true
88
+ sh "git push #{template.push_tag} v#{version}"
89
+ end
88
90
  end
89
91
  end
90
92
  end
@@ -27,57 +27,63 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- module Toys::Templates
31
- ##
32
- # A template for tools that run minitest
33
- #
34
- class Minitest
35
- include ::Toys::Template
30
+ module Toys
31
+ module Templates
32
+ ##
33
+ # A template for tools that run minitest
34
+ #
35
+ class Minitest
36
+ include Template
36
37
 
37
- def initialize(opts = {})
38
- @name = opts[:name] || "test"
39
- @libs = opts[:libs] || ["lib"]
40
- @files = opts[:files] || ["test/test*.rb"]
41
- @warnings = opts.include?(:warnings) ? opts[:warnings] : true
42
- end
38
+ def initialize(opts = {})
39
+ @name = opts[:name] || "test"
40
+ @libs = opts[:libs] || ["lib"]
41
+ @files = opts[:files] || ["test/test*.rb"]
42
+ @warnings = opts.include?(:warnings) ? opts[:warnings] : true
43
+ end
43
44
 
44
- attr_accessor :name
45
- attr_accessor :libs
46
- attr_accessor :files
47
- attr_accessor :warnings
45
+ attr_accessor :name
46
+ attr_accessor :libs
47
+ attr_accessor :files
48
+ attr_accessor :warnings
48
49
 
49
- to_expand do |template|
50
- name(template.name) do
51
- short_desc "Run minitest"
50
+ to_expand do |template|
51
+ name(template.name) do
52
+ desc "Run minitest on the current project."
52
53
 
53
- use :exec
54
+ use :exec
54
55
 
55
- switch(
56
- :warnings, "-w", "--[no-]warnings",
57
- default: template.warnings,
58
- doc: "Turn on Ruby warnings (defaults to #{template.warnings})"
59
- )
60
- remaining_args(:tests, doc: "Paths to the tests to run (defaults to all tests)")
56
+ switch(
57
+ :warnings, "-w", "--[no-]warnings",
58
+ default: template.warnings,
59
+ doc: "Turn on Ruby warnings (defaults to #{template.warnings})"
60
+ )
61
+ remaining_args(:tests, doc: "Paths to the tests to run (defaults to all tests)")
61
62
 
62
- execute do
63
- ruby_args = []
64
- unless template.libs.empty?
65
- lib_path = template.libs.join(::File::PATH_SEPARATOR)
66
- ruby_args << "-I#{lib_path}"
67
- end
68
- ruby_args << "-w" if self[:warnings]
63
+ execute do
64
+ ruby_args = []
65
+ unless template.libs.empty?
66
+ lib_path = template.libs.join(::File::PATH_SEPARATOR)
67
+ ruby_args << "-I#{lib_path}"
68
+ end
69
+ ruby_args << "-w" if self[:warnings]
69
70
 
70
- tests = self[:tests]
71
- if tests.empty?
72
- Array(template.files).each do |pattern|
73
- tests.concat(::Dir.glob(pattern))
71
+ tests = self[:tests]
72
+ if tests.empty?
73
+ Array(template.files).each do |pattern|
74
+ tests.concat(::Dir.glob(pattern))
75
+ end
76
+ tests.uniq!
74
77
  end
75
- tests.uniq!
76
- end
77
78
 
78
- ruby(ruby_args, in_from: :controller, exit_on_nonzero_status: true) do |controller|
79
- tests.each do |file|
80
- controller.in.puts("load '#{file}'")
79
+ result = ruby(ruby_args, in_from: :controller) do |controller|
80
+ tests.each do |file|
81
+ controller.in.puts("load '#{file}'")
82
+ end
83
+ end
84
+ if result.error?
85
+ logger.error("Minitest failed!")
86
+ exit(result.exit_code)
81
87
  end
82
88
  end
83
89
  end
@@ -27,37 +27,39 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- module Toys::Templates
31
- ##
32
- # A template for tools that run rubocop
33
- #
34
- class Rubocop
35
- include ::Toys::Template
30
+ module Toys
31
+ module Templates
32
+ ##
33
+ # A template for tools that run rubocop
34
+ #
35
+ class Rubocop
36
+ include Template
36
37
 
37
- def initialize(opts = {})
38
- @name = opts[:name] || "rubocop"
39
- @fail_on_error = opts.include?(:fail_on_error) ? opts[:fail_on_error] : true
40
- @options = opts[:options] || []
41
- end
38
+ def initialize(opts = {})
39
+ @name = opts[:name] || "rubocop"
40
+ @fail_on_error = opts.include?(:fail_on_error) ? opts[:fail_on_error] : true
41
+ @options = opts[:options] || []
42
+ end
42
43
 
43
- attr_accessor :name
44
- attr_accessor :fail_on_error
45
- attr_accessor :options
44
+ attr_accessor :name
45
+ attr_accessor :fail_on_error
46
+ attr_accessor :options
46
47
 
47
- to_expand do |template|
48
- name(template.name) do
49
- short_desc "Run RuboCop"
48
+ to_expand do |template|
49
+ name(template.name) do
50
+ desc "Run rubocop on the current project."
50
51
 
51
- use :exec
52
+ use :exec
52
53
 
53
- execute do
54
- require "rubocop"
55
- cli = ::RuboCop::CLI.new
56
- logger.info "Running RuboCop..."
57
- result = cli.run(template.options)
58
- if result.nonzero?
59
- logger.error "RuboCop failed!"
60
- exit(1) if template.fail_on_error
54
+ execute do
55
+ require "rubocop"
56
+ cli = ::RuboCop::CLI.new
57
+ logger.info "Running RuboCop..."
58
+ result = cli.run(template.options)
59
+ if result.nonzero?
60
+ logger.error "RuboCop failed!"
61
+ exit(1) if template.fail_on_error
62
+ end
61
63
  end
62
64
  end
63
65
  end
@@ -27,50 +27,52 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- module Toys::Templates
31
- ##
32
- # A template for tools that run yardoc
33
- #
34
- class Yardoc
35
- include ::Toys::Template
30
+ module Toys
31
+ module Templates
32
+ ##
33
+ # A template for tools that run yardoc
34
+ #
35
+ class Yardoc
36
+ include Template
36
37
 
37
- def initialize(opts = {})
38
- @name = opts[:name] || "yardoc"
39
- @files = opts[:files] || []
40
- @options = opts[:options] || []
41
- @stats_options = opts[:stats_options] || []
42
- end
38
+ def initialize(opts = {})
39
+ @name = opts[:name] || "yardoc"
40
+ @files = opts[:files] || []
41
+ @options = opts[:options] || []
42
+ @stats_options = opts[:stats_options] || []
43
+ end
43
44
 
44
- attr_accessor :name
45
- attr_accessor :files
46
- attr_accessor :options
47
- attr_accessor :stats_options
45
+ attr_accessor :name
46
+ attr_accessor :files
47
+ attr_accessor :options
48
+ attr_accessor :stats_options
48
49
 
49
- to_expand do |template|
50
- name(template.name) do
51
- short_desc "Run yardoc"
50
+ to_expand do |template|
51
+ name(template.name) do
52
+ desc "Run yardoc on the current project."
52
53
 
53
- use :exec
54
+ use :exec
54
55
 
55
- execute do
56
- require "yard"
57
- files = []
58
- patterns = Array(template.files)
59
- patterns = ["lib/**/*.rb"] if patterns.empty?
60
- patterns.each do |pattern|
61
- files.concat(::Dir.glob(pattern))
62
- end
63
- files.uniq!
56
+ execute do
57
+ require "yard"
58
+ files = []
59
+ patterns = Array(template.files)
60
+ patterns = ["lib/**/*.rb"] if patterns.empty?
61
+ patterns.each do |pattern|
62
+ files.concat(::Dir.glob(pattern))
63
+ end
64
+ files.uniq!
64
65
 
65
- unless template.stats_options.empty?
66
- template.options << "--no-stats"
67
- template.stats_options << "--use-cache"
68
- end
66
+ unless template.stats_options.empty?
67
+ template.options << "--no-stats"
68
+ template.stats_options << "--use-cache"
69
+ end
69
70
 
70
- yardoc = ::YARD::CLI::Yardoc.new
71
- yardoc.run(*(template.options + files))
72
- unless template.stats_options.empty?
73
- ::YARD::CLI::Stats.run(*template.stats_options)
71
+ yardoc = ::YARD::CLI::Yardoc.new
72
+ yardoc.run(*(template.options + files))
73
+ unless template.stats_options.empty?
74
+ ::YARD::CLI::Stats.run(*template.stats_options)
75
+ end
74
76
  end
75
77
  end
76
78
  end
@@ -34,17 +34,17 @@ module Toys
34
34
  # A tool definition
35
35
  #
36
36
  class Tool
37
- def initialize(lookup, full_name)
38
- @lookup = lookup
37
+ def initialize(full_name, middleware_stack)
39
38
  @full_name = full_name
39
+ @middleware_stack = middleware_stack.dup
40
40
 
41
41
  @definition_path = nil
42
42
  @cur_path = nil
43
-
44
43
  @alias_target = nil
44
+ @definition_finished = false
45
45
 
46
+ @desc = nil
46
47
  @long_desc = nil
47
- @short_desc = nil
48
48
 
49
49
  @default_data = {}
50
50
  @switches = []
@@ -57,7 +57,6 @@ module Toys
57
57
  @executor = nil
58
58
  end
59
59
 
60
- attr_reader :lookup
61
60
  attr_reader :full_name
62
61
  attr_reader :switches
63
62
  attr_reader :required_args
@@ -68,6 +67,8 @@ module Toys
68
67
  attr_reader :helpers
69
68
  attr_reader :executor
70
69
  attr_reader :alias_target
70
+ attr_reader :middleware_stack
71
+ attr_reader :definition_path
71
72
 
72
73
  def simple_name
73
74
  full_name.last
@@ -81,40 +82,41 @@ module Toys
81
82
  full_name.empty?
82
83
  end
83
84
 
84
- def leaf?
85
- @executor.is_a?(::Proc)
85
+ def includes_executor?
86
+ executor.is_a?(::Proc)
86
87
  end
87
88
 
88
89
  def alias?
89
90
  !alias_target.nil?
90
91
  end
91
92
 
92
- def only_collection?
93
- @executor == false
93
+ def effective_desc
94
+ @desc || default_desc
94
95
  end
95
96
 
96
- def parent
97
- return nil if root?
98
- @lookup.exact_tool(full_name.slice(0..-2))
97
+ def effective_long_desc
98
+ @long_desc || @desc || default_desc
99
99
  end
100
100
 
101
- def effective_short_desc
102
- @short_desc || default_desc
101
+ def includes_description?
102
+ !@long_desc.nil? || !@desc.nil?
103
103
  end
104
104
 
105
- def effective_long_desc
106
- @long_desc || @short_desc || default_desc
105
+ def includes_arguments?
106
+ !default_data.empty? || !switches.empty? ||
107
+ !required_args.empty? || !optional_args.empty? || !remaining_args.nil?
107
108
  end
108
109
 
109
- def includes_description?
110
- !@long_desc.nil? || !@short_desc.nil?
110
+ def includes_helpers?
111
+ !helpers.empty? || !modules.empty?
111
112
  end
112
113
 
113
114
  def includes_definition?
114
- !@default_data.empty? || !@switches.empty? ||
115
- !@required_args.empty? || !@optional_args.empty? ||
116
- !@remaining_args.nil? || leaf? ||
117
- !@helpers.empty? || !@modules.empty?
115
+ alias? || includes_arguments? || includes_executor? || includes_helpers?
116
+ end
117
+
118
+ def used_switches
119
+ @switches.reduce([]) { |used, switch| used + switch.switches }.uniq
118
120
  end
119
121
 
120
122
  def defining_from(path)
@@ -142,22 +144,17 @@ module Toys
142
144
  if root?
143
145
  raise ToolDefinitionError, "Cannot make the root tool an alias"
144
146
  end
145
- if only_collection?
146
- raise ToolDefinitionError, "Tool #{display_name.inspect} is already" \
147
- " a collection and cannot be made an alias"
148
- end
149
147
  if includes_description? || includes_definition?
150
148
  raise ToolDefinitionError, "Tool #{display_name.inspect} already has" \
151
149
  " a definition and cannot be made an alias"
152
150
  end
153
- parent.ensure_collection_only(full_name) unless root?
154
151
  @alias_target = target_word
155
152
  self
156
153
  end
157
154
 
158
- def short_desc=(str)
155
+ def desc=(str)
159
156
  check_definition_state
160
- @short_desc = str
157
+ @desc = str
161
158
  end
162
159
 
163
160
  def long_desc=(str)
@@ -166,95 +163,109 @@ module Toys
166
163
  end
167
164
 
168
165
  def add_helper(name, &block)
169
- check_definition_state(true)
166
+ check_definition_state
170
167
  name_str = name.to_s
171
168
  unless name_str =~ /^[a-z]\w+$/
172
169
  raise ToolDefinitionError, "Illegal helper name: #{name_str.inspect}"
173
170
  end
174
171
  @helpers[name.to_sym] = block
172
+ self
175
173
  end
176
174
 
177
- def use_module(mod)
178
- check_definition_state(true)
179
- case mod
175
+ def use_module(name)
176
+ check_definition_state
177
+ case name
180
178
  when ::Module
181
- @modules << mod
179
+ @modules << name
182
180
  when ::Symbol
183
- mod = mod.to_s
184
- file_name =
185
- mod
186
- .gsub(/([a-zA-Z])([A-Z])/) { |_m| "#{$1}_#{$2.downcase}" }
187
- .downcase
188
- require "toys/helpers/#{file_name}"
189
- const_name = mod.gsub(/(^|_)([a-zA-Z0-9])/) { |_m| $2.upcase }
190
- @modules << Helpers.const_get(const_name)
181
+ mod = Helpers.lookup(name.to_s)
182
+ if mod.nil?
183
+ raise ToolDefinitionError, "Module not found: #{name.inspect}"
184
+ end
185
+ @modules << mod
191
186
  else
192
- raise ToolDefinitionError, "Illegal helper module name: #{mod.inspect}"
187
+ raise ToolDefinitionError, "Illegal helper module name: #{name.inspect}"
193
188
  end
189
+ self
194
190
  end
195
191
 
196
- def add_switch(key, *switches, accept: nil, default: nil, doc: nil)
197
- check_definition_state(true)
198
- @default_data[key] = default
192
+ def add_switch(key, *switches,
193
+ accept: nil, default: nil, doc: nil, only_unique: false, handler: nil)
194
+ check_definition_state
199
195
  switches << "--#{Tool.canonical_switch(key)}=VALUE" if switches.empty?
200
196
  switches << accept unless accept.nil?
201
197
  switches += Array(doc)
202
- @switches << SwitchInfo.new(key, switches)
198
+ switch_info = SwitchInfo.new(key, switches, handler)
199
+ if only_unique
200
+ switch_info.remove_switches(used_switches)
201
+ end
202
+ if switch_info.active?
203
+ @default_data[key] = default
204
+ @switches << switch_info
205
+ end
206
+ self
203
207
  end
204
208
 
205
209
  def add_required_arg(key, accept: nil, doc: nil)
206
- check_definition_state(true)
210
+ check_definition_state
207
211
  @default_data[key] = nil
208
212
  @required_args << ArgInfo.new(key, accept, Array(doc))
213
+ self
209
214
  end
210
215
 
211
216
  def add_optional_arg(key, accept: nil, default: nil, doc: nil)
212
- check_definition_state(true)
217
+ check_definition_state
213
218
  @default_data[key] = default
214
219
  @optional_args << ArgInfo.new(key, accept, Array(doc))
220
+ self
215
221
  end
216
222
 
217
223
  def set_remaining_args(key, accept: nil, default: [], doc: nil)
218
- check_definition_state(true)
224
+ check_definition_state
219
225
  @default_data[key] = default
220
226
  @remaining_args = ArgInfo.new(key, accept, Array(doc))
227
+ self
221
228
  end
222
229
 
223
230
  def executor=(executor)
224
- check_definition_state(true)
231
+ check_definition_state
225
232
  @executor = executor
226
233
  end
227
234
 
228
- def execute(context_base, args)
229
- Execution.new(self).execute(context_base, args)
235
+ def finish_definition
236
+ if !alias? && !@definition_finished
237
+ config_proc = proc {}
238
+ middleware_stack.reverse.each do |middleware|
239
+ config_proc = make_config_proc(middleware, config_proc)
240
+ end
241
+ config_proc.call
242
+ end
243
+ @definition_finished = true
244
+ self
230
245
  end
231
246
 
232
- protected
233
-
234
- def ensure_collection_only(source_name)
235
- if includes_definition?
236
- raise ToolDefinitionError, "Cannot create tool #{source_name.inspect}" \
237
- " because #{display_name.inspect} is already a tool."
238
- end
239
- unless @executor == false
240
- @executor = false
241
- parent.ensure_collection_only(source_name) unless root?
242
- end
247
+ def execute(context_base, args, verbosity: 0)
248
+ finish_definition unless @definition_finished
249
+ Execution.new(self).execute(context_base, args, verbosity: verbosity)
243
250
  end
244
251
 
245
252
  private
246
253
 
254
+ def make_config_proc(middleware, next_config)
255
+ proc { middleware.config(self, &next_config) }
256
+ end
257
+
247
258
  def default_desc
248
259
  if alias?
249
260
  "(Alias of #{@alias_target.inspect})"
250
- elsif leaf?
261
+ elsif includes_executor?
251
262
  "(No description available)"
252
263
  else
253
- "(A collection of commands)"
264
+ "(A group of commands)"
254
265
  end
255
266
  end
256
267
 
257
- def check_definition_state(execution_field = false)
268
+ def check_definition_state
258
269
  if alias?
259
270
  raise ToolDefinitionError, "Tool #{display_name.inspect} is an alias"
260
271
  end
@@ -264,13 +275,9 @@ module Toys
264
275
  "Cannot redefine tool #{display_name.inspect} #{in_clause}" \
265
276
  "(already defined in #{@definition_path})"
266
277
  end
267
- if execution_field
268
- if only_collection?
269
- raise ToolDefinitionError,
270
- "Cannot make tool #{display_name.inspect} executable because" \
271
- " a descendant is already executable"
272
- end
273
- parent.ensure_collection_only(full_name) unless root?
278
+ if @definition_finished
279
+ raise ToolDefinitionError,
280
+ "Defintion of tool #{display_name.inspect} is already finished"
274
281
  end
275
282
  end
276
283
 
@@ -284,13 +291,44 @@ module Toys
284
291
  # Representation of a formal switch
285
292
  #
286
293
  class SwitchInfo
287
- def initialize(key, optparse_info)
294
+ def initialize(key, optparse_info, handler = nil)
288
295
  @key = key
289
296
  @optparse_info = optparse_info
297
+ @handler = handler || ->(val, _cur) { val }
298
+ @switches = nil
290
299
  end
291
300
 
292
301
  attr_reader :key
293
302
  attr_reader :optparse_info
303
+ attr_reader :handler
304
+
305
+ def switches
306
+ @switches ||= optparse_info.map { |s| extract_switch(s) }.flatten
307
+ end
308
+
309
+ def active?
310
+ !switches.empty?
311
+ end
312
+
313
+ def remove_switches(switches)
314
+ @optparse_info.select! do |s|
315
+ extract_switch(s).all? { |ss| !switches.include?(ss) }
316
+ end
317
+ @switches = nil
318
+ self
319
+ end
320
+
321
+ def extract_switch(str)
322
+ if str =~ /^(-[\?\w])/
323
+ [$1]
324
+ elsif str =~ /^--\[no-\](\w[\w-]*)/
325
+ ["--#{$1}", "--no-#{$1}"]
326
+ elsif str =~ /^(--\w[\w-]*)/
327
+ [$1]
328
+ else
329
+ []
330
+ end
331
+ end
294
332
  end
295
333
 
296
334
  ##
@@ -309,7 +347,7 @@ module Toys
309
347
 
310
348
  def process_value(val)
311
349
  return val unless accept
312
- n = canonical_switch(key)
350
+ n = canonical_name
313
351
  result = val
314
352
  optparse = ::OptionParser.new
315
353
  optparse.on("--#{n}=VALUE", accept) { |v| result = v }
@@ -329,235 +367,59 @@ module Toys
329
367
  class Execution
330
368
  def initialize(tool)
331
369
  @tool = tool
370
+ @data = @tool.default_data.dup
371
+ @data[Context::TOOL] = tool
372
+ @data[Context::TOOL_NAME] = tool.full_name
332
373
  end
333
374
 
334
- def execute(context_base, args)
375
+ def execute(context_base, args, verbosity: 0)
335
376
  return execute_alias(context_base, args) if @tool.alias?
336
377
 
337
- parsed_args = ParsedArgs.new(@tool, context_base.binary_name, args)
338
- context = create_child_context(context_base, parsed_args, args)
378
+ parse_args(args, verbosity)
379
+ context = create_child_context(context_base)
339
380
 
340
- if parsed_args.usage_error
341
- puts(parsed_args.usage_error)
342
- puts("")
343
- show_usage(parsed_args.optparse)
344
- -1
345
- elsif parsed_args.show_help
346
- show_usage(parsed_args.optparse, recursive: parsed_args.recursive)
347
- 0
348
- else
349
- catch(:result) do
350
- context.instance_eval(&@tool.executor)
351
- 0
352
- end
381
+ original_level = context.logger.level
382
+ context.logger.level = context_base.base_level - @data[Context::VERBOSITY]
383
+ begin
384
+ perform_execution(context)
385
+ ensure
386
+ context.logger.level = original_level
353
387
  end
354
388
  end
355
389
 
356
390
  private
357
391
 
358
- def create_child_context(context_base, parsed_args, args)
359
- context = context_base.create_context(@tool.full_name, args, parsed_args.data)
360
- context.logger.level += parsed_args.delta_severity
361
- @tool.modules.each do |mod|
362
- context.extend(mod)
363
- end
364
- @tool.helpers.each do |name, block|
365
- context.define_singleton_method(name, &block)
366
- end
367
- context
368
- end
369
-
370
- def show_usage(optparse, recursive: false)
371
- puts(optparse.to_s)
372
- if @tool.leaf?
373
- required_args = @tool.required_args
374
- optional_args = @tool.optional_args
375
- remaining_args = @tool.remaining_args
376
- if !required_args.empty? || !optional_args.empty? || remaining_args
377
- show_positional_arguments(required_args, optional_args, remaining_args)
378
- end
379
- else
380
- show_command_list(recursive)
381
- end
382
- end
383
-
384
- def show_positional_arguments(required_args, optional_args, remaining_args)
385
- puts("")
386
- puts("Positional arguments:")
387
- args_to_display = required_args + optional_args
388
- args_to_display << remaining_args if remaining_args
389
- args_to_display.each do |arg_info|
390
- puts(" #{arg_info.canonical_name.ljust(31)} #{arg_info.doc.first}")
391
- next if arg_info.doc.empty?
392
- arg_info.doc[1..-1].each do |d|
393
- puts(" #{d}")
394
- end
395
- end
396
- end
397
-
398
- def show_command_list(recursive)
399
- puts("")
400
- puts("Commands:")
401
- name_len = @tool.full_name.length
402
- @tool.lookup.list_subtools(@tool.full_name, recursive).each do |subtool|
403
- desc = subtool.effective_short_desc
404
- tool_name = subtool.full_name.slice(name_len..-1).join(" ").ljust(31)
405
- puts(" #{tool_name} #{desc}")
406
- end
407
- end
408
-
409
- def execute_alias(context_base, args)
410
- target_name = @tool.full_name.slice(0..-2) + [@tool.alias_target]
411
- target_tool = @tool.lookup.lookup(target_name)
412
- if target_tool.full_name == target_name
413
- target_tool.execute(context_base, args)
414
- else
415
- logger.fatal("Alias target #{@tool.alias_target.inspect} not found")
416
- -1
417
- end
418
- end
419
- end
420
-
421
- ##
422
- # An internal class that manages parsing of tool arguments
423
- # @private
424
- #
425
- class ParsedArgs
426
- def initialize(tool, binary_name, args)
427
- binary_name ||= ::File.basename($PROGRAM_NAME)
428
- @show_help = !tool.leaf?
429
- @usage_error = nil
430
- @delta_severity = 0
431
- @recursive = false
432
- @data = tool.default_data.dup
433
- @optparse = create_option_parser(tool, binary_name)
434
- parse_args(args, tool)
435
- end
436
-
437
- attr_reader :show_help
438
- attr_reader :usage_error
439
- attr_reader :delta_severity
440
- attr_reader :recursive
441
- attr_reader :data
442
- attr_reader :optparse
443
-
444
- private
445
-
446
- ##
447
- # Well-known flags
448
- # @private
449
- #
450
- SPECIAL_FLAGS = %w[
451
- -q
452
- --quiet
453
- -v
454
- --verbose
455
- -?
456
- -h
457
- --help
458
- ].freeze
459
-
460
- def parse_args(args, tool)
461
- remaining = @optparse.parse(args)
462
- remaining = parse_required_args(remaining, tool, args)
463
- remaining = parse_optional_args(remaining, tool)
464
- parse_remaining_args(remaining, tool, args)
392
+ def parse_args(args, base_verbosity)
393
+ optparse = create_option_parser
394
+ @data[Context::VERBOSITY] = base_verbosity
395
+ @data[Context::ARGS] = args
396
+ @data[Context::USAGE_ERROR] = nil
397
+ remaining = optparse.parse(args)
398
+ remaining = parse_required_args(remaining, args)
399
+ remaining = parse_optional_args(remaining)
400
+ parse_remaining_args(remaining, args)
465
401
  rescue ::OptionParser::ParseError => e
466
- @usage_error = e
402
+ @data[Context::USAGE_ERROR] = e.message
467
403
  end
468
404
 
469
- def create_option_parser(tool, binary_name)
405
+ def create_option_parser
470
406
  optparse = ::OptionParser.new
471
- optparse.banner =
472
- if tool.leaf?
473
- leaf_banner(tool, binary_name)
474
- else
475
- collection_banner(tool, binary_name)
476
- end
477
- unless tool.effective_long_desc.empty?
478
- optparse.separator("")
479
- optparse.separator(tool.effective_long_desc)
480
- end
481
- optparse.separator("")
482
- optparse.separator("Options:")
483
- if tool.leaf?
484
- leaf_switches(tool, optparse)
485
- else
486
- collection_switches(optparse)
487
- end
488
- optparse
489
- end
490
-
491
- def leaf_banner(tool, binary_name)
492
- banner = ["Usage:", binary_name] + tool.full_name
493
- banner << "[<options...>]" unless tool.switches.empty?
494
- tool.required_args.each do |arg_info|
495
- banner << "<#{arg_info.canonical_name}>"
496
- end
497
- tool.optional_args.each do |arg_info|
498
- banner << "[<#{arg_info.canonical_name}>]"
499
- end
500
- if tool.remaining_args
501
- banner << "[<#{tool.remaining_args.canonical_name}...>]"
502
- end
503
- banner.join(" ")
504
- end
505
-
506
- def collection_banner(tool, binary_name)
507
- (["Usage:", binary_name] + tool.full_name + ["<command>", "[<options...>]"]).join(" ")
508
- end
509
-
510
- def leaf_switches(tool, optparse)
511
- found_special_flags = []
512
- leaf_normal_switches(tool.switches, optparse, found_special_flags)
513
- leaf_verbose_switch(optparse, found_special_flags)
514
- leaf_quiet_switch(optparse, found_special_flags)
515
- leaf_help_switch(optparse, found_special_flags)
516
- end
517
-
518
- def collection_switches(optparse)
519
- optparse.on("-?", "--help", "Show help message")
520
- optparse.on("-r", "--[no-]recursive", "Show all subcommands recursively") do |val|
521
- @recursive = val
522
- end
523
- end
524
-
525
- def leaf_normal_switches(switches, optparse, found_special_flags)
526
- switches.each do |switch|
527
- found_special_flags |= (switch.optparse_info & SPECIAL_FLAGS)
407
+ # The following clears out the Officious (hidden default switches).
408
+ optparse.remove
409
+ optparse.remove
410
+ optparse.new
411
+ optparse.new
412
+ @tool.switches.each do |switch|
528
413
  optparse.on(*switch.optparse_info) do |val|
529
- @data[switch.key] = val
414
+ @data[switch.key] = switch.handler.call(val, @data[switch.key])
530
415
  end
531
416
  end
417
+ optparse
532
418
  end
533
419
 
534
- def leaf_verbose_switch(optparse, found_special_flags)
535
- flags = ["-v", "--verbose"] - found_special_flags
536
- return if flags.empty?
537
- optparse.on(*(flags + ["Increase verbosity"])) do
538
- @delta_severity -= 1
539
- end
540
- end
541
-
542
- def leaf_quiet_switch(optparse, found_special_flags)
543
- flags = ["-q", "--quiet"] - found_special_flags
544
- return if flags.empty?
545
- optparse.on(*(flags + ["Decrease verbosity"])) do
546
- @delta_severity += 1
547
- end
548
- end
549
-
550
- def leaf_help_switch(optparse, found_special_flags)
551
- flags = ["-?", "-h", "--help"] - found_special_flags
552
- return if flags.empty?
553
- optparse.on(*(flags + ["Show help message"])) do
554
- @show_help = true
555
- end
556
- end
557
-
558
- def parse_required_args(remaining, tool, args)
559
- tool.required_args.each do |arg_info|
560
- if !@show_help && remaining.empty?
420
+ def parse_required_args(remaining, args)
421
+ @tool.required_args.each do |arg_info|
422
+ if remaining.empty?
561
423
  reason = "No value given for required argument named <#{arg_info.canonical_name}>"
562
424
  raise create_parse_error(args, reason)
563
425
  end
@@ -566,25 +428,25 @@ module Toys
566
428
  remaining
567
429
  end
568
430
 
569
- def parse_optional_args(remaining, tool)
570
- tool.optional_args.each do |arg_info|
431
+ def parse_optional_args(remaining)
432
+ @tool.optional_args.each do |arg_info|
571
433
  break if remaining.empty?
572
434
  @data[arg_info.key] = arg_info.process_value(remaining.shift)
573
435
  end
574
436
  remaining
575
437
  end
576
438
 
577
- def parse_remaining_args(remaining, tool, args)
439
+ def parse_remaining_args(remaining, args)
578
440
  return if remaining.empty?
579
- unless tool.remaining_args
580
- if tool.leaf?
441
+ unless @tool.remaining_args
442
+ if @tool.includes_executor?
581
443
  raise create_parse_error(remaining, "Extra arguments provided")
582
444
  else
583
- raise create_parse_error(tool.full_name + args, "Tool not found")
445
+ raise create_parse_error(@tool.full_name + args, "Tool not found")
584
446
  end
585
447
  end
586
- @data[tool.remaining_args.key] =
587
- remaining.map { |arg| tool.remaining_args.process_value(arg) }
448
+ @data[@tool.remaining_args.key] =
449
+ remaining.map { |arg| @tool.remaining_args.process_value(arg) }
588
450
  end
589
451
 
590
452
  def create_parse_error(path, reason)
@@ -592,6 +454,50 @@ module Toys
592
454
  e.reason = reason
593
455
  end
594
456
  end
457
+
458
+ def create_child_context(context_base)
459
+ context = context_base.create_context(@data)
460
+ @tool.modules.each do |mod|
461
+ context.extend(mod)
462
+ end
463
+ @tool.helpers.each do |name, block|
464
+ context.define_singleton_method(name, &block)
465
+ end
466
+ context
467
+ end
468
+
469
+ def perform_execution(context)
470
+ executor = proc do
471
+ if @tool.includes_executor?
472
+ context.instance_eval(&@tool.executor)
473
+ else
474
+ context.logger.fatal("No implementation for #{@tool.display_name.inspect}")
475
+ context.exit(-1)
476
+ end
477
+ end
478
+ @tool.middleware_stack.reverse.each do |middleware|
479
+ executor = make_executor(middleware, context, executor)
480
+ end
481
+ catch(:result) do
482
+ executor.call
483
+ 0
484
+ end
485
+ end
486
+
487
+ def make_executor(middleware, context, next_executor)
488
+ proc { middleware.execute(context, &next_executor) }
489
+ end
490
+
491
+ def execute_alias(context_base, args)
492
+ target_name = @tool.full_name.slice(0..-2) + [@tool.alias_target]
493
+ target_tool = context_base.loader.lookup(target_name)
494
+ if target_tool.full_name == target_name
495
+ target_tool.execute(context_base, args)
496
+ else
497
+ context_base.logger.fatal("Alias target #{@tool.alias_target.inspect} not found")
498
+ -1
499
+ end
500
+ end
595
501
  end
596
502
  end
597
503
  end