toys-core 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.md +16 -24
  4. data/README.md +307 -59
  5. data/docs/guide.md +44 -4
  6. data/lib/toys-core.rb +58 -49
  7. data/lib/toys/acceptor.rb +672 -0
  8. data/lib/toys/alias.rb +106 -0
  9. data/lib/toys/arg_parser.rb +624 -0
  10. data/lib/toys/cli.rb +422 -181
  11. data/lib/toys/compat.rb +83 -0
  12. data/lib/toys/completion.rb +442 -0
  13. data/lib/toys/context.rb +354 -0
  14. data/lib/toys/core_version.rb +18 -26
  15. data/lib/toys/dsl/flag.rb +213 -56
  16. data/lib/toys/dsl/flag_group.rb +237 -51
  17. data/lib/toys/dsl/positional_arg.rb +210 -0
  18. data/lib/toys/dsl/tool.rb +968 -317
  19. data/lib/toys/errors.rb +46 -28
  20. data/lib/toys/flag.rb +821 -0
  21. data/lib/toys/flag_group.rb +282 -0
  22. data/lib/toys/input_file.rb +18 -26
  23. data/lib/toys/loader.rb +110 -100
  24. data/lib/toys/middleware.rb +24 -31
  25. data/lib/toys/mixin.rb +90 -59
  26. data/lib/toys/module_lookup.rb +125 -0
  27. data/lib/toys/positional_arg.rb +184 -0
  28. data/lib/toys/source_info.rb +192 -0
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
  32. data/lib/toys/standard_middleware/show_help.rb +130 -113
  33. data/lib/toys/standard_middleware/show_root_version.rb +29 -35
  34. data/lib/toys/standard_mixins/exec.rb +116 -78
  35. data/lib/toys/standard_mixins/fileutils.rb +16 -24
  36. data/lib/toys/standard_mixins/gems.rb +29 -30
  37. data/lib/toys/standard_mixins/highline.rb +34 -41
  38. data/lib/toys/standard_mixins/terminal.rb +72 -26
  39. data/lib/toys/template.rb +51 -35
  40. data/lib/toys/tool.rb +1161 -206
  41. data/lib/toys/utils/completion_engine.rb +171 -0
  42. data/lib/toys/utils/exec.rb +279 -182
  43. data/lib/toys/utils/gems.rb +58 -49
  44. data/lib/toys/utils/help_text.rb +117 -111
  45. data/lib/toys/utils/terminal.rb +69 -62
  46. data/lib/toys/wrappable_string.rb +162 -0
  47. metadata +24 -22
  48. data/lib/toys/definition/acceptor.rb +0 -191
  49. data/lib/toys/definition/alias.rb +0 -112
  50. data/lib/toys/definition/arg.rb +0 -140
  51. data/lib/toys/definition/flag.rb +0 -370
  52. data/lib/toys/definition/flag_group.rb +0 -205
  53. data/lib/toys/definition/source_info.rb +0 -190
  54. data/lib/toys/definition/tool.rb +0 -842
  55. data/lib/toys/dsl/arg.rb +0 -132
  56. data/lib/toys/runner.rb +0 -188
  57. data/lib/toys/standard_middleware.rb +0 -47
  58. data/lib/toys/utils/module_lookup.rb +0 -135
  59. data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -1,132 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- module Toys
