rodish 1.0.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 +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +257 -0
- data/lib/rodish/command.rb +280 -0
- data/lib/rodish/dsl.rb +189 -0
- data/lib/rodish/errors.rb +58 -0
- data/lib/rodish/option_parser.rb +71 -0
- data/lib/rodish/processor.rb +79 -0
- data/lib/rodish/skip_option_parser.rb +19 -0
- data/lib/rodish.rb +15 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: be6ead8f4e3fb70c94b061d779c3f552842d78acb16e3828e37b64d02804772b
|
4
|
+
data.tar.gz: ec3ad13d30478c9048ea2ff74d07da4cd7c8e1d2efe99e3a569bd4fba09274f4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 93555b428ac603ad16d90fde03a0ff26ff2d8ce7097f8b9d32a8d57d1685b7042c1e8cee4842aeba3e2396ba990eb34b47e7d5df0ef3f90135d3892bb53e97f7
|
7
|
+
data.tar.gz: f306a8549e5b70abac451dc0d75add3a4266c2d66d3aa79c2f05b8efde26379505a91cfc5bbc99cfbcd72931942f8f536e91568e18df7f2e970b03dda537a028
|
data/CHANGELOG
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2025 Jeremy Evans
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
= Rodish
|
2
|
+
|
3
|
+
Rodish parses an argv array using a routing tree approach. It is
|
4
|
+
designed to make it easy to implement command line applications
|
5
|
+
that support multiple levels of subcommands, with options at each
|
6
|
+
level.
|
7
|
+
|
8
|
+
= Installation
|
9
|
+
|
10
|
+
gem install rodish
|
11
|
+
|
12
|
+
= Source Code
|
13
|
+
|
14
|
+
Source code is available on GitHub at https://github.com/jeremyevans/rodish
|
15
|
+
|
16
|
+
= Simple Example
|
17
|
+
|
18
|
+
Here's a simple commented example with a single subcommand:
|
19
|
+
|
20
|
+
require "rodish"
|
21
|
+
|
22
|
+
# This just creates a normal Ruby class. For each argv parse, Rodish will
|
23
|
+
# create an instance of this class
|
24
|
+
class CliExample
|
25
|
+
|
26
|
+
# This allows instances of the class to be instantiated with or without
|
27
|
+
# a default person.
|
28
|
+
def initialize(default_person=nil)
|
29
|
+
@default_person = default_person
|
30
|
+
end
|
31
|
+
|
32
|
+
# This installs the Rodish processor into CliExample. It also extends the
|
33
|
+
# CliExample class with the Rodish::Processor module). The block provided
|
34
|
+
# is evaluated in the context of a Rodish::DSL instance.
|
35
|
+
Rodish.processor(self) do
|
36
|
+
|
37
|
+
# This method call creates a hello subcommand of the current/root command.
|
38
|
+
# If the given argv is for the hello subcommand, the block will be
|
39
|
+
# executed in the context of the CliExample instance.
|
40
|
+
on "hello" do
|
41
|
+
|
42
|
+
# This adds a usage string and a -p options for the hello subcommand.
|
43
|
+
# The block passed is used to set the options via the optparse library.
|
44
|
+
options("cli-example hello [options]") do
|
45
|
+
on("-p", "--person=name", "say hello to specific person")
|
46
|
+
end
|
47
|
+
|
48
|
+
run do |opts|
|
49
|
+
"Hello #{opts[:person] || @default_person || 'World'}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# This requests Rodish to process the provided argv. Rodish will determine
|
56
|
+
# the related command block to execute and execute it. The return value of
|
57
|
+
# that block will be returned to the caller.
|
58
|
+
CliExample.process(["hello"])
|
59
|
+
# => "Hello World"
|
60
|
+
|
61
|
+
# Additional arguments passed to .process are passed to .new. In this
|
62
|
+
# example, this sets the default person.
|
63
|
+
CliExample.process(["hello"], "Adol")
|
64
|
+
# => "Hello Adol"
|
65
|
+
|
66
|
+
# This shows an example of passing an option to a subcommand, and using
|
67
|
+
# the option value when returning a response.
|
68
|
+
CliExample.process(["hello", "-p", "Feena"], "Adol")
|
69
|
+
# => "Hello Feena"
|
70
|
+
|
71
|
+
= Rodish DSL
|
72
|
+
|
73
|
+
Inside the <tt>Rodish.processor</tt> block, you are in the context of the root
|
74
|
+
command. The following methods are available for configuring the processing of
|
75
|
+
the command.
|
76
|
+
|
77
|
+
== +on+
|
78
|
+
|
79
|
+
The +on+ method adds a subcommand of the current command, and yields to the
|
80
|
+
block to configure the subcommand. All of the methods described in the Rodish
|
81
|
+
DSL section can be executed inside the +on+ block, and arbitrary levels of
|
82
|
+
subcommands are supported.
|
83
|
+
|
84
|
+
== +options+
|
85
|
+
|
86
|
+
The +options+ method sets up an options parser for the current command. The
|
87
|
+
default options parser disallows any options. Options are parsed into a hash,
|
88
|
+
which is yielded to commands (as in the above example).
|
89
|
+
|
90
|
+
This method requires a String argument for the usage for the current command.
|
91
|
+
You can also provide a +key+ keyword argument, to put parsed options into
|
92
|
+
a subhash of the main options hash, which can be useful when options are
|
93
|
+
parsed at multiple levels.
|
94
|
+
|
95
|
+
If a block is provided, it is executed in the context of a Rodish::OptionParser
|
96
|
+
instance. Rodish::OptionParser is a subclass of Ruby's standard OptionParser
|
97
|
+
(from +optparse+), with a few additional methods.
|
98
|
+
|
99
|
+
== +args+
|
100
|
+
|
101
|
+
The +args+ method sets the number of arguments accepted when running the command.
|
102
|
+
The default for +args+ is +0+. You can provide either an Integer to accept a
|
103
|
+
fixed number of arguments, or a Range to allow any number of arguments in that
|
104
|
+
range.
|
105
|
+
|
106
|
+
The method also accepts an +invalid_args_message+ keyword argument for the
|
107
|
+
message, to set the message to display if an invalid number of arguments is
|
108
|
+
provided.
|
109
|
+
|
110
|
+
== +run+
|
111
|
+
|
112
|
+
The +run+ method sets the block to run for the current command. If the
|
113
|
+
command accepts a fixed number of arguments, those arguments are yielded
|
114
|
+
as the first arguments to the command. If the command accepts a range of
|
115
|
+
argument numbers, then the remaining argv array will be passed as the
|
116
|
+
first argument.
|
117
|
+
|
118
|
+
The block will be passed two additional arguments, the options already
|
119
|
+
parsed, and the current Rodish::Command object.
|
120
|
+
|
121
|
+
== +is+
|
122
|
+
|
123
|
+
The +is+ method is a shortcut for calling the +on+ method and +run+ method.
|
124
|
+
For example:
|
125
|
+
|
126
|
+
is "hello" do
|
127
|
+
:world
|
128
|
+
end
|
129
|
+
|
130
|
+
is equivalent to:
|
131
|
+
|
132
|
+
on "hello" do
|
133
|
+
run do
|
134
|
+
:world
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
The +is+ method also takes +args+ and +invalid_args_message+ keyword arguments.
|
139
|
+
|
140
|
+
== +before+
|
141
|
+
|
142
|
+
The +before+ method takes a block, and this block is executed before command
|
143
|
+
or subcommand execution, in the same context that the +run+ block would be
|
144
|
+
executed in. It is passed the remaining argv array and the already parsed
|
145
|
+
options.
|
146
|
+
|
147
|
+
== +skip_option_parsing+
|
148
|
+
|
149
|
+
The +skip_option_parsing+ method makes the command do no option parsing,
|
150
|
+
treating all elements of the remaining argv as options. It requires a
|
151
|
+
usage string for the command, similar to +options+.
|
152
|
+
|
153
|
+
== +run_on+
|
154
|
+
|
155
|
+
The +run_on+ method is similar to +on+, except it creates a post subcommand
|
156
|
+
instead of a normal subcommand. Post subcommands allow the +run+ block
|
157
|
+
to parse part of the remaining argv array, and then call a subcommand with
|
158
|
+
the modified (or a new) argv array. You dispatch to post subcommands
|
159
|
+
inside the +run+ block by calling +run+ on the command argument:
|
160
|
+
|
161
|
+
on "hello" do
|
162
|
+
args(2...)
|
163
|
+
|
164
|
+
run do |argv, opts, command|
|
165
|
+
@name = argv.shift
|
166
|
+
command.run(self, opts, argv)
|
167
|
+
end
|
168
|
+
|
169
|
+
run_on "world" do
|
170
|
+
run do
|
171
|
+
"Hello #{@name.upcase} World!"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# process(%w[hello foo world])
|
177
|
+
# => "Hello FOO World!"
|
178
|
+
|
179
|
+
== +run_is+
|
180
|
+
|
181
|
+
The +run_is+ method operates similarly to +is+, but adds a post subcommand
|
182
|
+
instead of a normal subcommand.
|
183
|
+
|
184
|
+
== +post_options+
|
185
|
+
|
186
|
+
The +post_options+ method sets an option parser that is used for post
|
187
|
+
subcommands. This parses options from the argv array that passed to
|
188
|
+
+command.run+, before calling the related subcommand. Example:
|
189
|
+
|
190
|
+
on "hello" do
|
191
|
+
args(2...)
|
192
|
+
|
193
|
+
post_options("Usage: hello name [options] subcommand ...") do
|
194
|
+
on("-c", "--cap", "capitalize instead of uppercase")
|
195
|
+
end
|
196
|
+
|
197
|
+
run do |argv, opts, command|
|
198
|
+
@name = argv.shift
|
199
|
+
command.run(self, opts, argv)
|
200
|
+
end
|
201
|
+
|
202
|
+
run_is "world" do |opts|
|
203
|
+
name = opts[:cap] ? @name.capitalize : @name.upcase
|
204
|
+
"Hello #{name} World!"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# process(%w[hello foo world])
|
209
|
+
# => "Hello FOO World!"
|
210
|
+
# process(%w[hello foo -c world])
|
211
|
+
# => "Hello Foo World!"
|
212
|
+
|
213
|
+
== +autoload_subcommand_dir+
|
214
|
+
|
215
|
+
The +autoload_subcommand_dir+ takes a directory, and will autoload
|
216
|
+
subcommands from the given directory. Filenames ending in +.rb+ in
|
217
|
+
this directory will be treated as subcommands, and requiring the
|
218
|
+
file should add the appropriate subcommand.
|
219
|
+
|
220
|
+
This allows you to design complex command line programs where only
|
221
|
+
the parts of the program needed to handle the given argv are loaded.
|
222
|
+
|
223
|
+
== +autoload_post_subcommand_dir+
|
224
|
+
|
225
|
+
The +autoload_post_subcommand_dir+ operates the same as
|
226
|
+
+autoload_subcommand_dir+, but it handles post subcommands instead of
|
227
|
+
normal subcommands.
|
228
|
+
|
229
|
+
= Examples
|
230
|
+
|
231
|
+
The tests that ship with Rodish fully cover all of Rodish's functionality.
|
232
|
+
|
233
|
+
If you would like to view a production example using Rodish, which
|
234
|
+
uses most of Rodish's features, please see UbiCli, which is part of
|
235
|
+
Ubicloud:
|
236
|
+
|
237
|
+
* Main class: https://github.com/ubicloud/ubicloud/blob/main/lib/ubi_cli.rb
|
238
|
+
* Commands (separate command per file): https://github.com/ubicloud/ubicloud/tree/main/cli-commands
|
239
|
+
|
240
|
+
= History
|
241
|
+
|
242
|
+
Rodish was extracted from Ubicloud (https://github.com/ubicloud/ubicloud),
|
243
|
+
and is the argv processor used in Ubicloud's command line interface.
|
244
|
+
|
245
|
+
= Naming
|
246
|
+
|
247
|
+
The name Rodish was chosen because Rodish uses an API similar (-ish) to the Roda
|
248
|
+
web framework (http://roda.jeremyevans.net), and the library is designed for
|
249
|
+
use in applications executed from a shell (sh).
|
250
|
+
|
251
|
+
= License
|
252
|
+
|
253
|
+
MIT
|
254
|
+
|
255
|
+
= Author
|
256
|
+
|
257
|
+
Jeremy Evans <code@jeremyevans.net>
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "option_parser"
|
4
|
+
require_relative "skip_option_parser"
|
5
|
+
require_relative "errors"
|
6
|
+
|
7
|
+
module Rodish
|
8
|
+
# Rodish::Command is the main object in Rodish's processing.
|
9
|
+
# It handles a single command, and may have one or more
|
10
|
+
# subcommands and/or post subcommands, forming a tree.
|
11
|
+
#
|
12
|
+
# Rodish's argv processing starts with the root command,
|
13
|
+
# processing options and deleting appropriately to subcommands,
|
14
|
+
# until the requested command or subcommand is located,
|
15
|
+
# which is then executed.
|
16
|
+
class Command
|
17
|
+
option_parser = OptionParser.new
|
18
|
+
option_parser.set_banner("")
|
19
|
+
option_parser.freeze
|
20
|
+
|
21
|
+
# The default option parser if no options are given for
|
22
|
+
# the command.
|
23
|
+
DEFAULT_OPTION_PARSER = option_parser
|
24
|
+
|
25
|
+
# A hash of subcommands for the command. Keys are
|
26
|
+
# subcommand name strings.
|
27
|
+
attr_reader :subcommands
|
28
|
+
|
29
|
+
# A hash of post subcommands for the command. Keys are
|
30
|
+
# post subcommand name strings.
|
31
|
+
attr_reader :post_subcommands
|
32
|
+
|
33
|
+
# The block to execute if this command is the requested
|
34
|
+
# subcommand. May be nil if this subcommand cannot be
|
35
|
+
# executed, and can only dispatch to subcommands.
|
36
|
+
attr_accessor :run_block
|
37
|
+
|
38
|
+
# An array of command names that represent a path to the
|
39
|
+
# current command. Empty for the root command.
|
40
|
+
attr_accessor :command_path
|
41
|
+
|
42
|
+
# The option parser for the current command. May be nil,
|
43
|
+
# in which case the default option parser is used.
|
44
|
+
attr_accessor :option_parser
|
45
|
+
|
46
|
+
# If set, places parsed options in a subhash of the options
|
47
|
+
# hash, keyed by the given value. If nil, parsed options
|
48
|
+
# are placed directly in the options hash.
|
49
|
+
attr_accessor :option_key
|
50
|
+
|
51
|
+
# The post option parser for the current command. Called
|
52
|
+
# only before dispatching to post subcommands.
|
53
|
+
attr_accessor :post_option_parser
|
54
|
+
|
55
|
+
# Similar to +option_key+, but for post options instead
|
56
|
+
# of normal subcommands.
|
57
|
+
attr_accessor :post_option_key
|
58
|
+
|
59
|
+
# A before hook to execute before executing the current
|
60
|
+
# command or dispatching to subcommands.
|
61
|
+
attr_accessor :before
|
62
|
+
|
63
|
+
# The number of arguments the run block will accept.
|
64
|
+
# Should be either an integer or a range of integers.
|
65
|
+
attr_accessor :num_args
|
66
|
+
|
67
|
+
# The error message to use if an invalid number of
|
68
|
+
# arguments is provided.
|
69
|
+
attr_accessor :invalid_args_message
|
70
|
+
|
71
|
+
def initialize(command_path)
|
72
|
+
@command_path = command_path
|
73
|
+
@command_name = command_path.join(" ").freeze
|
74
|
+
@subcommands = {}
|
75
|
+
@post_subcommands = {}
|
76
|
+
@num_args = 0
|
77
|
+
end
|
78
|
+
|
79
|
+
# Freeze all subcommands and option parsers in
|
80
|
+
# addition to the command itself.
|
81
|
+
def freeze
|
82
|
+
@subcommands.each_value(&:freeze)
|
83
|
+
@subcommands.freeze
|
84
|
+
@post_subcommands.each_value(&:freeze)
|
85
|
+
@post_subcommands.freeze
|
86
|
+
@option_parser.freeze
|
87
|
+
@post_option_parser.freeze
|
88
|
+
super
|
89
|
+
end
|
90
|
+
|
91
|
+
# Run a post subcommand using the given context (generally self),
|
92
|
+
# options, and argv. Usually called inside a run block, after
|
93
|
+
# shifting one or more values off the given argv:
|
94
|
+
#
|
95
|
+
# run do |argv, opts, command|
|
96
|
+
# @name = argv.shift
|
97
|
+
# command.run(self, opts, argv)
|
98
|
+
# end
|
99
|
+
def run(context, options, argv)
|
100
|
+
begin
|
101
|
+
process_options(argv, options, @post_option_key, @post_option_parser)
|
102
|
+
rescue ::OptionParser::InvalidOption => e
|
103
|
+
raise CommandFailure.new(e.message, @post_option_parser)
|
104
|
+
end
|
105
|
+
|
106
|
+
arg = argv[0]
|
107
|
+
if arg && @post_subcommands[arg]
|
108
|
+
process_subcommand(@post_subcommands, context, options, argv)
|
109
|
+
else
|
110
|
+
process_command_failure(arg, @post_subcommands, @post_option_parser, "post ")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
alias run_post_subcommand run
|
114
|
+
|
115
|
+
# Process the current command. This first processes the options.
|
116
|
+
# After processing the options, it checks if the first argument
|
117
|
+
# in the remaining argv is a subcommand. If so, it dispatches to
|
118
|
+
# that subcommand. If not, it dispatches to the run block.
|
119
|
+
def process(context, options, argv)
|
120
|
+
process_options(argv, options, @option_key, @option_parser)
|
121
|
+
|
122
|
+
arg = argv[0]
|
123
|
+
if argv && @subcommands[arg]
|
124
|
+
process_subcommand(@subcommands, context, options, argv)
|
125
|
+
elsif run_block
|
126
|
+
if valid_args?(argv)
|
127
|
+
context.instance_exec(argv, options, &before) if before
|
128
|
+
|
129
|
+
if @num_args.is_a?(Integer)
|
130
|
+
context.instance_exec(*argv, options, self, &run_block)
|
131
|
+
else
|
132
|
+
context.instance_exec(argv, options, self, &run_block)
|
133
|
+
end
|
134
|
+
elsif @invalid_args_message
|
135
|
+
raise_failure("invalid arguments#{subcommand_name} (#{@invalid_args_message})")
|
136
|
+
else
|
137
|
+
raise_failure("invalid number of arguments#{subcommand_name} (accepts: #{@num_args}, given: #{argv.length})")
|
138
|
+
end
|
139
|
+
else
|
140
|
+
process_command_failure(arg, @subcommands, @option_parser, "")
|
141
|
+
end
|
142
|
+
rescue ::OptionParser::InvalidOption => e
|
143
|
+
if @option_parser || @post_option_parser
|
144
|
+
raise_failure(e.message)
|
145
|
+
else
|
146
|
+
raise
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# This yields the current command and all subcommands and
|
151
|
+
# post subcommands, recursively.
|
152
|
+
def each_subcommand(names = [].freeze, &block)
|
153
|
+
yield names, self
|
154
|
+
_each_subcommand(names, @subcommands, &block)
|
155
|
+
_each_subcommand(names, @post_subcommands, &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Raise a CommandFailure with the given error and the given
|
159
|
+
# option parsers.
|
160
|
+
def raise_failure(message, option_parsers = self.option_parsers)
|
161
|
+
raise CommandFailure.new(message, option_parsers)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a string of options text for the command's option parsers.
|
165
|
+
def options_text
|
166
|
+
option_parsers = self.option_parsers
|
167
|
+
unless option_parsers.empty?
|
168
|
+
_options_text(option_parsers)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns a Command instance for the named subcommand.
|
173
|
+
# This will autoload the subcommand if not already loaded.
|
174
|
+
def subcommand(name)
|
175
|
+
_subcommand(@subcommands, name)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns a Command instance for the named post subcommand.
|
179
|
+
# This will autoload the post subcommand if not already loaded.
|
180
|
+
def post_subcommand(name)
|
181
|
+
_subcommand(@post_subcommands, name)
|
182
|
+
end
|
183
|
+
|
184
|
+
# An array of option parsers for the command. May be empty
|
185
|
+
# if the command has no option parsers.
|
186
|
+
def option_parsers
|
187
|
+
[@option_parser, @post_option_parser].compact
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
# Yield to the block for each subcommand in the given
|
193
|
+
# subcommands. Internals of #each_subcommand.
|
194
|
+
def _each_subcommand(names, subcommands, &block)
|
195
|
+
subcommands.each_key do |name|
|
196
|
+
sc_names = names + [name]
|
197
|
+
_subcommand(subcommands, name).each_subcommand(sc_names.freeze, &block)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Return the named subcommand from the given subcommands hash,
|
202
|
+
# autoloading it if it is not already loaded.
|
203
|
+
def _subcommand(subcommands, name)
|
204
|
+
subcommand = subcommands[name]
|
205
|
+
|
206
|
+
if subcommand.is_a?(String)
|
207
|
+
require subcommand
|
208
|
+
subcommand = subcommands[name]
|
209
|
+
unless subcommand.is_a?(Command)
|
210
|
+
raise ProgramBug, "program bug, autoload of subcommand #{name} failed"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
subcommand
|
215
|
+
end
|
216
|
+
|
217
|
+
# Return a string containing all option parser text.
|
218
|
+
def _options_text(option_parsers)
|
219
|
+
option_parsers.join("\n\n")
|
220
|
+
end
|
221
|
+
|
222
|
+
# Handle command failures for both subcommands and post subcommands.
|
223
|
+
def process_command_failure(arg, subcommands, option_parser, prefix)
|
224
|
+
if subcommands.empty?
|
225
|
+
raise ProgramBug, "program bug, no run block or #{prefix}subcommands defined#{subcommand_name}"
|
226
|
+
elsif arg
|
227
|
+
raise_failure("invalid #{prefix}subcommand: #{arg}", option_parser)
|
228
|
+
else
|
229
|
+
raise_failure("no #{prefix}subcommand provided", option_parser)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Process options for the given command. If option_key is set,
|
234
|
+
# parsed options are added as a options subhash under the given key.
|
235
|
+
# Otherwise, parsed options placed directly into options.
|
236
|
+
def process_options(argv, options, option_key, option_parser)
|
237
|
+
case option_parser
|
238
|
+
when SkipOptionParser
|
239
|
+
# do nothing
|
240
|
+
when nil
|
241
|
+
DEFAULT_OPTION_PARSER.order!(argv)
|
242
|
+
else
|
243
|
+
command_options = option_key ? {} : options
|
244
|
+
|
245
|
+
option_parser.order!(argv, into: command_options)
|
246
|
+
|
247
|
+
if option_key
|
248
|
+
options[option_key] = command_options
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Dispatch to the appropriate subcommand using the first entry in
|
254
|
+
# the provided argv.
|
255
|
+
def process_subcommand(subcommands, context, options, argv)
|
256
|
+
subcommand = _subcommand(subcommands, argv[0])
|
257
|
+
argv.shift
|
258
|
+
context.instance_exec(argv, options, &before) if before
|
259
|
+
subcommand.process(context, options, argv)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Helper used for constructing error messages.
|
263
|
+
def subcommand_name
|
264
|
+
if @command_name.empty?
|
265
|
+
" for command"
|
266
|
+
else
|
267
|
+
" for #{@command_name} subcommand"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Return whether the given argv has a valid number of arguments.
|
272
|
+
def valid_args?(argv)
|
273
|
+
if @num_args.is_a?(Integer)
|
274
|
+
argv.length == @num_args
|
275
|
+
else
|
276
|
+
@num_args.include?(argv.length)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
data/lib/rodish/dsl.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "command"
|
4
|
+
require_relative "option_parser"
|
5
|
+
require_relative "skip_option_parser"
|
6
|
+
|
7
|
+
module Rodish
|
8
|
+
# The Rodish::DSL class implements Rodish's DSL. Blocks
|
9
|
+
# passed to Rodish.processor and on/run_on blocks inside
|
10
|
+
# those blocks evaluated in the context of an instance of
|
11
|
+
# Rodish::DSL.
|
12
|
+
#
|
13
|
+
# Each Rodish::DSL instance is bound to a single
|
14
|
+
# Rodish::Command and allows the DSL to modify the state
|
15
|
+
# of the command.
|
16
|
+
class DSL
|
17
|
+
# Create a new command with the given path and evaluate
|
18
|
+
# the given block in the context of a new instance using
|
19
|
+
# that command.
|
20
|
+
def self.command(command_path, &block)
|
21
|
+
command = Command.new(command_path)
|
22
|
+
new(command).instance_exec(&block)
|
23
|
+
command
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(command)
|
27
|
+
@command = command
|
28
|
+
end
|
29
|
+
|
30
|
+
# Skip option parsing for the command. This is different
|
31
|
+
# then the default option parsing, which will error if any
|
32
|
+
# options are given. A banner must be provided, setting
|
33
|
+
# the usage for the command.
|
34
|
+
#
|
35
|
+
# The main reason to use this is if you are going to pass
|
36
|
+
# the entire remaining argv as the argv to another
|
37
|
+
# program.
|
38
|
+
def skip_option_parsing(banner)
|
39
|
+
@command.option_parser = SkipOptionParser.new(banner)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set the option parser for the command to based on the
|
43
|
+
# provided block, which is executed in the context of a new
|
44
|
+
# instance of Rodish::OptionParser. These options are parsed
|
45
|
+
# for execuction of both subcommands and the current command.
|
46
|
+
#
|
47
|
+
# The banner argument is required and sets the usage string
|
48
|
+
# for the command.
|
49
|
+
#
|
50
|
+
# If +key+ is given, parsed options
|
51
|
+
# will be placed in a subhash using that key.
|
52
|
+
#
|
53
|
+
# The block is optional, allowing you to set a usage banner for
|
54
|
+
# commands without allowing any options.
|
55
|
+
def options(banner, key: nil, &block)
|
56
|
+
@command.option_key = key
|
57
|
+
@command.option_parser = create_option_parser(banner, @command.subcommands, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Similar to +options+, but sets the option parser for post
|
61
|
+
# subcommands. This option parser is only used when the
|
62
|
+
# command is executed and chooses to run a post subcommand.
|
63
|
+
def post_options(banner, key: nil, &block)
|
64
|
+
@command.post_option_key = key
|
65
|
+
@command.post_option_parser = create_option_parser(banner, @command.post_subcommands, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Sets the before block. This block is executed in the same
|
69
|
+
# context as the run block would be executed, before either
|
70
|
+
# subcommand execution or execution of the current command.
|
71
|
+
def before(&block)
|
72
|
+
@command.before = block
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set the number of arguments supported by this command.
|
76
|
+
# The default is 0. To support a fixed number of arguments,
|
77
|
+
# pass an Integer. To support a variable number of arguments,
|
78
|
+
# pass a Range. The +invalid_args_message+ argument sets the
|
79
|
+
# error message to use if an invalid number of arguments is
|
80
|
+
# passed.
|
81
|
+
def args(args, invalid_args_message: nil)
|
82
|
+
@command.num_args = args
|
83
|
+
@command.invalid_args_message = invalid_args_message
|
84
|
+
end
|
85
|
+
|
86
|
+
# Autoload subcommands from the given directory. Filenames
|
87
|
+
# ending in .rb in this directory should be valid subcommands,
|
88
|
+
# and requiring the related file should load the subcommand.
|
89
|
+
#
|
90
|
+
# You can use this so that your argv parser does not need to
|
91
|
+
# load code not needed to support processing the command.
|
92
|
+
def autoload_subcommand_dir(dir)
|
93
|
+
_autoload_subcommand_dir(@command.subcommands, dir)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Similar to +autoload_subcommand_dir+, but for post
|
97
|
+
# subcommands instead of normal subcommands.
|
98
|
+
def autoload_post_subcommand_dir(dir)
|
99
|
+
_autoload_subcommand_dir(@command.post_subcommands, dir)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create a new subcommand with the given name and yield to
|
103
|
+
# the block to configure the subcommand.
|
104
|
+
def on(command_name, &block)
|
105
|
+
_on(@command.subcommands, command_name, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Same as +on+, but for post subcommands instead of normal
|
109
|
+
# subcommands.
|
110
|
+
def run_on(command_name, &block)
|
111
|
+
_on(@command.post_subcommands, command_name, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Set the block to run for subcommand execution. Commands
|
115
|
+
# should have subcommands and/or a run block, otherwise it
|
116
|
+
# is not possible to use the command successfully.
|
117
|
+
def run(&block)
|
118
|
+
@command.run_block = block
|
119
|
+
end
|
120
|
+
|
121
|
+
# A shortcut for calling +on+ and +run+.
|
122
|
+
#
|
123
|
+
# is "hello" do
|
124
|
+
# :world
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# is equivalent to:
|
128
|
+
#
|
129
|
+
# on "hello" do
|
130
|
+
# run do
|
131
|
+
# :world
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# The +args+ argument sets the number of arguments supported by
|
136
|
+
# the command.
|
137
|
+
#
|
138
|
+
# The +invalid_args_message+ arguments set the error message to
|
139
|
+
# use if an invalid number of arguments is provided.
|
140
|
+
def is(command_name, args: 0, invalid_args_message: nil, &block)
|
141
|
+
_is(:on, command_name, args:, invalid_args_message:, &block)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Similar to +is+, but for post subcommands instead of normal
|
145
|
+
# subcommands.
|
146
|
+
def run_is(command_name, args: 0, invalid_args_message: nil, &block)
|
147
|
+
_is(:run_on, command_name, args:, invalid_args_message:, &block)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Internals of autoloading of normal and post subcommands.
|
153
|
+
# This sets the value of the subcommand as a string instead of a
|
154
|
+
# Command instance, and the Command#_subcommand method recognizes
|
155
|
+
# this and handles the autoloading.
|
156
|
+
def _autoload_subcommand_dir(hash, base)
|
157
|
+
Dir.glob("*.rb", base:).each do |filename|
|
158
|
+
hash[filename.chomp(".rb")] = File.expand_path(File.join(base, filename))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Internals of +is+ and +run_is+.
|
163
|
+
def _is(meth, command_name, args:, invalid_args_message: nil, &block)
|
164
|
+
public_send(meth, command_name) do
|
165
|
+
args(args, invalid_args_message:)
|
166
|
+
run(&block)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Internals of +on+ and +run_on+.
|
171
|
+
def _on(hash, command_name, &block)
|
172
|
+
command_path = @command.command_path + [command_name]
|
173
|
+
hash[command_name] = DSL.command(command_path.freeze, &block)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Internals of +options+ and +post_options+.
|
177
|
+
def create_option_parser(banner, subcommands, &block)
|
178
|
+
option_parser = OptionParser.new
|
179
|
+
option_parser.set_banner("Usage: #{banner}")
|
180
|
+
if block
|
181
|
+
option_parser.separator ""
|
182
|
+
option_parser.separator "Options:"
|
183
|
+
option_parser.instance_exec(&block)
|
184
|
+
end
|
185
|
+
option_parser.subcommands = subcommands
|
186
|
+
option_parser
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodish
|
4
|
+
# Rodish::CommandExit is the base error class for Rodish, signaling
|
5
|
+
# that a command execution finished. Callers of
|
6
|
+
# Rodish::Processor#process should rescue CommandExit to handle
|
7
|
+
# both failures as well as early exits.
|
8
|
+
#
|
9
|
+
# Direct instances represent successful execution. This is
|
10
|
+
# raised when calling halt inside an options parser block (if an
|
11
|
+
# option should result in an early exit). It can also be raised
|
12
|
+
# manually inside command run blocks to exit early.
|
13
|
+
class CommandExit < StandardError
|
14
|
+
# Whether or not the command failed. For CommandExit, this always
|
15
|
+
# returns false, since CommandExit represents successful execution
|
16
|
+
# exits.
|
17
|
+
def failure?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Rodish::CommandFailure is used for failures of commands, such as:
|
23
|
+
#
|
24
|
+
# * Invalid options
|
25
|
+
# * Invalid number of arguments for a command
|
26
|
+
# * Invalid subcommands
|
27
|
+
# * No subcommand given for a command that only supports subcommands
|
28
|
+
class CommandFailure < CommandExit
|
29
|
+
def initialize(message, option_parsers = [])
|
30
|
+
option_parsers = [option_parsers] unless option_parsers.is_a?(Array)
|
31
|
+
@option_parsers = option_parsers.compact
|
32
|
+
super(message)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Always returns false, since CommandFailure represents failures.
|
36
|
+
def failure?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the message along with the content of any related option
|
41
|
+
# parsers. This can be used to show usage an options along with
|
42
|
+
# error messages for failing commands.
|
43
|
+
def message_with_usage
|
44
|
+
if @option_parsers.empty?
|
45
|
+
message
|
46
|
+
else
|
47
|
+
"#{message}\n\n#{@option_parsers.join("\n\n")}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Rodish::ProgramBug is a subclass of Rodish::CommandFailure only used
|
53
|
+
# in cases where there is a bug in the program, such as a command with
|
54
|
+
# no subcommands or run block, or when subcommand autoloads do not
|
55
|
+
# result in the subcommand being defined.
|
56
|
+
class ProgramBug < CommandFailure
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require_relative "errors"
|
5
|
+
|
6
|
+
module Rodish
|
7
|
+
# Rodish::OptionPaser is a subclass of Ruby's standard OptionParser
|
8
|
+
# (from the optparse library).
|
9
|
+
class OptionParser < ::OptionParser
|
10
|
+
# A hash of subcommands for the option parser. If not empty,
|
11
|
+
# shows available subcommands when showing options.
|
12
|
+
attr_accessor :subcommands
|
13
|
+
|
14
|
+
# Don't add officious, which includes options that call exit.
|
15
|
+
# With Rodish, there are no secret options, only options you define.
|
16
|
+
def add_officious
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add the available subcommands to the returned string if there are
|
20
|
+
# any subcommands.
|
21
|
+
def to_s
|
22
|
+
string = super
|
23
|
+
|
24
|
+
if subcommands.length > 6
|
25
|
+
string += "\nSubcommands:\n #{subcommands.keys.sort.join("\n ")}\n"
|
26
|
+
elsif !subcommands.empty?
|
27
|
+
string += "\nSubcommands: #{subcommands.keys.sort.join(" ")}\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
string
|
31
|
+
end
|
32
|
+
|
33
|
+
# Helper method that takes an array of values, wraps them to the given
|
34
|
+
# limit, and adds each line as a separator. This is useful when you
|
35
|
+
# have a large amount of information you want to display and you want
|
36
|
+
# to wrap if for display to the user when showing options.
|
37
|
+
def wrap(prefix, values, separator: " ", limit: 80)
|
38
|
+
line = [prefix]
|
39
|
+
lines = [line]
|
40
|
+
prefix_length = length = prefix.length
|
41
|
+
sep_length = separator.length
|
42
|
+
indent = " " * prefix_length
|
43
|
+
|
44
|
+
values.each do |value|
|
45
|
+
value_length = value.length
|
46
|
+
new_length = sep_length + length + value_length
|
47
|
+
if new_length > limit
|
48
|
+
line = [indent, separator, value]
|
49
|
+
lines << line
|
50
|
+
length = prefix_length
|
51
|
+
else
|
52
|
+
line << separator << value
|
53
|
+
end
|
54
|
+
length += sep_length + value_length
|
55
|
+
end
|
56
|
+
|
57
|
+
lines.each do |line|
|
58
|
+
separator line.join
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Halt processing with a CommandExit using the given string.
|
63
|
+
# This can be used to implement early exits, by calling this
|
64
|
+
# method in a block:
|
65
|
+
#
|
66
|
+
# on("--version", "show program version") { halt VERSION }
|
67
|
+
def halt(string)
|
68
|
+
raise CommandExit, string
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dsl"
|
4
|
+
|
5
|
+
module Rodish
|
6
|
+
module Processor
|
7
|
+
attr_reader :command
|
8
|
+
|
9
|
+
# Process an argv array using a new instance of the class that is
|
10
|
+
# extended with Rodish::Processor. Additional arguments are passed to
|
11
|
+
# new when creating the instance.
|
12
|
+
#
|
13
|
+
# Callers of this method are encouraged to rescue Rodish::CommandExit,
|
14
|
+
# to handle both early exits and command failures.
|
15
|
+
def process(argv, *a, **kw)
|
16
|
+
# Deliberately do not pass a block here, to reserve
|
17
|
+
# block handling for future use.
|
18
|
+
@command.process(new(*a, **kw), {}, argv)
|
19
|
+
rescue ::OptionParser::InvalidOption => e
|
20
|
+
@command.raise_failure(e.message)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Without a block, returns the Command instance for related subcommand
|
24
|
+
# (a nested subcommand if multiple command names are given).
|
25
|
+
#
|
26
|
+
# With a block, uses the last command name to create a subcommand under
|
27
|
+
# the other named commands, configuring the created subcommand using the
|
28
|
+
# block.
|
29
|
+
def on(*command_names, &block)
|
30
|
+
if block
|
31
|
+
command_name = command_names.pop
|
32
|
+
dsl(command_names).on(command_name, &block)
|
33
|
+
else
|
34
|
+
dsl(command_names)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Uses the last command name to create a subcommand under the other
|
39
|
+
# named commands, with the block being the commands
|
40
|
+
def is(*command_names, command_name, args: 0, invalid_args_message: nil, &block)
|
41
|
+
dsl(command_names).is(command_name, args:, invalid_args_message:, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Freeze the command when freezing the object.
|
45
|
+
def freeze
|
46
|
+
command.freeze
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return a hash of usage strings for the root command and all subcommands,
|
51
|
+
# recursively. The hash has string keys for the command name, and
|
52
|
+
# string values for the option parser(s) for the command.
|
53
|
+
def usages
|
54
|
+
usages = {}
|
55
|
+
|
56
|
+
command.each_subcommand do |names, command|
|
57
|
+
option_parsers = command.option_parsers
|
58
|
+
unless option_parsers.empty?
|
59
|
+
usages[names.join(" ")] = command.option_parsers.join("\n\n")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
usages
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Use the array of command names to find the appropriate subcommand
|
69
|
+
# (which may be empty to use the root command), and return a DSL instance
|
70
|
+
# for it.
|
71
|
+
def dsl(command_names)
|
72
|
+
command = self.command
|
73
|
+
command_names.each do |name|
|
74
|
+
command = command.subcommands.fetch(name)
|
75
|
+
end
|
76
|
+
DSL.new(command)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodish
|
4
|
+
# Rodish::SkipOptionParser is used when option parsing should be
|
5
|
+
# skipped, treating all entries in argv as arguments.
|
6
|
+
class SkipOptionParser
|
7
|
+
# A usage banner to use for the related command.
|
8
|
+
attr_reader :banner
|
9
|
+
|
10
|
+
# The same as banner, but ending in a newline, similarly
|
11
|
+
# to how OptionParser#to_s works.
|
12
|
+
attr_reader :to_s
|
13
|
+
|
14
|
+
def initialize(banner)
|
15
|
+
@banner = "Usage: #{banner}".freeze
|
16
|
+
@to_s = (@banner + "\n").freeze
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/rodish.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rodish/processor"
|
4
|
+
require_relative "rodish/dsl"
|
5
|
+
|
6
|
+
module Rodish
|
7
|
+
# Install a Rodish processor in the given class. This extends the class
|
8
|
+
# with Rodish::Processor, and uses the block to configure the processor
|
9
|
+
# using Rodish::DSL.
|
10
|
+
def self.processor(klass, &block)
|
11
|
+
klass.extend(Processor)
|
12
|
+
klass.instance_variable_set(:@command, DSL.command([].freeze, &block))
|
13
|
+
klass
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rodish
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Evans
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-27 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: optparse
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: minitest
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: minitest-global_expectations
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
description: |
|
55
|
+
Rodish parses an argv array using a routing tree approach. It is
|
56
|
+
designed to make it easy to implement command line applications
|
57
|
+
that support multiple levels of subcommands, with options at each
|
58
|
+
level.
|
59
|
+
email: code@jeremyevans.net
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files:
|
63
|
+
- README.rdoc
|
64
|
+
- CHANGELOG
|
65
|
+
- MIT-LICENSE
|
66
|
+
files:
|
67
|
+
- CHANGELOG
|
68
|
+
- MIT-LICENSE
|
69
|
+
- README.rdoc
|
70
|
+
- lib/rodish.rb
|
71
|
+
- lib/rodish/command.rb
|
72
|
+
- lib/rodish/dsl.rb
|
73
|
+
- lib/rodish/errors.rb
|
74
|
+
- lib/rodish/option_parser.rb
|
75
|
+
- lib/rodish/processor.rb
|
76
|
+
- lib/rodish/skip_option_parser.rb
|
77
|
+
homepage: http://github.com/jeremyevans/rodish
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata:
|
81
|
+
bug_tracker_uri: https://github.com/jeremyevans/rodish/issues
|
82
|
+
changelog_uri: https://github.com/jeremyevans/rodish/blob/master/CHANGELOG
|
83
|
+
source_code_uri: https://github.com/jeremyevans/rodish
|
84
|
+
rdoc_options:
|
85
|
+
- "--quiet"
|
86
|
+
- "--line-numbers"
|
87
|
+
- "--inline-source"
|
88
|
+
- "--title"
|
89
|
+
- 'Rodish: Routing tree argv parser'
|
90
|
+
- "--main"
|
91
|
+
- README.rdoc
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '3.1'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubygems_version: 3.6.2
|
106
|
+
specification_version: 4
|
107
|
+
summary: Routing tree argv parser
|
108
|
+
test_files: []
|