Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support root commands that doesn't implement call #135

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 30 additions & 3 deletions lib/dry/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Dry
class CLI
require "dry/cli/version"
require "dry/cli/errors"
require "dry/cli/namespace"
require "dry/cli/command"
require "dry/cli/registry"
require "dry/cli/parser"
Expand All @@ -26,11 +27,36 @@ class CLI
# @since 0.1.0
# @api private
def self.command?(command)
case command
inherits?(command, Command)
end

# Check if namespace
#
# @param namespace [Object] the namespace to check
#
# @return [TrueClass,FalseClass] true if instance of `Dry::CLI::Namespace`
#
# @since 1.1.1
# @api private
def self.namespace?(namespace)
inherits?(namespace, Namespace)
end

# Check if `obj` inherits from `klass`
#
# @param obj [Object] object to check
# @param klass [Object] class that should be inherited
#
# @return [TrueClass,FalseClass] true if `obj` inherits from `klass`
#
# @since 1.1.1
# @api private
def self.inherits?(obj, klass)
case obj
when Class
command.ancestors.include?(Command)
obj.ancestors.include?(klass)
else
command.is_a?(Command)
obj.is_a?(klass)
end
end

Expand Down Expand Up @@ -111,6 +137,7 @@ def perform_registry(arguments)
return usage(result) unless result.found?

command, args = parse(result.command, result.arguments, result.names)
return usage(result) unless command.respond_to?(:call)

result.before_callbacks.run(command, args)
command.call(**args)
Expand Down
48 changes: 41 additions & 7 deletions lib/dry/cli/banner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@ class CLI
# @since 0.1.0
# @api private
module Banner
# Prints command banner
# Prints command/namespace banner
#
# @param command [Dry::CLI::Command] the command
# @param command [Dry::CLI::Command, Dry::CLI::Namespace] the command/namespace
# @param out [IO] standard output
#
# @since 0.1.0
# @api private
def self.call(command, name)
b = if CLI.command?(command)
command_banner(command, name)
else
namespace_banner(command, name)
end

b.compact.join("\n")
end

# @since 1.1.1
# @api private
def self.command_banner(command, name)
[
command_name(name),
command_name_and_arguments(command, name),
Expand All @@ -25,21 +37,43 @@ def self.call(command, name)
command_arguments(command),
command_options(command),
command_examples(command, name)
].compact.join("\n")
]
end

# @since 1.1.1
# @api private
def self.namespace_banner(namespace, name)
[
command_name(name, "Namespace"),
command_name_and_arguments(namespace, name),
command_description(namespace),
command_subcommands(namespace),
command_options(namespace)
]
end

# @since 0.1.0
# @api private
def self.command_name(name)
"Command:\n #{name}"
def self.command_name(name, label = "Command")
"#{label}:\n #{name}"
end

# @since 0.1.0
# @api private
def self.command_name_and_arguments(command, name)
usage = "\nUsage:\n #{name}#{arguments(command)}"
usage = "\nUsage:\n"

return usage + " | #{name} SUBCOMMAND" if command.subcommands.any?
callable_root_command = false
if command.new.respond_to?(:call)
callable_root_command = true
usage += " #{name}#{arguments(command)}"
end

if command.subcommands.any?
usage += " "
usage += "|" if callable_root_command
usage += " #{name} SUBCOMMAND"
end

usage
end
Expand Down
86 changes: 86 additions & 0 deletions lib/dry/cli/namespace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

module Dry
class CLI
# Base class for namespaces
#
# @since 1.1.1
class Namespace
# @since 1.1.1
# @api private
def self.inherited(base)
super
base.class_eval do
@description = nil
@examples = []
@arguments = []
@options = []
@subcommands = []
end
base.extend ClassMethods
end

# @since 1.1.1
# @api private
module ClassMethods
# @since 1.1.1
# @api private
attr_reader :description

# @since 1.1.1
# @api private
attr_reader :examples

# @since 1.1.1
# @api private
attr_reader :arguments

# @since 1.1.1
# @api private
attr_reader :options

# @since 1.1.1
# @api private
attr_accessor :subcommands
end

# Set the description of the namespace
#
# @param description [String] the description
#
# @since 1.1.1
#
# @example
# require "dry/cli"
#
# class YourNamespace < Dry::CLI::Namespace
# desc "Collection of really useful commands"
#
# class YourCommand < Dry::CLI::Command
# # ...
# end
# end
def self.desc(description)
@description = description
end

# @since 1.1.1
# @api private
def self.default_params
{}
end

# @since 1.1.1
# @api private
def self.required_arguments
[]
end

# @since 1.1.1
# @api private
def self.subcommands
subcommands
end
end
end
end
21 changes: 11 additions & 10 deletions lib/dry/cli/usage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ def self.call(result)
def self.commands_and_arguments(result)
max_length = 0
ret = commands(result).each_with_object({}) do |(name, node), memo|
args = if node.command && node.leaf? && node.children?
ROOT_COMMAND_WITH_SUBCOMMANDS_BANNER
elsif node.leaf?
arguments(node.command)
else
SUBCOMMAND_BANNER
end