33
- module DSL
34
- ##
35
- # DSL for an arg definition block. Lets you set arg attributes in a block
36
- # instead of a long series of keyword arguments.
37
- #
38
- # These directives are available inside a block passed to
39
- # {Toys::DSL::Tool#required_arg}, {Toys::DSL::Tool#optional_arg}, or
40
- # {Toys::DSL::Tool#remaining_args}.
41
- #
42
- class Arg
43
- ## @private
44
- def initialize(accept, default, display_name, desc, long_desc)
45
- @accept = accept
46
- @default = default
47
- @display_name = display_name
48
- @desc = desc
49
- @long_desc = long_desc || []
50
- end
51
-
52
- ##
53
- # Set the OptionParser acceptor.
54
- #
55
- # @param [Object] accept
56
- # @return [Toys::DSL::Tool] self, for chaining.
57
- #
58
- def accept(accept)
59
- @accept = accept
60
- self
61
- end
62
-
63
- ##
64
- # Set the default value.
65
- #
66
- # @param [Object] default
67
- # @return [Toys::DSL::Tool] self, for chaining.
68
- #
69
- def default(default)
70
- @default = default
71
- self
72
- end
73
-
74
- ##
75
- # Set the name of this arg as it appears in help screens.
76
- #
77
- # @param [String] display_name
78
- # @return [Toys::DSL::Tool] self, for chaining.
79
- #
80
- def display_name(display_name)
81
- @display_name = display_name
82
- self
83
- end
84
-
85
- ##
86
- # Set the short description. See {Toys::DSL::Tool#desc} for the allowed
87
- # formats.
88
- #
89
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc
90
- # @return [Toys::DSL::Tool] self, for chaining.
91
- #
92
- def desc(desc)
93
- @desc = desc
94
- self
95
- end
96
-
97
- ##
98
- # Adds to the long description. This may be called multiple times, and
99
- # the results are cumulative. See {Toys::DSL::Tool#long_desc} for the
100
- # allowed formats.
101
- #
102
- # @param [String,Array<String>,Toys::Utils::WrappableString...] long_desc
103
- # @return [Toys::DSL::Tool] self, for chaining.
104
- #
105
- def long_desc(*long_desc)
106
- @long_desc += long_desc
107
- self
108
- end
109
-
110
- ## @private
111
- def _add_required_to(tool, key)
112
- tool.add_required_arg(key,
113
- accept: @accept, display_name: @display_name,
114
- desc: @desc, long_desc: @long_desc)
115
- end
116
-
117
- ## @private
118
- def _add_optional_to(tool, key)
119
- tool.add_optional_arg(key,
120
- accept: @accept, default: @default, display_name: @display_name,
121
- desc: @desc, long_desc: @long_desc)
122
- end
123
-
124
- ## @private
125
- def _set_remaining_on(tool, key)
126
- tool.set_remaining_args(key,
127
- accept: @accept, default: @default, display_name: @display_name,
128
- desc: @desc, long_desc: @long_desc)
129
- end
130
- end
131
- end
132
- end
@@ -1,188 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- require "optparse"
33
-
34
- module Toys
35
- ##
36
- # An internal class that orchestrates execution of a tool.
37
- #
38
- # Generaly, you should not need to use this class directly. Instead, run a
39
- # tool using {Toys::CLI#run}.
40
- #
41
- class Runner
42
- ##
43
- # Create a runner for a particular tool in a particular CLI.
44
- #
45
- # @param [Toys::CLI] cli The CLI that is running the tool. This will
46
- # provide needed context information.
47
- # @param [Toys::Definition::Tool] tool_definition The tool to run.
48
- #
49
- def initialize(cli, tool_definition)
50
- @cli = cli
51
- @tool_definition = tool_definition
52
- end
53
-
54
- ##
55
- # Run the tool, provided given arguments.
56
- #
57
- # @param [Array<String>] args Command line arguments passed to the tool.
58
- # @param [Integer] verbosity Initial verbosity. Default is 0.
59
- #
60
- # @return [Integer] The resulting status code
61
- #
62
- def run(args, verbosity: 0)
63
- data = create_data(args, verbosity)
64
- parse_args(args, data) unless @tool_definition.argument_parsing_disabled?
65
- tool = @tool_definition.tool_class.new(@cli, data)
66
- @tool_definition.run_initializers(tool)
67
-
68
- original_level = @cli.logger.level
69
- @cli.logger.level = @cli.base_level - data[Tool::Keys::VERBOSITY]
70
- begin
71
- perform_execution(tool)
72
- ensure
73
- @cli.logger.level = original_level
74
- end
75
- end
76
-
77
- private
78
-
79
- def create_data(args, base_verbosity)
80
- data = @tool_definition.default_data.dup
81
- data[Tool::Keys::TOOL_DEFINITION] = @tool_definition
82
- data[Tool::Keys::TOOL_SOURCE] = @tool_definition.source_info
83
- data[Tool::Keys::TOOL_NAME] = @tool_definition.full_name
84
- data[Tool::Keys::VERBOSITY] = base_verbosity
85
- data[Tool::Keys::ARGS] = args
86
- data[Tool::Keys::USAGE_ERROR] = nil
87
- data
88
- end
89
-
90
- def parse_args(args, data)
91
- optparse, seen = create_option_parser(data)
92
- remaining = optparse.parse(args)
93
- validate_flags(args, seen)
94
- remaining = parse_required_args(remaining, args, data)
95
- remaining = parse_optional_args(remaining, data)
96
- parse_remaining_args(remaining, args, data)
97
- rescue ::OptionParser::ParseError => e
98
- data[Tool::Keys::USAGE_ERROR] = e.message
99
- end
100
-
101
- def create_option_parser(data)
102
- seen = []
103
- optparse = ::OptionParser.new
104
- # The following clears out the Officious (hidden default flags).
105
- optparse.remove
106
- optparse.remove
107
- optparse.new
108
- optparse.new
109
- @tool_definition.flag_definitions.each do |flag|
110
- optparse.on(*flag.optparser_info) do |val|
111
- seen << flag.key
112
- data[flag.key] = flag.handler.call(val, data[flag.key])
113
- end
114
- end
115
- @tool_definition.custom_acceptors do |accept|
116
- optparse.accept(accept)
117
- end
118
- [optparse, seen]
119
- end
120
-
121
- def validate_flags(args, seen)
122
- @tool_definition.flag_groups.each do |group|
123
- error = group.validation_error(seen)
124
- raise create_parse_error(args, error) if error
125
- end
126
- end
127
-
128
- def parse_required_args(remaining, args, data)
129
- @tool_definition.required_arg_definitions.each do |arg_info|
130
- if remaining.empty?
131
- reason = "No value given for required argument #{arg_info.display_name}"
132
- raise create_parse_error(args, reason)
133
- end
134
- data[arg_info.key] = arg_info.process_value(remaining.shift)
135
- end
136
- remaining
137
- end
138
-
139
- def parse_optional_args(remaining, data)
140
- @tool_definition.optional_arg_definitions.each do |arg_info|
141
- break if remaining.empty?
142
- data[arg_info.key] = arg_info.process_value(remaining.shift)
143
- end
144
- remaining
145
- end
146
-
147
- def parse_remaining_args(remaining, args, data)
148
- return if remaining.empty?
149
- unless @tool_definition.remaining_args_definition
150
- if @tool_definition.runnable?
151
- raise create_parse_error(remaining, "Extra arguments provided")
152
- else
153
- raise create_parse_error(@tool_definition.full_name + args, "Tool not found")
154
- end
155
- end
156
- data[@tool_definition.remaining_args_definition.key] =
157
- remaining.map { |arg| @tool_definition.remaining_args_definition.process_value(arg) }
158
- end
159
-
160
- def create_parse_error(path, reason)
161
- ::OptionParser::ParseError.new(*path).tap do |e|
162
- e.reason = reason
163
- end
164
- end
165
-
166
- def perform_execution(tool)
167
- executor = proc do
168
- if @tool_definition.runnable?
169
- tool.run
170
- else
171
- @cli.logger.fatal("No implementation for tool #{@tool_definition.display_name.inspect}")
172
- tool.exit(-1)
173
- end
174
- end
175
- @tool_definition.middleware_stack.reverse_each do |middleware|
176
- executor = make_executor(middleware, tool, executor)
177
- end
178
- catch(:result) do
179
- executor.call
180
- 0
181
- end
182
- end
183
-
184
- def make_executor(middleware, tool, next_executor)
185
- proc { middleware.run(tool, &next_executor) }
186
- end
187
- end
188
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- module Toys
33
- ##
34
- # Namespace for standard middleware classes.
35
- #
36
- module StandardMiddleware
37
- ## @private
38
- COMMON_FLAG_GROUP = :__common
39
-
40
- ## @private
41
- def self.append_common_flag_group(tool)
42
- tool.add_flag_group(type: :optional, name: COMMON_FLAG_GROUP,
43
- desc: "Common Flags", report_collisions: false)
44
- COMMON_FLAG_GROUP
45
- end
46
- end
47
- end
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- module Toys
33
- module Utils
34
- ##
35
- # A helper module that provides methods to do module lookups. This is
36
- # used to obtain named helpers, middleware, and templates from the
37
- # respective modules.
38
- #
39
- class ModuleLookup
40
- class << self
41
- ##
42
- # Convert the given string to a path element. Specifically, converts
43
- # to `lower_snake_case`.
44
- #
45
- # @param [String,Symbol] str String to convert.
46
- # @return [String] Converted string
47
- #
48
- def to_path_name(str)
49
- str = str.to_s.sub(/^_/, "").sub(/_$/, "").gsub(/_+/, "_")
50
- while str.sub!(/([^_])([A-Z])/, "\\1_\\2") do end
51
- str.downcase
52
- end
53
-
54
- ##
55
- # Convert the given string to a module name. Specifically, converts
56
- # to `UpperCamelCase`, and then to a symbol.
57
- #
58
- # @param [String,Symbol] str String to convert.
59
- # @return [Symbol] Converted name
60
- #
61
- def to_module_name(str)
62
- str = str.to_s.sub(/^_/, "").sub(/_$/, "").gsub(/_+/, "_")
63
- str.to_s.gsub(/(^|_)([a-zA-Z])/) { |_m| $2.upcase }.to_sym
64
- end
65
-
66
- ##
67
- # Given a require path, return the module expected to be defined.
68
- #
69
- # @param [String] path File path, delimited by forward slash
70
- # @return [Module] The module loaded from that path
71
- #
72
- def path_to_module(path)
73
- path.split("/").reduce(::Object) do |running_mod, seg|
74
- mod_name = to_module_name(seg)
75
- unless running_mod.constants.include?(mod_name)
76
- raise ::NameError, "Module #{running_mod.name}::#{mod_name} not found"
77
- end
78
- running_mod.const_get(mod_name)
79
- end
80
- end
81
- end
82
-
83
- ##
84
- # Create an empty ModuleLookup
85
- #
86
- def initialize
87
- @paths = []
88
- end
89
-
90
- ##
91
- # Add a lookup path for modules.
92
- #
93
- # @param [String] path_base The base require path
94
- # @param [Module] module_base The base module, or `nil` (the default) to
95
- # infer a default from the path base.
96
- # @param [Boolean] high_priority If true, add to the head of the lookup
97
- # path, otherwise add to the end.
98
- #
99
- def add_path(path_base, module_base: nil, high_priority: false)
100
- module_base ||= ModuleLookup.path_to_module(path_base)
101
- if high_priority
102
- @paths.unshift([path_base, module_base])
103
- else
104
- @paths << [path_base, module_base]
105
- end
106
- self
107
- end
108
-
109
- ##
110
- # Obtain a named module. Returns `nil` if the name is not present.
111
- #
112
- # @param [String,Symbol] name The name of the module to return.
113
- #
114
- # @return [Module] The specified module
115
- #
116
- def lookup(name)
117
- @paths.each do |path_base, module_base|
118
- path = "#{path_base}/#{ModuleLookup.to_path_name(name)}"
119
- begin
120
- require path
121
- rescue ::LoadError
122
- next
123
- end
124
- mod_name = ModuleLookup.to_module_name(name)
125
- unless module_base.constants.include?(mod_name)
126
- raise ::NameError,
127
- "File #{path.inspect} did not define #{module_base.name}::#{mod_name}"
128
- end
129
- return module_base.const_get(mod_name)
130
- end
131
- nil
132
- end
133
- end
134
- end
135
- end