samovar 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e30e7064ff47b5ddb9d3461d291513e310fe7000d96abf00c473447d76c4f17
4
- data.tar.gz: 3ec1e17b743863c3df573f959450922c9aec4fcedbf4fe8a690657effcc643f6
3
+ metadata.gz: e1051db069257cf9d25e3599a5ddb5c5547a921cb90d04552a777d3af5c335b2
4
+ data.tar.gz: 632d0819eb51db7f9640feb8491e543706721a4313243527bb5380da576a578b
5
5
  SHA512:
6
- metadata.gz: 16adeb378b0451726310d57f76b7223c37a51a43fbf21d1348c3d309b94ae1b99fb943736362c3c66f0b1c651630982f9a5eba5b32bfb8202c0704370e67a6cc
7
- data.tar.gz: b9bbcfbdc77d330992c47eebfdb10c612d57f140ef0b6e8f97204e3d67d2ccf2f020313cdf54c816122cebb279c82a315db40820d60ef74f41de118c7f5d4873
6
+ metadata.gz: f5daa04c5dff0500f95c9dec3cabe02bbbb10fd5a9d5a06f89f6d9e6687eb11af89325472df628170ea04d0458d3c8600849c4fab1299c83bf523e58e1e268ed
7
+ data.tar.gz: 304cc7d54293e3998dae9e3e3247506db3f82d2d0f6d66e988b4ad21cad0150846a1325a039c4dd3f4746fe0903e6e6afd5a6e2f615d724d7e7ee788da49310b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,215 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `samovar` to build command-line tools and applications.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add samovar
11
+ ~~~
12
+
13
+ Or install it yourself as:
14
+
15
+ ~~~ bash
16
+ $ gem install samovar
17
+ ~~~
18
+
19
+ ## Core Concepts
20
+
21
+ Samovar provides a declarative class-based DSL for building command-line parsers. The main concepts include:
22
+
23
+ - **Commands**: Classes that represent specific functions in your program, inheriting from {ruby Samovar::Command}.
24
+ - **Options**: Command-line flags and arguments that can be parsed using the `options` block.
25
+ - **Nested Commands**: Sub-commands that can be composed using the `nested` method.
26
+ - **Tokens**: Positional arguments parsed using `one` and `many` methods.
27
+ - **Splits**: Separating arguments at a specific delimiter (e.g., `--`) using the `split` method.
28
+
29
+ ## Usage
30
+
31
+ Create `Command` classes that represent specific functions in your program. The top level command might look something like this:
32
+
33
+ ~~~ ruby
34
+ require "samovar"
35
+
36
+ class List < Samovar::Command
37
+ self.description = "List the current directory"
38
+
39
+ def call
40
+ system("ls -lah")
41
+ end
42
+ end
43
+
44
+ class Application < Samovar::Command
45
+ options do
46
+ option "--help", "Do you need help?"
47
+ end
48
+
49
+ nested :command, {
50
+ "list" => List
51
+ }, default: "list"
52
+
53
+ def call
54
+ if @options[:help]
55
+ self.print_usage
56
+ else
57
+ @command.call
58
+ end
59
+ end
60
+ end
61
+
62
+ Application.call # Defaults to ARGV.
63
+ ~~~
64
+
65
+ ### Basic Options
66
+
67
+ Options allow you to parse command-line flags and arguments:
68
+
69
+ ~~~ ruby
70
+ require "samovar"
71
+
72
+ class Application < Samovar::Command
73
+ options do
74
+ option "-f/--frobulate <text>", "Frobulate the text"
75
+ option "-x | -y", "Specify either x or y axis.", key: :axis
76
+ option "-F/--yeah/--flag", "A boolean flag with several forms."
77
+ option "--things <a,b,c>", "A list of things" do |value|
78
+ value.split(/\s*,\s*/)
79
+ end
80
+ end
81
+ end
82
+
83
+ application = Application.new(["-f", "Algebraic!"])
84
+ application.options[:frobulate] # 'Algebraic!'
85
+
86
+ application = Application.new(["-x", "-y"])
87
+ application.options[:axis] # :y
88
+
89
+ application = Application.new(["-F"])
90
+ application.options[:flag] # true
91
+
92
+ application = Application.new(["--things", "x,y,z"])
93
+ application.options[:things] # ['x', 'y', 'z']
94
+ ~~~
95
+
96
+ ### Nested Commands
97
+
98
+ You can create sub-commands that are nested within your main application:
99
+
100
+ ~~~ ruby
101
+ require "samovar"
102
+
103
+ class Create < Samovar::Command
104
+ def invoke(parent)
105
+ puts "Creating"
106
+ end
107
+ end
108
+
109
+ class Application < Samovar::Command
110
+ nested "<command>",
111
+ "create" => Create
112
+
113
+ def invoke(program_name: File.basename($0))
114
+ if @command
115
+ @command.invoke
116
+ else
117
+ print_usage(program_name)
118
+ end
119
+ end
120
+ end
121
+
122
+ Application.new(["create"]).invoke
123
+ ~~~
124
+
125
+ ### ARGV Splits
126
+
127
+ You can split arguments at a delimiter (typically `--`):
128
+
129
+ ~~~ ruby
130
+ require "samovar"
131
+
132
+ class Application < Samovar::Command
133
+ many :packages
134
+ split :argv
135
+ end
136
+
137
+ application = Application.new(["foo", "bar", "baz", "--", "apples", "oranges", "feijoas"])
138
+ application.packages # ['foo', 'bar', 'baz']
139
+ application.argv # ['apples', 'oranges', 'feijoas']
140
+ ~~~
141
+
142
+ ### Parsing Tokens
143
+
144
+ You can parse positional arguments using `one` and `many`:
145
+
146
+ ~~~ ruby
147
+ require "samovar"
148
+
149
+ class Application < Samovar::Command
150
+ self.description = "Mix together your favorite things."
151
+
152
+ one :fruit, "Name one fruit"
153
+ many :cakes, "Any cakes you like"
154
+ end
155
+
156
+ application = Application.new(["apple", "chocolate cake", "fruit cake"])
157
+ application.fruit # 'apple'
158
+ application.cakes # ['chocolate cake', 'fruit cake']
159
+ ~~~
160
+
161
+ ### Explicit Commands
162
+
163
+ Given a custom `Samovar::Command` subclass, you can instantiate it with options:
164
+
165
+ ~~~ ruby
166
+ application = Application["--root", path]
167
+ ~~~
168
+
169
+ You can also duplicate an existing command instance with additions/changes:
170
+
171
+ ~~~ ruby
172
+ concurrent_application = application["--threads", 12]
173
+ ~~~
174
+
175
+ These forms can be useful when invoking one command from another, or in unit tests.
176
+
177
+ ## Error Handling
178
+
179
+ Samovar provides two entry points with different error handling behaviors:
180
+
181
+ ### `call()` - High-Level Entry Point
182
+
183
+ Use `call()` for CLI applications. It handles parsing errors gracefully by printing usage information:
184
+
185
+ ~~~ ruby
186
+ # Automatically handles errors and prints usage
187
+ Application.call(ARGV)
188
+ ~~~
189
+
190
+ If parsing fails or the command raises a {ruby Samovar::Error}, it will:
191
+ - Print the usage information with the error highlighted
192
+ - Return `nil` instead of raising an exception
193
+
194
+ ### `parse()` - Low-Level Parsing
195
+
196
+ Use `parse()` when you need explicit error handling or for testing:
197
+
198
+ ~~~ ruby
199
+ begin
200
+ app = Application.parse(["--unknown-flag"])
201
+ rescue Samovar::InvalidInputError => error
202
+ # Handle the error yourself
203
+ puts "Invalid input: #{error.message}"
204
+ end
205
+ ~~~
206
+
207
+ The `parse()` method raises exceptions on parsing errors, giving you full control over error handling.
208
+
209
+ ### Error Types
210
+
211
+ Samovar defines several error types:
212
+
213
+ - {ruby Samovar::InvalidInputError}: Raised when unexpected command-line input is encountered
214
+ - {ruby Samovar::MissingValueError}: Raised when required arguments or options are missing
215
+
@@ -0,0 +1,14 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: Samovar is a flexible option parser excellent support for sub-commands
5
+ and help documentation.
6
+ metadata:
7
+ documentation_uri: https://ioquatix.github.io/samovar/
8
+ funding_uri: https://github.com/sponsors/ioquatix/
9
+ source_code_uri: https://github.com/ioquatix/samovar.git
10
+ files:
11
+ - path: getting-started.md
12
+ title: Getting Started
13
+ description: This guide explains how to use `samovar` to build command-line tools
14
+ and applications.
@@ -1,54 +1,85 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'table'
7
- require_relative 'options'
8
- require_relative 'nested'
9
- require_relative 'one'
10
- require_relative 'many'
11
- require_relative 'split'
6
+ require_relative "table"
7
+ require_relative "options"
8
+ require_relative "nested"
9
+ require_relative "one"
10
+ require_relative "many"
11
+ require_relative "split"
12
12
 
