samovar 2.2.0 → 2.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5439e3891e6619b96a1c07a1e47da8566e670c21fc5d8cbb083cc256c302cbb3
4
- data.tar.gz: e6a3830369c9f274e1e968205e2b172bbfefec4c1c9780637e2bdd9d8e22a252
3
+ metadata.gz: 85e0df9ca2ed86284ab62c49ef724f93818ecb8571a33cccca98759f0a6b2612
4
+ data.tar.gz: 833c98f6619c7fb73243f6bbbbbb84e25a8d100c03af55392bd97f20a4246803
5
5
  SHA512:
6
- metadata.gz: 3555993209a03ec7ed58c015391675a5ba4b04552b2a5e82116590431afffd435f77bf64155cfff5ca5cb177c4f6ea744f85e11a93647fc379cc51b131fbf155
7
- data.tar.gz: ca79ebbc4254117b212ff5274a6d5bcd685ee599f4125f031dffdbbec81d5122fe2b1e0b63cb586580d4c429a5c4513104fd55a9ecbdf3ffd2c76165f7bd565c
6
+ metadata.gz: 0aff1d4bc6f8c2154dcd7e216a8f082fc50b44239321a122b139edff54e0749ab7109a45ab87e2615677e181426717b4912d81118f87f01cd4bf7032653d2041
7
+ data.tar.gz: d3c94b2986da89eba71d18d06df18dd0541775f2f1c2ba2c327e8407a38e05757e073b5f97c137c8c0241ab29811fd9e0a3fed3c2613034ca7db15ef0e5aff12
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,76 +1,137 @@
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)
81
+ if method_defined?(row.key, false)
82
+ raise ArgumentError, "Method for key #{row.key} is already defined!"
83
+ end
84
+
49
85
  attr_accessor(row.key) if row.respond_to?(:key)
50
86
 
51
87
  self.table << row
52
88
  end
53
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}.
54
95
  def self.options(*arguments, **options, &block)
55
96
  append Options.parse(*arguments, **options, &block)
56
97
  end
57
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.
58
103
  def self.nested(*arguments, **options)
59
104
  append Nested.new(*arguments, **options)
60
105
  end
61
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.
62
111
  def self.one(*arguments, **options)
63
112
  append One.new(*arguments, **options)
64
113
  end
65
114
 
115
+ # Define multiple positional arguments.
116
+ #
117
+ # @parameter arguments [Array] The arguments for the positional parameters.
118
+ # @parameter options [Hash] Additional options.
66
119
  def self.many(*arguments, **options)
67
120
  append Many.new(*arguments, **options)
68
121
  end
69
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.
70
127
  def self.split(*arguments, **options)
71
128
  append Split.new(*arguments, **options)
72
129
  end
73
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.
74
135
  def self.usage(rows, name)
75
136
  rows.nested(name, self) do |rows|
76
137
  return unless table = self.table.merged
@@ -85,14 +146,22 @@ module Samovar
85
146
  end
86
147
  end
87
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.
88
153
  def self.command_line(name)
89
- if table = self.table.merged
90
- "#{name} #{table.merged.usage}"
91
- else
92
- name
93
- end
154
+ table = self.table.merged
155
+
156
+ return "#{name} #{table.usage}"
94
157
  end
95
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.
96
165
  def initialize(input = nil, name: File.basename($0), parent: nil, output: nil)
97
166
  @name = name
98
167
  @parent = parent
@@ -101,23 +170,47 @@ module Samovar
101
170
  parse(input) if input
102
171
  end
103
172
 
173
+ # The output stream for usage information.
174
+ #
175
+ # @attribute [IO]
104
176
  attr :output
105
177
 
178
+ # The output stream for usage information, defaults to `$stdout`.
179
+ #
180
+ # @returns [IO] The output stream.
106
181
  def output
107
182
  @output || $stdout
108
183
  end
109
184
 
185
+ # Generate a string representation of the command.
186
+ #
187
+ # @returns [String] The class name.
110
188
  def to_s
111
189
  self.class.name
112
190
  end
113
191
 
192
+ # The name of the command.
193
+ #
194
+ # @attribute [String]
114
195
  attr :name
