toys 0.2.1 → 0.2.2

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.
@@ -1,58 +1,90 @@
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
+
1
30
  require "rubygems/package"
2
31
 
3
- module Toys
4
- module Templates
5
- GemBuild = Toys::Template.new
6
-
7
- GemBuild.to_init_opts do |opts|
8
- {
9
- name: "build",
10
- gem_name: nil,
11
- push_gem: false,
12
- tag: false,
13
- push_tag: false
14
- }.merge(opts)
32
+ module Toys::Templates
33
+ ##
34
+ # A template for tools that build and release gems
35
+ #
36
+ class GemBuild
37
+ include ::Toys::Template
38
+
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]
15
45
  end
16
46
 
17
- GemBuild.to_expand do |opts|
18
- toy_name = opts[:name] || "build"
19
- gem_name = opts[:gem_name]
20
- unless gem_name
47
+ attr_accessor :name
48
+ attr_accessor :gem_name
49
+ attr_accessor :push_gem
50
+ attr_accessor :tag
51
+ attr_accessor :push_tag
52
+
53
+ to_expand do |template|
54
+ unless template.gem_name
21
55
  candidates = ::Dir.glob("*.gemspec")
22
- if candidates.size > 0
23
- gem_name = candidates.first.sub(/\.gemspec$/, "")
24
- else
25
- raise Toys::ToysDefinitionError, "Could not find a gemspec"
56
+ if candidates.empty?
57
+ raise ::Toys::ToolDefinitionError, "Could not find a gemspec"
26
58
  end
59
+ template.gem_name = candidates.first.sub(/\.gemspec$/, "")
27
60
  end
28
- push_gem = opts[:push_gem]
29
- tag = opts[:tag]
30
- push_tag = opts[:push_tag]
61
+ task_type = template.push_gem ? "Release" : "Build"
31
62
 
32
- name toy_name do
33
- short_desc "#{push_gem ? 'Release' : 'Build'} the gem: #{gem_name}"
63
+ name(template.name) do
64
+ short_desc "#{task_type} the gem: #{template.gem_name}"
34
65
 
35
66
  use :file_utils
36
67
  use :exec
37
68
 
38
69
  execute do
39
- gemspec = Gem::Specification.load "#{gem_name}.gemspec"
70
+ configure_exec(exit_on_nonzero_status: true)
71
+ gemspec = ::Gem::Specification.load "#{template.gem_name}.gemspec"
40
72
  version = gemspec.version
41
- gemfile = "#{gem_name}-#{version}.gem"
42
- Gem::Package.build gemspec
73
+ gemfile = "#{template.gem_name}-#{version}.gem"
74
+ ::Gem::Package.build gemspec
43
75
  mkdir_p "pkg"
44
76
  mv gemfile, "pkg"
45
- if push_gem
46
- if File.directory?(".git") && capture("git status -s").strip != ""
77
+ if template.push_gem
78
+ if ::File.directory?(".git") && capture("git status -s").strip != ""
47
79
  logger.error "Cannot push the gem when there are uncommited changes"
48
80
  exit(1)
49
81
  end
50
- sh "gem push pkg/#{gemfile}", report_subprocess_errors: true
51
- if tag
52
- sh "git tag v#{version}", report_subprocess_errors: true
53
- if push_tag
54
- push_tag = "origin" if push_tag == true
55
- sh "git push #{push_tag} v#{version}", report_subprocess_errors: true
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}"
56
88
  end
57
89
  end
58
90
  end
@@ -1,39 +1,85 @@
1
- require "shellwords"
2
-
3
- module Toys
4
- module Templates
5
- Minitest = Toys::Template.new
6
-
7
- Minitest.to_init_opts do |opts|
8
- {
9
- name: "test",
10
- libs: ["lib", "test"],
11
- test_files: [],
12
- warning: true
13
- }.merge(opts)
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
+ module Toys::Templates
31
+ ##
32
+ # A template for tools that run minitest
33
+ #
34
+ class Minitest
35
+ include ::Toys::Template
36
+
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
14
42
  end
15
43
 
16
- Minitest.to_expand do |opts|
17
- toy_name = opts[:name] || "build"
18
- libs = opts[:libs] || []
19
- warning = opts[:warning]
20
- test_files = opts[:test_files] || []
21
- lib_path = libs.join(File::PATH_SEPARATOR)
22
- cmd = []
23
- cmd << File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"])
24
- cmd << "-I#{lib_path}" unless libs.empty?
25
- cmd << "-w" if warning
26
- cmd << "-e" << "ARGV.each{|f| load f}"
27
- cmd << "--"
28
- cmd = Shellwords.join(cmd + test_files)
29
-
30
- name toy_name do
44
+ attr_accessor :name
45
+ attr_accessor :libs
46
+ attr_accessor :files
47
+ attr_accessor :warnings
48
+
49
+ to_expand do |template|
50
+ name(template.name) do
31
51
  short_desc "Run minitest"
32
52
 
33
53
  use :exec
34
54
 
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)")
61
+
35
62
  execute do
36
- sh(cmd, report_subprocess_errors: true)
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]
69
+
70
+ tests = self[:tests]
71
+ if tests.empty?
72
+ Array(template.files).each do |pattern|
73
+ tests.concat(::Dir.glob(pattern))
74
+ end
75
+ tests.uniq!
76
+ end
77
+
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}'")
81
+ end
82
+ end
37
83
  end
38
84
  end
39
85
  end
@@ -0,0 +1,66 @@
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
+ module Toys::Templates
31
+ ##
32
+ # A template for tools that run rubocop
33
+ #
34
+ class Rubocop
35
+ include ::Toys::Template
36
+
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
42
+
43
+ attr_accessor :name
44
+ attr_accessor :fail_on_error
45
+ attr_accessor :options
46
+
47
+ to_expand do |template|
48
+ name(template.name) do
49
+ short_desc "Run RuboCop"
50
+
51
+ use :exec
52
+
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
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,79 @@
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
+ module Toys::Templates
31
+ ##
32
+ # A template for tools that run yardoc
33
+ #
34
+ class Yardoc
35
+ include ::Toys::Template
36
+
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
43
+
44
+ attr_accessor :name
45
+ attr_accessor :files
46
+ attr_accessor :options
47
+ attr_accessor :stats_options
48
+
49
+ to_expand do |template|
50
+ name(template.name) do
51
+ short_desc "Run yardoc"
52
+
53
+ use :exec
54
+
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!
64
+
65
+ unless template.stats_options.empty?
66
+ template.options << "--no-stats"
67
+ template.stats_options << "--use-cache"
68
+ end
69
+
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)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
data/lib/toys/tool.rb CHANGED
@@ -1,14 +1,42 @@
1
- require "logger"
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
+
2
30
  require "optparse"
3
31
 
4
32
  module Toys
33
+ ##
34
+ # A tool definition
35
+ #
5
36
  class Tool
6
-
7
- def initialize(parent, name)
8
- @parent = parent
9
- @simple_name = name
10
- @full_name = name ? [name] : []
11
- @full_name = parent.full_name + @full_name if parent
37
+ def initialize(lookup, full_name)
38
+ @lookup = lookup
39
+ @full_name = full_name
12
40
 
13
41
  @definition_path = nil
14
42
  @cur_path = nil
@@ -23,22 +51,53 @@ module Toys
23
51
  @required_args = []
24
52
  @optional_args = []
25
53
  @remaining_args = nil
54
+
26
55
  @helpers = {}
27
56
  @modules = []
28
57
  @executor = nil
29
58
  end
30
59
 
31
- attr_reader :simple_name
60
+ attr_reader :lookup
32
61
  attr_reader :full_name
33
-
34
- def root?
35
- @parent.nil?
62
+ attr_reader :switches
63
+ attr_reader :required_args
64
+ attr_reader :optional_args
65
+ attr_reader :remaining_args
66
+ attr_reader :default_data
67
+ attr_reader :modules
68
+ attr_reader :helpers
69
+ attr_reader :executor
70
+ attr_reader :alias_target
71
+
72
+ def simple_name
73
+ full_name.last
36
74
  end