13
- require_relative 'output'
14
13
 
15
- require_relative 'error'
14
+ require_relative "output"
15
+
16
+ require_relative "error"
16
17
 
17
18
  module Samovar
19
+ # Represents a command in the command-line interface.
20
+ #
21
+ # Commands are the main building blocks of Samovar applications. Each command is a class that can parse command-line arguments, options, and sub-commands.
18
22
  class Command
19
- def self.call(input = ARGV)
20
- if command = self.parse(input)
21
- command.call
22
- end
23
- end
24
-
25
- # The top level entry point for parsing ARGV.
26
- def self.parse(input)
27
- self.new(input)
23
+ # Parse and execute the command with the given input.
24
+ #
25
+ # This is the high-level entry point for CLI applications. It handles errors gracefully by printing usage and returning nil.
26
+ #
27
+ # @parameter input [Array(String)] The command-line arguments to parse.
28
+ # @parameter output [IO] The output stream for error messages.
29
+ # @returns [Object | Nil] The result of the command's call method, or nil if parsing/execution failed.
30
+ def self.call(input = ARGV, output: $stderr)
31
+ self.parse(input).call
28
32
  rescue Error => error
29
- error.command.print_usage(output: $stderr) do |formatter|
33
+ error.command.print_usage(output: output) do |formatter|
30
34
  formatter.map(error)
