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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1051db069257cf9d25e3599a5ddb5c5547a921cb90d04552a777d3af5c335b2
4
- data.tar.gz: 632d0819eb51db7f9640feb8491e543706721a4313243527bb5380da576a578b
3
+ metadata.gz: c9c0de84734b05646d8e1effd9c06339148112dc6202636d436f2909c6bb5835
4
+ data.tar.gz: 5d2a79b6375044119235462e71af0b9ca63f7c9ab272cd118f125a4967bae3bf
5
5
  SHA512:
6
- metadata.gz: f5daa04c5dff0500f95c9dec3cabe02bbbb10fd5a9d5a06f89f6d9e6687eb11af89325472df628170ea04d0458d3c8600849c4fab1299c83bf523e58e1e268ed
7
- data.tar.gz: 304cc7d54293e3998dae9e3e3247506db3f82d2d0f6d66e988b4ad21cad0150846a1325a039c4dd3f4746fe0903e6e6afd5a6e2f615d724d7e7ee788da49310b
6
+ metadata.gz: '09729f11890c05e4f15aa07b9af5202f718c5769f1e794fb9a90b8b843b230c98215cfb96d260923d7fad50cf6bb734f71c24f8f871e38e30c39cc9fc8a2242a'
7
+ data.tar.gz: a8927f7dd61c3423ea892049dcc4d776811601fd6932a3a0ed1834d6e0751deb3c04bffeea434884a2ea3649640e12d3a02048413b3f5392fb3bc94c1ab68932
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,206 @@
1
+ # Completion
2
+
3
+ This guide explains how to add shell completion to commands built with `samovar`.
4
+
5
+ Samovar can complete command lines using the same grammar used for parsing. It can complete option flags, boolean flag variants, nested command names, option values, positional arguments, and split arguments.
6
+
7
+ ## Command Entry Point
8
+
9
+ Commands expose a completion entry point alongside the normal execution entry point:
10
+
11
+ ~~~ ruby
12
+ Application.call(ARGV) # Parse and execute the command.
13
+ Application.complete # Print completion candidates.
14
+ ~~~
15
+
16
+ `complete` expects the command-line arguments to be truncated to the cursor. The final argument is the token being completed. When completing after a space, pass an empty string as the final argument:
17
+
18
+ ~~~ ruby
19
+ Application.complete(["serve", "--bind", ""])
20
+ ~~~
21
+
22
+ Completion candidates are printed as tab-separated values:
23
+
24
+ ~~~ text
25
+ type value description key=value
26
+ ~~~
27
+
28
+ The first three fields are always the completion type, value, and description. Additional fields are optional metadata entries encoded as `key=value`.
29
+
30
+ ## Static Completions
31
+
32
+ Option flags and nested command names are completed automatically. You can add static completions for option values and positional arguments with `completions:`.
33
+
34
+ ~~~ ruby
35
+ require "samovar"
36
+
37
+ class Serve < Samovar::Command
38
+ self.description = "Run the server."
39
+
40
+ options do
41
+ option "--format <name>", "The output format.", default: "text", completions: ["json", "text", "yaml"]
42
+ end
43
+ end
44
+
45
+ class Application < Samovar::Command
46
+ options do
47
+ option "-h/--help", "Print help."
48
+ end
49
+
50
+ nested :command, {
51
+ "serve" => Serve
52
+ }, default: "serve"
53
+ end
54
+ ~~~
55
+
56
+ Examples:
57
+
58
+ ~~~ ruby
59
+ Application.complete(["ser"])
60
+ # command serve Run the server.
61
+
62
+ Application.complete(["serve", "--format", "j"])
63
+ # value json
64
+ ~~~
65
+
66
+ If an option has a default value, the default is offered before other value completions.
67
+
68
+ ## Dynamic Completions
69
+
70
+ Use a callable provider when completions depend on runtime state.
71
+
72
+ ~~~ ruby
73
+ class Serve < Samovar::Command
74
+ def self.host_completions(context)
75
+ ["localhost", "0.0.0.0"].select do |host|
76
+ host.start_with?(context.current)
77
+ end
78
+ end
79
+
80
+ options do
81
+ option "--bind <host>", "The bind address.", completions: method(:host_completions)
82
+ end
83
+ end
84
+ ~~~
85
+
86
+ The provider receives a `Samovar::Completion::Context` with:
87
+
88
+ - `current`: The token being completed.
89
+ - `arguments`: The full truncated argument list.
90
+ - `environment`: The environment hash passed to `complete`.
91
+ - `row`: The parser row whose value is being completed. This can be an option or a positional argument.
92
+
93
+ Providers can return strings, hashes, or `Samovar::Completion::Suggestion` instances:
94
+
95
+ ~~~ ruby
96
+ option "--mode <name>", "The mode.",
97
+ completions: [
98
+ {value: "development", description: "Local development", type: :value, suffix: " "},
99
+ {value: "production", description: "Production", type: :value}
100
+ ]
101
+ ~~~
102
+
103
+ ## Path Completion
104
+
105
+ For path-like arguments, let the shell do native path expansion by using one of the native completion providers:
106
+
107
+ ~~~ ruby
108
+ class Process < Samovar::Command
109
+ options do
110
+ option "--output <path>", "The output path.", completions: :path
111
+ option "--root <path>", "The root directory.", completions: :directory
112
+ end
113
+
114
+ one :input, "The input path.", completions: :file
115
+ end
116
+ ~~~
117
+
118
+ Supported native providers:
119
+
120
+ - `:path`: Complete files and directories using the shell.
121
+ - `:file`: Alias for `:path`.
122
+ - `:directory`: Complete directories using the shell.
123
+ - `:executable`: Complete executable commands using the shell.
124
+
125
+ Samovar does not inspect the filesystem for these providers. It emits a typed completion request, and the shell adapter translates it to native shell path completion.
126
+
127
+ For split arguments, `:executable` completes the command immediately after the split marker. Once a command is present, Samovar emits a `delegate` completion with an `index` metadata field. The index is the zero-based argument index where delegated completion begins.
128
+
129
+ ## Dedicated Completion Executable
130
+
131
+ Shell adapters call a dedicated completion executable named `completion-<command>`. This avoids running the normal command during completion.
132
+
133
+ For a command named `falcon`, provide:
134
+
135
+ ~~~ text
136
+ bin/falcon
137
+ bin/completion-falcon
138
+ ~~~
139
+
140
+ The completion executable can be very small:
141
+
142
+ ~~~ ruby
143
+ #!/usr/bin/env ruby
144
+ # frozen_string_literal: true
145
+
146
+ require_relative "../lib/my/application"
147
+
148
+ My::Application.complete
149
+ ~~~
150
+
151
+ When the user completes a command by path, the shell adapter resolves the completion executable next to that command:
152
+
153
+ ~~~ text
154
+ falcon -> completion-falcon
155
+ bin/falcon -> bin/completion-falcon
156
+ ./bin/falcon -> ./bin/completion-falcon
157
+ /path/falcon -> /path/completion-falcon
158
+ ~~~
159
+
160
+ ## Installing Shell Adapters
161
+
162
+ Shell adapter generation and installation is provided by the `completion` gem.
163
+
164
+ Generate an adapter script:
165
+
166
+ ~~~ bash
167
+ $ completion generate --shell zsh --command falcon
168
+ ~~~
169
+
170
+ Install a generic adapter script into the default directory for the current shell:
171
+
172
+ ~~~ bash
173
+ $ completion install
174
+ ~~~
175
+
176
+ The generic adapter checks whether a matching `completion-<command>` executable exists before handling a command. You can install an adapter for a specific command instead:
177
+
178
+ ~~~ bash
179
+ $ completion install --command falcon
180
+ ~~~
181
+
182
+ You can specify the shell and directory explicitly:
183
+
184
+ ~~~ bash
185
+ $ completion install --shell fish --directory ~/.config/fish/completions --command falcon
186
+ ~~~
187
+
188
+ The installed adapter calls the matching `completion-<command>` executable when completion is requested.
189
+
190
+ ## Testing Completion
191
+
192
+ You can test completion directly without involving a shell:
193
+
194
+ ~~~ ruby
195
+ output = StringIO.new
196
+
197
+ Application.complete(["serve", "--format", "j"], output: output)
198
+
199
+ expect(output.string).to be == "value\tjson\t\n"
200
+ ~~~
201
+
202
+ For a trailing-space completion, pass an empty final token:
203
+
204
+ ~~~ ruby
205
+ Application.complete(["serve", "--format", ""], output: output)
206
+ ~~~
data/context/index.yaml CHANGED
@@ -12,3 +12,7 @@ files:
12
12
  title: Getting Started
