toys-core 0.3.6 → 0.3.7

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/toys-core.rb +20 -5
  4. data/lib/toys/cli.rb +39 -32
  5. data/lib/toys/core_version.rb +1 -1
  6. data/lib/toys/{tool → definition}/acceptor.rb +21 -15
  7. data/lib/toys/{utils/line_output.rb → definition/alias.rb} +47 -59
  8. data/lib/toys/{tool/arg_definition.rb → definition/arg.rb} +17 -7
  9. data/lib/toys/{tool/flag_definition.rb → definition/flag.rb} +19 -9
  10. data/lib/toys/definition/tool.rb +574 -0
  11. data/lib/toys/dsl/arg.rb +118 -0
  12. data/lib/toys/dsl/flag.rb +132 -0
  13. data/lib/toys/dsl/tool.rb +521 -0
  14. data/lib/toys/errors.rb +2 -2
  15. data/lib/toys/helpers.rb +3 -3
  16. data/lib/toys/helpers/exec.rb +31 -25
  17. data/lib/toys/helpers/fileutils.rb +8 -2
  18. data/lib/toys/helpers/highline.rb +8 -1
  19. data/lib/toys/{alias.rb → helpers/terminal.rb} +44 -53
  20. data/lib/toys/input_file.rb +61 -0
  21. data/lib/toys/loader.rb +87 -77
  22. data/lib/toys/middleware.rb +3 -3
  23. data/lib/toys/middleware/add_verbosity_flags.rb +22 -20
  24. data/lib/toys/middleware/base.rb +53 -5
  25. data/lib/toys/middleware/handle_usage_errors.rb +9 -12
  26. data/lib/toys/middleware/set_default_descriptions.rb +6 -7
  27. data/lib/toys/middleware/show_help.rb +71 -67
  28. data/lib/toys/middleware/show_root_version.rb +9 -9
  29. data/lib/toys/runner.rb +157 -0
  30. data/lib/toys/template.rb +4 -3
  31. data/lib/toys/templates.rb +2 -2
  32. data/lib/toys/templates/clean.rb +2 -2
  33. data/lib/toys/templates/gem_build.rb +5 -5
  34. data/lib/toys/templates/minitest.rb +2 -2
  35. data/lib/toys/templates/rubocop.rb +2 -2
  36. data/lib/toys/templates/yardoc.rb +2 -2
  37. data/lib/toys/tool.rb +168 -625
  38. data/lib/toys/utils/exec.rb +19 -18
  39. data/lib/toys/utils/gems.rb +140 -0
  40. data/lib/toys/utils/help_text.rb +25 -20
  41. data/lib/toys/utils/terminal.rb +412 -0
  42. data/lib/toys/utils/wrappable_string.rb +3 -1
  43. metadata +15 -24
  44. data/lib/toys/config_dsl.rb +0 -699
  45. data/lib/toys/context.rb +0 -290
  46. data/lib/toys/helpers/spinner.rb +0 -142
@@ -28,7 +28,7 @@
28
28
  ;
29
29
 
30
30
  require "toys/middleware/base"
31
- require "toys/utils/line_output"
31
+ require "toys/utils/terminal"
32
32
 
33
33
  module Toys
34
34
  module Middleware
@@ -65,16 +65,16 @@ module Toys
65
65
  @version_string = version_string
66
66
  @version_flags = version_flags
67
67
  @version_flag_desc = version_flag_desc
68
- @output = Utils::LineOutput.new(stream)
68
+ @terminal = Utils::Terminal.new(output: stream)
69
69
  end
70
70
 
71
71
  ##
72
72
  # Adds the version flag if requested.
73
73
  #
74
- def config(tool, _loader)
75
- if @version_string && tool.root?
76
- tool.add_flag(:_show_version, @version_flags,
77
- report_collisions: false, desc: @version_flag_desc)
74
+ def config(tool_definition, _loader)
75
+ if @version_string && tool_definition.root?
76
+ tool_definition.add_flag(:_show_version, @version_flags,
77
+ report_collisions: false, desc: @version_flag_desc)
78
78
  end
79
79
  yield
80
80
  end
@@ -82,9 +82,9 @@ module Toys
82
82
  ##
83
83
  # This middleware displays the version.
84
84
  #
85
- def execute(context)
86
- if context[:_show_version]
87
- @output.puts(@version_string)
85
+ def run(tool)
86
+ if tool[:_show_version]
87
+ @terminal.puts(@version_string)
88
88
  else
89
89
  yield
90
90
  end