31
35
  end
32
36
 
33
37
  return nil
34
38
  end
35
39
 
40
+ # Parse the command-line input and create a command instance.
41
+ #
42
+ # This is the low-level parsing primitive. It raises {Error} exceptions on parsing failures.
43
+ # For CLI applications, use {call} instead which handles errors gracefully.
44
+ #
45
+ # @parameter input [Array(String)] The command-line arguments to parse.
46
+ # @returns [Command] The parsed command instance.
47
+ # @raises [Error] If parsing fails.
48
+ def self.parse(input)
49
+ self.new(input)
50
+ end
51
+
52
+ # Create a new command instance with the given arguments.
53
+ #
54
+ # This is a convenience method for creating command instances with explicit arguments.
55
+ #
56
+ # @parameter input [Array(String)] The command-line arguments to parse.
57
+ # @parameter options [Hash] Additional options to pass to the command.
58
+ # @returns [Command] The command instance.
36
59
  def self.[](*input, **options)
37
60
  self.new(input, **options)
38
61
  end
39
62
 
40
63
  class << self
64
+ # A description of the command's purpose.
65
+ #
66
+ # @attribute [String]
41
67
  attr_accessor :description
42
68
  end
43
69
 
70
+ # The table of rows for parsing command-line arguments.
71
+ #
72
+ # @returns [Table] The table of parsing rows.
44
73
  def self.table
45
74
  @table ||= Table.nested(self)
46
75
  end
47
76
 