37
75
 
38
76
  def display_name
39
77
  full_name.join(" ")
40
78
  end
41
79
 
80
+ def root?
81
+ full_name.empty?
82
+ end
83
+
84
+ def leaf?
85
+ @executor.is_a?(::Proc)
86
+ end
87
+
88
+ def alias?
89
+ !alias_target.nil?
90
+ end
91
+
92
+ def only_collection?
93
+ @executor == false
94
+ end
95
+
96
+ def parent
97
+ return nil if root?
98
+ @lookup.exact_tool(full_name.slice(0..-2))
99
+ end
100
+
42
101
  def effective_short_desc
43
102
  @short_desc || default_desc
44
103
  end
@@ -47,28 +106,24 @@ module Toys
47
106
  @long_desc || @short_desc || default_desc
48
107
  end
49
108
 
50
- def has_description?
109
+ def includes_description?
51
110
  !@long_desc.nil? || !@short_desc.nil?
52
111
  end
53
112
 
54
- def has_definition?
113
+ def includes_definition?
55
114
  !@default_data.empty? || !@switches.empty? ||
56
115
  !@required_args.empty? || !@optional_args.empty? ||
57
- !@remaining_args.nil? || !!@executor ||
116
+ !@remaining_args.nil? || leaf? ||
58
117
  !@helpers.empty? || !@modules.empty?
59
118
  end
60
119
 
61
- def only_collection?
62
- @executor == false
63
- end
64
-
65
120
  def defining_from(path)
66
121
  raise ToolDefinitionError, "Already being defined" if @cur_path
67
122
  @cur_path = path
68
123
  begin
69
124
  yield
70
125
  ensure
71
- @definition_path = @cur_path if has_description? || has_definition?
126
+ @definition_path = @cur_path if includes_description? || includes_definition?
72
127
  @cur_path = nil
73
128
  end
74
129
  end
@@ -83,24 +138,21 @@ module Toys
83
138
  end
84
139
  end
85
140
 
86
- def set_alias_target(target_tool)
87
- unless target_tool.is_a?(Toys::Tool)
88
- raise ArgumentError, "Illegal target type"
141
+ def make_alias_of(target_word)
142
+ if root?
143
+ raise ToolDefinitionError, "Cannot make the root tool an alias"
89
144
  end
90
145
  if only_collection?
91
146
  raise ToolDefinitionError, "Tool #{display_name.inspect} is already" \
92
147
  " a collection and cannot be made an alias"
93
148
  end
94
- if has_description? || has_definition?
149
+ if includes_description? || includes_definition?
95
150
  raise ToolDefinitionError, "Tool #{display_name.inspect} already has" \
96
151
  " a definition and cannot be made an alias"
97
152
  end
98
- if @executor == false
99
- raise ToolDefinitionError, "Cannot make tool #{display_name.inspect}" \
100
- " an alias because a descendant is already executable"
101
- end
102
- @parent.ensure_collection_only(full_name) if @parent
103
- @alias_target = target_tool
153
+ parent.ensure_collection_only(full_name) unless root?
154
+ @alias_target = target_word
155
+ self
104
156
  end
105
157
 
106
158
  def short_desc=(str)
@@ -122,17 +174,20 @@ module Toys
122
174
  @helpers[name.to_sym] = block
123
175
  end
124
176
 
125
- def use_helper_module(mod)
177
+ def use_module(mod)
126
178
  check_definition_state(true)
127
179
  case mod
128
- when Module
180
+ when ::Module
129
181
  @modules << mod
130
- when Symbol
182
+ when ::Symbol
131
183
  mod = mod.to_s