@@ -0,0 +1,157 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ require "optparse"
31
+
32
+ module Toys
33
+ ##
34
+ # An internal class that manages execution of a tool
35
+ # @private
36
+ #
37
+ class Runner
38
+ def initialize(cli, tool_definition)
39
+ @cli = cli
40
+ @tool_definition = tool_definition
41
+ end
42
+
43
+ def run(args, verbosity: 0)
44
+ data = create_data(args, verbosity)
45
+ parse_args(args, data)
46
+ tool = @tool_definition.tool_class.new(@cli, data)
47
+
48
+ original_level = @cli.logger.level
49
+ @cli.logger.level = @cli.base_level - data[Tool::Keys::VERBOSITY]
50
+ begin
51
+ perform_execution(tool)
52
+ ensure
53
+ @cli.logger.level = original_level
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def create_data(args, base_verbosity)
60
+ data = @tool_definition.default_data.dup
61
+ data[Tool::Keys::TOOL_DEFINITION] = @tool_definition
62
+ data[Tool::Keys::TOOL_NAME] = @tool_definition.full_name
63
+ data[Tool::Keys::VERBOSITY] = base_verbosity
64
+ data[Tool::Keys::ARGS] = args
65
+ data[Tool::Keys::USAGE_ERROR] = nil
66
+ data
67
+ end
68
+
69
+ def parse_args(args, data)
70
+ optparse = create_option_parser(data)
71
+ remaining = optparse.parse(args)
72
+ remaining = parse_required_args(remaining, args, data)
73
+ remaining = parse_optional_args(remaining, data)
74
+ parse_remaining_args(remaining, args, data)
75
+ rescue ::OptionParser::ParseError => e
76
+ data[Tool::Keys::USAGE_ERROR] = e.message
77
+ end
78
+
79
+ def create_option_parser(data)
80
+ optparse = ::OptionParser.new
81
+ # The following clears out the Officious (hidden default flags).
82
+ optparse.remove
83
+ optparse.remove
84
+ optparse.new
85
+ optparse.new
86
+ @tool_definition.flag_definitions.each do |flag|
87
+ optparse.on(*flag.optparser_info) do |val|
88
+ data[flag.key] = flag.handler.call(val, data[flag.key])
89
+ end
90
+ end
91
+ @tool_definition.custom_acceptors do |accept|
92
+ optparse.accept(accept)
93
+ end
94
+ optparse
95
+ end
96
+
97
+ def parse_required_args(remaining, args, data)
98
+ @tool_definition.required_arg_definitions.each do |arg_info|
99
+ if remaining.empty?
100
+ reason = "No value given for required argument #{arg_info.display_name}"
101
+ raise create_parse_error(args, reason)
102
+ end
103
+ data[arg_info.key] = arg_info.process_value(remaining.shift)
104
+ end
105
+ remaining
106
+ end
107
+
108
+ def parse_optional_args(remaining, data)
109
+ @tool_definition.optional_arg_definitions.each do |arg_info|
110
+ break if remaining.empty?
111
+ data[arg_info.key] = arg_info.process_value(remaining.shift)
112
+ end
113
+ remaining
114
+ end
115
+
116
+ def parse_remaining_args(remaining, args, data)
117
+ return if remaining.empty?
118
+ unless @tool_definition.remaining_args_definition
119
+ if @tool_definition.runnable?
120
+ raise create_parse_error(remaining, "Extra arguments provided")
121
+ else
122
+ raise create_parse_error(@tool_definition.full_name + args, "Tool not found")
123
+ end
124
+ end
125
+ data[@tool_definition.remaining_args_definition.key] =
126
+ remaining.map { |arg| @tool_definition.remaining_args_definition.process_value(arg) }
127
+ end
128
+
129
+ def create_parse_error(path, reason)
130
+ OptionParser::ParseError.new(*path).tap do |e|
131
+ e.reason = reason
132
+ end
133
+ end
134
+
135
+ def perform_execution(tool)
136
+ executor = proc do
137
+ if @tool_definition.runnable?
138
+ tool.run
139
+ else
140
+ @cli.logger.fatal("No implementation for tool #{@tool_definition.display_name.inspect}")
141
+ tool.exit(-1)
142
+ end
143
+ end
144
+ @tool_definition.middleware_stack.reverse.each do |middleware|
145
+ executor = make_executor(middleware, tool, executor)
146
+ end
147
+ catch(:result) do
148
+ executor.call
149
+ 0
150
+ end
151
+ end
152
+
153
+ def make_executor(middleware, tool, next_executor)
154
+ proc { middleware.run(tool, &next_executor) }
155
+ end
156
+ end
157
+ end
data/lib/toys/template.rb CHANGED
@@ -43,8 +43,8 @@ module Toys
43
43
  # The class defines the "configuration" of the template. If your template
44
44
  # has options/parameters, you should provide a constructor, and methods
45
45
  # appropriate to edit those options. The arguments given to the
46
- # {Toys::ConfigDSL#expand} method are passed to your constructor, and your
47
- # template object is passed to any block given to {Toys::ConfigDSL#expand}.
46
+ # {Toys::DSL::Tool#expand} method are passed to your constructor, and your
47
+ # template object is passed to any block given to {Toys::DSL::Tool#expand}.
48
48
  #
49
49
  # Next, in your template class, call the `to_expand` method, which is defined
50
50
  # in {Toys::Template::ClassMethods#to_expand}. Pass this a block which
@@ -77,7 +77,7 @@ module Toys
77
77
  # to_expand do |template|
78
78
  # desc "Prints a greeting to #{template.name}"
79
79
  # tool "templated-greeting" do
80
- # script do
80
+ # run do
81
81
  # puts "Hello, #{template.name}!"
82
82
  # end
83
83
  # end
@@ -100,6 +100,7 @@ module Toys
100
100
  ## @private
101
101
  def self.included(mod)
102
102
  mod.extend(ClassMethods)
103
+ mod.include(Tool::Keys)
103
104
  end
104
105
 
105
106
  ##
@@ -48,8 +48,8 @@ module Toys
48
48
  # @param [String,Symbol] name Name of the template class to return
49
49
  # @return [Class,nil] The class, or `nil` if not found
50
50
  #
51
- def self.lookup(name)
52
- Utils::ModuleLookup.lookup(:templates, name)
51
+ def self.lookup!(name)
52
+ Utils::ModuleLookup.lookup!(:templates, name)
53
53
  end
54
54
  end
55
55
  end
@@ -61,9 +61,9 @@ module Toys
61
61
  tool(template.name) do
62
62
  desc "Clean built files and directories."
63
63
 
64
- use :fileutils
64
+ include :fileutils
65
65
 
66
- script do
66
+ run do
67
67
  files = []
68
68
  patterns = Array(template.paths)
