toys-core 0.7.0 → 0.8.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -0
- data/LICENSE.md +16 -24
- data/README.md +307 -59
- data/docs/guide.md +44 -4
- data/lib/toys-core.rb +58 -49
- data/lib/toys/acceptor.rb +672 -0
- data/lib/toys/alias.rb +106 -0
- data/lib/toys/arg_parser.rb +624 -0
- data/lib/toys/cli.rb +422 -181
- data/lib/toys/compat.rb +83 -0
- data/lib/toys/completion.rb +442 -0
- data/lib/toys/context.rb +354 -0
- data/lib/toys/core_version.rb +18 -26
- data/lib/toys/dsl/flag.rb +213 -56
- data/lib/toys/dsl/flag_group.rb +237 -51
- data/lib/toys/dsl/positional_arg.rb +210 -0
- data/lib/toys/dsl/tool.rb +968 -317
- data/lib/toys/errors.rb +46 -28
- data/lib/toys/flag.rb +821 -0
- data/lib/toys/flag_group.rb +282 -0
- data/lib/toys/input_file.rb +18 -26
- data/lib/toys/loader.rb +110 -100
- data/lib/toys/middleware.rb +24 -31
- data/lib/toys/mixin.rb +90 -59
- data/lib/toys/module_lookup.rb +125 -0
- data/lib/toys/positional_arg.rb +184 -0
- data/lib/toys/source_info.rb +192 -0
- data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
- data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
- data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
- data/lib/toys/standard_middleware/show_help.rb +130 -113
- data/lib/toys/standard_middleware/show_root_version.rb +29 -35
- data/lib/toys/standard_mixins/exec.rb +116 -78
- data/lib/toys/standard_mixins/fileutils.rb +16 -24
- data/lib/toys/standard_mixins/gems.rb +29 -30
- data/lib/toys/standard_mixins/highline.rb +34 -41
- data/lib/toys/standard_mixins/terminal.rb +72 -26
- data/lib/toys/template.rb +51 -35
- data/lib/toys/tool.rb +1161 -206
- data/lib/toys/utils/completion_engine.rb +171 -0
- data/lib/toys/utils/exec.rb +279 -182
- data/lib/toys/utils/gems.rb +58 -49
- data/lib/toys/utils/help_text.rb +117 -111
- data/lib/toys/utils/terminal.rb +69 -62
- data/lib/toys/wrappable_string.rb +162 -0
- metadata +24 -22
- data/lib/toys/definition/acceptor.rb +0 -191
- data/lib/toys/definition/alias.rb +0 -112
- data/lib/toys/definition/arg.rb +0 -140
- data/lib/toys/definition/flag.rb +0 -370
- data/lib/toys/definition/flag_group.rb +0 -205
- data/lib/toys/definition/source_info.rb +0 -190
- data/lib/toys/definition/tool.rb +0 -842
- data/lib/toys/dsl/arg.rb +0 -132
- data/lib/toys/runner.rb +0 -188
- data/lib/toys/standard_middleware.rb +0 -47
- data/lib/toys/utils/module_lookup.rb +0 -135
- data/lib/toys/utils/wrappable_string.rb +0 -165
data/docs/guide.md
CHANGED
@@ -2,15 +2,55 @@
|
|
2
2
|
|
3
3
|
# Toys-Core User Guide
|
4
4
|
|
5
|
-
Toys-Core is the command line
|
6
|
-
|
5
|
+
Toys-Core is the command line framework underlying Toys. It implements most of
|
6
|
+
the core functionality of Toys, including the tool DSL, argument parsing,
|
7
|
+
loading Toys files, online help, subprocess control, and so forth. It can be
|
8
|
+
used to create custom command line executables using the same facilities. You
|
9
|
+
can generally write
|
7
10
|
|
8
11
|
This user's guide covers everything you need to know to build your own command
|
9
|
-
line
|
12
|
+
line executables in Ruby using the Toys-Core framework.
|
10
13
|
|
11
14
|
This guide assumes you are already familiar with Toys itself, including how to
|
12
15
|
define tools by writing Toys files, parsing arguments and flags, and how tools
|
13
16
|
are executed. For background, please see the
|
14
17
|
[Toys User's Guide](https://www.rubydoc.info/gems/toys/file/docs/guide.md).
|
15
18
|
|
16
|
-
(
|
19
|
+
**(This user's guide is still under construction.)**
|
20
|
+
|
21
|
+
## Conceptual overview
|
22
|
+
|
23
|
+
Toys-Core is a command line *framework* in the traditional sense. It is
|
24
|
+
intended to be used to write custom command line executables in Ruby. It
|
25
|
+
provides libraries to handle basic functions such as argumet parsing and online
|
26
|
+
help, and you provide the actual behavior.
|
27
|
+
|
28
|
+
The entry point for Toys-Core is the **cli object**. Typically your executable
|
29
|
+
script instantiates a CLI, configures it with the desired tool implementations,
|
30
|
+
and runs it.
|
31
|
+
|
32
|
+
An executable defines its functionality using the **Toys DSL** which can be
|
33
|
+
written in **toys files** or in **blocks** passed to the CLI. It uses the same
|
34
|
+
DSL used by Toys itself, and supports tools, subtools, flags, arguments, help
|
35
|
+
text, and all the other features of Toys.
|
36
|
+
|
37
|
+
An executable may customize its own facilities for writing tools by providing
|
38
|
+
**built-in mixins** and **built-in templates**, and can implement default
|
39
|
+
behavior across all tools by providing **middleware**.
|
40
|
+
|
41
|
+
Most executables will provide a set of **static tools**, but it is possible to
|
42
|
+
support user-provided tools as Toys does. Executables can customize how tool
|
43
|
+
definitions are searched and loaded from the file system.
|
44
|
+
|
45
|
+
Finally, an executable may customize many aspects of its behavior, such as the
|
46
|
+
**logging output**, **error handling**, and even shell **tab completion**.
|
47
|
+
|
48
|
+
## Using the CLI object
|
49
|
+
|
50
|
+
## Writing tools
|
51
|
+
|
52
|
+
## Customizing the tool environment
|
53
|
+
|
54
|
+
## Customizing default behavior
|
55
|
+
|
56
|
+
## Packaging your executable
|
data/lib/toys-core.rb
CHANGED
@@ -1,52 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
6
11
|
#
|
7
|
-
#
|
8
|
-
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
9
14
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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.
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
21
|
+
# IN THE SOFTWARE.
|
30
22
|
;
|
31
23
|
|
32
24
|
##
|
33
|
-
# Toys is a
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
25
|
+
# Toys is a configurable command line tool. Write commands in config files
|
26
|
+
# using a simple DSL, and Toys will provide the command line executable and
|
27
|
+
# take care of all the details such as argument parsing, online help, and error
|
28
|
+
# reporting. Toys is designed for software developers, IT professionals, and
|
29
|
+
# other power users who want to write and organize scripts to automate their
|
30
|
+
# workflows. It can also be used as a Rake replacement, providing a more
|
31
|
+
# natural command line interface for your project's build tasks.
|
32
|
+
#
|
33
|
+
# This module contains the command line framework underlying Toys. It can be
|
34
|
+
# used to create command line executables using the Toys DSL and classes.
|
37
35
|
#
|
38
36
|
module Toys
|
39
|
-
##
|
40
|
-
# Namespace for object definition classes.
|
41
|
-
#
|
42
|
-
module Definition; end
|
43
|
-
|
44
37
|
##
|
45
38
|
# Namespace for DSL classes. These classes provide the directives that can be
|
46
39
|
# used in configuration files. Most are defined in {Toys::DSL::Tool}.
|
47
40
|
#
|
48
41
|
module DSL; end
|
49
42
|
|
43
|
+
##
|
44
|
+
# Namespace for standard middleware classes.
|
45
|
+
#
|
46
|
+
module StandardMiddleware
|
47
|
+
## @private
|
48
|
+
COMMON_FLAG_GROUP = :__common
|
49
|
+
|
50
|
+
## @private
|
51
|
+
def self.append_common_flag_group(tool)
|
52
|
+
tool.add_flag_group(type: :optional, name: COMMON_FLAG_GROUP,
|
53
|
+
desc: "Common Flags", report_collisions: false)
|
54
|
+
COMMON_FLAG_GROUP
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
50
58
|
##
|
51
59
|
# Namespace for standard mixin classes.
|
52
60
|
#
|
@@ -55,34 +63,35 @@ module Toys
|
|
55
63
|
##
|
56
64
|
# Namespace for common utility classes.
|
57
65
|
#
|
66
|
+
# These classes are not loaded by default, and must be required explicitly.
|
67
|
+
# For example, before using {Toys::Utils::Exec}, you must
|
68
|
+
# `require "toys/utils/exec"`.
|
69
|
+
#
|
58
70
|
module Utils; end
|
59
71
|
end
|
60
72
|
|
73
|
+
require "toys/acceptor"
|
74
|
+
require "toys/alias"
|
75
|
+
require "toys/arg_parser"
|
61
76
|
require "toys/cli"
|
77
|
+
require "toys/compat"
|
78
|
+
require "toys/completion"
|
79
|
+
require "toys/context"
|
62
80
|
require "toys/core_version"
|
63
|
-
require "toys/definition/acceptor"
|
64
|
-
require "toys/definition/alias"
|
65
|
-
require "toys/definition/arg"
|
66
|
-
require "toys/definition/flag"
|
67
|
-
require "toys/definition/flag_group"
|
68
|
-
require "toys/definition/source_info"
|
69
|
-
require "toys/definition/tool"
|
70
|
-
require "toys/dsl/arg"
|
71
81
|
require "toys/dsl/flag"
|
72
82
|
require "toys/dsl/flag_group"
|
83
|
+
require "toys/dsl/positional_arg"
|
73
84
|
require "toys/dsl/tool"
|
74
85
|
require "toys/errors"
|
86
|
+
require "toys/flag"
|
87
|
+
require "toys/flag_group"
|
75
88
|
require "toys/input_file"
|
76
89
|
require "toys/loader"
|
77
90
|
require "toys/middleware"
|
78
91
|
require "toys/mixin"
|
79
|
-
require "toys/
|
80
|
-
require "toys/
|
92
|
+
require "toys/module_lookup"
|
93
|
+
require "toys/positional_arg"
|
94
|
+
require "toys/source_info"
|
81
95
|
require "toys/template"
|
82
96
|
require "toys/tool"
|
83
|
-
require "toys/
|
84
|
-
require "toys/utils/gems"
|
85
|
-
require "toys/utils/help_text"
|
86
|
-
require "toys/utils/module_lookup"
|
87
|
-
require "toys/utils/terminal"
|
88
|
-
require "toys/utils/wrappable_string"
|
97
|
+
require "toys/wrappable_string"
|
@@ -0,0 +1,672 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
21
|
+
# IN THE SOFTWARE.
|
22
|
+
;
|
23
|
+
|
24
|
+
module Toys
|
25
|
+
##
|
26
|
+
# An Acceptor validates and converts arguments. It is designed to be
|
27
|
+
# compatible with the OptionParser accept mechanism.
|
28
|
+
#
|
29
|
+
# First, an acceptor validates the argument via its
|
30
|
+
# {Toys::Acceptor::Base#match} method. This method should determine whether
|
31
|
+
# the argument is valid, and return information that will help with
|
32
|
+
# conversion of the argument.
|
33
|
+
#
|
34
|
+
# Second, an acceptor converts the argument to its final form via the
|
35
|
+
# {Toys::Acceptor::Base#convert} method.
|
36
|
+
#
|
37
|
+
# Finally, an acceptor has a name that may appear in help text for flags and
|
38
|
+
# arguments that use it.
|
39
|
+
#
|
40
|
+
module Acceptor
|
41
|
+
##
|
42
|
+
# A sentinel that may be returned from a function-based acceptor to
|
43
|
+
# indicate invalid input.
|
44
|
+
# @return [Object]
|
45
|
+
#
|
46
|
+
REJECT = ::Object.new.freeze
|
47
|
+
|
48
|
+
##
|
49
|
+
# The default type description.
|
50
|
+
# @return [String]
|
51
|
+
#
|
52
|
+
DEFAULT_TYPE_DESC = "string"
|
53
|
+
|
54
|
+
##
|
55
|
+
# A base class for acceptors.
|
56
|
+
#
|
57
|
+
# The base acceptor does not do any validation (i.e. it accepts all
|
58
|
+
# arguments) or conversion (i.e. it returns the original string). You can
|
59
|
+
# subclass this base class and override the {#match} and {#convert} methods
|
60
|
+
# to implement an acceptor.
|
61
|
+
#
|
62
|
+
class Base
|
63
|
+
##
|
64
|
+
# Create a base acceptor.
|
65
|
+
#
|
66
|
+
# @param type_desc [String] Type description string, shown in help.
|
67
|
+
# Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
|
68
|
+
# @param well_known_spec [Object] The well-known acceptor spec associated
|
69
|
+
# with this acceptor, or `nil` for none.
|
70
|
+
#
|
71
|
+
def initialize(type_desc: nil, well_known_spec: nil)
|
72
|
+
@type_desc = type_desc || DEFAULT_TYPE_DESC
|
73
|
+
@well_known_spec = well_known_spec
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Type description string, shown in help.
|
78
|
+
# @return [String]
|
79
|
+
#
|
80
|
+
attr_reader :type_desc
|
81
|
+
|
82
|
+
##
|
83
|
+
# The well-known acceptor spec associated with this acceptor, if any.
|
84
|
+
# This generally identifies an OptionParser-compatible acceptor spec. For
|
85
|
+
# example, the acceptor object that corresponds to `Integer` will return
|
86
|
+
# `Integer` from this attribute.
|
87
|
+
#
|
88
|
+
# @return [Object] the well-known acceptor
|
89
|
+
# @return [nil] if there is no corresponding well-known acceptor
|
90
|
+
#
|
91
|
+
attr_reader :well_known_spec
|
92
|
+
|
93
|
+
##
|
94
|
+
# Type description string, shown in help.
|
95
|
+
# @return [String]
|
96
|
+
#
|
97
|
+
def to_s
|
98
|
+
type_desc.to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Validate the given input.
|
103
|
+
#
|
104
|
+
# When given a valid input, return an array in which the first element is
|
105
|
+
# the original input string, and the remaining elements (which may be
|
106
|
+
# empty) comprise any additional information that may be useful during
|
107
|
+
# conversion. If there is no additional information, you may return the
|
108
|
+
# original input string by itself without wrapping in an array.
|
109
|
+
#
|
110
|
+
# When given an invalid input, return a falsy value such as `nil`.
|
111
|
+
#
|
112
|
+
# Note that a `MatchData` object is a legitimate return value because it
|
113
|
+
# duck-types the appropriate array.
|
114
|
+
#
|
115
|
+
# This default implementation simply returns the original input string,
|
116
|
+
# as the only array element, indicating all inputs are valid. You can
|
117
|
+
# override this method to provide a different validation function.
|
118
|
+
#
|
119
|
+
# @param str [String,nil] The input argument string. May be `nil` if the
|
120
|
+
# value is optional and not provided.
|
121
|
+
# @return [String,Array,nil]
|
122
|
+
#
|
123
|
+
def match(str)
|
124
|
+
[str]
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Convert the given input.
|
129
|
+
#
|
130
|
+
# This method is passed the results of a successful match, including the
|
131
|
+
# original input string and any other values returned from {#match}. It
|
132
|
+
# must return the final converted value to use.
|
133
|
+
#
|
134
|
+
# @param str [String,nil] Original argument string. May be `nil` if the
|
135
|
+
# value is optional and not provided.
|
136
|
+
# @param extra [Object...] Zero or more additional arguments comprising
|
137
|
+
# additional elements returned from the match function.
|
138
|
+
# @return [Object] The converted argument as it should be stored in the
|
139
|
+
# context data.
|
140
|
+
#
|
141
|
+
def convert(str, *extra) # rubocop:disable Lint/UnusedMethodArgument
|
142
|
+
str.nil? ? true : str
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Return suggestions for a given non-matching string.
|
147
|
+
#
|
148
|
+
# This method may be called when a match fails. It should return a
|
149
|
+
# (possibly empty) array of suggestions that could be displayed to the
|
150
|
+
# user as "did you mean..."
|
151
|
+
#
|
152
|
+
# The default implementation returns the empty list.
|
153
|
+
#
|
154
|
+
# @param str [String] A string that failed matching.
|
155
|
+
# @return [Array<String>] A possibly empty array of alternative
|
156
|
+
# suggestions that could be displayed with "did you mean..."
|
157
|
+
#
|
158
|
+
def suggestions(str) # rubocop:disable Lint/UnusedMethodArgument
|
159
|
+
[]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# The default acceptor. Corresponds to the well-known acceptor for
|
165
|
+
# `Object`.
|
166
|
+
# @return [Toys::Acceptor::Base]
|
167
|
+
#
|
168
|
+
DEFAULT = Base.new(type_desc: "string", well_known_spec: ::Object)
|
169
|
+
|
170
|
+
##
|
171
|
+
# An acceptor that uses a simple function to validate and convert input.
|
172
|
+
# The function must take the input string as its argument, and either
|
173
|
+
# return the converted object to indicate success, or raise an exception or
|
174
|
+
# return the sentinel {Toys::Acceptor::REJECT} to indicate invalid input.
|
175
|
+
#
|
176
|
+
class Simple < Base
|
177
|
+
##
|
178
|
+
# Create a simple acceptor.
|
179
|
+
#
|
180
|
+
# You should provide an acceptor function, either as a proc in the
|
181
|
+
# `function` argument, or as a block. The function must take as its one
|
182
|
+
# argument the input string. If the string is valid, the function must
|
183
|
+
# return the value to store in the tool's data. If the string is invalid,
|
184
|
+
# the function may either raise an exception (which must descend from
|
185
|
+
# `StandardError`) or return {Toys::Acceptor::REJECT}.
|
186
|
+
#
|
187
|
+
# @param function [Proc] The acceptor function
|
188
|
+
# @param type_desc [String] Type description string, shown in help.
|
189
|
+
# Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
|
190
|
+
# @param well_known_spec [Object] The well-known acceptor spec associated
|
191
|
+
# with this acceptor, or `nil` for none.
|
192
|
+
# @param block [Proc] The acceptor function, if not provided as a normal
|
193
|
+
# parameter.
|
194
|
+
#
|
195
|
+
def initialize(function = nil, type_desc: nil, well_known_spec: nil, &block)
|
196
|
+
super(type_desc: type_desc, well_known_spec: well_known_spec)
|
197
|
+
@function = function || block || proc { |s| s }
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Overrides {Toys::Acceptor::Base#match} to use the given function.
|
202
|
+
# @private
|
203
|
+
#
|
204
|
+
def match(str)
|
205
|
+
result = @function.call(str) rescue REJECT # rubocop:disable Style/RescueModifier
|
206
|
+
result.equal?(REJECT) ? nil : [str, result]
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Overrides {Toys::Acceptor::Base#convert} to use the given function's
|
211
|
+
# result.
|
212
|
+
# @private
|
213
|
+
#
|
214
|
+
def convert(_str, result)
|
215
|
+
result
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
# An acceptor that uses a regex to validate input. It also supports a
|
221
|
+
# custom conversion function that generates the final value from the match
|
222
|
+
# results.
|
223
|
+
#
|
224
|
+
class Pattern < Base
|
225
|
+
##
|
226
|
+
# Create a pattern acceptor.
|
227
|
+
#
|
228
|
+
# You must provide a regular expression (or any object that duck-types
|
229
|
+
# `Regexp#match`) as a validator.
|
230
|
+
#
|
231
|
+
# You may also optionally provide a converter, either as a proc or a
|
232
|
+
# block. A converter must take as its arguments the values in the
|
233
|
+
# `MatchData` returned from a successful regex match. That is, the first
|
234
|
+
# argument is the original input string, and the remaining arguments are
|
235
|
+
# the captures. The converter must return the final converted value.
|
236
|
+
# If no converter is provided, no conversion is done and the input string
|
237
|
+
# is returned.
|
238
|
+
#
|
239
|
+
# @param regex [Regexp] Regular expression defining value values.
|
240
|
+
# @param converter [Proc] An optional converter function. May also be
|
241
|
+
# given as a block. Note that the converter will be passed all
|
242
|
+
# elements of the `MatchData`.
|
243
|
+
# @param type_desc [String] Type description string, shown in help.
|
244
|
+
# Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
|
245
|
+
# @param well_known_spec [Object] The well-known acceptor spec associated
|
246
|
+
# with this acceptor, or `nil` for none.
|
247
|
+
# @param block [Proc] A converter function, if not provided as a normal
|
248
|
+
# parameter.
|
249
|
+
#
|
250
|
+
def initialize(regex, converter = nil, type_desc: nil, well_known_spec: nil, &block)
|
251
|
+
super(type_desc: type_desc, well_known_spec: well_known_spec)
|
252
|
+
@regex = regex
|
253
|
+
@converter = converter || block
|
254
|
+
end
|
255
|
+
|
256
|
+
##
|
257
|
+
# Overrides {Toys::Acceptor::Base#match} to use the given regex.
|
258
|
+
# @private
|
259
|
+
#
|
260
|
+
def match(str)
|
261
|
+
str.nil? ? [nil] : @regex.match(str)
|
262
|
+
end
|
263
|
+
|
264
|
+
##
|
265
|
+
# Overrides {Toys::Acceptor::Base#convert} to use the given converter.
|
266
|
+
# @private
|
267
|
+
#
|
268
|
+
def convert(str, *extra)
|
269
|
+
@converter ? @converter.call(str, *extra) : str
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# An acceptor that recognizes a fixed set of values.
|
275
|
+
#
|
276
|
+
# You provide a list of valid values. The input argument string will be
|
277
|
+
# matched against the string forms of these valid values. If it matches,
|
278
|
+
# the converter will return the actual value from the valid list.
|
279
|
+
#
|
280
|
+
# For example, you could pass `[:one, :two, 3]` as the set of values. If
|
281
|
+
# an argument of `"two"` is passed in, the converter will yield a final
|
282
|
+
# value of the symbol `:two`. If an argument of "3" is passed in, the
|
283
|
+
# converter will yield the integer `3`. If an argument of "three" is
|
284
|
+
# passed in, the match will fail.
|
285
|
+
#
|
286
|
+
class Enum < Base
|
287
|
+
##
|
288
|
+
# Create an acceptor.
|
289
|
+
#
|
290
|
+
# @param values [Array<Object>] Valid values.
|
291
|
+
# @param type_desc [String] Type description string, shown in help.
|
292
|
+
# Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
|
293
|
+
# @param well_known_spec [Object] The well-known acceptor spec associated
|
294
|
+
# with this acceptor, or `nil` for none.
|
295
|
+
#
|
296
|
+
def initialize(values, type_desc: nil, well_known_spec: nil)
|
297
|
+
super(type_desc: type_desc, well_known_spec: well_known_spec)
|
298
|
+
@values = Array(values).map { |v| [v.to_s, v] }
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# The array of enum values.
|
303
|
+
# @return [Array<Object>]
|
304
|
+
#
|
305
|
+
attr_reader :values
|
306
|
+
|
307
|
+
##
|
308
|
+
# Overrides {Toys::Acceptor::Base#match} to find the value.
|
309
|
+
# @private
|
310
|
+
#
|
311
|
+
def match(str)
|
312
|
+
str.nil? ? [nil, nil] : @values.find { |s, _e| s == str }
|
313
|
+
end
|
314
|
+
|
315
|
+
##
|
316
|
+
# Overrides {Toys::Acceptor::Base#convert} to return the actual enum
|
317
|
+
# element.
|
318
|
+
# @private
|
319
|
+
#
|
320
|
+
def convert(_str, elem)
|
321
|
+
elem
|
322
|
+
end
|
323
|
+
|
324
|
+
##
|
325
|
+
# Overrides {Toys::Acceptor::Base#suggestions} to return close matches
|
326
|
+
# from the enum.
|
327
|
+
# @private
|
328
|
+
#
|
329
|
+
def suggestions(str)
|
330
|
+
Compat.suggestions(str, @values.map(&:first))
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
##
|
335
|
+
# An acceptor that recognizes a range of values.
|
336
|
+
#
|
337
|
+
# The input argument is matched against the given range. For example, you
|
338
|
+
# can match against the integers from 1 to 10 by passing the range
|
339
|
+
# `(1..10)`.
|
340
|
+
#
|
341
|
+
# You can also provide a conversion function that takes the input string
|
342
|
+
# and converts it an object that can be compared by the range. If you do
|
343
|
+
# not provide a converter, a default converter will be provided depending
|
344
|
+
# on the types of objects serving as the range limits. Specifically:
|
345
|
+
#
|
346
|
+
# * If the range beginning and end are both `Integer`, then input strings
|
347
|
+
# are likewise converted to `Integer` when matched against the range.
|
348
|
+
# Accepted values are returned as integers.
|
349
|
+
# * If the range beginning and end are both `Float`, then input strings
|
350
|
+
# are likewise converted to `Float`.
|
351
|
+
# * If the range beginning and end are both `Rational`, then input
|
352
|
+
# strings are likewise converted to `Rational`.
|
353
|
+
# * If the range beginning and end are both `Numeric` types but different
|
354
|
+
# subtypes (e.g. an `Integer` and a `Float`), then any type of numeric
|
355
|
+
# input (integer, float, rational) is accepted and matched against the
|
356
|
+
# range.
|
357
|
+
# * If the range beginning and/or end are not numeric types, then no
|
358
|
+
# conversion is done by default.
|
359
|
+
#
|
360
|
+
class Range < Simple
|
361
|
+
##
|
362
|
+
# Create an acceptor.
|
363
|
+
#
|
364
|
+
# @param range [Range] The range of acceptable values
|
365
|
+
# @param converter [Proc] A converter proc that takes an input string and
|
366
|
+
# attempts to convert it to a type comparable by the range. For
|
367
|
+
# numeric ranges, this can be omitted because one is provided by
|
368
|
+
# default. You should provide a converter for other types of ranges.
|
369
|
+
# You can also pass the converter as a block.
|
370
|
+
# @param type_desc [String] Type description string, shown in help.
|
371
|
+
# Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
|
372
|
+
# @param well_known_spec [Object] The well-known acceptor spec associated
|
373
|
+
# with this acceptor, or `nil` for none.
|
374
|
+
# @param block [Proc] Converter function, if not provided as a normal
|
375
|
+
# parameter.
|
376
|
+
#
|
377
|
+
def initialize(range, converter = nil, type_desc: nil, well_known_spec: nil, &block)
|
378
|
+
converter ||= block || make_converter(range.begin, range.end)
|
379
|
+
super(type_desc: type_desc, well_known_spec: well_known_spec) do |val|
|
380
|
+
val = converter.call(val) if converter
|
381
|
+
val.nil? || range.include?(val) ? val : REJECT
|
382
|
+
end
|
383
|
+
@range = range
|
384
|
+
end
|
385
|
+
|
386
|
+
##
|
387
|
+
# The range being checked.
|
388
|
+
# @return [Range]
|
389
|
+
#
|
390
|
+
attr_reader :range
|
391
|
+
|
392
|
+
private
|
393
|
+
|
394
|
+
def make_converter(val1, val2)
|
395
|
+
if val1.is_a?(::Integer) && val2.is_a?(::Integer)
|
396
|
+
INTEGER_CONVERTER
|
397
|
+
elsif val1.is_a?(::Float) && val2.is_a?(::Float)
|
398
|
+
FLOAT_CONVERTER
|
399
|
+
elsif val1.is_a?(::Rational) && val2.is_a?(::Rational)
|
400
|
+
RATIONAL_CONVERTER
|
401
|
+
elsif val1.is_a?(::Numeric) && val2.is_a?(::Numeric)
|
402
|
+
NUMERIC_CONVERTER
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# A converter proc that handles integers. Useful in Simple and Range
|
409
|
+
# acceptors.
|
410
|
+
# @return [Proc]
|
411
|
+
#
|
412
|
+
INTEGER_CONVERTER = proc { |s| s.nil? ? nil : Integer(s) }
|
413
|
+
|
414
|
+
##
|
415
|
+
# A converter proc that handles floats. Useful in Simple and Range
|
416
|
+
# acceptors.
|
417
|
+
# @return [Proc]
|
418
|
+
#
|
419
|
+
FLOAT_CONVERTER = proc { |s| s.nil? ? nil : Float(s) }
|
420
|
+
|
421
|
+
##
|
422
|
+
# A converter proc that handles rationals. Useful in Simple and Range
|
423
|
+
# acceptors.
|
424
|
+
# @return [Proc]
|
425
|
+
#
|
426
|
+
RATIONAL_CONVERTER = proc { |s| s.nil? ? nil : Rational(s) }
|
427
|
+
|
428
|
+
##
|
429
|
+
# A converter proc that handles any numeric value. Useful in Simple and
|
430
|
+
# Range acceptors.
|
431
|
+
# @return [Proc]
|
432
|
+
#
|
433
|
+
NUMERIC_CONVERTER =
|
434
|
+
proc do |s|
|
435
|
+
if s.nil?
|
436
|
+
nil
|
437
|
+
elsif s.include?("/")
|
438
|
+
Rational(s)
|
439
|
+
elsif s.include?(".") || (s.include?("e") && s !~ /\A-?0x/)
|
440
|
+
Float(s)
|
441
|
+
else
|
442
|
+
Integer(s)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
class << self
|
447
|
+
##
|
448
|
+
# Lookup a standard acceptor name recognized by OptionParser.
|
449
|
+
#
|
450
|
+
# @param spec [Object] A well-known acceptor specification, such as
|
451
|
+
# `String`, `Integer`, `Array`, `OptionParser::DecimalInteger`, etc.
|
452
|
+
# @return [Toys::Acceptor::Base] The corresponding Acceptor object
|
453
|
+
# @return [nil] if the given standard acceptor was not recognized.
|
454
|
+
#
|
455
|
+
def lookup_well_known(spec)
|
456
|
+
result = standard_well_knowns[spec]
|
457
|
+
if result.nil? && defined?(::OptionParser)
|
458
|
+
result = optparse_well_knowns[spec]
|
459
|
+
end
|
460
|
+
result
|
461
|
+
end
|
462
|
+
|
463
|
+
##
|
464
|
+
# Create an acceptor from a variety of specification formats. The
|
465
|
+
# acceptor is constructed from the given specification object and/or the
|
466
|
+
# given block. Additionally, some acceptors can take an optional type
|
467
|
+
# description string used to describe the type of data in online help.
|
468
|
+
#
|
469
|
+
# Recognized specs include:
|
470
|
+
#
|
471
|
+
# * Any well-known acceptor recognized by OptionParser, such as
|
472
|
+
# `Integer`, `Array`, or `OptionParser::DecimalInteger`. Any block
|
473
|
+
# and type description you provide are ignored.
|
474
|
+
#
|
475
|
+
# * Any **regular expression**. The returned acceptor validates only if
|
476
|
+
# the regex matches the *entire string parameter*.
|
477
|
+
#
|
478
|
+
# You can also provide an optional conversion function as a block. If
|
479
|
+
# provided, the block must take a variable number of arguments, the
|
480
|
+
# first being the matched string and the remainder being the captures
|
481
|
+
# from the regular expression. It should return the converted object
|
482
|
+
# that will be stored in the context data. If you do not provide a
|
483
|
+
# block, no conversion takes place, and the original string is used.
|
484
|
+
#
|
485
|
+
# * An **array** of possible values. The acceptor validates if the
|
486
|
+
# string parameter matches the *string form* of one of the array
|
487
|
+
# elements (i.e. the results of calling `to_s` on the element.)
|
488
|
+
#
|
489
|
+
# An array acceptor automatically converts the string parameter to
|
490
|
+
# the actual array element that it matched. For example, if the
|
491
|
+
# symbol `:foo` is in the array, it will match the string `"foo"`,
|
492
|
+
# and then store the symbol `:foo` in the tool data. You may not
|
493
|
+
# further customize the conversion function; any block is ignored.
|
494
|
+
#
|
495
|
+
# * A **range** of possible values. The acceptor validates if the
|
496
|
+
# string parameter, after conversion to the range type, lies within
|
497
|
+
# the range. The final value stored in context data is the converted
|
498
|
+
# value. For numeric ranges, conversion is provided, but if the range
|
499
|
+
# has a different type, you must provide the conversion function as
|
500
|
+
# a block.
|
501
|
+
#
|
502
|
+
# * A **function** as a Proc (where the block is ignored) or a block
|
503
|
+
# (if the spec is nil). This function performs *both* validation and
|
504
|
+
# conversion. It should take the string parameter as its argument,
|
505
|
+
# and it must either return the object that should be stored in the
|
506
|
+
# tool data, or raise an exception (descended from `StandardError`)
|
507
|
+
# to indicate that the string parameter is invalid. You may also
|
508
|
+
# return the sentinel value {Toys::Acceptor::REJECT} to indicate that
|
509
|
+
# the string is invalid.
|
510
|
+
#
|
511
|
+
# * The value `nil` or `:default` with no block, to indicate the
|
512
|
+
# default pass-through acceptor {Toys::Acceptor::DEFAULT}. Any type
|
513
|
+
# description you provide is ignored.
|
514
|
+
#
|
515
|
+
# @param spec [Object] See the description for recognized values.
|
516
|
+
# @param type_desc [String] The type description for interpolating into
|
517
|
+
# help text. Ignored if the spec indicates the default acceptor or a
|
518
|
+
# well-known acceptor.
|
519
|
+
# @param block [Proc] See the description for recognized forms.
|
520
|
+
# @return [Toys::Acceptor::Base,Proc]
|
521
|
+
#
|
522
|
+
def create(spec = nil, type_desc: nil, &block)
|
523
|
+
well_known = lookup_well_known(spec)
|
524
|
+
return well_known if well_known
|
525
|
+
case spec
|
526
|
+
when Base
|
527
|
+
spec
|
528
|
+
when ::Regexp
|
529
|
+
Pattern.new(spec, type_desc: type_desc, &block)
|
530
|
+
when ::Array
|
531
|
+
Enum.new(spec, type_desc: type_desc)
|
532
|
+
when ::Proc
|
533
|
+
Simple.new(spec, type_desc: type_desc)
|
534
|
+
when ::Range
|
535
|
+
Range.new(spec, type_desc: type_desc, &block)
|
536
|
+
when nil, :default
|
537
|
+
block ? Simple.new(type_desc: type_desc, &block) : DEFAULT
|
538
|
+
else
|
539
|
+
raise ToolDefinitionError, "Illegal acceptor spec: #{spec.inspect}"
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
private
|
544
|
+
|
545
|
+
def standard_well_knowns
|
546
|
+
@standard_well_knowns ||= {
|
547
|
+
::Object => DEFAULT,
|
548
|
+
::NilClass => build_nil,
|
549
|
+
::String => build_string,
|
550
|
+
::Integer => build_integer,
|
551
|
+
::Float => build_float,
|
552
|
+
::Rational => build_rational,
|
553
|
+
::Numeric => build_numeric,
|
554
|
+
::TrueClass => build_boolean(::TrueClass, true),
|
555
|
+
::FalseClass => build_boolean(::FalseClass, false),
|
556
|
+
::Array => build_array,
|
557
|
+
::Regexp => build_regexp,
|
558
|
+
}
|
559
|
+
end
|
560
|
+
|
561
|
+
def optparse_well_knowns
|
562
|
+
@optparse_well_knowns ||= {
|
563
|
+
::OptionParser::DecimalInteger => build_decimal_integer,
|
564
|
+
::OptionParser::OctalInteger => build_octal_integer,
|
565
|
+
::OptionParser::DecimalNumeric => build_decimal_numeric,
|
566
|
+
}
|
567
|
+
end
|
568
|
+
|
569
|
+
def build_nil
|
570
|
+
Simple.new(type_desc: "string", well_known_spec: ::NilClass) { |s| s }
|
571
|
+
end
|
572
|
+
|
573
|
+
def build_string
|
574
|
+
Pattern.new(/.+/m, type_desc: "nonempty string", well_known_spec: ::String)
|
575
|
+
end
|
576
|
+
|
577
|
+
def build_integer
|
578
|
+
Simple.new(INTEGER_CONVERTER, type_desc: "integer", well_known_spec: ::Integer)
|
579
|
+
end
|
580
|
+
|
581
|
+
def build_float
|
582
|
+
Simple.new(FLOAT_CONVERTER, type_desc: "floating point number", well_known_spec: ::Float)
|
583
|
+
end
|
584
|
+
|
585
|
+
def build_rational
|
586
|
+
Simple.new(RATIONAL_CONVERTER, type_desc: "rational number", well_known_spec: ::Rational)
|
587
|
+
end
|
588
|
+
|
589
|
+
def build_numeric
|
590
|
+
Simple.new(NUMERIC_CONVERTER, type_desc: "number", well_known_spec: ::Numeric)
|
591
|
+
end
|
592
|
+
|
593
|
+
TRUE_STRINGS = ["+", "true", "yes"].freeze
|
594
|
+
FALSE_STRINGS = ["-", "false", "no", "nil"].freeze
|
595
|
+
private_constant :TRUE_STRINGS, :FALSE_STRINGS
|
596
|
+
|
597
|
+
def build_boolean(spec, default)
|
598
|
+
Simple.new(type_desc: "boolean", well_known_spec: spec) do |s|
|
599
|
+
if s.nil?
|
600
|
+
default
|
601
|
+
else
|
602
|
+
s = s.downcase
|
603
|
+
if s.empty?
|
604
|
+
REJECT
|
605
|
+
elsif TRUE_STRINGS.any? { |t| t.start_with?(s) }
|
606
|
+
true
|
607
|
+
elsif FALSE_STRINGS.any? { |f| f.start_with?(s) }
|
608
|
+
false
|
609
|
+
else
|
610
|
+
REJECT
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
def build_array
|
617
|
+
Simple.new(type_desc: "string array", well_known_spec: ::Array) do |s|
|
618
|
+
if s.nil?
|
619
|
+
nil
|
620
|
+
else
|
621
|
+
s.split(",").collect { |elem| elem unless elem.empty? }
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def build_regexp
|
627
|
+
Simple.new(type_desc: "regular expression", well_known_spec: ::Regexp) do |s|
|
628
|
+
if s.nil?
|
629
|
+
nil
|
630
|
+
else
|
631
|
+
flags = 0
|
632
|
+
if (match = %r{\A/((?:\\.|[^\\])*)/([[:alpha:]]*)\z}.match(s))
|
633
|
+
s = match[1]
|
634
|
+
opts = match[2] || ""
|
635
|
+
flags |= ::Regexp::IGNORECASE if opts.include?("i")
|
636
|
+
flags |= ::Regexp::MULTILINE if opts.include?("m")
|
637
|
+
flags |= ::Regexp::EXTENDED if opts.include?("x")
|
638
|
+
end
|
639
|
+
::Regexp.new(s, flags)
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
def build_decimal_integer
|
645
|
+
Simple.new(type_desc: "decimal integer",
|
646
|
+
well_known_spec: ::OptionParser::DecimalInteger) do |s|
|
647
|
+
s.nil? ? nil : Integer(s, 10)
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def build_octal_integer
|
652
|
+
Simple.new(type_desc: "octal integer",
|
653
|
+
well_known_spec: ::OptionParser::OctalInteger) do |s|
|
654
|
+
s.nil? ? nil : Integer(s, 8)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
def build_decimal_numeric
|
659
|
+
Simple.new(type_desc: "decimal number",
|
660
|
+
well_known_spec: ::OptionParser::DecimalNumeric) do |s|
|
661
|
+
if s.nil?
|
662
|
+
nil
|
663
|
+
elsif s.include?(".") || (s.include?("e") && s !~ /\A-?0x/)
|
664
|
+
Float(s)
|
665
|
+
else
|
666
|
+
Integer(s, 10)
|
667
|
+
end
|
668
|
+
end
|
669
|
+
end
|
670
|
+
end
|
671
|
+
end
|
672
|
+
end
|