partial = " #{command_name(result, name)}#{args}"
args = arguments(node.command)
args_banner = if node.command && node.leaf? && node.children? && args
ROOT_COMMAND_WITH_SUBCOMMANDS_BANNER
elsif node.leaf? && args
args
elsif node.children?
SUBCOMMAND_BANNER
end

partial = " #{command_name(result, name)}#{args_banner}"
max_length = partial.bytesize if max_length < partial.bytesize
memo[partial] = node
end
Expand All @@ -65,7 +66,7 @@ def self.arguments(command)
# @since 0.1.0
# @api private
def self.description(command)
return unless CLI.command?(command)
return unless CLI.command?(command) || CLI.namespace?(command)

" # #{command.description}" unless command.description.nil?
end
Expand Down
12 changes: 12 additions & 0 deletions spec/support/fixtures/shared_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,18 @@ def call(**params)
end
end

class Namespace < Dry::CLI::Namespace
desc "This is a namespace"

class SubCommand < Dry::CLI::Command
desc "I'm a concrete command"

def call(**params)
puts "I'm a concrete command"
end
end
end

class InitializedCommand < Dry::CLI::Command
attr_reader :prop

Expand Down
3 changes: 3 additions & 0 deletions spec/support/fixtures/with_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
cli.register "root-command", Commands::RootCommand do |prefix|
prefix.register "sub-command", Commands::RootCommands::SubCommand
end
cli.register "namespace", Commands::Namespace do |prefix|
prefix.register "sub-command", Commands::Namespace::SubCommand
end

cli.register "options-with-aliases", Commands::OptionsWithAliases
cli.register "variadic default", Commands::VariadicArguments
Expand Down
2 changes: 2 additions & 0 deletions spec/support/fixtures/with_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ module Commands
register "with-initializer", ::Commands::InitializedCommand.new(prop: "prop_val")
register "root-command", ::Commands::RootCommand
register "root-command sub-command", ::Commands::RootCommands::SubCommand
register "namespace", ::Commands::Namespace
register "namespace sub-command", ::Commands::Namespace::SubCommand

register "options-with-aliases", ::Commands::OptionsWithAliases
register "variadic default", ::Commands::VariadicArguments
Expand Down
4 changes: 4 additions & 0 deletions spec/support/fixtures/with_zero_arity_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
register "root-command" do
register "sub-command", Commands::RootCommands::SubCommand
end
register "namespace", Commands::Namespace
register "namespace" do
register "sub-command", Commands::Namespace::SubCommand
end

register "options-with-aliases", Commands::OptionsWithAliases
register "variadic default", Commands::VariadicArguments
Expand Down
30 changes: 30 additions & 0 deletions spec/support/shared_examples/inherited_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@
expect(error).to eq(expected)
end

it "shows subcommands when calling a namespace" do
error = capture_error { cli.call(arguments: %w[namespace]) }
expected = <<~DESC
Commands:
#{cmd} namespace sub-command # I'm a concrete command
DESC
expect(error).to eq(expected)
end

it "shows namespace help when using --help" do
output = capture_output { cli.call(arguments: %w[namespace --help]) }
expected = <<~DESC
Namespace:
#{cmd} namespace

Usage:
#{cmd} namespace SUBCOMMAND

Description:
This is a namespace

Subcommands:
sub-command # I'm a concrete command

Options:
--help, -h # Print this help
DESC
expect(output).to eq(expected)
end

it "shows run's help" do
output = capture_output { cli.call(arguments: %w[i run --help]) }
expected = <<~DESC
Expand Down
3 changes: 3 additions & 0 deletions spec/support/shared_examples/rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#{cmd} greeting [RESPONSE]
#{cmd} hello # Print a greeting
#{cmd} inherited [SUBCOMMAND]
#{cmd} namespace [SUBCOMMAND] # This is a namespace
#{cmd} new PROJECT # Generate a new Foo project
#{cmd} options-with-aliases # Accepts options with aliases
#{cmd} root-command [ARGUMENT|SUBCOMMAND] # Root command with arguments and subcommands
Expand Down Expand Up @@ -80,6 +81,7 @@
#{cmd} greeting [RESPONSE]
#{cmd} hello # Print a greeting
#{cmd} inherited [SUBCOMMAND]
#{cmd} namespace [SUBCOMMAND] # This is a namespace
#{cmd} new PROJECT # Generate a new Foo project
#{cmd} options-with-aliases # Accepts options with aliases
#{cmd} root-command [ARGUMENT|SUBCOMMAND] # Root command with arguments and subcommands
Expand Down Expand Up @@ -109,6 +111,7 @@
#{cmd} greeting [RESPONSE]
#{cmd} hello # Print a greeting
#{cmd} inherited [SUBCOMMAND]
#{cmd} namespace [SUBCOMMAND] # This is a namespace
#{cmd} new PROJECT # Generate a new Foo project
#{cmd} options-with-aliases # Accepts options with aliases
#{cmd} root-command [ARGUMENT|SUBCOMMAND] # Root command with arguments and subcommands
Expand Down