69
69
  patterns.each do |pattern|
@@ -90,11 +90,11 @@ module Toys
90
90
 
91
91
  flag :yes, "-y", "--yes", desc: "Do not ask for interactive confirmation"
92
92
 
93
- use :exec
94
- use :fileutils
95
- use :highline
93
+ include :exec
94
+ include :fileutils
95
+ include :terminal
96
96
 
97
- script do
97
+ run do
98
98
  configure_exec(exit_on_nonzero_status: true)
99
99
  gemspec = ::Gem::Specification.load "#{template.gem_name}.gemspec"
100
100
  version = gemspec.version
@@ -107,7 +107,7 @@ module Toys
107
107
  logger.error "Cannot push the gem when there are uncommited changes"
108
108
  exit(1)
109
109
  end
110
- exit(1) unless options[:yes] || agree("Release #{gemfile}? (y/n) ")
110
+ exit(1) unless option(:yes) || confirm("Release #{gemfile}?")
111
111
  sh "gem push pkg/#{gemfile}"
112
112
  if template.tag
113
113
  sh "git tag v#{version}"
@@ -84,7 +84,7 @@ module Toys
84
84
  tool(template.name) do
85
85
  desc "Run minitest on the current project."
86
86
 
87
- use :exec
87
+ include :exec
88
88
 
89
89
  flag :warnings, "-w", "--[no-]warnings",
90
90
  default: template.warnings,
@@ -92,7 +92,7 @@ module Toys
92
92
 
93
93
  remaining_args :tests, desc: "Paths to the tests to run (defaults to all tests)"
94
94
 
95
- script do
95
+ run do
96
96
  ruby_args = []
97
97
  unless template.libs.empty?
98
98
  lib_path = template.libs.join(::File::PATH_SEPARATOR)
@@ -67,9 +67,9 @@ module Toys
67
67
  tool(template.name) do
68
68
  desc "Run rubocop on the current project."
69
69
 
70
- use :exec
70
+ include :exec
71
71
 
72
- script do
72
+ run do
73
73
  require "rubocop"
74
74
  cli = ::RuboCop::CLI.new
75
75
  logger.info "Running RuboCop..."
@@ -71,9 +71,9 @@ module Toys
71
71
  tool(template.name) do
72
72
  desc "Run yardoc on the current project."
73
73
 
74
- use :exec
74
+ include :exec
75
75
 
76
- script do
76
+ run do
77
77
  require "yard"
78
78
  files = []
79
79
  patterns = Array(template.files)
data/lib/toys/tool.rb CHANGED
@@ -27,712 +27,255 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "optparse"
31
-
32
- require "toys/utils/wrappable_string"
30
+ require "logger"
33
31
 
34
32
  module Toys
35
33
  ##
36
- # A Tool is a single command that can be invoked using Toys.
37
- # It has a name, a series of one or more words that you use to identify
38
- # the tool on the command line. It also has a set of formal flags and
39
- # command line arguments supported, and a block that gets run when the
40
- # tool is executed.
34
+ # This class manages the object context in effect during the execution of a
35
+ # tool. The context is a hash of key-value pairs.
36
+ #
37
+ # Flags and arguments defined by your tool normally report their values in
38
+ # the context, using keys that are strings or symbols.
39
+ #
40
+ # Keys that are neither strings nor symbols are by convention used for other
41
+ # context information, including:
42
+ # * Common information such as the {Toys::Definition::Tool} object being
43
+ # executed, the arguments originally passed to it, or the usage error
44
+ # string. These well-known keys can be accessed via constants in the
45
+ # {Toys::Tool::Keys} module.
46
+ # * Common settings such as the verbosity level, and whether to exit
47
+ # immediately if a subprocess exits with a nonzero result. These keys are
48
+ # also present as {Toys::Context} constants.
49
+ # * Private information used internally by middleware and helpers.
50
+ #
51
+ # This class provides convenience accessors for common keys and settings, and
52
+ # you can retrieve argument-set keys using the {#options} hash.
41
53
  #
42
54
  class Tool
43
55
  ##