132
- file_name = mod.gsub(/([a-zA-Z])([A-Z])/){ |m| "#{$1}_#{$2.downcase}" }.downcase
184
+ file_name =
185
+ mod
186
+ .gsub(/([a-zA-Z])([A-Z])/) { |_m| "#{$1}_#{$2.downcase}" }
187
+ .downcase
133
188
  require "toys/helpers/#{file_name}"
134
- const_name = mod.gsub(/(^|_)([a-zA-Z0-9])/){ |m| $2.upcase }
135
- @modules << Toys::Helpers.const_get(const_name)
189
+ const_name = mod.gsub(/(^|_)([a-zA-Z0-9])/) { |_m| $2.upcase }
190
+ @modules << Helpers.const_get(const_name)
136
191
  else
137
192
  raise ToolDefinitionError, "Illegal helper module name: #{mod.inspect}"
138
193
  end
@@ -141,28 +196,28 @@ module Toys
141
196
  def add_switch(key, *switches, accept: nil, default: nil, doc: nil)
142
197
  check_definition_state(true)
143
198
  @default_data[key] = default
144
- switches << "--#{canonical_switch(key)}=VALUE" if switches.empty?
199
+ switches << "--#{Tool.canonical_switch(key)}=VALUE" if switches.empty?
145
200
  switches << accept unless accept.nil?
146
201
  switches += Array(doc)
147
- @switches << [key, switches]
202
+ @switches << SwitchInfo.new(key, switches)
148
203
  end
149
204
 
150
205
  def add_required_arg(key, accept: nil, doc: nil)
151
206
  check_definition_state(true)
152
207
  @default_data[key] = nil
153
- @required_args << [key, accept, Array(doc)]
208
+ @required_args << ArgInfo.new(key, accept, Array(doc))
154
209
  end
155
210
 
156
211
  def add_optional_arg(key, accept: nil, default: nil, doc: nil)
157
212
  check_definition_state(true)
158
213
  @default_data[key] = default
159
- @optional_args << [key, accept, Array(doc)]
214
+ @optional_args << ArgInfo.new(key, accept, Array(doc))
160
215
  end
161
216
 
162
217
  def set_remaining_args(key, accept: nil, default: [], doc: nil)
163
218
  check_definition_state(true)
164
219
  @default_data[key] = default
165
- @remaining_args = [key, accept, Array(doc)]
220
+ @remaining_args = ArgInfo.new(key, accept, Array(doc))
166
221
  end
167
222
 
168
223
  def executor=(executor)
@@ -170,243 +225,373 @@ module Toys
170
225
  @executor = executor
171
226
  end
172
227
 
173
- def execute(context, args)
174
- return @alias_target.execute(context, args) if @alias_target
175
- execution_data = parse_args(args, context.binary_name)
176
- context = create_child_context(context, args, execution_data)
177
- if execution_data[:usage_error]
178
- puts(execution_data[:usage_error])
179
- puts("")
180
- show_usage(context, execution_data[:optparse])
181
- -1
182
- elsif execution_data[:show_help]
183
- show_usage(context, execution_data[:optparse],
184
- recursive: execution_data[:recursive])
185
- 0
186
- else
187
- catch(:result) do
188
- context.instance_eval(&@executor)
189
- 0
190
- end
191
- end
228
+ def execute(context_base, args)
229
+ Execution.new(self).execute(context_base, args)
192
230
  end
193
231
 
194
232
  protected
195
233
 
196
234
  def ensure_collection_only(source_name)
197
- if has_definition?
235
+ if includes_definition?
198
236
  raise ToolDefinitionError, "Cannot create tool #{source_name.inspect}" \
199
237
  " because #{display_name.inspect} is already a tool."
200
238
  end
201
- if @executor != false
239
+ unless @executor == false
202
240
  @executor = false
203
- @parent.ensure_collection_only(source_name) if @parent
241
+ parent.ensure_collection_only(source_name) unless root?
204
242
  end
205
243
  end
206
244
 
207
245
  private
208
246
 
209
- SPECIAL_FLAGS = ["-q", "--quiet", "-v", "--verbose", "-?", "-h", "--help"]
210
-
211
247
  def default_desc