13
13
  description: This guide explains how to use `samovar` to build command-line tools
14
14
  and applications.
15
+ - path: completion.md
16
+ title: Completion
17
+ description: This guide explains how to add shell completion to commands built with
18
+ `samovar`.
@@ -14,6 +14,7 @@ require_relative "split"
14
14
  require_relative "output"
15
15
 
16
16
  require_relative "error"
17
+ require_relative "completion"
17
18
 
18
19
  module Samovar
19
20
  # Represents a command in the command-line interface.
@@ -23,12 +24,13 @@ module Samovar
23
24
  # Parse and execute the command with the given input.
24
25
  #
25
26
  # This is the high-level entry point for CLI applications. It handles errors gracefully by printing usage and returning nil.
27
+ # The given arguments are passed to the parser, which consumes them as mutable input.
26
28
  #
27
- # @parameter input [Array(String)] The command-line arguments to parse.
29
+ # @parameter arguments [Array(String)] The command-line arguments to parse.
28
30
  # @parameter output [IO] The output stream for error messages.
29
31
  # @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
32
+ def self.call(arguments = ARGV, output: $stderr)
33
+ self.parse(arguments).call
32
34
  rescue Error => error
33
35
  error.command.print_usage(output: output) do |formatter|
34
36
  formatter.map(error)
@@ -37,27 +39,40 @@ module Samovar
37
39
  return nil
38
40
  end
39
41
 
42
+ # Complete the command-line input without executing the command.
43
+ # The given arguments are treated as the stable command-line boundary; completion internals consume derived input arrays.
44
+ #
45
+ # @parameter arguments [Array(String)] The command-line arguments to complete.
46
+ # @parameter environment [Hash] The environment for completion callbacks.
47
+ # @parameter output [IO] The output stream for printing completion results.
48
+ # @returns [Completion::Result] The completion result.
49
+ def self.complete(arguments = ARGV, environment: ENV, output: $stdout)
50
+ Completion.complete(self, arguments, environment: environment).tap do |result|
51
+ result.print(output)
52
+ end
53
+ end
54
+
40
55
  # Parse the command-line input and create a command instance.
41
56
  #
42
57
  # This is the low-level parsing primitive. It raises {Error} exceptions on parsing failures.
43
58
  # For CLI applications, use {call} instead which handles errors gracefully.
44
59
  #
45
- # @parameter input [Array(String)] The command-line arguments to parse.
60
+ # @parameter arguments [Array(String)] The command-line arguments to parse.
46
61
  # @returns [Command] The parsed command instance.
47
62
  # @raises [Error] If parsing fails.
48
- def self.parse(input)
49
- self.new(input)
63
+ def self.parse(arguments)
64
+ self.new(arguments)
50
65
  end
51
66
 
52
67
  # Create a new command instance with the given arguments.
53
68
  #
54
69
  # This is a convenience method for creating command instances with explicit arguments.
55
70
  #
56
- # @parameter input [Array(String)] The command-line arguments to parse.
71
+ # @parameter arguments [Array(String)] The command-line arguments to parse.
57
72
  # @parameter options [Hash] Additional options to pass to the command.
58
73
  # @returns [Command] The command instance.
59
- def self.[](*input, **options)
60
- self.new(input, **options)
74
+ def self.[](*arguments, **options)
75
+ self.new(arguments, **options)
61
76
  end
62
77
 