44
- # Built-in acceptors (i.e. those recognized by OptionParser).
45
- # You can reference these acceptors directly. Otherwise, you have to add
46
- # one explicitly to the tool using {Tool#add_acceptor}.
47
- #
48
- OPTPARSER_ACCEPTORS = [
49
- ::Object,
50
- ::NilClass,
51
- ::String,
52
- ::Integer,
53
- ::Float,
54
- ::Numeric,
55
- ::TrueClass,
56
- ::FalseClass,
57
- ::Array,
58
- ::Regexp,
59
- ::OptionParser::DecimalInteger,
60
- ::OptionParser::OctalInteger,
61
- ::OptionParser::DecimalNumeric
62
- ].freeze
63
-
64
- ##
65
- # Create a new tool.
66
- #
67
- # @param [Array<String>] full_name The name of the tool
68
- #
69
- def initialize(full_name)
70
- @full_name = full_name.dup.freeze
71
- @middleware_stack = []
72
-
73
- @definition_path = nil
74
- @definition_finished = false
75
-
76
- @desc = Toys::Utils::WrappableString.new("")
77
- @long_desc = []
78
-
79
- @default_data = {}
80
- @acceptors = {}
81
- OPTPARSER_ACCEPTORS.each { |a| @acceptors[a] = a }
82
- @used_flags = []
83
-
84
- @flag_definitions = []
85
- @required_arg_definitions = []
86
- @optional_arg_definitions = []
87
- @remaining_args_definition = nil
88
-
89
- @helpers = {}
90
- @modules = []
91
- @script = nil
56
+ # Well-known context keys.
57
+ #
58
+ module Keys
59
+ ##
60
+ # Context key for the currently running CLI.
61
+ # @return [Object]
62
+ #
63
+ CLI = ::Object.new.freeze
64
+
65
+ ##
66
+ # Context key for the verbosity value. Verbosity is an integer defaulting
67
+ # to 0, with higher values meaning more verbose and lower meaning quieter.
68
+ # @return [Object]
69
+ #
70
+ VERBOSITY = ::Object.new.freeze
71
+
72
+ ##
73
+ # Context key for the `Toys::Definition::Tool` object being executed.
74
+ # @return [Object]
75
+ #
76
+ TOOL_DEFINITION = ::Object.new.freeze
77
+
78
+ ##
79
+ # Context key for the full name of the tool being executed. Value is an
80
+ # array of strings.
81
+ # @return [Object]
82
+ #
83
+ TOOL_NAME = ::Object.new.freeze
84
+
85
+ ##
86
+ # Context key for the active `Toys::Loader` object.
87
+ # @return [Object]
88
+ #
89
+ LOADER = ::Object.new.freeze
90
+
91
+ ##
92
+ # Context key for the active `Logger` object.
93
+ # @return [Object]
94
+ #
95
+ LOGGER = ::Object.new.freeze
96
+
97
+ ##
98
+ # Context key for the name of the toys binary. Value is a string.
99
+ # @return [Object]
100
+ #
101
+ BINARY_NAME = ::Object.new.freeze
102
+
103
+ ##
104
+ # Context key for the argument list passed to the current tool. Value is
105
+ # an array of strings.
106
+ # @return [Object]
107
+ #
108
+ ARGS = ::Object.new.freeze
109
+
110
+ ##
111
+ # Context key for the usage error raised. Value is a string if there was
112
+ # an error, or nil if there was no error.
113
+ # @return [Object]
114
+ #
115
+ USAGE_ERROR = ::Object.new.freeze
92
116
  end
93
117
 
94
118
  ##
95
- # Return the name of the tool as an array of strings.
96
- # This array may not be modified.
97
- # @return [Array<String>]
98
- #
99
- attr_reader :full_name
100
-
101
- ##
102
- # Returns the short description string.
103
- # @return [Toys::Utils::WrappableString]
104
- #
105
- attr_reader :desc
106
-
107
- ##
108
- # Returns the long description strings as an array.
109
- # @return [Array<Toys::Utils::WrappableString>]
110
- #
111
- attr_reader :long_desc
112
-
113
- ##
114
- # Return a list of all defined flags.
115
- # @return [Array<Toys::Tool::FlagDefinition>]
116
- #
117
- attr_reader :flag_definitions
118
-
119
- ##
120
- # Return a list of all defined required positional arguments.
121
- # @return [Array<Toys::Tool::ArgDefinition>]
122
- #
123
- attr_reader :required_arg_definitions
124
-
125
- ##
126
- # Return a list of all defined optional positional arguments.
127
- # @return [Array<Toys::Tool::ArgDefinition>]
128
- #
129
- attr_reader :optional_arg_definitions
130
-
131
- ##
132
- # Return the remaining arguments specification, or `nil` if remaining
133
- # arguments are currently not supported by this tool.
134
- # @return [Toys::Tool::ArgDefinition,nil]
135
- #
136
- attr_reader :remaining_args_definition
137
-
138
- ##
139
- # Return a list of flags that have been used in the flag definitions.
140
- # @return [Array<String>]
141
- #
142
- attr_reader :used_flags
143
-
144
- ##
145
- # Return the default argument data.
146
- # @return [Hash]
147
- #
148
- attr_reader :default_data
149
-
150
- ##
151
- # Return a list of modules that will be available during execution.
152
- # @return [Array<Module>]
153
- #
154
- attr_reader :modules
155
-
156
- ##
157
- # Return a list of helper methods that will be available during execution.
158
- # @return [Hash{Symbol => Proc}]
159
- #
160
- attr_reader :helpers
161
-
162
- ##
163
- # Return the script block, or `nil` if not present.
164
- # @return [Proc,nil]
165
- #
166
- attr_reader :script
167
-
168
- ##
169
- # Returns the middleware stack
170
- # @return [Array<Object>]
171
- #
172
- attr_reader :middleware_stack
173
-
174
- ##
175
- # Returns the path to the file that contains the definition of this tool.
176
- # @return [String]
177
- #
178
- attr_reader :definition_path
179
-
180
- ##
181
- # Returns the local name of this tool.
182
- # @return [String]
183
- #
184
- def simple_name
185
- full_name.last
186
- end
187
-
188
- ##
189
- # Returns a displayable name of this tool, generally the full name
190
- # delimited by spaces.
191
- # @return [String]
192
- #
193
- def display_name
194
- full_name.join(" ")
195
- end
196
-
197
- ##
198
- # Returns true if this tool is a root tool.
199
- # @return [Boolean]
119
+ # Create a Context object. Applications generally will not need to create
120
+ # these objects directly; they are created by the tool when it is preparing
121
+ # for execution.
122
+ # @private
200
123
  #
201
- def root?
202
- full_name.empty?
203
- end
204
-
205
- ##
206
- # Returns true if this tool has an script defined.
207
- # @return [Boolean]
124
+ # @param [Toys::CLI] cli
125
+ # @param [Hash] data
208
126
  #
209
- def includes_script?
210
- script.is_a?(::Proc)
127
+ def initialize(cli, data)
128
+ @__data = data
129
+ @__data[Keys::CLI] = cli
130
+ @__data[Keys::LOADER] = cli.loader
131
+ @__data[Keys::BINARY_NAME] = cli.binary_name
132
+ @__data[Keys::LOGGER] = cli.logger
211
133
  end