77
+ # Append a row to the parsing table.
78
+ #
79
+ # @parameter row The row to append to the table.
48
80
  def self.append(row)
49
81
  if method_defined?(row.key, false)
50
- warning "Method for key #{row.key} is already defined!", caller
51
- # raise ArgumentError, "Method for key #{row.key} is already defined!"
82
+ raise ArgumentError, "Method for key #{row.key} is already defined!"
52
83
  end
53
84
 
54
85
  attr_accessor(row.key) if row.respond_to?(:key)
@@ -56,26 +87,51 @@ module Samovar
56
87
  self.table << row
57
88
  end
58
89
 
90
+ # Define command-line options for this command.
91
+ #
92
+ # @parameter arguments [Array] The arguments for the options.
93
+ # @parameter options [Hash] Additional options.
94
+ # @yields {|...| ...} A block that defines the options using {Options}.
59
95
  def self.options(*arguments, **options, &block)
60
96
  append Options.parse(*arguments, **options, &block)
61
97
  end
62
98
 
99
+ # Define a nested sub-command.
100
+ #
101
+ # @parameter arguments [Array] The arguments for the nested command.
102
+ # @parameter options [Hash] A hash mapping command names to command classes.
63
103
  def self.nested(*arguments, **options)
64
104
  append Nested.new(*arguments, **options)
65
105
  end
66
106
 
107
+ # Define a single required positional argument.
108
+ #
109
+ # @parameter arguments [Array] The arguments for the positional parameter.
110
+ # @parameter options [Hash] Additional options.
67
111
  def self.one(*arguments, **options)
68
112
  append One.new(*arguments, **options)
69
113
  end
70
114
 
115
+ # Define multiple positional arguments.
116
+ #
117
+ # @parameter arguments [Array] The arguments for the positional parameters.
118
+ # @parameter options [Hash] Additional options.
71
119
  def self.many(*arguments, **options)
72
120
  append Many.new(*arguments, **options)
73
121
  end
74
122
 
123
+ # Define a split point in the argument list (typically `--`).
124
+ #
125
+ # @parameter arguments [Array] The arguments for the split.
126
+ # @parameter options [Hash] Additional options.
75
127
  def self.split(*arguments, **options)
76
128
  append Split.new(*arguments, **options)
77
129
  end
78
130
 
131
+ # Generate usage information for this command.
132
+ #
133
+ # @parameter rows [Output::Rows] The rows to append usage information to.
134
+ # @parameter name [String] The name of the command.
79
135
  def self.usage(rows, name)
80
136
  rows.nested(name, self) do |rows|
81
137
  return unless table = self.table.merged
@@ -90,14 +146,22 @@ module Samovar
90
146
  end
91
147
  end
92
148
 
149
+ # Generate a command-line usage string.
150
+ #
151
+ # @parameter name [String] The name of the command.
152
+ # @returns [String] The command-line usage string.
93
153
  def self.command_line(name)
94
- if table = self.table.merged
95
- "#{name} #{table.merged.usage}"
96
- else
97
- name
98
- end
154
+ table = self.table.merged
155
+
156
+ return "#{name} #{table.usage}"
99
157
  end
100
158
 
159
+ # Initialize a new command instance.
160
+ #
161
+ # @parameter input [Array(String) | Nil] The command-line arguments to parse.
162
+ # @parameter name [String] The name of the command (defaults to the script name).
163
+ # @parameter parent [Command | Nil] The parent command, if this is a nested command.
164
+ # @parameter output [IO | Nil] The output stream for usage information.
101
165
  def initialize(input = nil, name: File.basename($0), parent: nil, output: nil)
102
166
  @name = name
103
167
  @parent = parent
@@ -106,23 +170,47 @@ module Samovar
106
170
  parse(input) if input
107
171
  end
108
172
 
173
+ # The output stream for usage information.
174
+ #
175
+ # @attribute [IO]
109
176
  attr :output
110
177
 
178
+ # The output stream for usage information, defaults to `$stdout`.
179
+ #
180
+ # @returns [IO] The output stream.
111
181
  def output
