diff --git a/lib/dry/cli.rb b/lib/dry/cli.rb index 6de9689..b281cdd 100644 --- a/lib/dry/cli.rb +++ b/lib/dry/cli.rb @@ -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" @@ -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 diff --git a/lib/dry/cli/banner.rb b/lib/dry/cli/banner.rb index 1f686a2..1a43693 100644 --- a/lib/dry/cli/banner.rb +++ b/lib/dry/cli/banner.rb @@ -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), @@ -25,13 +37,25 @@ 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, "Commands"), + 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 @@ -70,10 +94,10 @@ def self.command_description(command) "\nDescription:\n #{command.description}" end - def self.command_subcommands(command) + def self.command_subcommands(command, label = "Subcommands") return if command.subcommands.empty? - "\nSubcommands:\n#{build_subcommands_list(command.subcommands)}" + "\n#{label}:\n#{build_subcommands_list(command.subcommands)}" end # @since 0.1.0 diff --git a/lib/dry/cli/namespace.rb b/lib/dry/cli/namespace.rb new file mode 100644 index 0000000..f89f15c --- /dev/null +++ b/lib/dry/cli/namespace.rb @@ -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 diff --git a/lib/dry/cli/usage.rb b/lib/dry/cli/usage.rb index e19a0ba..f7de207 100644 --- a/lib/dry/cli/usage.rb +++ b/lib/dry/cli/usage.rb @@ -66,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 diff --git a/spec/support/fixtures/shared_commands.rb b/spec/support/fixtures/shared_commands.rb index 3cde0f8..05256f2 100644 --- a/spec/support/fixtures/shared_commands.rb +++ b/spec/support/fixtures/shared_commands.rb @@ -438,7 +438,7 @@ def call(**params) end end - class Namespace < Dry::CLI::Command + class Namespace < Dry::CLI::Namespace desc "This is a namespace" class SubCommand < Dry::CLI::Command diff --git a/spec/support/shared_examples/inherited_commands.rb b/spec/support/shared_examples/inherited_commands.rb index 4643937..c8712c6 100644 --- a/spec/support/shared_examples/inherited_commands.rb +++ b/spec/support/shared_examples/inherited_commands.rb @@ -18,7 +18,7 @@ expect(error).to eq(expected) end - it "shows subcommands when root command doesn't implement #call" do + it "shows subcommands when calling a namespace" do error = capture_error { cli.call(arguments: %w[namespace]) } expected = <<~DESC Commands: @@ -27,10 +27,10 @@ expect(error).to eq(expected) end - it "shows root command help considering if it implements #call" do + it "shows namespace help when using --help" do output = capture_output { cli.call(arguments: %w[namespace --help]) } expected = <<~DESC - Command: + Namespace: #{cmd} namespace Usage: @@ -39,7 +39,7 @@ Description: This is a namespace - Subcommands: + Commands: sub-command # I'm a concrete command Options: