suboptparse 0.1.24

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
+ [![Gem Version](https://badge.fury.io/rb/suboptparse.svg)](https://badge.fury.io/rb/suboptparse)
4
+ [![Known Vulnerabilities](https://snyk.io/test/github/basking2/suboptparse/badge.svg)](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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SubOptParse
4
+ VERSION = "0.1.24"
5
+ end
@@ -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:
@@ -0,0 +1,4 @@
1
+ module Suboptparse
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []