suboptparse 0.1.24
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/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +86 -0
- data/LICENSE.txt +21 -0
- data/README.md +308 -0
- data/Rakefile +41 -0
- data/lib/suboptparse/auto_require.rb +157 -0
- data/lib/suboptparse/shared_state.rb +32 -0
- data/lib/suboptparse/util.rb +33 -0
- data/lib/suboptparse/version.rb +5 -0
- data/lib/suboptparse.rb +286 -0
- data/sig/suboptparse.rbs +4 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 24db536b0fc85771658568b756c7989ee2c8d3ff8e09bca9d5276e3a03c38967
|
4
|
+
data.tar.gz: e4350502b4931b3a8b353adfda993fdfb56c18c85889b924d9d8e6ef7d443d8f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a79c1a8d07d00ad44cfae635e26c65709b0676be125e5437ce1c1d1a31c949d9cf882a439644d6d1005274b5fc41231935eee8772745268e7490aa5c09eb4056
|
7
|
+
data.tar.gz: 9ce6eec459229b9f3d56f42790523c55d66021769187dbca616ae62315ddc75019571a765940ca334c68f270c7e46fd13ef257628c7c8e88a30438328838e48f
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.0
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
EnforcedStyle: double_quotes
|
6
|
+
|
7
|
+
Style/StringLiteralsInInterpolation:
|
8
|
+
EnforcedStyle: double_quotes
|
9
|
+
|
10
|
+
Lint/ShadowingOuterLocalVariable:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Metrics/BlockLength:
|
14
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
## [0.1.24]
|
2
|
+
|
3
|
+
- RDoc updates.
|
4
|
+
- GitHub build updates.
|
5
|
+
|
6
|
+
## [0.1.14]
|
7
|
+
|
8
|
+
- Support post_parse and after_parse.
|
9
|
+
- Alias on_parse to pre_parse.
|
10
|
+
|
11
|
+
## [0.1.13] - 2025-02-04
|
12
|
+
|
13
|
+
- Throw the LoadError what prevents an command from being auto-required
|
14
|
+
if that command is ever executed.
|
15
|
+
|
16
|
+
## [0.1.12] - 2025-02-03
|
17
|
+
|
18
|
+
- Adjust SubOptParse::AutoRequire.require to automatically add the sub-command
|
19
|
+
and pass the created SubOptParse object to the callback for further
|
20
|
+
configuration.
|
21
|
+
- Improve help output to remove duplicate commands and sort the commands by
|
22
|
+
command name. Duplicates could appear when a command was registered by
|
23
|
+
cmdadd() and cmddocsadd().
|
24
|
+
|
25
|
+
## [0.1.11] - 2025-02-03
|
26
|
+
|
27
|
+
- Extend cmddocadd() to also support dynamic autloading of commands
|
28
|
+
defined in this way. This allows cmd documentation to exist when
|
29
|
+
autoloading is defined.
|
30
|
+
|
31
|
+
## [0.1.10] - 2025-02-03
|
32
|
+
|
33
|
+
- Add recurisve help. Help is printed from the root command down to the
|
34
|
+
last called child command.
|
35
|
+
|
36
|
+
## [0.1.9] - 2025-02-02
|
37
|
+
|
38
|
+
- Add sub-command documentation when there is no loaded command.
|
39
|
+
- Allow auto-loading of files if an autorequire_root path is defined.
|
40
|
+
- Migrate cmdpath to a list of command names rather than a single string.
|
41
|
+
|
42
|
+
## [0.1.8] - 2025-01-31
|
43
|
+
|
44
|
+
- Update changelog and readme to match new release process.
|
45
|
+
|
46
|
+
## [0.1.7] - 2025-01-31
|
47
|
+
|
48
|
+
- *No Change* - Configuring GitHub actions to release code.
|
49
|
+
|
50
|
+
## [0.1.6] - 2025-01-31
|
51
|
+
|
52
|
+
- *No Change* - Configuring GitHub actions to release code.
|
53
|
+
|
54
|
+
## [0.1.5] - 2025-01-30
|
55
|
+
|
56
|
+
- Update project meta data.
|
57
|
+
- Update project rdoc documentation.
|
58
|
+
- Add RDoc::Task to Rakefile. Output to ./rdoc.
|
59
|
+
|
60
|
+
## [0.1.4] - 2025-01-30
|
61
|
+
|
62
|
+
- Set @cmdpath in the command ussage. This is the path in the command tree
|
63
|
+
that leads to the executing command.
|
64
|
+
- Add SubOptParse::Util.merge_recursive() to assist in merging
|
65
|
+
configurations loaded into Ruby Hash objects.
|
66
|
+
- Add SubOptParse::SharedState to help use shared_state correctly
|
67
|
+
in instances of SubOptParsers.
|
68
|
+
|
69
|
+
## [0.1.3] - 2025-01-28
|
70
|
+
|
71
|
+
- Set banner correctly on sub-commands.
|
72
|
+
|
73
|
+
## [0.1.2] - 2025-01-28
|
74
|
+
|
75
|
+
- Update usage on sub-commands.
|
76
|
+
- Allow an optional description to be included when calling .cmdadd().
|
77
|
+
|
78
|
+
## [0.1.1] - 2025-01-28
|
79
|
+
|
80
|
+
- Add default "help" sub-command to all commands.
|
81
|
+
- Allow for custom command lookups by overriding SubOptParser#[].
|
82
|
+
- Allow for adding commands are parse time with .on_parse { |so| ... }
|
83
|
+
|
84
|
+
## [0.1.0] - 2025-01-21
|
85
|
+
|
86
|
+
- Initial release
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Sam Baskinger <basking2@yahoo.com>
|
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 FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
# SubOptParse
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/suboptparse)
|
4
|
+
[](https://snyk.io/test/github/basking2/suboptparse)
|
5
|
+
|
6
|
+
|
7
|
+
[SubOptParse](https://basking2.github.io/suboptparse) is a collection of classes and utilities to extend Ruby's
|
8
|
+
[OptionParser](https://ruby-doc.org/current/optparse/tutorial_rdoc.html) with some understanding of sub-commands.
|
9
|
+
|
10
|
+
Sub-commands maybe thought of as a traditional CLI command but with
|
11
|
+
a single parent command as the entry poiont. As an example, consider
|
12
|
+
a CLI application that buys things from a store. We'll call the command
|
13
|
+
`./buy`. You can have a sub-command `./buy apples --count=3` that
|
14
|
+
knows how to buy apples. Now, purchasing from the deli is different
|
15
|
+
and so we isolate that into a different sub-command, `deli`.
|
16
|
+
You could run `./buy deli potato_salad`.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Install the gem and add to the application's Gemfile by executing:
|
21
|
+
|
22
|
+
$ bundle add suboptparse
|
23
|
+
|
24
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
25
|
+
|
26
|
+
$ gem install suboptparse
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
### Looks Like OptionParser
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require "suboptparse"
|
34
|
+
|
35
|
+
# Looks like OptionParser.
|
36
|
+
parser = SubOptParser.new do |sop|
|
37
|
+
# Normal OptionParser calls.
|
38
|
+
sop.on("--my-option=foo", "-m", "Sets a value") do |v|
|
39
|
+
# Record value somewhere.
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Still looks like OptionParser, but a command to execute is returned.
|
44
|
+
cmd = parser.parse!
|
45
|
+
|
46
|
+
# If you don't specify a command, don't call this! It throws an exception.
|
47
|
+
cmd.call()
|
48
|
+
```
|
49
|
+
|
50
|
+
### Define A Root Command
|
51
|
+
|
52
|
+
This is like the previous example, but we define a command to call.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
require "suboptparse"
|
56
|
+
|
57
|
+
# Looks like OptionParser.
|
58
|
+
parser = SubOptParser.new do |sop|
|
59
|
+
# Normal OptionParser calls.
|
60
|
+
sop.on("--my-option=foo", "-m", "Sets a value") do |v|
|
61
|
+
# Record value somewhere.
|
62
|
+
end
|
63
|
+
|
64
|
+
sop.cmd do |args|
|
65
|
+
puts "Root command run."
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Still looks like OptionParser, but a command to execute is returned.
|
70
|
+
cmd = parser.parse!
|
71
|
+
|
72
|
+
# Now this prints, "Root command run."
|
73
|
+
cmd.call()
|
74
|
+
```
|
75
|
+
|
76
|
+
### Sub Command Example
|
77
|
+
|
78
|
+
This example shows a sub-command and a few features.
|
79
|
+
|
80
|
+
You can raise an exception if arguments are not consumed during parsing
|
81
|
+
using `raise_unknown=true`.
|
82
|
+
|
83
|
+
You can share state between commands so they can parse into a common location.
|
84
|
+
This makes parent commands sharing common values with child commands
|
85
|
+
easier.
|
86
|
+
|
87
|
+
Finally, if you set `raise_unknown=false` (the default value), then
|
88
|
+
unparsed command line options are passed to the called command.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
require "suboptparse"
|
92
|
+
|
93
|
+
# Looks like OptionParser.
|
94
|
+
parser = SubOptParser.new do |sop|
|
95
|
+
|
96
|
+
# Set shared state. All sub-commands may add values to this.
|
97
|
+
# This is set at command creation time and should not be changed after.
|
98
|
+
sop.shared_state = SubOptParse::SharedState.new
|
99
|
+
|
100
|
+
# This command should raise an error if it executes with unconsumed options.
|
101
|
+
# Default is false.
|
102
|
+
sop.raise_unknown = true
|
103
|
+
|
104
|
+
# Normal OptionParser calls.
|
105
|
+
sop.on("--my-option=foo", "-m", "Sets a value") do |v|
|
106
|
+
# Record value somewhere.
|
107
|
+
end
|
108
|
+
|
109
|
+
sop.cmd do |args|
|
110
|
+
puts "Root command run."
|
111
|
+
end
|
112
|
+
|
113
|
+
sop.cmdadd("subcommand", "This is a sub-command.") do |sop|
|
114
|
+
sop.on("--sub-command-option=value", "A sub-command option.") do |v|
|
115
|
+
sop.shared_state["sub-command-option"] = v
|
116
|
+
end
|
117
|
+
|
118
|
+
sop.cmd do |unconsumed_arguments|
|
119
|
+
sop.shared_state["sub-command-option"]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Parse and call the command in 1 call.
|
125
|
+
ret = parser.call("subcommand", "--sub-command-option=foo", "--my-option=bar")
|
126
|
+
|
127
|
+
# The returns "foo".
|
128
|
+
puts "Calling subcommand returned #{ret}."
|
129
|
+
```
|
130
|
+
|
131
|
+
## How Tos
|
132
|
+
|
133
|
+
### Intercept -h
|
134
|
+
|
135
|
+
Calling `-h` normally has the effect of terminating parsing and printing the
|
136
|
+
help of the parent command. You can register an `on_parse` handler to
|
137
|
+
remove `-h` and append `help` to the end of the command line arguemnts.
|
138
|
+
This will cause the default help function of the child command to be called
|
139
|
+
when `-h` is on the command line.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
require "suboptparse"
|
143
|
+
|
144
|
+
# Looks like OptionParser.
|
145
|
+
parser = SubOptParser.new do |sop|
|
146
|
+
|
147
|
+
sop.on_parse do |op, argv|
|
148
|
+
if argv.include? "-h" or argv.include? "--help"
|
149
|
+
argv.delete("-h")
|
150
|
+
argv.delete("--help")
|
151
|
+
argv.push('help')
|
152
|
+
end
|
153
|
+
argv
|
154
|
+
end
|
155
|
+
|
156
|
+
sop.shared_state = SubOptParse::SharedState.new
|
157
|
+
|
158
|
+
# Normal OptionParser calls.
|
159
|
+
sop.on("--my-option=foo", "-m", "Sets a value") do |v|
|
160
|
+
# Record value somewhere.
|
161
|
+
end
|
162
|
+
|
163
|
+
sop.cmd do |args|
|
164
|
+
puts "Root command run."
|
165
|
+
end
|
166
|
+
|
167
|
+
sop.cmdadd("subcommand", "This is a sub-command.") do |sop|
|
168
|
+
sop.on("--sub-command-option=value", "A sub-command option.") do |v|
|
169
|
+
sop.shared_state["sub-command-option"] = v
|
170
|
+
end
|
171
|
+
|
172
|
+
sop.cmd do
|
173
|
+
sop.shared_state["sub-command-option"]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Parse and call the command in 1 call.
|
179
|
+
ret = parser.call("-h", "subcommand")
|
180
|
+
|
181
|
+
puts "Calling subcommand returned #{ret}."
|
182
|
+
```
|
183
|
+
|
184
|
+
### Auto Requiring Commands
|
185
|
+
|
186
|
+
You can configure the SubOptParser to call `require` on files in your
|
187
|
+
`$LOAD_PATH` to load and define sub-commands dynamically. There
|
188
|
+
are two approaches, implicit and explicit. Both methods require that the
|
189
|
+
loading class define itself when loading.
|
190
|
+
|
191
|
+
This may be done by using class variables or class methods that capture
|
192
|
+
the current command name being loaded and the current SubOptParse instance
|
193
|
+
doing the loading.
|
194
|
+
|
195
|
+
A dynamically loaded command might look like this:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
# frozen_string_literal: true
|
199
|
+
|
200
|
+
require "suboptparse/auto_require"
|
201
|
+
|
202
|
+
SubOptParser::AutoRequire.register do |so, name|
|
203
|
+
so.addcmd(name, "A command.") do |so|
|
204
|
+
# Define the command...
|
205
|
+
so.cmd { puts so.help }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
The `register` method is just a conveneint way to access the class
|
211
|
+
variables
|
212
|
+
`SubOptParse::AutoRequire::auto_require_command_parent`
|
213
|
+
and
|
214
|
+
`SubOptParse::AutoRequire::auto_require_command_name`.
|
215
|
+
You may access them directly, though, that is a lot of typing.
|
216
|
+
|
217
|
+
### Implicit Auto Requiring
|
218
|
+
|
219
|
+
First, to enable the feature, you must define an `autorequire_root`.
|
220
|
+
This path will be prefixed to any file attempted to be loaded with a call
|
221
|
+
to `require`.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
so = SubOptParser.new do |opt|
|
225
|
+
opt.autorequire_root = "suboptparse/autoreqtest"
|
226
|
+
opt.autorequire_suffix = "_command"
|
227
|
+
end
|
228
|
+
|
229
|
+
# This calls require "suboptparse/autoreqtest/a_command"
|
230
|
+
so.get_subcommand("a").help
|
231
|
+
|
232
|
+
# This calls require "suboptparse/autoreqtest/a/b/c_command"
|
233
|
+
so.get_subcommand("a", "b", "c").help
|
234
|
+
```
|
235
|
+
|
236
|
+
There is a problem with this approache. Because the commands are not
|
237
|
+
loaded, calling `help` on the parent command will not show the child
|
238
|
+
commands. Calling `help` on the child commands will recursively
|
239
|
+
print the help from the root command.
|
240
|
+
|
241
|
+
You can define documentation for a child command by calling
|
242
|
+
`cmddocadd()` as shown here.
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
so = SubOptParser.new do |opt|
|
246
|
+
opt.autorequire_root = "suboptparse/autoreqtest"
|
247
|
+
opt.autorequire_suffix = "_command"
|
248
|
+
opt.cmddocadd("a", "This is the A command")
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
Now the help text for the root command will include a listing for the
|
253
|
+
sub-command, "a".
|
254
|
+
|
255
|
+
### Explicit Auto Requiring
|
256
|
+
|
257
|
+
Like with implicit loading, to enable the feature, you must define
|
258
|
+
an `autorequire_root`. This path will be prefixed to any file attempted to
|
259
|
+
be loaded with a call to `require`.
|
260
|
+
|
261
|
+
To define a sub-command to be loaded, again, `cmddocadd()` is used, but
|
262
|
+
included is a 3rd argument which is the `require` path
|
263
|
+
*relative to the `autorequire_root`.*
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
so = SubOptParser.new do |opt|
|
267
|
+
opt.shared_state = {}
|
268
|
+
opt.autorequire_root = "suboptparse/autoreqtest2"
|
269
|
+
opt.cmddocadd("a", "A", "a_command")
|
270
|
+
opt.cmddocadd("b", "B", "a/b_command")
|
271
|
+
opt.cmddocadd("c", "C", "a/b/c_command")
|
272
|
+
end
|
273
|
+
|
274
|
+
# Calls `require "suboptparse/autoreqtest2/a_command"`.
|
275
|
+
so.get_subcommand("a")
|
276
|
+
|
277
|
+
# Calls `require "suboptparse/autoreqtest2/a/b/c_command"`.
|
278
|
+
so.call("a", "b", "c")
|
279
|
+
```
|
280
|
+
|
281
|
+
## Development
|
282
|
+
|
283
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
284
|
+
Then, run `rake spec` to run the tests.
|
285
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
286
|
+
|
287
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
288
|
+
To release a new version, update the version number in `version.rb`, and then run
|
289
|
+
`bundle exec rake release`, which will create a git tag for the version,
|
290
|
+
push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
291
|
+
|
292
|
+
## Releasing
|
293
|
+
|
294
|
+
```shell
|
295
|
+
# Tag with a 3 digit tag prefixed with a "v".
|
296
|
+
git tag v0.1.2
|
297
|
+
|
298
|
+
# Push the tag. GitHub actions will do the rest.
|
299
|
+
git push origin tag v0.1.2
|
300
|
+
```
|
301
|
+
|
302
|
+
## Contributing
|
303
|
+
|
304
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/basking2/suboptparse.
|
305
|
+
|
306
|
+
## License
|
307
|
+
|
308
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "rdoc/task"
|
6
|
+
require "English"
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
|
10
|
+
require "rubocop/rake_task"
|
11
|
+
|
12
|
+
RuboCop::RakeTask.new
|
13
|
+
|
14
|
+
RDoc::Task.new do |rdoc|
|
15
|
+
rdoc.main = "README.md"
|
16
|
+
rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
|
17
|
+
rdoc.options << "--format=darkfish"
|
18
|
+
rdoc.title = "SubOptParse"
|
19
|
+
rdoc.rdoc_dir = "rdoc"
|
20
|
+
end
|
21
|
+
|
22
|
+
task :version, [:val] do |_t, args|
|
23
|
+
raise StandardError.new, "Version must be formatted as v[digit].[digit].[digit]." \
|
24
|
+
unless args[:val] =~ /^v([0-9]+\.[0-9]+\.[0-9]+)$/
|
25
|
+
|
26
|
+
ver = $LAST_MATCH_INFO[1]
|
27
|
+
puts ver
|
28
|
+
vfile = [
|
29
|
+
"# frozen_string_literal: true",
|
30
|
+
"",
|
31
|
+
"module SubOptParse",
|
32
|
+
" VERSION = \"#{ver}\"",
|
33
|
+
"end",
|
34
|
+
""
|
35
|
+
].join("\n")
|
36
|
+
File.open("./lib/suboptparse/version.rb", "wt") do |io|
|
37
|
+
io.write(vfile)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task default: %i[spec rubocop]
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SubOptParser
|
4
|
+
# Methods and logic to support auto-requiring commands that are not found in a sub-command.
|
5
|
+
#
|
6
|
+
# This is done by loading files named after the command, as shown in the example.
|
7
|
+
# When the `.rb` file is loaded, the values # AutoRequire.auto_require_command_parent and
|
8
|
+
# AutoRequire.auto_require_command_name are set to the current parent command and the name
|
9
|
+
# of the command trying to be loaded.
|
10
|
+
#
|
11
|
+
# so = SubOptParser.new do |opt|
|
12
|
+
# opt.autorequire_root = "my_proj/my_commands"
|
13
|
+
# opt.autorequire_suffix = "_command"
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # This will require...
|
17
|
+
# # require "my_proj/my_commands/a_command"
|
18
|
+
# # require "my_proj/my_commands/a/b_command"
|
19
|
+
# # require "my_proj/my_commands/a/b/c_command"
|
20
|
+
# so.call("a", "b", "c")
|
21
|
+
#
|
22
|
+
# The contents of `c_command.rb` might look like this...
|
23
|
+
#
|
24
|
+
# require "suboptparse/auto_require"
|
25
|
+
#
|
26
|
+
# SubOptParser::AutoRequire.register do |so, name|
|
27
|
+
# so.addcmd(name, "A command.") do |so|
|
28
|
+
# so.cmd do
|
29
|
+
# so.shared_state["x"] = 3
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
module AutoRequire
|
34
|
+
# rubocop:disable Style/ClassVars
|
35
|
+
@@auto_require_command_parent = nil
|
36
|
+
@@auto_require_command_name = nil
|
37
|
+
@@auto_require_command_description = nil
|
38
|
+
# rubocop:enable Style/ClassVars
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def auto_require_command_parent
|
42
|
+
@@auto_require_command_parent
|
43
|
+
end
|
44
|
+
|
45
|
+
def auto_require_command_name
|
46
|
+
@@auto_require_command_name
|
47
|
+
end
|
48
|
+
|
49
|
+
def auto_require_command_description
|
50
|
+
@@auto_require_command_description
|
51
|
+
end
|
52
|
+
|
53
|
+
# This registers a command with a description with the current values of
|
54
|
+
# @@auto_require_command_name and @@auto_require_command_description and passes
|
55
|
+
# the resulting SubOptParser object to the given block.
|
56
|
+
#
|
57
|
+
# The calling module should them setup the particulars of the newly registered command.
|
58
|
+
def register(&blk) # :yields: sub_command
|
59
|
+
@@auto_require_command_parent.cmdadd(@@auto_require_command_name, @@auto_require_command_description, &blk)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Appended to the command name to load by a call to `require "path/to/cmd_command"`
|
64
|
+
attr_accessor :autorequire_suffix
|
65
|
+
|
66
|
+
# When non-nil the +cmdpath+ of this command and a sub-command will be used to
|
67
|
+
# automatically `require` the Ruby file to register the command.
|
68
|
+
#
|
69
|
+
attr_accessor :autorequire_root
|
70
|
+
|
71
|
+
# Auto-require the command.
|
72
|
+
#
|
73
|
+
# The root app name is ignored as it can change by program invocation.
|
74
|
+
def autorequire(name)
|
75
|
+
path = generate_require_path(name)
|
76
|
+
|
77
|
+
do_in_autorequire(name) do
|
78
|
+
require path
|
79
|
+
self[name]
|
80
|
+
rescue LoadError => e
|
81
|
+
generate_default_command(name, e)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Build a path for the given command name to pass to require.
|
86
|
+
#
|
87
|
+
# p = generate_require_path(foo)
|
88
|
+
# require p
|
89
|
+
#
|
90
|
+
def generate_require_path(name)
|
91
|
+
if @cmddocs.member?(name) && @cmddocs[name]["require"]
|
92
|
+
"#{@autorequire_root}/#{@cmddocs[name]["require"]}"
|
93
|
+
else
|
94
|
+
# Don't consider the root app name.
|
95
|
+
cmdpath = @cmdpath[1..] || []
|
96
|
+
|
97
|
+
[@autorequire_root, *cmdpath, "#{name}#{autorequire_suffix}"].join("/")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Add the name and description of a command to be documented in help text.
|
102
|
+
# This is for use with autoloaded commands which may not fully document themselves unless called.
|
103
|
+
#
|
104
|
+
# name:: Name of the command.
|
105
|
+
# description:: The description of the command.
|
106
|
+
# req:: String you can pass to "require" to load the command.
|
107
|
+
def cmddocadd(name, description, req = nil)
|
108
|
+
@cmddocs[name] = { "name" => name, "description" => description, "require" => req }
|
109
|
+
@op.banner = @banner + cmdhelp
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# Generate a command which, if executed, throws the exception that prevented it from being created.
|
115
|
+
def generate_default_command(name, err)
|
116
|
+
cmdadd(name, "Autogenerated command.") do |so|
|
117
|
+
so.cmd do
|
118
|
+
raise err
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# rubocop:disable Style/ClassVars, Metrics/MethodLength
|
124
|
+
def do_in_autorequire(name)
|
125
|
+
prev_auto_require_command_parent = @@auto_require_command_parent
|
126
|
+
prev_auto_require_command_name = @@auto_require_command_name
|
127
|
+
prev_auto_require_command_description = @@auto_require_command_description
|
128
|
+
@@auto_require_command_parent = self
|
129
|
+
@@auto_require_command_name = name
|
130
|
+
@@auto_require_command_description = get_sub_command_description(name)
|
131
|
+
yield if block_given?
|
132
|
+
ensure
|
133
|
+
@@auto_require_command_parent = prev_auto_require_command_parent
|
134
|
+
@@auto_require_command_name = prev_auto_require_command_name
|
135
|
+
@@auto_require_command_description = prev_auto_require_command_description
|
136
|
+
end
|
137
|
+
# rubocop:enable Style/ClassVars, Metrics/MethodLength:
|
138
|
+
|
139
|
+
def get_sub_command_description(name)
|
140
|
+
if (cmddoc = @cmddocs[name])
|
141
|
+
cmddoc["description"]
|
142
|
+
else
|
143
|
+
""
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def autorequire_init
|
150
|
+
@autorequire_root = nil
|
151
|
+
@autorequire_suffix = "_command"
|
152
|
+
|
153
|
+
# Documentation for unloaded commands.
|
154
|
+
@cmddocs = {}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./util"
|
4
|
+
|
5
|
+
module SubOptParse
|
6
|
+
# A helper class that may be used in SubOptParse.shared_state.
|
7
|
+
#
|
8
|
+
# This class wraps a Hash and allows other hash-like objects to be
|
9
|
+
# merged into this hash without creating a new container object.
|
10
|
+
class SharedState
|
11
|
+
# The current state.
|
12
|
+
attr_accessor :curr
|
13
|
+
|
14
|
+
def initialize(initial_state = {})
|
15
|
+
@curr = initial_state
|
16
|
+
end
|
17
|
+
|
18
|
+
def merge!(other)
|
19
|
+
@curr = SubOptParse::Util.recursive_merge(@curr, other)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Convenience function equivalent to shared_state.curr[name] = value.
|
23
|
+
def []=(name, value)
|
24
|
+
@curr[name] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
# Convenience function equivalent to shared_state.curr[name].
|
28
|
+
def [](name)
|
29
|
+
@curr[name]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SubOptParse
|
4
|
+
# Utility methods that may help in writing commands.
|
5
|
+
module Util
|
6
|
+
# Merge obj2 into obj1, if possible.
|
7
|
+
# Only hashes and lists are mergable.
|
8
|
+
# :stopdoc:
|
9
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
10
|
+
# :startdoc:
|
11
|
+
def self.recursive_merge(obj1, obj2)
|
12
|
+
if obj1.nil?
|
13
|
+
obj2
|
14
|
+
elsif obj2.nil?
|
15
|
+
obj1
|
16
|
+
elsif obj1.instance_of?(Hash) && obj2.instance_of?(Hash)
|
17
|
+
obj2.each { |k, v| obj1[k] = recursive_merge(obj1[k], v) }
|
18
|
+
obj1
|
19
|
+
elsif obj1.instance_of?(Array) && obj2.instance_of?(Array)
|
20
|
+
h = {}
|
21
|
+
obj1.each { |v| h[v] = recursive_merge(h[v], v) }
|
22
|
+
obj2.each { |v| h[v] = recursive_merge(h[v], v) }
|
23
|
+
h.values
|
24
|
+
else
|
25
|
+
# Can't merge. Return object 2.
|
26
|
+
obj2
|
27
|
+
end
|
28
|
+
end
|
29
|
+
# :stopdoc:
|
30
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
31
|
+
# :startdoc:
|
32
|
+
end
|
33
|
+
end
|
data/lib/suboptparse.rb
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "suboptparse/version"
|
4
|
+
require_relative "suboptparse/shared_state"
|
5
|
+
require_relative "suboptparse/util"
|
6
|
+
require_relative "suboptparse/auto_require"
|
7
|
+
|
8
|
+
require "optparse"
|
9
|
+
|
10
|
+
module SubOptParse
|
11
|
+
class Error < StandardError; end
|
12
|
+
end
|
13
|
+
|
14
|
+
# An adaptation of Ruby's default OptionParser to support sub-commands.
|
15
|
+
# :stopdoc:
|
16
|
+
# rubocop:disable Metrics/ClassLength
|
17
|
+
# :startdoc:
|
18
|
+
class SubOptParser
|
19
|
+
include SubOptParser::AutoRequire
|
20
|
+
|
21
|
+
# The description of this command.
|
22
|
+
attr_accessor :description
|
23
|
+
|
24
|
+
# The path of parent commands to this command.
|
25
|
+
# This is automatically set by #cmdadd().
|
26
|
+
attr_accessor :cmdpath
|
27
|
+
|
28
|
+
# The parent command, or nil? if this is the root command.
|
29
|
+
attr_accessor :cmdparent
|
30
|
+
|
31
|
+
# Arbitrary user data that is shared with all child objects.
|
32
|
+
# If the user does not change this, all child commands get the same
|
33
|
+
# state assigned when created with #cmdadd().
|
34
|
+
#
|
35
|
+
# NOTE: This must be set before calling #cmdadd().
|
36
|
+
#
|
37
|
+
# This may be any object, but the SubOptParse::ShareState class is a
|
38
|
+
# useful helper.
|
39
|
+
attr_accessor :shared_state
|
40
|
+
|
41
|
+
# When non-nil the +cmdpath+ of this command and a sub-command will be used to
|
42
|
+
# automatically `require` the Ruby file to register the command.
|
43
|
+
#
|
44
|
+
attr_accessor :autorequire_root
|
45
|
+
|
46
|
+
# Initialize a new SubOptParser.
|
47
|
+
#
|
48
|
+
# If block is given, this object is passed to allow for further initialization.
|
49
|
+
#
|
50
|
+
# banner:: Passed to OptionParser.new.
|
51
|
+
# width:: Passed to OptionParser.new.
|
52
|
+
# indent:: Passed to OptionParser.new.
|
53
|
+
# :parent => parent:: Defines the parent command to this one.
|
54
|
+
#
|
55
|
+
# :stopdoc:
|
56
|
+
# rubocop:disable Metrics/MethodLength
|
57
|
+
# :startdoc:
|
58
|
+
def initialize(banner = nil, width = 32, indent = " " * 4, **args) # :yields: self
|
59
|
+
autorequire_init
|
60
|
+
@op = OptionParser.new(banner, width, indent)
|
61
|
+
self.raise_unknown = false
|
62
|
+
@banner = @op.banner
|
63
|
+
@pre_parse_blk = nil
|
64
|
+
@post_parse_blk = nil
|
65
|
+
@cmdpath = [File.basename($PROGRAM_NAME)]
|
66
|
+
|
67
|
+
# This command's body.
|
68
|
+
@cmd = proc { raise StandardError, "No command defined." }
|
69
|
+
|
70
|
+
# Sub-command which are SubOptParser objects.
|
71
|
+
@cmds = {}
|
72
|
+
|
73
|
+
@cmdparent = args.delete(:parent)
|
74
|
+
|
75
|
+
yield(self) if block_given?
|
76
|
+
end
|
77
|
+
# :stopdoc:
|
78
|
+
# rubocop:enable Metrics/MethodLength
|
79
|
+
# :startdoc:
|
80
|
+
|
81
|
+
def method_missing(name, *args, &block)
|
82
|
+
@op.__send__(name, *args, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def respond_to_missing?(_name, _include_private = false)
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
# If true, an exception will be thrown when an unknown argument is given.
|
90
|
+
def raise_unknown
|
91
|
+
@op.raise_unknown
|
92
|
+
end
|
93
|
+
|
94
|
+
# If true, an exception will be thrown when an unknown argument is given.
|
95
|
+
def raise_unknown=(value)
|
96
|
+
@op.raise_unknown = value
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a sub command as the given name.
|
100
|
+
# No automatic initializtion is done. Prefer using #cmdadd().
|
101
|
+
#
|
102
|
+
# parser["sub_parser"] = SubOptParser.new do { |sub| ... }
|
103
|
+
def []=(name, subcmd)
|
104
|
+
@cmds[name] = subcmd
|
105
|
+
@op.banner = @banner + cmdhelp
|
106
|
+
end
|
107
|
+
|
108
|
+
# Users may override how to lookup a command or return "nil" if none is found.
|
109
|
+
def [](name)
|
110
|
+
@cmds[name]
|
111
|
+
end
|
112
|
+
|
113
|
+
# A callable that is invoked when this SubOptParser starts parsing arguments.
|
114
|
+
# This is primarily here to allow for lazy-populating of commands
|
115
|
+
# instead of requiring them to be defined at program invokation *or*
|
116
|
+
# to allow filtering or manipulating the command line arguments before
|
117
|
+
# parsing.
|
118
|
+
#
|
119
|
+
# The proc takes 2 arguments, this SubOptParse object and the current
|
120
|
+
# command line options array. Whatever is returned by this call
|
121
|
+
# is assigned to the command line options array value and is parsed. Eg:
|
122
|
+
#
|
123
|
+
# parser = SubOptParser.new
|
124
|
+
# parser.on_parse { |p,args| p["subcmd"] = SubOptParser.new ; args}
|
125
|
+
#
|
126
|
+
# Be careful to not create infinite recursion by adding
|
127
|
+
# commands that call themselves and then add themselves.
|
128
|
+
def on_parse(&blk) # :yields: sub_opt_parser, arguments
|
129
|
+
@pre_parse_blk = blk
|
130
|
+
end
|
131
|
+
|
132
|
+
# Similar to #on_parse but happens after parsing.
|
133
|
+
def after_parse(&blk)
|
134
|
+
@post_parse_blk = blk
|
135
|
+
end
|
136
|
+
|
137
|
+
alias pre_parse on_parse
|
138
|
+
alias post_parse after_parse
|
139
|
+
|
140
|
+
# Add a command (and return the resulting command).
|
141
|
+
def cmdadd(name, description = nil, *args) # :yields: self
|
142
|
+
o = _create_sub_command(name, description, *args)
|
143
|
+
|
144
|
+
# Add default "help" sub-job (unless we are the help job).
|
145
|
+
_add_default_help_cmd(o) if name != "help"
|
146
|
+
|
147
|
+
@cmds[name] = o
|
148
|
+
yield(o) if block_given?
|
149
|
+
@op.banner = @banner + cmdhelp
|
150
|
+
o
|
151
|
+
end
|
152
|
+
|
153
|
+
def cmd(prc = nil, &blk) # :yields: unconsumed_arguments
|
154
|
+
@cmd = prc unless prc.nil?
|
155
|
+
@cmd = blk unless blk.nil?
|
156
|
+
end
|
157
|
+
|
158
|
+
# Put the parent help text at the start of this command's help.
|
159
|
+
# This allows for building recursive help.
|
160
|
+
def help
|
161
|
+
if @cmdparent.nil?
|
162
|
+
@op.help
|
163
|
+
else
|
164
|
+
"#{@cmdparent.help}\n#{@op.help}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def cmdhelp
|
169
|
+
cmds = _build_cmd_doc_map
|
170
|
+
cmds = _build_cmddocs_doc_map(cmds)
|
171
|
+
h = _build_help_body(cmds)
|
172
|
+
|
173
|
+
# Append extra line.
|
174
|
+
"#{h}\n"
|
175
|
+
end
|
176
|
+
|
177
|
+
alias addcmd cmdadd
|
178
|
+
|
179
|
+
def parse!(argv, into: nil)
|
180
|
+
_parse!(argv, into: into)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Calls parse!.
|
184
|
+
def parse(*argv, into: nil)
|
185
|
+
_parse!(argv, into: into)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Parse the arguments in *argv and execute #call() on the returned command.
|
189
|
+
# Any unparsed values are passed to the invocation of #call().
|
190
|
+
#
|
191
|
+
# This is equivalent to
|
192
|
+
#
|
193
|
+
# cmd = parser.parse!(args)
|
194
|
+
# cmd.call(args)
|
195
|
+
def call(*argv, into: nil)
|
196
|
+
cmd, rest = _parse!(argv, into: into)
|
197
|
+
|
198
|
+
# Explode if we have arguments left but should not.
|
199
|
+
raise StandardError, "Unconsumed arguments: #{argv.join(",")}" if raise_unknown && !rest.empty?
|
200
|
+
|
201
|
+
cmd.call(rest)
|
202
|
+
end
|
203
|
+
|
204
|
+
# How sub-commands are loaded.
|
205
|
+
# If no sub-command can be loaded for the name, +nil+ is returned.
|
206
|
+
def get_subcommand(name)
|
207
|
+
if (cmd = self[name])
|
208
|
+
cmd
|
209
|
+
elsif autorequire_root && (cmd = autorequire(name))
|
210
|
+
cmd
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def _parse!(argv, into: nil)
|
217
|
+
argv = @pre_parse_blk.call(self, argv) if @pre_parse_blk
|
218
|
+
|
219
|
+
# Parse, removing all matching arguments.
|
220
|
+
@op.parse!(argv, into: into)
|
221
|
+
|
222
|
+
argv = @post_parse_blk.call(self, argv) if @post_parse_blk
|
223
|
+
|
224
|
+
if !argv.empty? && (cmd = get_subcommand(argv[0]))
|
225
|
+
argv.shift
|
226
|
+
cmd.parse!(argv, into: into)
|
227
|
+
else
|
228
|
+
[@cmd, argv]
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def _add_default_help_cmd(opt_parser)
|
233
|
+
opt_parser.cmdadd("help") do |o2|
|
234
|
+
o2.cmd do
|
235
|
+
puts opt_parser.help
|
236
|
+
exit 0
|
237
|
+
end
|
238
|
+
o2.description = "Print help."
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def _create_sub_command(name, description, *args)
|
243
|
+
cmdpath = @cmdpath.dup.append(name)
|
244
|
+
o = SubOptParser.new("Usage: #{cmdpath.join(" ")} [options]", *args, parent: self)
|
245
|
+
o.cmdpath = cmdpath
|
246
|
+
o.description ||= description
|
247
|
+
o.shared_state = @shared_state
|
248
|
+
o.raise_unknown = raise_unknown
|
249
|
+
o.autorequire_root = autorequire_root
|
250
|
+
o
|
251
|
+
end
|
252
|
+
|
253
|
+
# Build a map of command names to documentaton strings from @cmds.
|
254
|
+
def _build_cmd_doc_map
|
255
|
+
# Map of command names to descriptions.
|
256
|
+
@cmds.each_with_object({}) do |arr, h|
|
257
|
+
h[arr[0]] = arr[1].description
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Add to the given +cmds+ map of commands to documentation those
|
262
|
+
# commands in the @cmddocs map.
|
263
|
+
#
|
264
|
+
# If a command was already added by _build_cmd_doc_map, it is not
|
265
|
+
# overwritten.
|
266
|
+
#
|
267
|
+
# The final map is returned of commands to documentation.
|
268
|
+
def _build_cmddocs_doc_map(cmds)
|
269
|
+
# Map of documented command names to descriptions.
|
270
|
+
@cmddocs.each_with_object(cmds) do |arr, h|
|
271
|
+
h[arr[0]] = arr[1]["description"] unless @cmds.member?(arr[0])
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Given a map of command names to documentation strings,
|
276
|
+
# return a string that lists them in sorted order suitable for use in
|
277
|
+
# -h, --help or help output.
|
278
|
+
def _build_help_body(cmds)
|
279
|
+
cmds.keys.sort.inject("\n\n") do |h, key|
|
280
|
+
"#{h}#{key} - #{cmds[key]}\n"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
# :stopdoc:
|
285
|
+
# rubocop:enable Metrics/ClassLength
|
286
|
+
# :startdoc:
|
data/sig/suboptparse.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: suboptparse
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.24
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Baskinger
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-02-12 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Augment the default optparse OptionParser with some support for sub commands.
|
14
|
+
email:
|
15
|
+
- basking2@yahoo.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rspec"
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- CHANGELOG.md
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- lib/suboptparse.rb
|
27
|
+
- lib/suboptparse/auto_require.rb
|
28
|
+
- lib/suboptparse/shared_state.rb
|
29
|
+
- lib/suboptparse/util.rb
|
30
|
+
- lib/suboptparse/version.rb
|
31
|
+
- sig/suboptparse.rbs
|
32
|
+
homepage: https://basking2.github.io/suboptparse/
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
metadata:
|
36
|
+
allowed_push_host: https://rubygems.org
|
37
|
+
homepage_uri: https://basking2.github.io/suboptparse/
|
38
|
+
source_code_uri: https://github.com/basking2/suboptparse
|
39
|
+
changelog_uri: https://github.com/basking2/suboptparse/blob/main/CHANGELOG.md
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 3.0.0
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubygems_version: 3.5.9
|
56
|
+
signing_key:
|
57
|
+
specification_version: 4
|
58
|
+
summary: Add subcommands to OptionParser.
|
59
|
+
test_files: []
|