63
78
  class << self
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "result"
7
+
8
+ module Samovar
9
+ module Completion
10
+ # The context provided to dynamic completion callbacks.
11
+ class Context
12
+ # Build a context for a command class and argument list.
13
+ #
14
+ # @parameter command_class [Class] The command class to complete.
15
+ # @parameter arguments [Array(String)] The truncated command-line arguments.
16
+ # @parameter environment [Hash] The environment for completion callbacks.
17
+ # @returns [Context] The completion context.
18
+ def self.for(command_class, arguments, environment: ENV)
19
+ return self.new(
20
+ command_class.table.merged,
21
+ arguments,
22
+ arguments.last || "",
23
+ environment: environment,
24
+ )
25
+ end
26
+
27
+ # Initialize a new completion context.
28
+ #
29
+ # @parameter table [Table] The command table to complete.
30
+ # @parameter arguments [Array(String)] The truncated command-line arguments.
31
+ # @parameter current [String] The token being completed.
32
+ # @parameter row [Object | Nil] The parser row whose value is being completed.
33
+ # @parameter environment [Hash] The environment for completion callbacks.
34
+ def initialize(table, arguments, current, row = nil, environment: ENV)
35
+ @table = table
36
+ @arguments = arguments
37
+ @current = current
38
+ @row = row
39
+ @environment = environment
40
+ end
41
+
42
+ # @attribute [Table] The command table to complete.
43
+ attr :table
44
+
45
+ # @attribute [Array(String)] The truncated command-line arguments.
46
+ attr :arguments
47
+
48
+ # @attribute [String] The token being completed.
49
+ attr :current
50
+
51
+ # @attribute [Object | Nil] The parser row whose value is being completed.
52
+ attr :row
53
+
54
+ # @attribute [Hash] The environment for completion callbacks.
55
+ attr :environment
56
+
57
+ # Create a context for completing the given parser row.
58
+ #
59
+ # @parameter row [Object] The parser row whose value is being completed.
60
+ # @returns [Context] The specialized completion context.
61
+ def with_row(row)
62
+ return self.class.new(
63
+ @table,
64
+ @arguments,
65
+ @current,
66
+ row,
67
+ environment: @environment,
68
+ )
69
+ end
70
+
71
+ # The completed words before the current token.
72
+ #
73
+ # @returns [Array(String)] The arguments before the token being completed.
74
+ def words
75
+ @arguments.take(@arguments.size - 1)
76
+ end
77
+
78
+ # Complete the current command class.
79
+ #
80
+ # @returns [Result] The completion result.
81
+ def complete
82
+ complete_rows(@table, words)
83
+ end
84
+
85
+ # Complete the given command class with completed words.
86
+ #
87
+ # @parameter command_class [Class] The command class to complete.
88
+ # @parameter words [Array(String)] The completed words before the current token.
89
+ # @returns [Result] The completion result.
90
+ def complete_command(command_class, words = [])
91
+ complete_rows(command_class.table.merged, words)
92
+ end
93
+
94
+ # Complete the rows in a command table.
95
+ # The input array is mutable and may be consumed by parser rows.
96
+ #
97
+ # @parameter table [Table] The command table to complete.
98
+ # @parameter input [Array(String)] The mutable completed words to consume.
99
+ # @returns [Result] The completion result.
100
+ def complete_rows(table, input)
101
+ collected = []
102
+
103
+ table.each do |row|
104
+ if row.respond_to?(:complete)
105
+ if result = row.complete(input, self, collected)
106
+ return result
107
+ end
108
+ end
109
+ end
110
+
111
+ return Result.new(collected)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "result"
7
+ require_relative "suggestion"
8
+
9
+ module Samovar
10
+ module Completion
11
+ # Expands static, dynamic, and native completion providers.
12
+ class Provider
13
+ # Initialize a new completion provider.
14
+ #
15
+ # @parameter context [Context] The completion context.
16
+ # @parameter completions [Array | Proc | Symbol | Nil] The static, dynamic, or native completions.
17
+ def initialize(context, completions)
18
+ @context = context
19
+ @completions = completions
20
+ end
21
+
22
+ # Generate suggestions from the provider.
23
+ #
24
+ # @returns [Result] The matching completion suggestions.
25
+ def suggestions
26
+ case @completions
27
+ when nil
28
+ Result.new
29
+ when Symbol
30
+ native_suggestions
31
+ else
32
+ matching_suggestions
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ # Generate matching suggestions from static or dynamic completions.
39
+ #
40
+ # @returns [Result] The matching completion suggestions.
41
+ def matching_suggestions
42
+ values = @completions
43
+
44
+ if values.respond_to?(:call)
45
+ values = values.call(@context)
46
+ end
47
+
48
+ values = Array(values).filter_map do |value|
49
+ suggestion = Suggestion.wrap(value)
50
+
51
+ suggestion if suggestion.start_with?(@context.current)
52
+ end
53
+
54
+ return Result.new(values)
55
+ end
56
+
57
+ # Generate native shell completion requests.
58
+ #
59
+ # @returns [Result] The native completion request suggestions.
60
+ def native_suggestions
61
+ case @completions
62
+ when :path, :file
63
+ Result.new([Suggestion.new(@context.current, description: "Path", type: :path)])
64
+ when :directory
65
+ Result.new([Suggestion.new(@context.current, description: "Directory", type: :directory)])
66
+ when :executable
67
+ Result.new([Suggestion.new(@context.current, description: "Executable", type: :executable)])
68
+ else
69
+ Result.new
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Samovar
7
+ module Completion
8
+ # A collection of completion suggestions.
9
+ class Result
10
+ include Enumerable
11
+
12
+ # Initialize a new completion result.
13
+ #
14
+ # @parameter suggestions [Array(Suggestion)] The suggestions in this result.
15
+ def initialize(suggestions = [])
16
+ @suggestions = suggestions
17
+ end
18
+
19
+ # @attribute [Array(Suggestion)] The suggestions in this result.
20
+ attr :suggestions
21
+
22
+ # Iterate over each suggestion.
23
+ #
24
+ # @yields {|suggestion| ...} The block to call for each suggestion.
25
+ def each(&block)
26
+ @suggestions.each(&block)
27
+ end
28
+
29
+ # Whether this result contains no suggestions.
30
+ #
31
+ # @returns [Boolean] True if there are no suggestions.
32
+ def empty?
33
+ @suggestions.empty?
34
+ end
35
+
36
+ # Combine this result with another result.
37
+ #
38
+ # @parameter other [Result] The other result to append.
39
+ # @returns [Result] The combined result.
40
+ def +(other)
41
+ self.class.new(@suggestions + other.suggestions)
42
+ end
43
+
44
+ # Print suggestions as tab-separated completion records.
45
+ #
46
+ # @parameter output [IO] The output stream to write to.
47
+ def print(output = $stdout)
48
+ each do |suggestion|
49
+ output.puts suggestion.to_record
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Samovar
7
+ module Completion
8
+ # A single completion suggestion.
9
+ class Suggestion
10
+ # Wrap a raw completion value in a suggestion.
11
+ #
12
+ # @parameter value [Suggestion | Hash | Object] The value to wrap.
13
+ # @returns [Suggestion] The normalized suggestion.
14
+ def self.wrap(value)
15
+ case value
16
+ when self
17
+ return value
18
+ when Hash
19
+ value = value.dup
20
+ suggestion = value.fetch(:value)
21
+ value.delete(:value)
22
+
23
+ return self.new(suggestion, **value)
24
+ else
25
+ return self.new(value)
26
+ end
27
+ end
28
+
29
+ # Initialize a new completion suggestion.
30
+ #
31
+ # @parameter value [Object] The completion value.
32
+ # @parameter type [Symbol | String | Nil] The completion type.
33
+ # @parameter description [String | Nil] The completion description.
34
+ # @parameter options [Hash] Additional completion metadata.
35
+ def initialize(value, type: nil, description: nil, **options)
36
+ @type = type
37
+ @value = value
38
+ @description = description
39
+ @options = options
40
+ end
41
+
42
+ # @attribute [Symbol | String | Nil] The completion type.
43
+ attr :type
44
+
45
+ # @attribute [Object] The completion value.
46
+ attr :value
47
+
48
+ # @attribute [String | Nil] The completion description.
49
+ attr :description
50
+
51
+ # @attribute [Hash] Additional completion metadata.
52
+ attr :options
53
+
54
+ # Whether this suggestion starts with the given prefix.
55
+ #
56
+ # @parameter prefix [String] The prefix to check.
57
+ # @returns [Boolean] True if the suggestion starts with the given prefix.
58
+ def start_with?(prefix)
59
+ to_s.start_with?(prefix)
60
+ end
61
+
62
+ # Convert the suggestion to a tab-separated completion record.
63
+ #
64
+ # @returns [String] The escaped completion record.
65
+ def to_record
66
+ fields = [
67
+ escape(@type),
68
+ escape(@value),
69
+ escape(@description),
70
+ ]
71
+ @options.each do |key, value|
72
+ next if value.nil?
73
+
74
+ fields << "#{escape(key)}=#{escape(value)}"
75
+ end
76
+
77
+ return fields.join("\t")
78
+ end
79
+
80
+ # Convert the suggestion to its value.
81
+ #
82
+ # @returns [String] The suggestion value.
83
+ def to_s
84
+ @value.to_s
85
+ end
86
+
87
+ private
88
+
89
+ def escape(value)
90
+ value.to_s.gsub(/[\t\r\n]/, " ")
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "completion/context"
7
+ require_relative "completion/provider"
8
+ require_relative "completion/result"
9
+ require_relative "completion/suggestion"
10
+
11
+ module Samovar
12
+ # Shell completion support for Samovar commands.
13
+ module Completion
14
+ # Complete the command line for the given command class.
15
+ #
16
+ # @parameter command_class [Class] The command class to complete.
17
+ # @parameter arguments [Array(String)] The application arguments.
18
+ # @parameter environment [Hash] The environment for completion callbacks.
19
+ # @returns [Result] The completion result.
20
+ def self.complete(command_class, arguments, environment: ENV)
21
+ Context.for(command_class, arguments, environment: environment).complete
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2023, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
7
  # Represents a runtime failure in command execution.