diff --git a/docsite/source/index.html.md b/docsite/source/index.html.md
index 67318aa..6d466e3 100644
--- a/docsite/source/index.html.md
+++ b/docsite/source/index.html.md
@@ -12,6 +12,7 @@ sections:
- variadic-arguments
- commands-with-subcommands-and-params
- callbacks
+ - shell-completion
---
`dry-cli` is a general-purpose framework for developing Command Line Interface (CLI) applications. It represents commands as objects that can be registered and offers support for arguments, options and forwarding variadic arguments to a sub-command.
diff --git a/docsite/source/shell-completion.html.md b/docsite/source/shell-completion.html.md
new file mode 100644
index 0000000..79144e9
--- /dev/null
+++ b/docsite/source/shell-completion.html.md
@@ -0,0 +1,43 @@
+---
+title: Shell completion
+layout: gem-single
+name: dry-cli
+---
+
+You can add shell completion to your CLI by registering the `Dry::CLI::ShellCompletion` command:
+
+```ruby
+module Foo
+ module CLI
+ module Commands
+ extend Dry::CLI::Registry
+
+ # ...
+
+ register "complete", Dry::CLI::ShellCompletion
+
+ # ...
+ end
+ end
+end
+```
+
+Now your users need to configure their shell of choice. For Bash users, the configuration is very simple:
+
+```sh
+complete -F get_foo_targets foo
+function get_foo_targets()
+{
+ COMPREPLY=(`foo complete "${COMP_WORDS[@]:1}"`)
+}
+
+```
+
+When using your CLI, `` will trigger the completion:
+
+```sh
+$ foo s
+start stop
+$ foo generate # The completion works for subcommands too.
+config test
+```
diff --git a/lib/dry/cli.rb b/lib/dry/cli.rb
index 5f93860..fbb39f4 100644
--- a/lib/dry/cli.rb
+++ b/lib/dry/cli.rb
@@ -17,6 +17,7 @@ class CLI
require "dry/cli/spell_checker"
require "dry/cli/banner"
require "dry/cli/inflector"
+ require "dry/cli/shell_completion"
# Check if command
#
@@ -110,6 +111,7 @@ def perform_command(arguments)
def perform_registry(arguments)
result = registry.get(arguments)
return spell_checker(result, arguments) unless result.found?
+ return shell_completion(registry, result.arguments) if result.command == Dry::CLI::ShellCompletion
command, args = parse(result.command, result.arguments, result.names)
@@ -171,6 +173,13 @@ def spell_checker(result, arguments)
exit(1)
end
+ # @since 1.3.0
+ # @api private
+ def shell_completion(registry, prefixes)
+ puts registry.complete(prefixes)
+ exit(0)
+ end
+
# Handles Exit codes for signals
# Fatal error signal "n". Say 130 = 128 + 2 (SIGINT) or 137 = 128 + 9 (SIGKILL)
#
diff --git a/lib/dry/cli/command_registry.rb b/lib/dry/cli/command_registry.rb
index fa3881f..0f3d57d 100644
--- a/lib/dry/cli/command_registry.rb
+++ b/lib/dry/cli/command_registry.rb
@@ -75,6 +75,23 @@ def get(arguments)
end
# rubocop:enable Metrics/AbcSize
+ # Search for commands/subcommands that match the `prefixes`
+ #
+ # @param prefixes [Array] the prefixes to be matched
+ #
+ # @since 1.3.0
+ # @api private
+ def complete(prefixes)
+ initial_lookup = get(prefixes)
+
+ candidates = initial_lookup.children.keys
+ candidates += @root.aliases.keys if initial_lookup.names.empty?
+
+ pending_prefix = prefixes != initial_lookup.names ? prefixes.last : nil
+ candidates.filter! {|c| c.start_with?(prefixes.last)} unless pending_prefix.nil?
+ candidates.join("\n")
+ end
+
# Node of the registry
#
# @since 0.1.0
diff --git a/lib/dry/cli/registry.rb b/lib/dry/cli/registry.rb
index de1c2f6..ffdb176 100644
--- a/lib/dry/cli/registry.rb
+++ b/lib/dry/cli/registry.rb
@@ -270,6 +270,12 @@ def get(arguments)
@commands.get(arguments)
end
+ # @since 1.3.0
+ # @api private
+ def complete(prefixes)
+ @commands.complete(prefixes)
+ end
+
private
COMMAND_NAME_SEPARATOR = " "
diff --git a/lib/dry/cli/shell_completion.rb b/lib/dry/cli/shell_completion.rb
new file mode 100644
index 0000000..1db50d4
--- /dev/null
+++ b/lib/dry/cli/shell_completion.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Dry
+ class CLI
+ # Help users to build a shell completion script by searching commands based on a prefix
+ #
+ # @since 1.3.0
+ class ShellCompletion < Command
+ desc "Help you to build a shell completion script by searching commands based on a prefix"
+
+ argument :prefix, desc: "Command name prefix", required: true
+ end
+ end
+end
diff --git a/spec/integration/rendering_spec.rb b/spec/integration/rendering_spec.rb
index 3b0209d..3c1767b 100644
--- a/spec/integration/rendering_spec.rb
+++ b/spec/integration/rendering_spec.rb
@@ -8,6 +8,7 @@
expected = <<~DESC
Commands:
+ foo -c PREFIX # Help you to build a shell completion script by searching commands based on a prefix
foo assets [SUBCOMMAND]
foo callbacks DIR # Command with callbacks
foo console # Starts Foo console
diff --git a/spec/integration/shell_completion_spec.rb b/spec/integration/shell_completion_spec.rb
new file mode 100644
index 0000000..58a2bfd
--- /dev/null
+++ b/spec/integration/shell_completion_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "open3"
+
+RSpec.describe "Shell completion" do
+ it "returns all available commands when no prefix are provided" do
+ out, = Open3.capture3("foo -c")
+
+ expected = <<~DESC
+ -c
+ assets
+ console
+ db
+ destroy
+ generate
+ new
+ routes
+ server
+ version
+ completion
+ exec
+ hello
+ greeting
+ sub
+ root-command
+ variadic
+ callbacks
+ d
+ g
+ s
+ v
+ -v
+ --version
+ DESC
+ expect(out).to eq(expected)
+ end
+
+ it "returns all available commands that matches the provided prefix" do
+ out, = Open3.capture3("foo -c -")
+
+ expected = <<~DESC
+ -c
+ -v
+ --version
+ DESC
+ expect(out).to eq(expected)
+ end
+
+ it "returns nothing when the command is found" do
+ out, = Open3.capture3("foo -c console")
+
+ expected = <<~DESC
+
+ DESC
+ expect(out).to eq(expected)
+ end
+
+ it "returns all subcommands when command is found but no prefix are provided" do
+ out, = Open3.capture3("foo -c db")
+
+ expected = <<~DESC
+ apply
+ console
+ create
+ drop
+ migrate
+ prepare
+ version
+ rollback
+ DESC
+ expect(out).to eq(expected)
+ end
+
+ it "returns all subcommands that matches the provided prefix" do
+ out, = Open3.capture3("foo -c db c")
+
+ expected = <<~DESC
+ console
+ create
+ DESC
+ expect(out).to eq(expected)
+ end
+
+ it "returns nothing when subcommand is found" do
+ out, = Open3.capture3("foo -c db create")
+
+ expected = <<~DESC
+
+ DESC
+ expect(out).to eq(expected)
+ end
+end
diff --git a/spec/integration/spell_checker_spec.rb b/spec/integration/spell_checker_spec.rb
index 30ab94a..2cbc6c1 100644
--- a/spec/integration/spell_checker_spec.rb
+++ b/spec/integration/spell_checker_spec.rb
@@ -9,6 +9,7 @@
expected = <<~DESC
I don't know how to 'routs'. Did you mean: 'routes' ?
Commands:
+ foo -c PREFIX # Help you to build a shell completion script by searching commands based on a prefix
foo assets [SUBCOMMAND]
foo callbacks DIR # Command with callbacks
foo console # Starts Foo console
diff --git a/spec/support/fixtures/foo b/spec/support/fixtures/foo
index bf37e0f..5931520 100755
--- a/spec/support/fixtures/foo
+++ b/spec/support/fixtures/foo
@@ -437,6 +437,7 @@ module Foo
end
end
+Foo::CLI::Commands.register "-c", Dry::CLI::ShellCompletion
Foo::CLI::Commands.register "assets precompile", Foo::CLI::Commands::Assets::Precompile
Foo::CLI::Commands.register "console", Foo::CLI::Commands::Console
Foo::CLI::Commands.register "db" do |prefix|