196
+
197
+ # The parent command, if this is a nested command.
198
+ #
199
+ # @attribute [Command | Nil]
115
200
  attr :parent
116
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.
117
206
  def [](*input)
118
207
  self.dup.tap{|command| command.parse(input)}
119
208
  end
120
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.
121
214
  def parse(input)
122
215
  self.class.table.merged.parse(input, self)
123
216
 
@@ -128,6 +221,11 @@ module Samovar
128
221
  end
129
222
  end
130
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.
131
229
  def print_usage(output: self.output, formatter: Output::UsageFormatter, &block)
132
230
  rows = Output::Rows.new
133
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
data/lib/samovar/flags.rb CHANGED
@@ -1,40 +1,65 @@
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
6
  module Samovar
7
+ # Represents a collection of flag alternatives for an option.
8
+ #
9
+ # Flags parse text like `-f/--flag <value>` into individual flag parsers.
7
10
  class Flags
11
+ # Initialize a new flags parser.
12
+ #
13
+ # @parameter text [String] The flags specification string (e.g., `-f/--flag <value>`).
8
14
  def initialize(text)
9
15
  @text = text
10
16
 
11
- @ordered = text.split(/\s+\|\s+/).map{|part| Flag.new(part)}
17
+ @ordered = text.split(/\s+\|\s+/).map{|part| Flag.parse(part)}
12
18
  end
13
19
 
20
+ # Iterate over each flag.
21
+ #
22
+ # @yields {|flag| ...} Each flag in the collection.
14
23
  def each(&block)
15
24
  @ordered.each(&block)
16
25
  end
17
26
 
27
+ # Get the first flag.
28
+ #
29
+ # @returns [Flag] The first flag.
18
30
  def first
19
31
  @ordered.first
20
32
  end
21
33
 
22
- # Whether or not this flag should have a true/false value if not specified otherwise.
34
+ # Whether this flag should have a true/false value if not specified otherwise.
35
+ #
36
+ # @returns [Boolean] True if this is a boolean flag.
23
37
  def boolean?
24
- @ordered.count == 1 and @ordered.first.value.nil?
38
+ @ordered.count == 1 and @ordered.first.boolean?
25
39
  end
26
40
 
41
+ # The number of flag alternatives.
42
+ #
43
+ # @returns [Integer] The count of flags.
27
44
  def count
28
45
  return @ordered.count
29
46
  end
30
47
 
48
+ # Generate a string representation for usage output.
49
+ #
50
+ # @returns [String] The usage string.
31
51
  def to_s
32
- '[' + @ordered.join(' | ') + ']'
52
+ "[#{@ordered.join(' | ')}]"
33
53
  end
34
54
 
55
+ # Parse a flag from the input.
56
+ #
57
+ # @parameter input [Array(String)] The command-line arguments.
58
+ # @returns [Object | Nil] The parsed value, or nil if no match.
35
59
  def parse(input)
36
60
  @ordered.each do |flag|
37
- if result = flag.parse(input)
61
+ result = flag.parse(input)
62
+ if result != nil
38
63
  return result
39
64
  end
40
65
  end
@@ -43,46 +68,168 @@ module Samovar
43
68
  end
44
69
  end
45
70
 
71
+ # Represents a single command-line flag.
72
+ #
73
+ # A flag can be a simple boolean flag or a flag that accepts a value.
46
74
  class Flag
47
- def initialize(text)
48
- @text = text
49
-
75
+ # Parse a flag specification string into a flag instance.
76
+ #
77
+ # @parameter text [String] The flag specification (e.g., `-f <value>` or `--flag`).
78
+ # @returns [Flag] A flag instance (either {ValueFlag} or {BooleanFlag}).
79
+ def self.parse(text)
50
80
  if text =~ /(.*?)\s(\<.*?\>)/
51
- @prefix = $1
52
- @value = $2
81
+ ValueFlag.new(text, $1, $2)
82
+ elsif text =~ /--\[no\]-(.*?)$/
83
+ BooleanFlag.new(text, "--#{$1}")
53
84
  else
54
- @prefix = @text
55
- @value = nil
85
+ ValueFlag.new(text, text, nil)
56
86
  end
