rodish 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The run_is plugin adds a +run_is+ method that is similar to +is+,
4
+ # but adds a post subcommand instead of a regular subcommand.
5
+ #
6
+ # This does not allow you to set a command description or usage, so it is
7
+ # not recommended in new code.
8
+ #
9
+ # This plugin depends on the is and post_commands plugins.
10
+
11
+ #
12
+ module Rodish
13
+ module Plugins
14
+ module RunIs
15
+ def self.before_load(app)
16
+ app.plugin :is
17
+ app.plugin :post_commands
18
+ end
19
+
20
+ module DSLMethods
21
+ # Similar to +is+, but for post subcommands instead of normal
22
+ # subcommands.
23
+ def run_is(command_name, args: 0, &block)
24
+ _is(:run_on, command_name, args:, &block)
25
+ end
26
+ end
27
+ end
28
+
29
+ register(:run_is, RunIs)
30
+ end
31
+ end
32
+
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The skip_option_parsing plugin allows skipping option parsing
4
+ # for a command, treating all elements of argv as arguments instead
5
+ # of options. This is different than the default behavior, where
6
+ # all options use will fail by default (as no options are supported
7
+ # by default.
8
+ #
9
+ # After loading the plugin, when configuring the command, you can
10
+ # call skip_options_parsing with the usage banner:
11
+ #
12
+ # skip_options_parsing("usage banner")
13
+
14
+ #
15
+ module Rodish
16
+ module Plugins
17
+ module SkipOptionParsing
18
+ module DSLMethods
19
+ # Skip option parsing for the command. This is different
20
+ # then the default option parsing, which will error if any
21
+ # options are given. A banner must be provided, setting
22
+ # the usage for the command.
23
+ #
24
+ # The main reason to use this is if you are going to pass
25
+ # the entire remaining argv as the argv to another
26
+ # program.
27
+ def skip_option_parsing(banner)
28
+ @command.banner = banner
29
+ @command.option_parser = :skip
30
+ end
31
+ end
32
+
33
+ module CommandMethods
34
+ private
35
+
36
+ # Do not process options if the option parser is set to skip.
37
+ def process_options(argv, options, option_key, option_parser)
38
+ super unless option_parser == :skip
39
+ end
40
+
41
+ # Whether the given option parser should be ommitted from the
42
+ # command help output.
43
+ def omit_option_parser_from_help?(parser)
44
+ super || parser == :skip
45
+ end
46
+ end
47
+ end
48
+
49
+ register(:skip_option_parsing, SkipOptionParsing)
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The usages plugin adds the #usages method to the Rodish processor.
4
+ # This returns a hash for the command help for all commands and subcommands
5
+ # of the processor.
6
+
7
+ #
8
+ module Rodish
9
+ module Plugins
10
+ module Usages
11
+ module ProcessorMethods
12
+ # Return a hash of usage strings for the root command and all subcommands,
13
+ # recursively. The hash has string keys for the command name, and
14
+ # string values for the help for the command.
15
+ def usages
16
+ usages = {}
17
+
18
+ command.each_subcommand do |names, command|
19
+ usages[names.join(" ")] = command.help
20
+ end
21
+
22
+ usages
23
+ end
24
+ end
25
+ end
26
+
27
+ register(:usages, Usages)
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The wrapped_options_separator plugin adds wrap to the option
4
+ # parser, which includes a separator with wrapped content.
5
+
6
+ require_relative "_wrap"
7
+
8
+ #
9
+ module Rodish
10
+ module Plugins
11
+ module WrappedOptionsSeparator
12
+ module OptionParserMethods
13
+ include Wrap_
14
+
15
+ # Helper method that takes an array of values, wraps them to the given
16
+ # limit, and adds each line as a separator. This is useful when you
17
+ # have a large amount of information you want to display and you want
18
+ # to wrap if for display to the user when showing options.
19
+ def wrap(prefix, values, separator: " ", limit: 80)
20
+ super.each do |line|
21
+ separator line
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ register(:wrapped_options_separator, WrappedOptionsSeparator)
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodish
4
+ MUTEX = Mutex.new
5
+ private_constant :MUTEX
6
+
7
+ # Hash of symbol keys to module values for registered rodish plugins.
8
+ PLUGINS = {}
9
+ private_constant :PLUGINS
10
+
11
+ # Namespace for Rodish plugins. Plugins do not have to be in this
12
+ # namespace, but this is what plugins that ship with Rodish use.
13
+ module Plugins
14
+ # Load a Rodish plugin. +name+ should be a symbol.
15
+ def self.fetch(name)
16
+ MUTEX.synchronize{PLUGINS[name]}
17
+ end
18
+
19
+ # Register a Rodish plugin. +name+ should be a symbol, and +mod+
20
+ # should be a module.
21
+ def self.register(name, mod)
22
+ MUTEX.synchronize{PLUGINS[name] = mod}
23
+ end
24
+ end
25
+ end
@@ -1,11 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "dsl"
4
+ require_relative "plugins"
4
5
 
