suboptparse 0.1.24
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|