toys 0.2.1 → 0.2.2

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