212
- if @alias_target
213
- "(Alias of #{@alias_target.display_name.inspect})"
214
- elsif @executor
248
+ if alias?
249
+ "(Alias of #{@alias_target.inspect})"
250
+ elsif leaf?
215
251
  "(No description available)"
216
252
  else
217
253
  "(A collection of commands)"
218
254
  end
219
255
  end
220
256
 
221
- def check_definition_state(execution_field=false)
222
- if @alias_target
257
+ def check_definition_state(execution_field = false)
258
+ if alias?
223
259
  raise ToolDefinitionError, "Tool #{display_name.inspect} is an alias"
224
260
  end
225
261
  if @definition_path
226
262
  in_clause = @cur_path ? "in #{@cur_path} " : ""
227
263
  raise ToolDefinitionError,
228
- "Cannot redefine tool #{display_name.inspect} #{in_clause}" \
229
- "(already defined in #{@definition_path})"
264
+ "Cannot redefine tool #{display_name.inspect} #{in_clause}" \
265
+ "(already defined in #{@definition_path})"
230
266
  end
231
267
  if execution_field
232
- if @executor == false
268
+ if only_collection?
233
269
  raise ToolDefinitionError,
234
- "Cannot make tool #{display_name.inspect} executable because a" \
235
- " descendant is already executable"
270
+ "Cannot make tool #{display_name.inspect} executable because" \
271
+ " a descendant is already executable"
236
272
  end
237
- @parent.ensure_collection_only(full_name) if @parent
273
+ parent.ensure_collection_only(full_name) unless root?
238
274
  end
239
275
  end
240
276
 
241
- def leaf_option_parser(execution_data, binary_name)
242
- optparse = OptionParser.new
243
- banner = ["Usage:", binary_name] + full_name
244
- banner << "[<options...>]" unless @switches.empty?
245
- @required_args.each do |key, opts|
246
- banner << "<#{canonical_switch(key)}>"
277
+ class << self
278
+ def canonical_switch(name)
279
+ name.to_s.downcase.tr("_", "-").gsub(/[^a-z0-9-]/, "")
247
280
  end
248
- @optional_args.each do |key, opts|
249
- banner << "[<#{canonical_switch(key)}>]"
281
+ end
282
+
283
+ ##
284
+ # Representation of a formal switch
285
+ #
286
+ class SwitchInfo
287
+ def initialize(key, optparse_info)
288
+ @key = key
289
+ @optparse_info = optparse_info
290
+ end
291
+
292
+ attr_reader :key
293
+ attr_reader :optparse_info
294
+ end
295
+
296
+ ##
297
+ # Representation of a formal argument
298
+ #
299
+ class ArgInfo
300
+ def initialize(key, accept, doc)
301
+ @key = key
302
+ @accept = accept
303
+ @doc = doc
250
304
  end
251
- if @remaining_args
252
- banner << "[<#{canonical_switch(@remaining_args.first)}...>]"
305
+
306
+ attr_reader :key
307
+ attr_reader :accept
308
+ attr_reader :doc
309
+
310
+ def process_value(val)
311
+ return val unless accept
312
+ n = canonical_switch(key)
313
+ result = val
314
+ optparse = ::OptionParser.new
315
+ optparse.on("--#{n}=VALUE", accept) { |v| result = v }
316
+ optparse.parse(["--#{n}", val])
317
+ result
253
318
  end
254
- optparse.banner = banner.join(" ")
255
- desc = @long_desc || @short_desc || default_desc
256
- unless desc.empty?
257
- optparse.separator("")
258
- optparse.separator(desc)
259
- end
260
- optparse.separator("")
261
- optparse.separator("Options:")
262
- found_special_flags = []
263
- @switches.each do |key, opts|
264
- found_special_flags |= (opts & SPECIAL_FLAGS)
265
- optparse.on(*opts) do |val|
266
- execution_data[:options][key] = val
319
+
320
+ def canonical_name
321
+ Tool.canonical_switch(key)
322
+ end
323
+ end
324
+
325
+ ##
326
+ # An internal class that manages execution of a tool
327
+ # @private
328
+ #
329
+ class Execution
330
+ def initialize(tool)
331
+ @tool = tool
332
+ end
333
+
334
+ def execute(context_base, args)
335
+ return execute_alias(context_base, args) if @tool.alias?
336
+
337
+ parsed_args = ParsedArgs.new(@tool, context_base.binary_name, args)
338
+ context = create_child_context(context_base, parsed_args, args)
339
+
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
267
353
  end