212
134
 
213
135
  ##
214
- # Returns true if there is a specific description set for this tool.
215
- # @return [Boolean]
136
+ # Return the currently running CLI.
137
+ # @return [Toys::CLI]
216
138
  #
217
- def includes_description?
218
- !long_desc.empty? || !desc.empty?
139
+ def cli
140
+ @__data[Keys::CLI]
219
141
  end
220
142
 
221
143
  ##
222
- # Returns true if at least one flag or positional argument is defined
223
- # for this tool.
224
- # @return [Boolean]
144
+ # Return the current verbosity setting as an integer.
145
+ # @return [Integer]
225
146
  #
226
- def includes_arguments?
227
- !default_data.empty? || !flag_definitions.empty? ||
228
- !required_arg_definitions.empty? || !optional_arg_definitions.empty? ||
229
- !remaining_args_definition.nil?
147
+ def verbosity
148
+ @__data[Keys::VERBOSITY]
230
149
  end
231
150
 
232
151
  ##
233
- # Returns true if at least one helper method or module is added to this
234
- # tool.
235
- # @return [Boolean]
152
+ # Return the tool being executed.
153
+ # @return [Toys::Definition::Tool]
236
154
  #
237
- def includes_helpers?
238
- !helpers.empty? || !modules.empty?
155
+ def tool_definition
156
+ @__data[Keys::TOOL_DEFINITION]
239
157
  end
240
158
 
241
159
  ##
242
- # Returns true if this tool has any definition information.
243
- # @return [Boolean]
160
+ # Return the name of the tool being executed, as an array of strings.
161
+ # @return [Array[String]]
244
162
  #
245
- def includes_definition?
246
- includes_arguments? || includes_script? || includes_helpers?
163
+ def tool_name
164
+ @__data[Keys::TOOL_NAME]
247
165
  end
248
166
 
249
167
  ##
250
- # Returns true if this tool's definition has been finished and is locked.
251
- # @return [Boolean]
168
+ # Return the raw arguments passed to the tool, as an array of strings.
169
+ # This does not include the tool name itself.
170
+ # @return [Array[String]]
252
171
  #
253
- def definition_finished?
254
- @definition_finished
172
+ def args
173
+ @__data[Keys::ARGS]
255
174
  end
256
175
 
257
176
  ##
258
- # Returns all arg definitions in order: required, optional, remaining.
259
- # @return [Array<Toys::Tool::ArgDefinition>]
177
+ # Return any usage error detected during argument parsing, or `nil` if
178
+ # no error was detected.
179
+ # @return [String,nil]
260
180
  #
261
- def arg_definitions
262
- result = required_arg_definitions + optional_arg_definitions
263
- result << remaining_args_definition if remaining_args_definition
264
- result
181
+ def usage_error
182
+ @__data[Keys::USAGE_ERROR]
265
183
  end
266
184
 
267
185
  ##
268
- # Returns a list of all custom acceptors used by this tool.
269
- # @return [Array<Toys::Tool::Acceptor>]
186
+ # Return the logger for this execution.
187
+ # @return [Logger]
270
188
  #
271
- def custom_acceptors
272
- result = []
273
- flag_definitions.each do |f|
274
- result << f.accept if f.accept.is_a?(Acceptor)
275
- end
276
- arg_definitions.each do |a|
277
- result << a.accept if a.accept.is_a?(Acceptor)
278
- end
279
- result.uniq
189
+ def logger
190
+ @__data[Keys::LOGGER]
280
191
  end
281
192
 
282
193
  ##
283
- # Sets the path to the file that defines this tool.
284
- # A tool may be defined from at most one path. If a different path is
285
- # already set, raises {Toys::ToolDefinitionError}
194
+ # Return the active loader that can be used to get other tools.
195
+ # @return [Toys::Loader]
286
196
  #
287
- # @param [String] path The path to the file defining this tool
288
- #
289
- def lock_definition_path(path)
290
- if definition_path && definition_path != path
291
- raise ToolDefinitionError,
292
- "Cannot redefine tool #{display_name.inspect} in #{path}" \
293
- " (already defined in #{definition_path})"
294
- end
295
- @definition_path = path
197
+ def loader
198
+ @__data[Keys::LOADER]
296
199
  end
297
200
 
298
201
  ##
299
- # Set the short description string.
300
- #
301
- # The description may be provided as a {Toys::Utils::WrappableString}, a
302
- # single string (which will be wrapped), or an array of strings, which will
303
- # be interpreted as string fragments that will be concatenated and wrapped.
304
- #
305
- # @param [Toys::Utils::WrappableString,String,Array<String>] desc
202
+ # Return the name of the binary that was executed.
203
+ # @return [String]
306
204
  #
307
- def desc=(desc)
308
- check_definition_state
309
- @desc = Utils::WrappableString.make(desc)
205
+ def binary_name
206
+ @__data[Keys::BINARY_NAME]
310
207
  end
311
208
 
312
209
  ##
313
- # Set the long description strings.
210
+ # Return an option or other piece of data by key.
314
211
  #
315
- # Each string may be provided as a {Toys::Utils::WrappableString}, a single
316
- # string (which will be wrapped), or an array of strings, which will be
317
- # interpreted as string fragments that will be concatenated and wrapped.
212
+ # @param [Symbol] key
213
+ # @return [Object]
318
214
  #
319
- # @param [Array<Toys::Utils::WrappableString,String,Array<String>>] descs
320
- #
321
- def long_desc=(descs)
322
- check_definition_state
323
- @long_desc = Utils::WrappableString.make_array(descs)
215
+ def [](key)
216
+ @__data[key]
324
217
  end