5
6
  module Rodish
6
7
  module Processor
8
+ # The root command for the processor.
7
9
  attr_reader :command
8
10
 
11
+ # Load a plugin into the current processor.
12
+ def plugin(name, ...)
13
+ mod = load_plugin(name)
14
+
15
+ unless mod.respond_to?(:before_load) || mod.respond_to?(:after_load)
16
+ _plugin_without_before_or_after_load_check(...)
17
+ end
18
+
19
+ mod.before_load(self, ...) if mod.respond_to?(:before_load)
20
+ extend(mod::ProcessorMethods) if defined?(mod::ProcessorMethods)
21
+ self::DSL.include(mod::DSLMethods) if defined?(mod::DSLMethods)
22
+ self::DSL::Command.include(mod::CommandMethods) if defined?(mod::CommandMethods)
23
+ self::DSL::OptionParser.include(mod::OptionParserMethods) if defined?(mod::OptionParserMethods)
24
+ mod.after_load(self, ...) if mod.respond_to?(:after_load)
25
+ nil
26
+ end
27
+
9
28
  # Process an argv array using a new instance of the class that is
10
29
  # extended with Rodish::Processor. Additional arguments are passed to
11
30
  # new when creating the instance.
@@ -16,8 +35,6 @@ module Rodish
16
35
  # Deliberately do not pass a block here, to reserve
17
36
  # block handling for future use.
18
37
  @command.process(new(*a, **kw), {}, argv)
19
- rescue ::OptionParser::InvalidOption => e
20
- @command.raise_failure(e.message)
21
38
  end
22
39
 
23
40
  # Without a block, returns the Command instance for related subcommand
@@ -28,8 +45,11 @@ module Rodish
28
45
  # block.
29
46
  def on(*command_names, &block)
30
47
  if block
31
- command_name = command_names.pop
32
- dsl(command_names).on(command_name, &block)
48
+ if command_name = command_names.pop
49
+ dsl(command_names).on(command_name, &block)
50
+ else
51
+ dsl(command_names).instance_exec(&block)
52
+ end
33
53
  else
34
54
  dsl(command_names)
35
55
  end
@@ -37,43 +57,55 @@ module Rodish
37
57
 
38
58
  # Uses the last command name to create a subcommand under the other
39
59
  # named commands, with the block being the commands
40
- def is(*command_names, command_name, args: 0, invalid_args_message: nil, &block)
41
- dsl(command_names).is(command_name, args:, invalid_args_message:, &block)
60
+ def is(*command_names, command_name, **kw, &block)
61
+ dsl(command_names).is(command_name, **kw, &block)
42
62
  end
43
63
 
44
- # Freeze the command when freezing the object.
64
+ # Freeze the command and classes related to the processor when freezing the processor.
45
65
  def freeze
46
66
  command.freeze
