Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7162731
Add shell completion support
samuel-williams-shopify Jun 11, 2026
34c44ff
Move completion behavior onto parser rows
samuel-williams-shopify Jun 11, 2026
dec9ba5
Add samovar completions command
samuel-williams-shopify Jun 11, 2026
4d5888b
Use bin for samovar executable
samuel-williams-shopify Jun 11, 2026
2066af5
Move CLI commands under internal command namespace
samuel-williams-shopify Jun 11, 2026
4f8fb6b
Add completion install command
samuel-williams-shopify Jun 11, 2026
3c931eb
Use shell option for completion command
samuel-williams-shopify Jun 11, 2026
b1e19cc
Require command option for completion CLI
samuel-williams-shopify Jun 11, 2026
e3a422f
Fix zsh completion argv handling
samuel-williams-shopify Jun 11, 2026
3e0f72d
Fix fish completion cursor handling
samuel-williams-shopify Jun 11, 2026
4b55104
Add bash completion adapter tests
samuel-williams-shopify Jun 11, 2026
a53d801
Avoid env in fish completion adapter
samuel-williams-shopify Jun 11, 2026
4bcbf0b
Complete option defaults first
samuel-williams-shopify Jun 11, 2026
4d9c370
Use completed command for completion requests
samuel-williams-shopify Jun 11, 2026
8f12100
Fix zsh completion argv expansion
samuel-williams-shopify Jun 11, 2026
3cb24c4
Use generic completion protocol
samuel-williams-shopify Jun 11, 2026
fcefef7
Use protocol completion
samuel-williams-shopify Jun 16, 2026
7ffb08c
Move shell completion command out
samuel-williams-shopify Jun 16, 2026
005d44f
Require explicit completion mode
samuel-williams-shopify Jun 16, 2026
cff9e37
Inline completion result support
samuel-williams-shopify Jun 16, 2026
4674ea3
Separate command completion entry point
samuel-williams-shopify Jun 16, 2026
6114475
Simplify completion input handling
samuel-williams-shopify Jun 16, 2026
896e4d0
Support native path completion requests
samuel-williams-shopify Jun 16, 2026
4bbbdc2
Split completion support into classes
samuel-williams-shopify Jun 16, 2026
eab3703
Move completion entry point to command
samuel-williams-shopify Jun 16, 2026
b7c8556
Clarify completion control flow
samuel-williams-shopify Jun 16, 2026
3817461
Document completion implementation
samuel-williams-shopify Jun 16, 2026
a951745
Document generic completion adapters
samuel-williams-shopify Jun 16, 2026
c57978c
Exclude bin from packaged files
samuel-williams-shopify Jun 16, 2026
9637d3f
Rename completion context argv
samuel-williams-shopify Jun 16, 2026
8ffe364
Use default completion arguments in examples
samuel-williams-shopify Jun 16, 2026
64fcfea
Move completion details to guide
samuel-williams-shopify Jun 16, 2026
87ed641
Document completion APIs
samuel-williams-shopify Jun 16, 2026
657749b
Use tap for completion entry point
samuel-williams-shopify Jun 16, 2026
c959caf
Document indexed completion context methods.
samuel-williams-shopify Jun 17, 2026
8f7f4ca
Simplify completion provider context.
samuel-williams-shopify Jun 17, 2026
2b6bf6e
Merge branch 'main' into add-shell-completion
samuel-williams-shopify Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions guides/completion/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Completion

This guide explains how to add shell completion to commands built with `samovar`.

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.

## Command Entry Point

Commands expose a completion entry point alongside the normal execution entry point:

~~~ ruby
Application.call(ARGV) # Parse and execute the command.
Application.complete # Print completion candidates.
~~~