218
+ alias get []
325
219
 
326
220
  ##
327
- # Define a helper method that will be available during execution.
328
- # Pass the name of the method in the argument, and provide a block with
329
- # the method body. Note the method name may not start with an underscore.
221
+ # Set an option or other piece of context data by key.
330
222
  #
331
- # @param [String] name The method name
223
+ # @param [Symbol] key
224
+ # @param [Object] value
332
225
  #
333
- def add_helper(name, &block)
334
- check_definition_state
335
- name_str = name.to_s
336
- unless name_str =~ /^[a-z]\w+$/
337
- raise ToolDefinitionError, "Illegal helper name: #{name_str.inspect}"
338
- end
339
- @helpers[name.to_sym] = block
340
- self
226
+ def []=(key, value)
227
+ @__data[key] = value
341
228
  end
342
229
 
343
230
  ##
344
- # Mix in the given module during execution. You may provide the module
345
- # itself, or the name of a well-known module under {Toys::Helpers}.
231
+ # Set an option or other piece of context data by key.
346
232
  #
347
- # @param [Module,String] name The module or module name.
233
+ # @param [Symbol] key
234
+ # @param [Object] value
348
235
  #
349
- def use_module(name)
350
- check_definition_state
351
- case name
352
- when ::Module
353
- @modules << name
354
- when ::Symbol
355
- mod = Helpers.lookup(name.to_s)
356
- if mod.nil?
357
- raise ToolDefinitionError, "Module not found: #{name.inspect}"
358
- end
359
- @modules << mod
236
+ def set(key, value = nil)
237
+ if key.is_a?(::Hash)
238
+ @__data.merge!(key)
360
239
  else
361
- raise ToolDefinitionError, "Illegal helper module name: #{name.inspect}"
240
+ @__data[key] = value
362
241
  end
363
242
  self
364
243
  end
365
244
 
366
245
  ##
367
- # Add an acceptor to the tool. This acceptor may be refereneced by name
368
- # when adding a flag or an arg.
369
- #
370
- # @param [Toys::Tool::Acceptor] acceptor The acceptor to add.
246
+ # Returns the subset of the context that uses string or symbol keys. By
247
+ # convention, this includes keys that are set by tool flags and arguments,
248
+ # but does not include well-known context values such as verbosity or
249
+ # private context values used by middleware or helpers.
371
250
  #
372
- def add_acceptor(acceptor)
373
- @acceptors[acceptor.name] = acceptor
374
- self
375
- end
376
-
377
- ##
378
- # Add a flag to the current tool. Each flag must specify a key which
379
- # the script may use to obtain the flag value from the context.
380
- # You may then provide the flags themselves in `OptionParser` form.
381
- #
382
- # @param [Symbol] key The key to use to retrieve the value from the
383
- # execution context.
384
- # @param [Array<String>] flags The flags in OptionParser format.
385
- # @param [Object] accept An acceptor that validates and/or converts the
386
- # value. You may provide either the name of an acceptor you have
387
- # defined, or one of the default acceptors provided by OptionParser.
388
- # Optional. If not specified, accepts any value as a string.
389
- # @param [Object] default The default value. This is the value that will
390
- # be set in the context if this flag is not provided on the command
391
- # line. Defaults to `nil`.
392
- # @param [Proc,nil] handler An optional handler for setting/updating the
393
- # value. If given, it should take two arguments, the new given value
394
- # and the previous value, and it should return the new value that
395
- # should be set. The default handler simply replaces the previous
396
- # value. i.e. the default is effectively `-> (val, _prev) { val }`.
397
- # @param [Boolean] report_collisions Raise an exception if a flag is
398
- # requested that is already in use or marked as disabled. Default is
399
- # true.
400
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
401
- # description for the flag. See {Toys::Tool#desc=} for a description of
402
- # allowed formats. Defaults to the empty string.
403
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
404
- # Long description for the flag. See {Toys::Tool#long_desc=} for a
405
- # description of allowed formats. Defaults to the empty array.
406
- #
407
- def add_flag(key, flags = [],
408
- accept: nil, default: nil, handler: nil,
409
- report_collisions: true,
410
- desc: nil, long_desc: nil)
411
- check_definition_state
412
- accept = resolve_acceptor(accept)
413
- flag_def = FlagDefinition.new(key, flags, @used_flags, report_collisions,
414
- accept, handler, default)
415
- flag_def.desc = desc if desc
416
- flag_def.long_desc = long_desc if long_desc
417
- @flag_definitions << flag_def if flag_def.active?
418
- @default_data[key] = default
419
- self
420
- end
421
-
422
- ##
423
- # Mark one or more flags as disabled, preventing their use by any
424
- # subsequent flag definition. This may be used to prevent middleware from
425
- # defining a particular flag.
426
- #
427
- # @param [String...] flags The flags to disable
251
+ # @return [Hash]
428
252
  #
429
- def disable_flag(*flags)
430
- flags = flags.uniq
431
- intersection = @used_flags & flags
432
- unless intersection.empty?
433
- raise ToolDefinitionError, "Cannot disable flags already used: #{intersection.inspect}"
253
+ def options
254
+ @__data.select do |k, _v|
255
+ k.is_a?(::Symbol) || k.is_a?(::String)
434
256
  end
435
- @used_flags.concat(flags)
436
- self
437
257
  end
438
258
 
439
259
  ##
440
- # Add a required positional argument to the current tool. You must specify
441
- # a key which the script may use to obtain the argument value from the
442
- # context.
260
+ # Returns the value of the given option. Returns only options with string
261
+ # or symbol keys; returns `nil` if passed other well-known context keys
262
+ # such as verbosity.
443
263
  #