67
+ self::DSL.freeze
68
+ self::DSL::Command.freeze
69
+ self::DSL::OptionParser.freeze
47
70
  super
48
71
  end
49
72
 
50
- # Return a hash of usage strings for the root command and all subcommands,
51
- # recursively. The hash has string keys for the command name, and
52
- # string values for the option parser(s) for the command.
53
- def usages
54
- usages = {}
73
+ private
74
+
75
+ def _plugin_without_before_or_after_load_check
76
+ raise ArgumentError, "plugin doesn't support block" if defined?(yield)
77
+ end
55
78
 
56
- command.each_subcommand do |names, command|
57
- option_parsers = command.option_parsers
58
- unless option_parsers.empty?
59
- usages[names.join(" ")] = command.option_parsers.join("\n\n")
79
+ # Load the rodish plugin with the given name, which can be either a module
80
+ # (used directly), or a symbol (which will load a registered plugin), requiring
81
+ # the related plugin file if it is not already registered.
82
+ def load_plugin(name)
83
+ case name
84
+ when Module
85
+ name
86
+ when Symbol
87
+ unless mod = Rodish::Plugins.fetch(name)
88
+ require "rodish/plugins/#{name}"
89
+ unless mod = Rodish::Plugins.fetch(name)
90
+ raise RuntimeError, "rodish plugin did not properly register itself: #{name.inspect}"
91
+ end
60
92
  end
61
- end
62
93
 
63
- usages
94
+ mod
95
+ else
96
+ raise ArgumentError, "invalid argument to plugin: #{name.inspect}"
97
+ end
64
98
  end
65
99
 
66
- private
67
-
68
100
  # Use the array of command names to find the appropriate subcommand
69
101
  # (which may be empty to use the root command), and return a DSL instance
70
102
  # for it.
71
103
  def dsl(command_names)
72
104
  command = self.command
73
105
  command_names.each do |name|
74
- command = command.subcommands.fetch(name)
106
+ command = command.subcommand(name)
75
107
  end
76
- DSL.new(command)
108
+ self::DSL.new(command)
77
109
  end
78
110
  end
79
111
  end
data/lib/rodish.rb CHANGED
@@ -9,7 +9,22 @@ module Rodish
9
9
  # using Rodish::DSL.
10
10
  def self.processor(klass, &block)
11
11
  klass.extend(Processor)
12
- klass.instance_variable_set(:@command, DSL.command([].freeze, &block))
12
+
13
+ dsl_class = Class.new(DSL)
14
+ klass.const_set(:DSL, dsl_class)
15
+
16
+ command_class = Class.new(Command)
17
+ dsl_class.const_set(:Command, command_class)
18
+
19
+ option_parser_class = Class.new(OptionParser)
20
+ dsl_class.const_set(:OptionParser, option_parser_class)
21
+
22
+ option_parser = option_parser_class.new
23
+ option_parser.set_banner("")
24
+ option_parser.freeze
25
+ command_class.const_set(:DEFAULT_OPTION_PARSER, option_parser)
26
+
27
+ klass.instance_variable_set(:@command, dsl_class.command([].freeze, &block))
13
28
  klass
14
29
  end
15
30
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodish
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-04 00:00:00.000000000 Z
10
+ date: 2025-03-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: optparse
@@ -60,21 +60,31 @@ email: code@jeremyevans.net
60
60
  executables: []
61
61
  extensions: []
62
62
  extra_rdoc_files:
63
- - README.rdoc
64
- - CHANGELOG
65
63
  - MIT-LICENSE
66
64
  files:
67
- - CHANGELOG
68
65
  - MIT-LICENSE
69
- - README.rdoc
70
66
  - lib/rodish.rb
71
67
  - lib/rodish/command.rb
72
68
  - lib/rodish/dsl.rb
73
69
  - lib/rodish/errors.rb
74
70
  - lib/rodish/option_parser.rb
