samovar 2.4.1 → 2.5.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.
data/lib/samovar/flags.rb CHANGED
@@ -8,6 +8,8 @@ module Samovar
8
8
  #
9
9
  # Flags parse text like `-f/--flag <value>` into individual flag parsers.
10
10
  class Flags
11
+ include Enumerable
12
+
11
13
  # Initialize a new flags parser.
12
14
  #
13
15
  # @parameter text [String] The flags specification string (e.g., `-f/--flag <value>`).
@@ -24,6 +26,21 @@ module Samovar
24
26
  @ordered.each(&block)
25
27
  end
26
28
 
29
+ # Find the flag that matches the given token.
30
+ #
31
+ # @parameter token [String] The token to match.
32
+ # @returns [Flag | Nil] The matching flag.
33
+ def flag_for(token)
34
+ @ordered.find{|flag| flag.prefix?(token)}
35
+ end
36
+
37
+ # The possible flag prefixes for completion.
38
+ #
39
+ # @returns [Array(String)] The flag prefixes and alternatives.
40
+ def completions
41
+ @ordered.flat_map(&:completions)
42
+ end
43
+
27
44
  # Get the first flag.
28
45
  #
29
46
  # @returns [Flag] The first flag.
@@ -132,6 +149,21 @@ module Samovar
132
149
  def boolean?
133
150
  false
134
151
  end
152
+
153
+ # Check if the token matches this flag.
154
+ #
155
+ # @parameter token [String] The token to check.
156
+ # @returns [Boolean] True if the token matches.
157
+ def prefix?(token)
158
+ @prefix == token or @alternatives&.include?(token)
159
+ end
160
+
161
+ # The possible flag prefixes for completion.
162
+ #
163
+ # @returns [Array(String)] The flag prefix and alternatives.
164
+ def completions
165
+ [@prefix, *@alternatives]
166
+ end
135
167
  end
136
168
 
137
169
  # Represents a flag that accepts a value or acts as a boolean.
data/lib/samovar/many.rb CHANGED
@@ -1,7 +1,9 @@
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-2026, by Samuel Williams.
5
+
6
+ require_relative "completion"
5
7
 
6
8
  module Samovar
7
9
  # Represents multiple positional arguments in a command.
@@ -15,12 +17,14 @@ module Samovar
15
17
  # @parameter stop [Regexp] A pattern that indicates the end of this argument list.
16
18
  # @parameter default [Object] The default value if no arguments are provided.
17
19
  # @parameter required [Boolean] Whether at least one argument is required.
18
- def initialize(key, description = nil, stop: /^-/, default: nil, required: false)
20
+ # @parameter completions [Array | Proc | Nil] Completions for these arguments.
21
+ def initialize(key, description = nil, stop: /^-/, default: nil, required: false, completions: nil)
19
22
  @key = key
20
23
  @description = description
21
24
  @stop = stop
22
25
  @default = default
23
26
  @required = required
27
+ @completions = completions
24
28
  end
25
29
 
26
30
  # The name of the attribute to store the values in.
@@ -48,6 +52,11 @@ module Samovar
48
52
  # @attribute [Boolean]
49
53
  attr :required
50
54
 
55
+ # Completions for these arguments.
56
+ #
57
+ # @attribute [Array | Proc | Nil]
58
+ attr :completions
59
+
51
60
  # Generate a string representation for usage output.
52
61
  #
53
62
  # @returns [String] The usage string.
@@ -87,5 +96,27 @@ module Samovar
87
96
  raise MissingValueError.new(parent, @key)
88
97
  end
89
98
  end
99
+
100
+ # Complete this repeating positional argument.
101
+ #
102
+ # @parameter input [Array(String)] Previously completed command-line arguments.
103
+ # @parameter context [Completion::Context] The completion context.
104
+ # @parameter collected [Array(Completion::Suggestion)] Suggestions collected so far.
105
+ # @returns [Completion::Result | Nil] A final completion result, or nil to continue.
106
+ def complete(input, context, collected)
107
+ if @stop
108
+ input.shift while input.any? && !(@stop === input.first)
109
+
110
+ return nil if @stop === context.current
111
+ else
112
+ input.clear
113
+ end
114
+
115
+ if input.empty?
116
+ return Completion::Result.new(collected) + Completion::Provider.new(context.with_row(self), @completions).suggestions
117
+ end
118
+
119
+ return nil
120
+ end
90
121
  end
91
122
  end
@@ -1,7 +1,9 @@
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-2026, by Samuel Williams.
5
+
6
+ require_relative "completion"
5
7
 