268
354
  end
269
- flags = ["-v", "--verbose"] - found_special_flags
270
- unless flags.empty?
271
- optparse.on(*(flags + ["Increase verbosity"])) do
272
- execution_data[:delta_severity] -= 1
355
+
356
+ private
357
+
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)
273
363
  end
364
+ @tool.helpers.each do |name, block|
365
+ context.define_singleton_method(name, &block)
366
+ end
367
+ context
274
368
  end
275
- flags = ["-q", "--quiet"] - found_special_flags
276
- unless flags.empty?
277
- optparse.on(*(flags + ["Decrease verbosity"])) do
278
- execution_data[:delta_severity] += 1
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)
279
381
  end
280
382
  end
281
- flags = ["-?", "-h", "--help"] - found_special_flags
282
- unless flags.empty?
283
- optparse.on(*(flags + ["Show help message"])) do
284
- execution_data[:show_help] = true
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
285
417
  end
286
418
  end
287
- optparse
288
419
  end
289
420
 
290
- def collection_option_parser(execution_data, binary_name)
291
- optparse = OptionParser.new
292
- optparse.banner = (["Usage:", binary_name] + full_name + ["<command>", "[<options...>]"]).join(" ")
293
- desc = @long_desc || @short_desc || default_desc
294
- unless desc.empty?
295
- optparse.separator("")
296
- optparse.separator(desc)
297
- end
298
- optparse.separator("")
299
- optparse.separator("Options:")
300
- optparse.on("-?", "--help", "Show help message")
301
- optparse.on("-r", "--[no-]recursive", "Show all subcommands recursively") do |val|
302
- execution_data[:recursive] = val
303
- end
304
- execution_data[:show_help] = true
305
- optparse
306
- end
307
-
308
- def parse_args(args, binary_name)
309
- optdata = @default_data.dup
310
- execution_data = {
311
- show_help: false,
312
- usage_error: nil,
313
- delta_severity: 0,
314
- recursive: false,
315
- options: optdata
316
- }
317
- begin
318
- binary_name ||= File.basename($0)
319
- option_parser = @executor ?
320
- leaf_option_parser(execution_data, binary_name) :
321
- collection_option_parser(execution_data, binary_name)
322
- execution_data[:optparse] = option_parser
323
- remaining = option_parser.parse(args)
324
- @required_args.each do |key, accept, doc|
325
- if !execution_data[:show_help] && remaining.empty?
326
- error = OptionParser::ParseError.new(*args)
327
- error.reason = "No value given for required argument named <#{canonical_switch(key)}>"
328
- raise error
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)
465
+ rescue ::OptionParser::ParseError => e
466
+ @usage_error = e
467
+ end
468
+
469
+ def create_option_parser(tool, binary_name)
470
+ optparse = ::OptionParser.new
471
+ optparse.banner =
472
+ if tool.leaf?
473
+ leaf_banner(tool, binary_name)
474
+ else
475
+ collection_banner(tool, binary_name)
329
476
  end
330
- optdata[key] = process_value(key, remaining.shift, accept)
477
+ unless tool.effective_long_desc.empty?
478
+ optparse.separator("")
479
+ optparse.separator(tool.effective_long_desc)
331
480
  end
332
- @optional_args.each do |key, accept, doc|
333
- break if remaining.empty?
334
- optdata[key] = process_value(key, remaining.shift, accept)
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
335
522
  end