57
-
58
- *@alternatives, @prefix = @prefix.split('/')
59
87
  end
60
88
 
89
+ # Initialize a new flag.
90
+ #
91
+ # @parameter text [String] The full flag specification text.
92
+ # @parameter prefix [String] The primary flag prefix (e.g., `--flag`).
93
+ # @parameter alternatives [Array(String) | Nil] Alternative flag prefixes.
94
+ def initialize(text, prefix, alternatives = nil)
95
+ @text = text
96
+ @prefix = prefix
97
+ @alternatives = alternatives
98
+ end
99
+
100
+ # The full flag specification text.
101
+ #
102
+ # @attribute [String]
61
103
  attr :text
104
+
105
+ # The primary flag prefix.
106
+ #
107
+ # @attribute [String]
62
108
  attr :prefix
109
+
110
+ # Alternative flag prefixes.
111
+ #
112
+ # @attribute [Array(String) | Nil]
63
113
  attr :alternatives
64
- attr :value
65
114
 
115
+ # Generate a string representation for usage output.
116
+ #
117
+ # @returns [String] The flag text.
66
118
  def to_s
67
119
  @text
68
120
  end
69
121
 
70
- def prefix?(token)
71
- @prefix == token or @alternatives.include?(token)
122
+ # Generate a key name for this flag.
123
+ #
124
+ # @returns [Symbol] The key name.
125
+ def key
126
+ @key ||= @prefix.sub(/^-*/, "").gsub("-", "_").to_sym
72
127
  end
73
128
 
74
- def key
75
- @key ||= @prefix.sub(/^-*/, '').gsub('-', '_').to_sym
129
+ # Whether this is a boolean flag.
130
+ #
131
+ # @returns [Boolean] False by default.
132
+ def boolean?
133
+ false
76
134
  end
135
+ end
136
+
137
+ # Represents a flag that accepts a value or acts as a boolean.
138
+ class ValueFlag < Flag
139
+ # Initialize a new value flag.
140
+ #
141
+ # @parameter text [String] The full flag specification text.
142
+ # @parameter prefix [String] The primary flag prefix with alternatives (e.g., `-f/--flag`).
143
+ # @parameter value [String | Nil] The value placeholder (e.g., `<file>`).
144
+ def initialize(text, prefix, value)
145
+ super(text, prefix)
146
+
147
+ @value = value
148
+
149
+ *@alternatives, @prefix = @prefix.split("/")
150
+ end
151
+
152
+ # Alternative flag prefixes.
153
+ #
154
+ # @attribute [Array(String)]
155
+ attr :alternatives
77
156
 
157
+ # The value placeholder.
158
+ #
159
+ # @attribute [String | Nil]
160
+ attr :value
161
+
162
+ # Whether this is a boolean flag (no value required).
163
+ #
164
+ # @returns [Boolean] True if no value is required.
165
+ def boolean?
166
+ @value.nil?
167
+ end
168
+
169
+ # Check if the token matches this flag.
170
+ #
171
+ # @parameter token [String] The token to check.
172
+ # @returns [Boolean] True if the token matches.
173
+ def prefix?(token)
174
+ @prefix == token or @alternatives.include?(token)
175
+ end
176
+
177
+ # Parse this flag from the input.
178
+ #
179
+ # @parameter input [Array(String)] The command-line arguments.
180
+ # @returns [String | Symbol | Nil] The parsed value.
78
181
  def parse(input)
79
182
  if prefix?(input.first)
183
+ # Whether we are expecting to parse a value from input:
80
184
  if @value
81
- input.shift(2).last
185
+ # Get the actual value from input:
186
+ flag, value = input.shift(2)
187
+ return value
82
188
  else
83
- input.shift; key
189
+ # Otherwise, we are just a boolean flag:
190
+ input.shift
191
+ return key
84
192
  end
85
193
  end
86
194
  end
87
195
  end
