samovar 2.4.2 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/completion.md +206 -0
- data/context/index.yaml +4 -0
- data/lib/samovar/command.rb +24 -9
- data/lib/samovar/completion/context.rb +115 -0
- data/lib/samovar/completion/provider.rb +75 -0
- data/lib/samovar/completion/result.rb +54 -0
- data/lib/samovar/completion/suggestion.rb +94 -0
- data/lib/samovar/completion.rb +24 -0
- data/lib/samovar/flags.rb +32 -0
- data/lib/samovar/many.rb +32 -1
- data/lib/samovar/nested.rb +45 -2
- data/lib/samovar/one.rb +27 -1
- data/lib/samovar/option.rb +63 -5
- data/lib/samovar/options.rb +90 -6
- data/lib/samovar/split.rb +45 -1
- data/lib/samovar/version.rb +1 -1
- data/readme.md +6 -6
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +7 -1
- 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: c9c0de84734b05646d8e1effd9c06339148112dc6202636d436f2909c6bb5835
|
|
4
|
+
data.tar.gz: 5d2a79b6375044119235462e71af0b9ca63f7c9ab272cd118f125a4967bae3bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`.
|
data/lib/samovar/command.rb
CHANGED
|
@@ -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
|
|
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(
|
|
31
|
-
self.parse(
|
|
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
|
|
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(
|
|
49
|
-
self.new(
|
|
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
|
|
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.[](*
|
|
60
|
-
self.new(
|
|
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
|
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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2016-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require_relative "completion"
|
|
7
|
+
|
|
6
8
|
module Samovar
|
|
7
9
|
# Represents multiple positional arguments in a command.
|
|
8
10
|
#
|
|
@@ -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
|
-
|
|
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
|
data/lib/samovar/nested.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2016-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require_relative "completion"
|
|
7
|
+
|
|
6
8
|
module Samovar
|
|
7
9
|
# Represents nested sub-commands in a command.
|
|
8
10
|
#
|
|
@@ -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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2016-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require_relative "completion"
|
|
7
|
+
|
|
6
8
|
module Samovar
|
|
7
9
|
# Represents a single positional argument in a command.
|
|
8
10
|
#
|
|
@@ -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
|
-
|
|
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
|
data/lib/samovar/option.rb
CHANGED
|
@@ -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
|
-
# @
|
|
63
|
-
|
|
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
|
|
145
|
-
[@flags, @description, "(default: #{
|
|
202
|
+
if default?
|
|
203
|
+
[@flags, @description, "(default: #{default})"]
|
|
146
204
|
elsif @required
|
|
147
205
|
[@flags, @description, "(required)"]
|
|
148
206
|
else
|
data/lib/samovar/options.rb
CHANGED
|
@@ -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
|
-
# @
|
|
72
|
-
|
|
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
|
|
135
|
-
@defaults[option.key] = option
|
|
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 ||
|
|
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
|
|
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
|
data/lib/samovar/split.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2016-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require_relative "completion"
|
|
7
|
+
|
|
6
8
|
module Samovar
|
|
7
9
|
# Represents a split point in the command-line arguments.
|
|
8
10
|
#
|
|
@@ -15,12 +17,14 @@ module Samovar
|
|
|
15
17
|
# @parameter marker [String] The marker that indicates the split point.
|
|
16
18
|
# @parameter default [Object] The default value if no split is present.
|
|
17
19
|
# @parameter required [Boolean] Whether the split is required.
|
|
18
|
-
|
|
20
|
+
# @parameter completions [Array | Proc | Nil] Completions for split arguments.
|
|
21
|
+
def initialize(key, description, marker: "--", default: nil, required: false, completions: nil)
|
|
19
22
|
@key = key
|
|
20
23
|
@description = description
|
|
21
24
|
@marker = marker
|
|
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 after the split.
|
|
@@ -48,6 +52,11 @@ module Samovar
|
|
|
48
52
|
# @attribute [Boolean]
|
|
49
53
|
attr :required
|
|
50
54
|
|
|
55
|
+
# Completions for split 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.
|
|
@@ -85,5 +94,40 @@ module Samovar
|
|
|
85
94
|
raise MissingValueError.new(parent, @key)
|
|
86
95
|
end
|
|
87
96
|
end
|
|
97
|
+
|
|
98
|
+
# Complete the split marker or arguments after it.
|
|
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 offset = input.index(@marker)
|
|
106
|
+
input.shift(offset + 1)
|
|
107
|
+
|
|
108
|
+
if @completions == :executable && input.any?
|
|
109
|
+
return Completion::Result.new(collected + [
|
|
110
|
+
Completion::Suggestion.new(
|
|
111
|
+
input.first,
|
|
112
|
+
description: "Delegate completion",
|
|
113
|
+
type: :delegate,
|
|
114
|
+
index: context.words.index(@marker) + 1,
|
|
115
|
+
),
|
|
116
|
+
])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
return Completion::Result.new(collected) + Completion::Provider.new(context.with_row(self), @completions).suggestions
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return Completion::Result.new(collected) unless input.empty?
|
|
123
|
+
|
|
124
|
+
suggestions = []
|
|
125
|
+
|
|
126
|
+
if @marker.start_with?(context.current)
|
|
127
|
+
suggestions << Completion::Suggestion.new(@marker, description: @description, type: :split)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return Completion::Result.new(collected + suggestions)
|
|
131
|
+
end
|
|
88
132
|
end
|
|
89
133
|
end
|
data/lib/samovar/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -18,10 +18,16 @@ Please see the [project documentation](https://ioquatix.github.io/samovar/) for
|
|
|
18
18
|
|
|
19
19
|
- [Getting Started](https://ioquatix.github.io/samovar/guides/getting-started/index) - This guide explains how to use `samovar` to build command-line tools and applications.
|
|
20
20
|
|
|
21
|
+
- [Completion](https://ioquatix.github.io/samovar/guides/completion/index) - This guide explains how to add shell completion to commands built with `samovar`.
|
|
22
|
+
|
|
21
23
|
## Releases
|
|
22
24
|
|
|
23
25
|
Please see the [project releases](https://ioquatix.github.io/samovar/releases/index) for all releases.
|
|
24
26
|
|
|
27
|
+
### v2.5.0
|
|
28
|
+
|
|
29
|
+
- Introduce `Command#complete` which provides a hook for command completion logic, allowing developers to implement custom tab-completion behavior for their commands.
|
|
30
|
+
|
|
25
31
|
### v2.4.2
|
|
26
32
|
|
|
27
33
|
- Drop dependency on `mapping` gem.
|
|
@@ -106,12 +112,6 @@ command list -- --help
|
|
|
106
112
|
|
|
107
113
|
In this case, do we show help? Some effort is required to disambiguate this. Initially, it makes sense to keep things as simple as possible. But, it might make sense for some options to be declared in a global scope, which are extracted before parsing begins. I'm not sure if this is really a good idea. It might just be better to give good error output in this case (you specified an option but it was in the wrong place).
|
|
108
114
|
|
|
109
|
-
### Shell Auto-completion
|
|
110
|
-
|
|
111
|
-
Because of the structure of the Samovar command parser, it should be possible to generate a list of all possible tokens at each point. Therefore, semantically correct tab completion should be possible.
|
|
112
|
-
|
|
113
|
-
As a secondary to this, it would be nice if `Samovar::One` and `Samovar::Many` could take a list of potential tokens so that auto-completion could give meaningful suggestions, and possibly improved validation.
|
|
114
|
-
|
|
115
115
|
### Short/Long Help
|
|
116
116
|
|
|
117
117
|
It might be interesting to explore whether it's possible to have `-h` and `--help` do different things. This could include command specific help output, more detailed help output (similar to a man page), and other useful help related tasks.
|
data/releases.md
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samovar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -58,10 +58,16 @@ executables: []
|
|
|
58
58
|
extensions: []
|
|
59
59
|
extra_rdoc_files: []
|
|
60
60
|
files:
|
|
61
|
+
- context/completion.md
|
|
61
62
|
- context/getting-started.md
|
|
62
63
|
- context/index.yaml
|
|
63
64
|
- lib/samovar.rb
|
|
64
65
|
- lib/samovar/command.rb
|
|
66
|
+
- lib/samovar/completion.rb
|
|
67
|
+
- lib/samovar/completion/context.rb
|
|
68
|
+
- lib/samovar/completion/provider.rb
|
|
69
|
+
- lib/samovar/completion/result.rb
|
|
70
|
+
- lib/samovar/completion/suggestion.rb
|
|
65
71
|
- lib/samovar/error.rb
|
|
66
72
|
- lib/samovar/failure.rb
|
|
67
73
|
- lib/samovar/flags.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|