444
- # @param [Symbol] key The key to use to retrieve the value from the
445
- # execution context.
446
- # @param [Object] accept An acceptor that validates and/or converts the
447
- # value. You may provide either the name of an acceptor you have
448
- # defined, or one of the default acceptors provided by OptionParser.
449
- # Optional. If not specified, accepts any value as a string.
450
- # @param [String] display_name A name to use for display (in help text and
451
- # error reports). Defaults to the key in upper case.
452
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
453
- # description for the flag. See {Toys::Tool#desc=} for a description of
454
- # allowed formats. Defaults to the empty string.
455
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
456
- # Long description for the flag. See {Toys::Tool#long_desc=} for a
457
- # description of allowed formats. Defaults to the empty array.
264
+ # @param [String,Symbol] key
265
+ # @return [Object]
458
266
  #
459
- def add_required_arg(key, accept: nil, display_name: nil, desc: nil, long_desc: nil)
460
- check_definition_state
461
- accept = resolve_acceptor(accept)
462
- arg_def = ArgDefinition.new(key, :required, accept, nil, desc, long_desc, display_name)
463
- @required_arg_definitions << arg_def
464
- self
267
+ def option(key)
268
+ key.is_a?(::Symbol) || key.is_a?(::String) ? @__data[key] : nil
465
269
  end
466
270
 
467
271
  ##
468
- # Add an optional positional argument to the current tool. You must specify
469
- # a key which the script may use to obtain the argument value from the
470
- # context. If an optional argument is not given on the command line, the
471
- # value is set to the given default.
272
+ # Exit immediately with the given status code
472
273
  #
473
- # @param [Symbol] key The key to use to retrieve the value from the
474
- # execution context.
475
- # @param [Object] default The default value. This is the value that will
476
- # be set in the context if this argument is not provided on the command
477
- # line. Defaults to `nil`.
478
- # @param [Object] accept An acceptor that validates and/or converts the
479
- # value. You may provide either the name of an acceptor you have
480
- # defined, or one of the default acceptors provided by OptionParser.
481
- # Optional. If not specified, accepts any value as a string.
482
- # @param [String] display_name A name to use for display (in help text and
483
- # error reports). Defaults to the key in upper case.
484
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
485
- # description for the flag. See {Toys::Tool#desc=} for a description of
486
- # allowed formats. Defaults to the empty string.
487
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
488
- # Long description for the flag. See {Toys::Tool#long_desc=} for a
489
- # description of allowed formats. Defaults to the empty array.
274
+ # @param [Integer] code The status code, which should be 0 for no error,
275
+ # or nonzero for an error condition.
490
276
  #