196
+
197
+ # Represents a boolean flag with `--flag` and `--no-flag` variants.
198
+ class BooleanFlag < Flag
199
+ # Initialize a new boolean flag.
200
+ #
201
+ # @parameter text [String] The full flag specification text.
202
+ # @parameter prefix [String] The primary flag prefix (e.g., `--flag`).
203
+ # @parameter value [Object | Nil] Reserved for future use.
204
+ def initialize(text, prefix, value = nil)
205
+ super(text, prefix)
206
+
207
+ @value = value
208
+
209
+ @negated = @prefix.sub(/^--/, "--no-")
210
+ @alternatives = [@negated]
211
+ end
212
+
213
+ # Check if the token matches this flag.
214
+ #
215
+ # @parameter token [String] The token to check.
216
+ # @returns [Boolean] True if the token matches.
217
+ def prefix?(token)
218
+ @prefix == token or @negated == token
219
+ end
220
+
221
+ # Parse this flag from the input.
222
+ #
223
+ # @parameter input [Array(String)] The command-line arguments.
224
+ # @returns [Boolean | Nil] True, false, or nil.
225
+ def parse(input)
226
+ if input.first == @prefix
227
+ input.shift
228
+ return true
229
+ elsif input.first == @negated
230
+ input.shift
231
+ return false
232
+ end
233
+ end
234
+ end
88
235
  end
data/lib/samovar/many.rb CHANGED
@@ -4,7 +4,17 @@
4
4
  # Copyright, 2016-2023, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents multiple positional arguments in a command.
8
+ #
9
+ # A `Many` parser extracts all arguments from the command line until it encounters a stop pattern (typically an option flag).
7
10
  class Many
11
+ # Initialize a new multi-argument parser.
12
+ #
13
+ # @parameter key [Symbol] The name of the attribute to store the values in.
14
+ # @parameter description [String | Nil] A description of the arguments for help output.
15
+ # @parameter stop [Regexp] A pattern that indicates the end of this argument list.
16
+ # @parameter default [Object] The default value if no arguments are provided.
17
+ # @parameter required [Boolean] Whether at least one argument is required.
8
18
  def initialize(key, description = nil, stop: /^-/, default: nil, required: false)
9
19
  @key = key
10
20
  @description = description
@@ -13,16 +23,41 @@ module Samovar
13
23
  @required = required
14
24
  end
15
25
 
26
+ # The name of the attribute to store the values in.
27
+ #
28
+ # @attribute [Symbol]
16
29
  attr :key
30
+
31
+ # A description of the arguments for help output.
32
+ #
33
+ # @attribute [String | Nil]
17
34
  attr :description
35
+
36
+ # A pattern that indicates the end of this argument list.
37
+ #
38
+ # @attribute [Regexp]
18
39
  attr :stop
40
+
41
+ # The default value if no arguments are provided.
42
+ #
43
+ # @attribute [Object]
19
44
  attr :default
45
+
46
+ # Whether at least one argument is required.
47
+ #
48
+ # @attribute [Boolean]
20
49
  attr :required
21
50
 
51
+ # Generate a string representation for usage output.
52
+ #
53
+ # @returns [String] The usage string.
22
54
  def to_s
23
55
  "<#{key}...>"
24
56
  end
25
57
 
58
+ # Generate an array representation for usage output.
59
+ #
60
+ # @returns [Array] The usage array.
26
61
  def to_a
27
62
  usage = [to_s, @description]
28
63
 
@@ -35,6 +70,12 @@ module Samovar
35
70
  return usage
36
71
  end
37
72
 
73
+ # Parse multiple arguments from the input.
74
+ #
75
+ # @parameter input [Array(String)] The command-line arguments.
76
+ # @parameter parent [Command | Nil] The parent command.
77
+ # @parameter default [Object | Nil] An override for the default value.
78
+ # @returns [Array(String) | Object | Nil] The parsed values, or the default if none match.
38
79
  def parse(input, parent = nil, default = nil)
39
80
  if @stop and stop_index = input.index{|item| @stop === item}
40
81
  input.shift(stop_index)
@@ -43,7 +84,7 @@ module Samovar
43
84
  elsif default ||= @default
44
85
  return default
45
86
  elsif @required
46
- raise MissingValueError.new(parent, self)
87
+ raise MissingValueError.new(parent, @key)
47
88
  end
48
89
  end
49
90
  end