6
8
  module Samovar
7
9
  # Represents nested sub-commands in a command.
@@ -85,16 +87,57 @@ module Samovar
85
87
  name = input.shift
86
88
 
87
89
  # puts "Instantiating #{command} with #{input}"
88
- command.new(input, name: name, parent: parent)
90
+ command.new(input, name: name, parent: parent, output: parent&.output)
89
91
  elsif default
90
92
  return default
91
93
  elsif @default
92
- @commands[@default].new(input, name: @default, parent: parent)
94
+ @commands[@default].new(input, name: @default, parent: parent, output: parent&.output)
93
95
  elsif @required
94
96
  raise MissingValueError.new(parent, @key)
95
97
  end
96
98
  end
97
99
 
100
+ # Complete nested command names or continue into a selected command.
101
+ #
102
+ # @parameter input [Array(String)] Previously completed command-line arguments.
103
+ # @parameter context [Completion::Context] The completion context.
104
+ # @parameter collected [Array(Completion::Suggestion)] Suggestions collected so far.
105
+ # @returns [Completion::Result | Nil] A final completion result, or nil to continue.
106
+ def complete(input, context, collected)
107
+ if input.empty?
108
+ result = suggestions(context)
109
+
110
+ if result.empty? && @default
111
+ return Completion::Result.new(collected) + context.complete_command(@commands.fetch(@default))
112
+ end
113
+
114
+ return Completion::Result.new(collected) + result
115
+ end
116
+
117
+ if command = @commands[input.first]
118
+ input.shift
119
+ return context.complete_command(command, input)
120
+ elsif @default
121
+ return context.complete_command(@commands.fetch(@default), input)
122
+ else
123
+ return Completion::Result.new(collected)
124
+ end
125
+ end
126
+
127
+ # Complete nested command names for the current token.
128
+ #
129
+ # @parameter context [Completion::Context] The completion context.
130
+ # @returns [Completion::Result] The matching nested command suggestions.
131
+ def suggestions(context)
132
+ suggestions = @commands.collect do |name, command_class|
133
+ next unless name.start_with?(context.current)
134
+
135
+ Completion::Suggestion.new(name, description: command_class.description, type: :command)
136
+ end.compact
137
+
138
+ Completion::Result.new(suggestions)
139
+ end
140
+
98
141
  # Generate usage information for this nested command.
99
142
  #
100
143
  # @parameter rows [Output::Rows] The rows to append usage information to.
data/lib/samovar/one.rb CHANGED
@@ -1,7 +1,9 @@
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-2026, by Samuel Williams.
5
+
6
+ require_relative "completion"
5
7
 
6
8
  module Samovar
7
9
  # Represents a single positional argument in a command.
@@ -15,12 +17,14 @@ module Samovar
15
17
  # @parameter pattern [Regexp] A pattern to match valid values.
16
18
  # @parameter default [Object] The default value if no argument is provided.
17
19
  # @parameter required [Boolean] Whether the argument is required.
