# frozen_string_literal: true require "forwardable" require "dry/cli/option" module Dry class CLI # Base class for commands # # @since 0.1.0 class Command # @since 0.1.0 # @api private def self.inherited(base) super base.class_eval do @_mutex = Mutex.new @description = nil @examples = [] @subcommands = [] @arguments = base.superclass_arguments || [] @options = base.superclass_options || [] end base.extend ClassMethods end # @since 0.1.0 # @api private module ClassMethods # @since 0.1.0 # @api private attr_reader :description # @since 0.1.0 # @api private attr_reader :examples # @since 0.1.0 # @api private attr_reader :arguments # @since 0.1.0 # @api private attr_reader :options # @since 0.7.0 # @api private attr_reader :subcommands # @since 0.7.0 # @api private attr_writer :subcommands end # Set the description of the command # # @param description [String] the description # # @since 0.1.0 # # @example # require "dry/cli" # # class Echo < Dry::CLI::Command # desc "Prints given input" # # def call(*) # # ... # end # end def self.desc(description) @description = description end # Describe the usage of the command # # @param examples [Array] one or more examples # # @since 0.1.0 # # @example # require "dry/cli" # # class Server < Dry::CLI::Command # example [ # " # Basic usage (it uses the bundled server engine)", # "--server=webrick # Force `webrick` server engine", # "--host=0.0.0.0 # Bind to a host", # "--port=2306 # Bind to a port", # "--no-code-reloading # Disable code reloading" # ] # # def call(*) # # ... # end # end # # # $ foo server --help # # # ... # # # # Examples: # # foo server # Basic usage (it uses the bundled server engine) # # foo server --server=webrick # Force `webrick` server engine # # foo server --host=0.0.0.0 # Bind to a host # # foo server --port=2306 # Bind to a port # # foo server --no-code-reloading # Disable code reloading def self.example(*examples) @examples += examples.flatten(1) end # Specify an argument # # @param name [Symbol] the argument name # @param options [Hash] a set of options # # @since 0.1.0 # # @example Optional argument # require "dry/cli" # # class Hello < Dry::CLI::Command # argument :name # # def call(name: nil, **) # if name.nil? # puts "Hello, stranger" # else # puts "Hello, #{name}" # end # end # end # # # $ foo hello # # Hello, stranger # # # $ foo hello Luca # # Hello, Luca # # @example Required argument # require "dry/cli" # # class Hello < Dry::CLI::Command # argument :name, required: true # # def call(name:, **) # puts "Hello, #{name}" # end # end # # # $ foo hello Luca # # Hello, Luca # # # $ foo hello # # ERROR: "foo hello" was called with no arguments # # Usage: "foo hello NAME" # # @example Multiple arguments # require "dry/cli" # # module Generate # class Action < Dry::CLI::Command # argument :app, required: true # argument :action, required: true # # def call(app:, action:, **) # puts "Generating action: #{action} for app: #{app}" # end # end # end # # # $ foo generate action web home # # Generating action: home for app: web # # # $ foo generate action # # ERROR: "foo generate action" was called with no arguments # # Usage: "foo generate action APP ACTION" # # @example Description # require "dry/cli" # # class Hello < Dry::CLI::Command # argument :name, desc: "The name of the person to greet" # # def call(name: nil, **) # # ... # end # end # # # $ foo hello --help # # Command: # # foo hello # # # # Usage: # # foo hello [NAME] # # # # Arguments: # # NAME # The name of the person to greet # # # # Options: # # --help, -h # Print this help def self.argument(name, options = {}) @arguments << Argument.new(name, options) end # Command line option (aka optional argument) # # @param name [Symbol] the param name # @param options [Hash] a set of options # # @since 0.1.0 # # @example Basic usage # require "dry/cli" # # class Console < Dry::CLI::Command # param :engine # # def call(engine: nil, **) # puts "starting console (engine: #{engine || :irb})" # end # end # # # $ foo console # # starting console (engine: irb) # # # $ foo console --engine=pry # # starting console (engine: pry) # # @example List values # require "dry/cli" # # class Console < Dry::CLI::Command # param :engine, values: %w(irb pry ripl) # # def call(engine: nil, **) # puts "starting console (engine: #{engine || :irb})" # end # end # # # $ foo console # # starting console (engine: irb) # # # $ foo console --engine=pry # # starting console (engine: pry) # # # $ foo console --engine=foo # # ERROR: Invalid param provided # # @example Description # require "dry/cli" # # class Console < Dry::CLI::Command # param :engine, desc: "Force a console engine" # # def call(engine: nil, **) # # ... # end # end # # # $ foo console --help # # # ... # # # # Options: # # --engine=VALUE # Force a console engine: (irb/pry/ripl) # # --help, -h # Print this help # # @example Boolean # require "dry/cli" # # class Server < Dry::CLI::Command # param :code_reloading, type: :boolean, default: true # # def call(code_reloading:, **) # puts "staring server (code reloading: #{code_reloading})" # end # end # # # $ foo server # # starting server (code reloading: true) # # # $ foo server --no-code-reloading # # starting server (code reloading: false) # # # $ foo server --help # # # ... # # # # Options: # # --[no]-code-reloading # # @example Aliases # require "dry/cli" # # class Server < Dry::CLI::Command # param :port, aliases: ["-p"] # # def call(options) # puts "staring server (port: #{options.fetch(:port, 2300)})" # end # end # # # $ foo server # # starting server (port: 2300) # # # $ foo server --port=2306 # # starting server (port: 2306) # # # $ foo server -p 2306 # # starting server (port: 2306) # # # $ foo server --help # # # ... # # # # Options: # # --port=VALUE, -p VALUE def self.option(name, options = {}) @options << Option.new(name, options) end # @since 0.1.0 # @api private def self.params @_mutex.synchronize do (@arguments + @options).uniq end end # @since 0.1.0 # @api private def self.default_params params.each_with_object({}) do |param, result| result[param.name] = param.default unless param.default.nil? end end # @since 0.1.0 # @api private def self.required_arguments arguments.select(&:required?) end # @since 0.1.0 # @api private def self.optional_arguments arguments.reject(&:required?) end # @since 0.7.0 # @api private def self.subcommands subcommands end # @since 0.7.0 # @api private def self.superclass_variable_dup(var) if superclass.instance_variable_defined?(var) superclass.instance_variable_get(var).dup end end # @since 0.7.0 # @api private def self.superclass_arguments superclass_variable_dup(:@arguments) end # @since 0.7.0 # @api private def self.superclass_options superclass_variable_dup(:@options) end extend Forwardable delegate %i[ description examples arguments options params default_params required_arguments optional_arguments subcommands ] => "self.class" end end end