336
- unless remaining.empty?
337
- if !@remaining_args
338
- if @executor
339
- error = OptionParser::ParseError.new(*remaining)
340
- error.reason = "Extra arguments provided"
341
- raise error
342
- else
343
- error = OptionParser::ParseError.new(*(full_name + args))
344
- error.reason = "Tool not found"
345
- raise error
346
- 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)
528
+ optparse.on(*switch.optparse_info) do |val|
529
+ @data[switch.key] = val
347
530
  end
348
- key = @remaining_args[0]
349
- accept = @remaining_args[1]
350
- optdata[key] = remaining.map{ |arg| process_value(key, arg, accept) }
351
531
  end
352
- rescue OptionParser::ParseError => e
353
- execution_data[:usage_error] = e
354
532
  end
355
- execution_data
356
- end
357
533
 
358
- def create_child_context(parent_context, args, execution_data)
359
- context = parent_context._create_child(
360
- full_name, args, execution_data[:options])
361
- context.logger.level += execution_data[:delta_severity]
362
- @modules.each do |mod|
363
- context.extend(mod)
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
364
540
  end
365
- @helpers.each do |name, block|
366
- context.define_singleton_method(name, &block)
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
367
548
  end
368
- context
369
- end
370
549
 
371
- def show_usage(context, optparse, recursive: false)
372
- puts(optparse.to_s)
373
- if @executor
374
- if !@required_args.empty? || !@optional_args.empty? || @remaining_args
375
- puts("")
376
- puts("Positional arguments:")
377
- args_to_display = @required_args + @optional_args
378
- args_to_display << @remaining_args if @remaining_args
379
- args_to_display.each do |key, accept, doc|
380
- puts(" #{canonical_switch(key).ljust(31)} #{doc.first}")
381
- doc[1..-1].each do |d|
382
- puts(" #{d}")
383
- end unless doc.empty?
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?
561
+ reason = "No value given for required argument named <#{arg_info.canonical_name}>"
562
+ raise create_parse_error(args, reason)
384
563
  end
564
+ @data[arg_info.key] = arg_info.process_value(remaining.shift)
385
565
  end
386
- else
387
- puts("")
388
- puts("Commands:")
389
- name_len = full_name.length
390
- context._lookup.list_subtools(full_name, recursive).each do |tool|
391
- desc = tool.effective_short_desc
392
- tool_name = tool.full_name.slice(name_len..-1).join(' ').ljust(31)
393
- puts(" #{tool_name} #{desc}")
566
+ remaining
567
+ end
568
+
569
+ def parse_optional_args(remaining, tool)
570
+ tool.optional_args.each do |arg_info|
571
+ break if remaining.empty?
572
+ @data[arg_info.key] = arg_info.process_value(remaining.shift)
394
573
  end
574
+ remaining
395
575
  end
396
- end
397
576
 
398
- def canonical_switch(name)
399
- name.to_s.downcase.gsub("_", "-").gsub(/[^a-z0-9-]/, "")
400
- end
577
+ def parse_remaining_args(remaining, tool, args)
578
+ return if remaining.empty?
579
+ unless tool.remaining_args
580
+ if tool.leaf?
581
+ raise create_parse_error(remaining, "Extra arguments provided")
582
+ else
583
+ raise create_parse_error(tool.full_name + args, "Tool not found")
584
+ end
585
+ end
586
+ @data[tool.remaining_args.key] =
587
+ remaining.map { |arg| tool.remaining_args.process_value(arg) }
588
+ end
401
589
 
402
- def process_value(key, val, accept)
403
- return val unless accept
404
- n = canonical_switch(key)
405
- result = val
406
- optparse = OptionParser.new
407
- optparse.on("--#{n}=VALUE", accept){ |v| result = v }
408
- optparse.parse(["--#{n}", val])
409
- result
590
+ def create_parse_error(path, reason)
591
+ OptionParser::ParseError.new(*path).tap do |e|
592
+ e.reason = reason
593
+ end
594
+ end
410
595
  end
411
596
  end
412
597
  end