18
- def initialize(key, description, pattern: //, default: nil, required: false)
20
+ # @parameter completions [Array | Proc | Nil] Completions for this argument.
21
+ def initialize(key, description, pattern: //, default: nil, required: false, completions: nil)
19
22
  @key = key
20
23
  @description = description
21
24
  @pattern = pattern
22
25
  @default = default
23
26
  @required = required
27
+ @completions = completions
24
28
  end
25
29
 
26
30
  # The name of the attribute to store the value in.
@@ -48,6 +52,11 @@ module Samovar
48
52
  # @attribute [Boolean]
49
53
  attr :required
50
54
 
55
+ # Completions for this argument.
56
+ #
57
+ # @attribute [Array | Proc | Nil]
58
+ attr :completions
59
+
51
60
  # Generate a string representation for usage output.
52
61
  #
53
62
  # @returns [String] The usage string.
@@ -85,5 +94,22 @@ module Samovar
85
94
  raise MissingValueError.new(parent, @key)
86
95
  end
87
96
  end
97
+
98
+ # Complete this positional argument.
99
+ #
100
+ # @parameter input [Array(String)] Previously completed command-line arguments.
101
+ # @parameter context [Completion::Context] The completion context.
102
+ # @parameter collected [Array(Completion::Suggestion)] Suggestions collected so far.
103
+ # @returns [Completion::Result | Nil] A final completion result, or nil to continue.
104
+ def complete(input, context, collected)
105
+ if input.empty?
106
+ return Completion::Result.new(collected) + Completion::Provider.new(context.with_row(self), @completions).suggestions
107
+ elsif @pattern =~ input.first
108
+ input.shift
109
+ return nil
110
+ else
111
+ return Completion::Result.new(collected)
112
+ end
113
+ end
88
114
  end
89
115
  end
@@ -5,6 +5,8 @@
5
5
 
6
6
  require_relative "flags"
7
7
  require_relative "error"
8
+ require_relative "completion/provider"
9
+ require_relative "completion/result"
8
10
 
9
11
  module Samovar
10
12
  # Represents a single command-line option.
@@ -20,8 +22,9 @@ module Samovar
20
22
  # @parameter value [Object | Nil] A fixed value to use regardless of user input.
21
23
  # @parameter type [Class | Proc | Nil] The type to coerce the value to.
22
24
  # @parameter required [Boolean] Whether the option is required.
25
+ # @parameter completions [Array | Proc | Nil] Completions for option values.
23
26
  # @yields {|value| ...} An optional block to transform the parsed value.
24
- def initialize(flags, description, key: nil, default: nil, value: nil, type: nil, required: false, &block)
27
+ def initialize(flags, description, key: nil, default: nil, value: nil, type: nil, required: false, completions: nil, &block)
25
28
  @flags = Flags.new(flags)
26
29
  @description = description
27
30
 
@@ -39,6 +42,7 @@ module Samovar
39
42
 
40
43
  @type = type
41
44
  @required = required
45
+ @completions = completions
42
46
  @block = block
43
47
  end
44
48
 
@@ -59,8 +63,21 @@ module Samovar
59
63
 
60
64
  # The default value if the option is not provided.
61
65
  #
62
- # @attribute [Object]
63
- attr :default
66
+ # @returns [Object | Nil] The resolved default value.
67
+ def default
68
+ if @default.respond_to?(:call)
69
+ @default.call
70
+ else
71
+ @default
72
+ end
73
+ end
74
+
75
+ # Whether this option has a default value.
76
+ #
77
+ # @returns [Boolean] True if the option has a default value.
78
+ def default?
79
+ !@default.nil?
80
+ end
64
81
 
65
82
  # A fixed value to use regardless of user input.
66
83
  #
@@ -77,11 +94,52 @@ module Samovar
77
94
  # @attribute [Boolean]
78
95
  attr :required
79
96
 
97
+ # Completions for option values.
98
+ #
99
+ # @attribute [Array | Proc | Nil]
100
+ attr :completions
101
+
80
102
  # An optional block to transform the parsed value.
81
103
  #
82
104
  # @attribute [Proc | Nil]
83
105
  attr :block
84
106
 
107
+ # Find the flag that matches the given token.
108
+ #
109
+ # @parameter token [String] The token to match.
110
+ # @returns [Flag | Nil] The matching flag.
111
+ def flag_for(token)
112
+ @flags.flag_for(token)
113
+ end
114
+
115
+ # Whether this option consumes a value after the flag.
116
+ #
117
+ # @returns [Boolean] True if any flag for this option consumes a value.
118
+ def value?
119
+ @flags.any?{|flag| !flag.boolean?}
120
+ end
121
+
122
+ # Complete values for this option.
123
+ #
124
+ # @parameter context [Completion::Context] The completion context.
125
+ # @returns [Completion::Result] The matching option value completions.
126
+ def suggestions(context)
127
+ suggestions = []
128
+ context = context.with_row(self)
129
+
130
+ if default?
131
+ suggestion = Completion::Provider.new(context, [default]).suggestions.first
132
+
133
+ suggestions << suggestion if suggestion
134
+ end
135
+
136
+ Completion::Provider.new(context, @completions).suggestions.each do |suggestion|
137
+ suggestions << suggestion unless suggestions.any?{|existing| existing.value == suggestion.value}
138
+ end
139
+
140
+ Completion::Result.new(suggestions)
141
+ end
142
+
85
143
  # Coerce the result to the specified type.
86
144
  #
87
145
  # @parameter result [Object] The value to coerce.
@@ -141,8 +199,8 @@ module Samovar
141
199
  #
142
200
  # @returns [Array] The usage array.
143
201
  def to_a
144
- if @default
145
- [@flags, @description, "(default: #{@default})"]
202
+ if default?
203
+ [@flags, @description, "(default: #{default})"]
146
204
  elsif @required
147
205
  [@flags, @description, "(required)"]
148
206
  else
@@ -4,12 +4,15 @@
4
4
  # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "option"
7
+ require_relative "completion"
7
8
 
8
9
  module Samovar
9
10
  # Represents a collection of command-line options.
10
11
  #
11
12
  # Options provide a DSL for defining multiple option flags in a single block.
12
13
  class Options
14
+ include Enumerable
15
+
13
16
  # Parse and create an options collection from a block.
14
17
  #
15
18
  # @parameter arguments [Array] The arguments for the options collection.
@@ -68,8 +71,10 @@ module Samovar
68
71
 
69
72
  # The default values for options.
70
73
  #
71
- # @attribute [Hash]
72
- attr :defaults
74
+ # @returns [Hash] The resolved default values.
75
+ def defaults
76
+ @defaults.transform_values(&:default)
77
+ end
73
78
 
74
79
  # Freeze this options collection.
75
80
  #
@@ -93,6 +98,21 @@ module Samovar
93
98
  @ordered.each(&block)
94
99
  end
95
100
 
101
+ # Find the option that matches the given flag token.
102
+ #
103
+ # @parameter token [String] The flag token to match.
104
+ # @returns [Option | Nil] The matching option.
105
+ def option_for(token)
106
+ @keyed[token]
107
+ end
108
+
109
+ # The possible flag prefixes for completion.
110
+ #
111
+ # @returns [Array(String)] The option flag prefixes and alternatives.
112
+ def completions
113
+ @ordered.flat_map{|option| option.flags.completions}
114
+ end
115
+
96
116
  # Check if this options collection is empty.
97
117
  #
98
118
  # @returns [Boolean] True if there are no options.
@@ -131,8 +151,8 @@ module Samovar
131
151
  end
132
152
  end
133
153
 
134
- if default = option.default
135
- @defaults[option.key] = option.default
154
+ if option.default?
155
+ @defaults[option.key] = option
136
156
  end
137
157
  end
138
158
 
@@ -143,7 +163,7 @@ module Samovar
143
163
  # @parameter default [Hash | Nil] Default values to use.
144
164
  # @returns [Hash] The parsed option values.
145
165
  def parse(input, parent = nil, default = nil)
146
- values = (default || @defaults).dup
166
+ values = (default || defaults).dup
147
167
 
148
168
  while option = @keyed[input.first]
149
169
  # prefix = input.first
@@ -161,7 +181,71 @@ module Samovar
161
181
  end
162
182
 
163
183
  return values
164
- end # Generate a string representation for usage output.
184
+ end
185
+
186
+ # Complete option flags or option values.
187
+ #
188
+ # @parameter input [Array(String)] Previously completed command-line arguments.
189
+ # @parameter context [Completion::Context] The completion context.
190
+ # @parameter collected [Array(Completion::Suggestion)] Suggestions collected so far.
191
+ # @returns [Completion::Result | Nil] A final completion result, or nil to continue.
192
+ def complete(input, context, collected)
193
+ result = consume(input, context)
194
+ return result if result
195
+
196
+ return nil unless input.empty?
197
+
198
+ flags = suggestions(context.current)
199
+
200
+ if context.current.start_with?("-") && flags.any?
201
+ return Completion::Result.new(flags)
202
+ elsif context.current.empty?
203
+ collected.concat(flags)
204
+ return nil
205
+ end
206
+
207
+ return nil
208
+ end
209
+
210
+ # Consume option tokens before the current completion position.
211
+ #
212
+ # @parameter input [Array(String)] Previously completed command-line arguments.
213
+ # @parameter context [Completion::Context] The completion context.
214
+ # @returns [Completion::Result | Nil] A completion result for an option value, or nil to continue.
215
+ def consume(input, context)
216
+ while token = input.first
217
+ break unless option = option_for(token)
218
+
219
+ flag = option.flag_for(token)
220
+ input.shift
221
+
222
+ if flag && !flag.boolean?
223
+ if input.any?
224
+ input.shift
225
+ else
226
+ return option.suggestions(context)
227
+ end
228
+ end
229
+ end
230
+
231
+ return nil
232
+ end
233
+
234
+ # Complete option flags for the given prefix.
235
+ #
236
+ # @parameter prefix [String] The option prefix being completed.
237
+ # @returns [Array(Completion::Suggestion)] The matching option flag suggestions.
238
+ def suggestions(prefix)
239
+ flat_map do |option|
240
+ option.flags.completions.collect do |value|
241
+ next unless value.start_with?(prefix)
242
+
243
+ Completion::Suggestion.new(value, description: option.description, type: :option)
244
+ end
245
+ end.compact
246
+ end
247
+
248
+ # Generate a string representation for usage output.
165
249
  #
166
250
  # @returns [String] The usage string.
167
251
  def to_s
@@ -1,7 +1,7 @@
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-2026, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
7
  # Namespace for output formatting classes.
@@ -1,7 +1,7 @@
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-2026, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
7
  module Output
@@ -2,8 +2,8 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2025, by Samuel Williams.
5
+ # Copyright, 2026, by Gerhard Schlager.
5
6
 
6
- require "mapping/model"
7
7
  require "console/terminal"
8
8
 
9
9
  require_relative "../error"
@@ -17,8 +17,8 @@ module Samovar
17
17
  module Output
18
18
  # Formats and prints usage information to a terminal.
19
19
  #
20
- # Uses the `mapping` gem to handle different output object types with custom formatting rules.
21
- class UsageFormatter < Mapping::Model
20
+ # Dispatches on the type of each output object to apply custom formatting rules.
21
+ class UsageFormatter
22
22
  # Print usage information to the output.
23
23
  #
24
24
  # @parameter rows [Rows] The rows to format and print.
@@ -39,7 +39,6 @@ module Samovar
39
39
  def initialize(output)
40
40
  @output = output
41
41
  @width = 80
42
- @first = true
43
42
 
44
43
  @terminal = Console::Terminal.for(@output)
45
44
  @terminal[:header] = @terminal.style(nil, nil, :bright)
@@ -47,43 +46,50 @@ module Samovar
47
46
  @terminal[:error] = @terminal.style(:red)
48
47
  end
49
48
 
50
- map(InvalidInputError) do |error|
51
- # This is a little hack which avoids printing out "--help" if it was part of an incomplete parse. In the future I'd prefer if this was handled explicitly.
52
- @terminal.puts("#{error.message} in:", style: :error) unless error.help?
53
- end
54
-
55
- map(MissingValueError) do |error|
56
- @terminal.puts("#{error.message} in:", style: :error)
57
- end
58
-
59
- map(Header) do |header, rows|
60
- if @first
61
- @first = false
49
+ # Format and print the given object according to its type.
50
+ #
51
+ # @parameter object [Object] The object to format (a {Rows}, {Row}, {Header}, or error).
52
+ # @parameter arguments [Array] Extra context passed through to nested rows (the containing {Rows}).
53
+ def map(object, *arguments, first: true)
54
+ case object
55
+ when InvalidInputError
56
+ # This is a little hack which avoids printing out "--help" if it was part of an incomplete parse. In the future I'd prefer if this was handled explicitly.
57
+ @terminal.puts("#{object.message} in:", style: :error) unless object.help?
58
+ when MissingValueError
59
+ @terminal.puts("#{object.message} in:", style: :error)
60
+ when Header
61
+ header, rows = object, arguments.first
62
+
63
+ if first
64
+ first = false
65
+ else
66
+ @terminal.puts
67
+ end
68
+
69
+ command_line = header.object.command_line(header.name)
70
+ @terminal.puts "#{rows.indentation}#{command_line}", style: :header
71
+
72
+ if description = header.object.description
73
+ @terminal.puts "#{rows.indentation}\t#{description}", style: :description
74
+ @terminal.puts
75
+ end
76
+ when Row
77
+ row, rows = object, arguments.first
78
+ @terminal.puts "#{rows.indentation}#{row.align(rows.columns)}"
79
+ when Rows
80
+ object.each do |row, rows|
81
+ first = map(row, rows, first: first)
82
+ end
62
83
  else
63
- @terminal.puts
84
+ raise ArgumentError, "Unable to format #{object.class}!"
64
85
  end
65
86
 
66
- command_line = header.object.command_line(header.name)
67
- @terminal.puts "#{rows.indentation}#{command_line}", style: :header
68
-
69
- if description = header.object.description
70
- @terminal.puts "#{rows.indentation}\t#{description}", style: :description
71
- @terminal.puts
72
- end
73
- end
74
-
75
- map(Row) do |row, rows|
76
- @terminal.puts "#{rows.indentation}#{row.align(rows.columns)}"
77
- end
78
-
79
- map(Rows) do |items|
80
- items.collect{|row, rows| map(row, rows)}
87
+ return first
81
88
  end
82
89
 
83
90
  # Print the formatted usage output.
84
- def print(rows, first: @first)
85
- @first = first
86
- map(rows)
91
+ def print(rows, first: true)
92
+ map(rows, first: first)
87
93
  end
88
94
  end
89
95
  end