112
182
  @output || $stdout
113
183
  end
114
184
 
185
+ # Generate a string representation of the command.
186
+ #
187
+ # @returns [String] The class name.
115
188
  def to_s
116
189
  self.class.name
117
190
  end
118
191
 
192
+ # The name of the command.
193
+ #
194
+ # @attribute [String]
119
195
  attr :name
196
+
197
+ # The parent command, if this is a nested command.
198
+ #
199
+ # @attribute [Command | Nil]
120
200
  attr :parent
121
201
 
202
+ # Duplicate the command with additional arguments.
203
+ #
204
+ # @parameter input [Array(String)] The additional command-line arguments to parse.
205
+ # @returns [Command] The duplicated command instance.
122
206
  def [](*input)
123
207
  self.dup.tap{|command| command.parse(input)}
124
208
  end
125
209
 
210
+ # Parse the command-line input.
211
+ #
212
+ # @parameter input [Array(String)] The command-line arguments to parse.
213
+ # @returns [Command] The command instance.
126
214
  def parse(input)
127
215
  self.class.table.merged.parse(input, self)
128
216
 
@@ -133,6 +221,11 @@ module Samovar
133
221
  end
134
222
  end
135
223
 
224
+ # Print usage information for this command.
225
+ #
226
+ # @parameter output [IO] The output stream to print to.
227
+ # @parameter formatter [Class] The formatter class to use for output.
228
+ # @yields {|formatter| ...} A block to customize the output.
136
229
  def print_usage(output: self.output, formatter: Output::UsageFormatter, &block)
137
230
  rows = Output::Rows.new
138
231
 
data/lib/samovar/error.rb CHANGED
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # The base class for all Samovar errors.
7
8
  class Error < StandardError
8
9
  end
9
-
10
+
11
+ # Raised when invalid input is provided on the command line.
10
12
  class InvalidInputError < Error
13
+ # Initialize a new invalid input error.
14
+ #
15
+ # @parameter command [Command] The command that encountered the error.
16
+ # @parameter input [Array(String)] The remaining input that could not be parsed.
11
17
  def initialize(command, input)
12
18
  @command = command
13
19
  @input = input
@@ -15,19 +21,37 @@ module Samovar
15
21
  super "Could not parse token #{input.first.inspect}"
16
22
  end
17
23
 
24
+ # The token that could not be parsed.
25
+ #
26
+ # @returns [String] The first unparsed token.
18
27
  def token
19
28
  @input.first
20
29
  end
21
30
 
31
+ # Check if the error was caused by a help request.
32
+ #
33
+ # @returns [Boolean] True if the token is `--help`.
22
34
  def help?
23
35
  self.token == "--help"
24
36
  end
25
37
 
38
+ # The command that encountered the error.
39
+ #
40
+ # @attribute [Command]
26
41
  attr :command
42
+
43
+ # The remaining input that could not be parsed.
44
+ #
45
+ # @attribute [Array(String)]
27
46
  attr :input
28
47
  end
29
48
 
49
+ # Raised when a required value is missing.
30
50
  class MissingValueError < Error
51
+ # Initialize a new missing value error.
52
+ #
53
+ # @parameter command [Command] The command that encountered the error.
54
+ # @parameter field [Symbol] The name of the missing field.
31
55
  def initialize(command, field)
32
56
  @command = command
33
57
  @field = field
@@ -35,7 +59,14 @@ module Samovar
35
59
  super "#{field} is required"
36
60
  end
37
61
 
62
+ # The command that encountered the error.
63
+ #
64
+ # @attribute [Command]
38
65
  attr :command
66
+
67
+ # The name of the missing field.
68
+ #
69
+ # @attribute [Symbol]
39
70
  attr :field
40
71
  end
41
72
  end
@@ -4,6 +4,7 @@
4
4
  # Copyright, 2017-2023, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents a runtime failure in command execution.
7
8
  class Failure < RuntimeError
8
9
  end
9
10
  end