toys 0.2.2 → 0.3.0

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