71
+ - lib/rodish/plugins.rb
72
+ - lib/rodish/plugins/_context_sensitive_help.rb
73
+ - lib/rodish/plugins/_wrap.rb
74
+ - lib/rodish/plugins/after_options_hook.rb
75
+ - lib/rodish/plugins/cache_help_output.rb
76
+ - lib/rodish/plugins/help_examples.rb
77
+ - lib/rodish/plugins/help_option_values.rb
78
+ - lib/rodish/plugins/help_order.rb
79
+ - lib/rodish/plugins/invalid_args_message.rb
80
+ - lib/rodish/plugins/is.rb
81
+ - lib/rodish/plugins/post_commands.rb
82
+ - lib/rodish/plugins/run_is.rb
83
+ - lib/rodish/plugins/skip_option_parsing.rb
84
+ - lib/rodish/plugins/usages.rb
85
+ - lib/rodish/plugins/wrapped_options_separator.rb
75
86
  - lib/rodish/processor.rb
76
- - lib/rodish/skip_option_parser.rb
77
- homepage: http://github.com/jeremyevans/rodish
87
+ homepage: https://rodish.jeremyevans.net/
78
88
  licenses:
79
89
  - MIT
80
90
  metadata:
@@ -87,8 +97,6 @@ rdoc_options:
87
97
  - "--inline-source"
88
98
  - "--title"
89
99
  - 'Rodish: Routing tree argv parser'
90
- - "--main"
91
- - README.rdoc
92
100
  require_paths:
93
101
  - lib
94
102
  required_ruby_version: !ruby/object:Gem::Requirement