`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:

~~~ ruby
Application.complete(["serve", "--bind", ""])
~~~

Completion candidates are printed as tab-separated values:

~~~ text
type value description key=value
~~~

The first three fields are always the completion type, value, and description. Additional fields are optional metadata entries encoded as `key=value`.

## Static Completions

Option flags and nested command names are completed automatically. You can add static completions for option values and positional arguments with `completions:`.

~~~ ruby
require "samovar"

class Serve < Samovar::Command
self.description = "Run the server."

options do
option "--format <name>", "The output format.", default: "text", completions: ["json", "text", "yaml"]
end
end

class Application < Samovar::Command
options do
option "-h/--help", "Print help."
end

nested :command, {
"serve" => Serve
}, default: "serve"
end
~~~

Examples:

~~~ ruby
Application.complete(["ser"])
# command serve Run the server.

Application.complete(["serve", "--format", "j"])
# value json
~~~

If an option has a default value, the default is offered before other value completions.

## Dynamic Completions

Use a callable provider when completions depend on runtime state.

~~~ ruby
class Serve < Samovar::Command
def self.host_completions(context)
["localhost", "0.0.0.0"].select do |host|
host.start_with?(context.current)
end
end

options do
option "--bind <host>", "The bind address.", completions: method(:host_completions)
end
end
~~~

The provider receives a `Samovar::Completion::Context` with:

- `current`: The token being completed.
- `arguments`: The full truncated argument list.
- `environment`: The environment hash passed to `complete`.
- `row`: The parser row whose value is being completed. This can be an option or a positional argument.

Providers can return strings, hashes, or `Samovar::Completion::Suggestion` instances:

~~~ ruby
option "--mode <name>", "The mode.",
completions: [
{value: "development", description: "Local development", type: :value, suffix: " "},
{value: "production", description: "Production", type: :value}
]
~~~

## Path Completion

For path-like arguments, let the shell do native path expansion by using one of the native completion providers:

~~~ ruby
class Process < Samovar::Command
options do
option "--output <path>", "The output path.", completions: :path
option "--root <path>", "The root directory.", completions: :directory
end

one :input, "The input path.", completions: :file
end
~~~

Supported native providers:

- `:path`: Complete files and directories using the shell.
- `:file`: Alias for `:path`.
- `:directory`: Complete directories using the shell.
- `:executable`: Complete executable commands using the shell.

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.

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.

## Dedicated Completion Executable

Shell adapters call a dedicated completion executable named `completion-<command>`. This avoids running the normal command during completion.

For a command named `falcon`, provide:

~~~ text
bin/falcon
bin/completion-falcon
~~~

The completion executable can be very small:

~~~ ruby
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative "../lib/my/application"

My::Application.complete
~~~

When the user completes a command by path, the shell adapter resolves the completion executable next to that command:

~~~ text
falcon -> completion-falcon
bin/falcon -> bin/completion-falcon
./bin/falcon -> ./bin/completion-falcon
/path/falcon -> /path/completion-falcon
~~~

## Installing Shell Adapters

Shell adapter generation and installation is provided by the `completion` gem.

Generate an adapter script:

~~~ bash
$ completion generate --shell zsh --command falcon
~~~

Install a generic adapter script into the default directory for the current shell:

~~~ bash
$ completion install
~~~

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:

~~~ bash
$ completion install --command falcon
~~~

You can specify the shell and directory explicitly:

~~~ bash
$ completion install --shell fish --directory ~/.config/fish/completions --command falcon
~~~

The installed adapter calls the matching `completion-<command>` executable when completion is requested.

## Testing Completion

You can test completion directly without involving a shell:

~~~ ruby
output = StringIO.new

Application.complete(["serve", "--format", "j"], output: output)

expect(output.string).to be == "value\tjson\t\n"
~~~

For a trailing-space completion, pass an empty final token:

~~~ ruby
Application.complete(["serve", "--format", ""], output: output)
~~~
2 changes: 2 additions & 0 deletions guides/links.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
getting-started:
order: 1
completion:
order: 2
33 changes: 24 additions & 9 deletions lib/samovar/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require_relative "output"

require_relative "error"
require_relative "completion"

module Samovar
# Represents a command in the command-line interface.
Expand All @@ -23,12 +24,13 @@ class Command
# Parse and execute the command with the given input.
#
# This is the high-level entry point for CLI applications. It handles errors gracefully by printing usage and returning nil.
# The given arguments are passed to the parser, which consumes them as mutable input.
#
# @parameter input [Array(String)] The command-line arguments to parse.
# @parameter arguments [Array(String)] The command-line arguments to parse.
# @parameter output [IO] The output stream for error messages.
# @returns [Object | Nil] The result of the command's call method, or nil if parsing/execution failed.
def self.call(input = ARGV, output: $stderr)
self.parse(input).call
def self.call(arguments = ARGV, output: $stderr)
self.parse(arguments).call
rescue Error => error
error.command.print_usage(output: output) do |formatter|
formatter.map(error)
Expand All @@ -37,27 +39,40 @@ def self.call(input = ARGV, output: $stderr)
return nil
end

# Complete the command-line input without executing the command.
# The given arguments are treated as the stable command-line boundary; completion internals consume derived input arrays.
#
# @parameter arguments [Array(String)] The command-line arguments to complete.
# @parameter environment [Hash] The environment for completion callbacks.
# @parameter output [IO] The output stream for printing completion results.
# @returns [Completion::Result] The completion result.
def self.complete(arguments = ARGV, environment: ENV, output: $stdout)
Completion.complete(self, arguments, environment: environment).tap do |result|
result.print(output)
end
end

# Parse the command-line input and create a command instance.
#
# This is the low-level parsing primitive. It raises {Error} exceptions on parsing failures.
# For CLI applications, use {call} instead which handles errors gracefully.
#
# @parameter input [Array(String)] The command-line arguments to parse.
# @parameter arguments [Array(String)] The command-line arguments to parse.
# @returns [Command] The parsed command instance.
# @raises [Error] If parsing fails.
def self.parse(input)
self.new(input)
def self.parse(arguments)
self.new(arguments)
end

# Create a new command instance with the given arguments.
#
# This is a convenience method for creating command instances with explicit arguments.
#
# @parameter input [Array(String)] The command-line arguments to parse.
# @parameter arguments [Array(String)] The command-line arguments to parse.
# @parameter options [Hash] Additional options to pass to the command.
# @returns [Command] The command instance.
def self.[](*input, **options)
self.new(input, **options)
def self.[](*arguments, **options)
self.new(arguments, **options)
end

class << self
Expand Down
24 changes: 24 additions & 0 deletions lib/samovar/completion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require_relative "completion/context"
require_relative "completion/provider"
require_relative "completion/result"
require_relative "completion/suggestion"

module Samovar
# Shell completion support for Samovar commands.
module Completion
# Complete the command line for the given command class.
#
# @parameter command_class [Class] The command class to complete.
# @parameter arguments [Array(String)] The application arguments.
# @parameter environment [Hash] The environment for completion callbacks.
# @returns [Result] The completion result.
def self.complete(command_class, arguments, environment: ENV)
Context.for(command_class, arguments, environment: environment).complete
end
end
end
Loading
Loading