491
- def add_optional_arg(key, default: nil, accept: nil, display_name: nil,
492
- desc: nil, long_desc: nil)
493
- check_definition_state
494
- accept = resolve_acceptor(accept)
495
- arg_def = ArgDefinition.new(key, :optional, accept, default, desc, long_desc, display_name)
496
- @optional_arg_definitions << arg_def
497
- @default_data[key] = default
498
- self
499
- end
500
-
501
- ##
502
- # Specify what should be done with unmatched positional arguments. You must
503
- # specify a key which the script may use to obtain the remaining args
504
- # from the context.
505
- #
506
- # @param [Symbol] key The key to use to retrieve the value from the
507
- # execution context.
508
- # @param [Object] default The default value. This is the value that will
509
- # be set in the context if no unmatched arguments are provided on the
510
- # command line. Defaults to the empty array `[]`.
511
- # @param [Object] accept An acceptor that validates and/or converts the
512
- # value. You may provide either the name of an acceptor you have
513
- # defined, or one of the default acceptors provided by OptionParser.
514
- # Optional. If not specified, accepts any value as a string.
515
- # @param [String] display_name A name to use for display (in help text and
516
- # error reports). Defaults to the key in upper case.
517
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
518
- # description for the flag. See {Toys::Tool#desc=} for a description of
519
- # allowed formats. Defaults to the empty string.
520
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
521
- # Long description for the flag. See {Toys::Tool#long_desc=} for a
522
- # description of allowed formats. Defaults to the empty array.
523
- #
524
- def set_remaining_args(key, default: [], accept: nil, display_name: nil,
525
- desc: nil, long_desc: nil)
526
- check_definition_state
527
- accept = resolve_acceptor(accept)
528
- arg_def = ArgDefinition.new(key, :remaining, accept, default, desc, long_desc, display_name)
529
- @remaining_args_definition = arg_def
530
- @default_data[key] = default
531
- self
532
- end
533
-
534
- ##
535
- # Set the script for this tool. This is a proc that will be called,
536
- # with `self` set to a {Toys::Context}.
537
- #
538
- # @param [Proc] script The script for this tool.
539
- #
540
- def script=(script)
541
- check_definition_state
542
- @script = script
543
- end
544
-
545
- ##
546
- # Execute this tool in the given context.
547
- #
548
- # @param [Toys::CLI] cli The CLI execution context
549
- # @param [Array<String>] args The arguments to pass to the tool. Should
550
- # not include the tool name.
551
- # @param [Integer] verbosity The starting verbosity. Defaults to 0.
552
- #
553
- # @return [Integer] The result code.
554
- #
555
- def execute(cli, args, verbosity: 0)
556
- ContextualError.capture_path(
557
- "Error during tool execution!", definition_path,
558
- tool_name: full_name, tool_args: args
559
- ) do
560
- Execution.new(self).execute(cli, args, verbosity: verbosity)
561
- end
562
- end
563
-
564
- ##
565
- # Complete definition and run middleware configs
566
- # @param [Toys::Loader] loader
567
- #
568
- # @private
569
- #
570
- def finish_definition(loader)
571
- unless @definition_finished
572
- ContextualError.capture("Error installing tool middleware!", tool_name: full_name) do
573
- config_proc = proc {}
574
- middleware_stack.reverse.each do |middleware|
575
- config_proc = make_config_proc(middleware, loader, config_proc)
576
- end
577
- config_proc.call
578
- end
579
- @definition_finished = true
580
- end
581
- self
582
- end
583
-
584
- private
585
-
586
- def make_config_proc(middleware, loader, next_config)
587
- proc { middleware.config(self, loader, &next_config) }
588
- end
589
-
590
- def check_definition_state
591
- if @definition_finished
592
- raise ToolDefinitionError,
593
- "Defintion of tool #{display_name.inspect} is already finished"
594
- end
595
- end
596
-
597
- def resolve_acceptor(accept)
598
- return accept if accept.nil? || accept.is_a?(Acceptor)
599
- unless @acceptors.key?(accept)
600
- raise ToolDefinitionError, "Unknown acceptor: #{accept.inspect}"
601
- end
602
- @acceptors[accept]
603
- end
604
-
605
- ##
606
- # An internal class that manages execution of a tool
607
- # @private
608
- #
609
- class Execution
610
- def initialize(tool)
611
- @tool = tool
612
- @data = @tool.default_data.dup
613
- @data[Context::TOOL] = tool
614
- @data[Context::TOOL_NAME] = tool.full_name
615
- end
616
-
617
- def execute(cli, args, verbosity: 0)
618
- parse_args(args, verbosity)
619
- context = create_child_context(cli)
620
-
621
- original_level = context.logger.level
622
- context.logger.level = cli.base_level - @data[Context::VERBOSITY]
623
- begin
624
- perform_execution(context)
625
- ensure
626
- context.logger.level = original_level
627
- end
628
- end
629
-
630
- private
631
-
632
- def parse_args(args, base_verbosity)
633
- optparse = create_option_parser
634
- @data[Context::VERBOSITY] = base_verbosity
635
- @data[Context::ARGS] = args
636
- @data[Context::USAGE_ERROR] = nil
637
- remaining = optparse.parse(args)
638
- remaining = parse_required_args(remaining, args)
639
- remaining = parse_optional_args(remaining)
640
- parse_remaining_args(remaining, args)
641
- rescue ::OptionParser::ParseError => e
642
- @data[Context::USAGE_ERROR] = e.message
643
- end
644
-
645
- def create_option_parser
646
- optparse = ::OptionParser.new
647
- # The following clears out the Officious (hidden default flags).
648
- optparse.remove
649
- optparse.remove
650
- optparse.new
651
- optparse.new
652
- @tool.flag_definitions.each do |flag|
653
- optparse.on(*flag.optparser_info) do |val|
654
- @data[flag.key] = flag.handler.call(val, @data[flag.key])
655
- end
656
- end
657
- @tool.custom_acceptors do |accept|
658
- optparse.accept(accept)
659
- end
660
- optparse
661
- end
662
-
663
- def parse_required_args(remaining, args)
664
- @tool.required_arg_definitions.each do |arg_info|
665
- if remaining.empty?
666
- reason = "No value given for required argument #{arg_info.display_name}"
667
- raise create_parse_error(args, reason)
668
- end
669
- @data[arg_info.key] = arg_info.process_value(remaining.shift)
670
- end
671
- remaining
672
- end
673
-
674
- def parse_optional_args(remaining)
675
- @tool.optional_arg_definitions.each do |arg_info|
676
- break if remaining.empty?
677
- @data[arg_info.key] = arg_info.process_value(remaining.shift)
678
- end
679
- remaining
680
- end
681
-
682
- def parse_remaining_args(remaining, args)
683
- return if remaining.empty?
684
- unless @tool.remaining_args_definition
685
- if @tool.includes_script?
686
- raise create_parse_error(remaining, "Extra arguments provided")
687
- else
688
- raise create_parse_error(@tool.full_name + args, "Tool not found")
689
- end
690
- end
691
- @data[@tool.remaining_args_definition.key] =
692
- remaining.map { |arg| @tool.remaining_args_definition.process_value(arg) }
693
- end
694
-
695
- def create_parse_error(path, reason)
696
- OptionParser::ParseError.new(*path).tap do |e|
697
- e.reason = reason
698
- end
699
- end
700
-
701
- def create_child_context(cli)
702
- context = Context.new(cli, @data)
703
- modules = @tool.modules
704
- context.extend(*modules) unless modules.empty?
705
- @tool.helpers.each do |name, block|
706
- context.define_singleton_method(name, &block)
707
- end
708
- context
709
- end
710
-
711
- def perform_execution(context)
712
- executor = proc do
713
- if @tool.includes_script?
714
- context.instance_eval(&@tool.script)
715
- else
716
- context.logger.fatal("No implementation for tool #{@tool.display_name.inspect}")
717
- context.exit(-1)
718
- end
719
- end
720
- @tool.middleware_stack.reverse.each do |middleware|
721
- executor = make_executor(middleware, context, executor)
722
- end
723
- catch(:result) do
724
- executor.call
725
- 0
726
- end
727
- end
728
-
729
- def make_executor(middleware, context, next_executor)
730
- proc { middleware.execute(context, &next_executor) }
731
- end
277
+ def exit(code)
278
+ throw :result, code
732
279
  end
733
280
  end
734
281
  end
735
-
736
- require "toys/tool/acceptor"
737
- require "toys/tool/arg_definition"
738
- require "toys/tool/flag_definition"