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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/samovar/command.rb +122 -24
- data/lib/samovar/error.rb +33 -2
- data/lib/samovar/failure.rb +1 -0
- data/lib/samovar/flags.rb +169 -22
- data/lib/samovar/many.rb +42 -1
- data/lib/samovar/nested.rb +40 -2
- data/lib/samovar/one.rb +42 -1
- data/lib/samovar/option.rb +68 -7
- data/lib/samovar/options.rb +75 -6
- data/lib/samovar/output/columns.rb +19 -0
- data/lib/samovar/output/header.rb +18 -0
- data/lib/samovar/output/row.rb +15 -2
- data/lib/samovar/output/rows.rb +40 -4
- data/lib/samovar/output/usage_formatter.rb +32 -14
- data/lib/samovar/output.rb +2 -2
- data/lib/samovar/split.rb +44 -3
- data/lib/samovar/table.rb +47 -4
- data/lib/samovar/version.rb +3 -2
- data/lib/samovar.rb +3 -3
- data/license.md +1 -1
- data/readme.md +10 -158
- data.tar.gz.sig +0 -0
- metadata +6 -51
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85e0df9ca2ed86284ab62c49ef724f93818ecb8571a33cccca98759f0a6b2612
|
4
|
+
data.tar.gz: 833c98f6619c7fb73243f6bbbbbb84e25a8d100c03af55392bd97f20a4246803
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0aff1d4bc6f8c2154dcd7e216a8f082fc50b44239321a122b139edff54e0749ab7109a45ab87e2615677e181426717b4912d81118f87f01cd4bf7032653d2041
|
7
|
+
data.tar.gz: d3c94b2986da89eba71d18d06df18dd0541775f2f1c2ba2c327e8407a38e05757e073b5f97c137c8c0241ab29811fd9e0a3fed3c2613034ca7db15ef0e5aff12
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/samovar/command.rb
CHANGED
@@ -1,76 +1,137 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2016-
|
4
|
+
# Copyright, 2016-2025, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require_relative
|
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
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
# The
|
26
|
-
def self.
|
27
|
-
self.
|
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:
|
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
|
-
|
90
|
-
|
91
|
-
|
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-
|
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
|
data/lib/samovar/failure.rb
CHANGED
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-
|
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.
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
52
|
-
|
81
|
+
ValueFlag.new(text, $1, $2)
|
82
|
+
elsif text =~ /--\[no\]-(.*?)$/
|
83
|
+
BooleanFlag.new(text, "--#{$1}")
|
53
84
|
else
|
54
|
-
|
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
|
-
|
71
|
-
|
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
|
-
|
75
|
-
|
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
|
185
|
+
# Get the actual value from input:
|
186
|
+
flag, value = input.shift(2)
|
187
|
+
return value
|
82
188
|
else
|
83
|
-
|
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,
|
87
|
+
raise MissingValueError.new(parent, @key)
|
47
88
|
end
|
48
89
|
end
|
49
90
|
end
|