data/CHANGELOG DELETED
@@ -1,7 +0,0 @@
1
- === 1.1.0 (2025-03-03)
2
-
3
- * Support after_options method for a block called after options processing (jeremyevans)
4
-
5
- === 1.0.0 (2025-02-27)
6
-
7
- * Initial Public Release
data/README.rdoc DELETED
@@ -1,265 +0,0 @@
1
- = Rodish
2
-
3
- Rodish parses an argv array using a routing tree approach. It is
4
- designed to make it easy to implement command line applications
5
- that support multiple levels of subcommands, with options at each
6
- level.
7
-
8
- = Installation
9
-
10
- gem install rodish
11
-
12
- = Source Code
13
-
14
- Source code is available on GitHub at https://github.com/jeremyevans/rodish
15
-
16
- = Simple Example
17
-
18
- Here's a simple commented example with a single subcommand:
19
-
20
- require "rodish"
21
-
22
- # This just creates a normal Ruby class. For each argv parse, Rodish will
23
- # create an instance of this class
24
- class CliExample
25
-
26
- # This allows instances of the class to be instantiated with or without
27
- # a default person.
28
- def initialize(default_person=nil)
29
- @default_person = default_person
30
- end
31
-
32
- # This installs the Rodish processor into CliExample. It also extends the
33
- # CliExample class with the Rodish::Processor module). The block provided
34
- # is evaluated in the context of a Rodish::DSL instance.
35
- Rodish.processor(self) do
36
-
37
- # This method call creates a hello subcommand of the current/root command.
38
- # If the given argv is for the hello subcommand, the block will be
39
- # executed in the context of the CliExample instance.
40
- on "hello" do
41
-
42
- # This adds a usage string and a -p options for the hello subcommand.
43
- # The block passed is used to set the options via the optparse library.
44
- options("cli-example hello [options]") do
45
- on("-p", "--person=name", "say hello to specific person")
46
- end
47
-
48
- run do |opts|
49
- "Hello #{opts[:person] || @default_person || 'World'}"
50
- end
51
- end
52
- end
53
- end
54
-
55
- # This requests Rodish to process the provided argv. Rodish will determine
56
- # the related command block to execute and execute it. The return value of
57
- # that block will be returned to the caller.
58
- CliExample.process(["hello"])
59
- # => "Hello World"
60
-
61
- # Additional arguments passed to .process are passed to .new. In this
62
- # example, this sets the default person.
63
- CliExample.process(["hello"], "Adol")
64
- # => "Hello Adol"
65
-
66
- # This shows an example of passing an option to a subcommand, and using
67
- # the option value when returning a response.
68
- CliExample.process(["hello", "-p", "Feena"], "Adol")
69
- # => "Hello Feena"
70
-
71
- = Rodish DSL
72
-
73
- Inside the <tt>Rodish.processor</tt> block, you are in the context of the root
74
- command. The following methods are available for configuring the processing of
75
- the command.
76
-
77
- == +on+
78
-
79
- The +on+ method adds a subcommand of the current command, and yields to the
80
- block to configure the subcommand. All of the methods described in the Rodish
81
- DSL section can be executed inside the +on+ block, and arbitrary levels of
82
- subcommands are supported.
83
-
84
- == +options+
85
-
86
- The +options+ method sets up an options parser for the current command. The
87
- default options parser disallows any options. Options are parsed into a hash,
88
- which is yielded to commands (as in the above example).
89
-
90
- This method requires a String argument for the usage for the current command.
91
- You can also provide a +key+ keyword argument, to put parsed options into
92
- a subhash of the main options hash, which can be useful when options are
93
- parsed at multiple levels.
94
-
95
- If a block is provided, it is executed in the context of a Rodish::OptionParser
96
- instance. Rodish::OptionParser is a subclass of Ruby's standard OptionParser
97
- (from +optparse+), with a few additional methods.
98
-
99
- == +args+
100
-
101
- The +args+ method sets the number of arguments accepted when running the command.
102
- The default for +args+ is +0+. You can provide either an Integer to accept a
103
- fixed number of arguments, or a Range to allow any number of arguments in that
104
- range.
105
-
106
- The method also accepts an +invalid_args_message+ keyword argument for the
107
- message, to set the message to display if an invalid number of arguments is
108
- provided.
109
-
110
- == +run+
111
-
112
- The +run+ method sets the block to run for the current command. If the
113
- command accepts a fixed number of arguments, those arguments are yielded
114
- as the first arguments to the command. If the command accepts a range of
115
- argument numbers, then the remaining argv array will be passed as the
116
- first argument.
117
-
118
- The block will be passed two additional arguments, the options already
119
- parsed, and the current Rodish::Command object.
120
-
121
- == +is+
122
-
123
- The +is+ method is a shortcut for calling the +on+ method and +run+ method.
124
- For example:
125
-
126
- is "hello" do
127
- :world
128
- end
129
-
130
- is equivalent to:
131
-
132
- on "hello" do
133
- run do
134
- :world
135
- end
136
- end
137
-
138
- The +is+ method also takes +args+ and +invalid_args_message+ keyword arguments.
139
-
140
- == +before+
141
-
142
- The +before+ method takes a block, and this block is executed before command
143
- or subcommand execution, in the same context that the +run+ block would be
144
- executed in. It is passed the remaining argv array and the already parsed
145
- options. This is not called if an invalid subcommand is requested and there
146
- is no run block for the command.
147
-
148
- == +after_options+
149
-
150
- The +after_options+ method takes a block, and this block is executed
151
- directly after options parsing, in the same context that the +run+ block would be
152
- executed in. It is passed the remaining argv array and the already parsed
153
- options.
154
-
155
- == +skip_option_parsing+
156
-
157
- The +skip_option_parsing+ method makes the command do no option parsing,
158
- treating all elements of the remaining argv as options. It requires a
159
- usage string for the command, similar to +options+.
160
-
161
- == +run_on+
162
-
163
- The +run_on+ method is similar to +on+, except it creates a post subcommand
164
- instead of a normal subcommand. Post subcommands allow the +run+ block
165
- to parse part of the remaining argv array, and then call a subcommand with
166
- the modified (or a new) argv array. You dispatch to post subcommands
167
- inside the +run+ block by calling +run+ on the command argument:
168
-
169
- on "hello" do
170
- args(2...)
171
-
172
- run do |argv, opts, command|
173
- @name = argv.shift
174
- command.run(self, opts, argv)
175
- end
176
-
177
- run_on "world" do
178
- run do
179
- "Hello #{@name.upcase} World!"
180
- end
181
- end
182
- end
183
-
184
- # process(%w[hello foo world])
185
- # => "Hello FOO World!"
186
-
187
- == +run_is+
188
-
189
- The +run_is+ method operates similarly to +is+, but adds a post subcommand
190
- instead of a normal subcommand.
191
-
192
- == +post_options+
193
-
194
- The +post_options+ method sets an option parser that is used for post
195
- subcommands. This parses options from the argv array that passed to
196
- +command.run+, before calling the related subcommand. Example:
197
-
198
- on "hello" do
199
- args(2...)
200
-
201
- post_options("Usage: hello name [options] subcommand ...") do
202
- on("-c", "--cap", "capitalize instead of uppercase")
203
- end
204
-
205
- run do |argv, opts, command|
206
- @name = argv.shift
207
- command.run(self, opts, argv)
208
- end
209
-
210
- run_is "world" do |opts|
211
- name = opts[:cap] ? @name.capitalize : @name.upcase
212
- "Hello #{name} World!"
213
- end
214
- end
215
-
216
- # process(%w[hello foo world])
217
- # => "Hello FOO World!"
218
- # process(%w[hello foo -c world])
219
- # => "Hello Foo World!"
220
-
221
- == +autoload_subcommand_dir+
222
-
223
- The +autoload_subcommand_dir+ takes a directory, and will autoload
224
- subcommands from the given directory. Filenames ending in +.rb+ in
225
- this directory will be treated as subcommands, and requiring the
226
- file should add the appropriate subcommand.
227
-
228
- This allows you to design complex command line programs where only
229
- the parts of the program needed to handle the given argv are loaded.
230
-
231
- == +autoload_post_subcommand_dir+
232
-
233
- The +autoload_post_subcommand_dir+ operates the same as
234
- +autoload_subcommand_dir+, but it handles post subcommands instead of
235
- normal subcommands.
236
-
237
- = Examples
238
-
239
- The tests that ship with Rodish fully cover all of Rodish's functionality.
240
-
241
- If you would like to view a production example using Rodish, which
242
- uses most of Rodish's features, please see UbiCli, which is part of
243
- Ubicloud:
244
-
245
- * Main class: https://github.com/ubicloud/ubicloud/blob/main/lib/ubi_cli.rb
246
- * Commands (separate command per file): https://github.com/ubicloud/ubicloud/tree/main/cli-commands
247
-
248
- = History
249
-
250
- Rodish was extracted from Ubicloud (https://github.com/ubicloud/ubicloud),
251
- and is the argv processor used in Ubicloud's command line interface.
252
-
253
- = Naming
254
-
255
- The name Rodish was chosen because Rodish uses an API similar (-ish) to the Roda
256
- web framework (http://roda.jeremyevans.net), and the library is designed for
257
- use in applications executed from a shell (sh).
258
-
259
- = License
260
-
261
- MIT
262
-
263
- = Author
264
-
265
- Jeremy Evans <code@jeremyevans.net>
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rodish
4
- # Rodish::SkipOptionParser is used when option parsing should be
5
- # skipped, treating all entries in argv as arguments.
6
- class SkipOptionParser
7
- # A usage banner to use for the related command.
8
- attr_reader :banner
9
-
10
- # The same as banner, but ending in a newline, similarly
11
- # to how OptionParser#to_s works.
12
- attr_reader :to_s
13
-
14
- def initialize(banner)
15
- @banner = "Usage: #{banner}".freeze
16
- @to_s = (@banner + "\n").freeze
17
- end
18
- end
19
- end