Skip to content
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
41 changes: 26 additions & 15 deletions lib/dry/cli/banner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,26 +135,37 @@ def self.extended_command_arguments(command)
end.join("\n")
end

# @since 1.5.0
# @api private
def self.simple_option(option)
name = Inflector.dasherize(option.name)
name = if option.boolean?
"[no-]#{name}"
elsif option.flag?
name
elsif option.array?
"#{name}=VALUE1,VALUE2,.."
else
"#{name}=VALUE"
end
name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any?
"--#{name}"
end

# @since 1.5.0
# @api private
def self.extended_option(option)
name = " #{simple_option(option).ljust(32)} # #{"REQUIRED " if option.required?}#{option.desc}"
name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
name
end

# @since 0.1.0
# @api private
#
def self.extended_command_options(command)
result = command.options.map do |option|
name = Inflector.dasherize(option.name)
name = if option.boolean?
"[no-]#{name}"
elsif option.flag?
name
elsif option.array?
"#{name}=VALUE1,VALUE2,.."
else
"#{name}=VALUE"
end
name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any?
name = " --#{name.ljust(30)}"
name = "#{name} # #{option.desc}"
name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
name
extended_option(option)
end

result << " --#{"help, -h".ljust(30)} # Print this help"
Expand Down
15 changes: 11 additions & 4 deletions lib/dry/cli/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,12 @@ def self.arguments_sorted_by_usage_order
end
# rubocop:enable Metrics/PerceivedComplexity

# @since 1.5.0
# @api private
def self.required_options
options.select(&:required?)
end

# @since 0.7.0
# @api private
def self.subcommands
Expand Down Expand Up @@ -404,15 +410,16 @@ def self.superclass_options
extend Forwardable

delegate %i[
arguments
arguments_sorted_by_usage_order
default_params
description
examples
arguments
optional_arguments
options
params
default_params
required_arguments
optional_arguments
arguments_sorted_by_usage_order
required_options
subcommands
] => "self.class"

Expand Down
6 changes: 6 additions & 0 deletions lib/dry/cli/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ def self.required_arguments
[]
end

# @since 1.5.0
# @api private
def self.required_options
[]
end

# @since 1.1.1
# @api private
def self.subcommands
Expand Down
71 changes: 51 additions & 20 deletions lib/dry/cli/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ def self.call(command, arguments, prog_name)
end
end.parse!(arguments)

parsed_options = command.default_params.merge(parsed_options)
parse_required_params(command, arguments, prog_name, parsed_options)
rescue ::OptionParser::ParseError, ValueError
Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"")
Expand All @@ -38,36 +37,68 @@ def self.call(command, arguments, prog_name)
# @since 0.1.0
# @api private
#
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Layout/LineLength
# rubocop:disable Metrics/AbcSize
def self.parse_required_params(command, arguments, prog_name, parsed_options)
parsed_params = match_arguments(command.arguments, arguments, parsed_options)
parsed_required_params = match_arguments(command.required_arguments, arguments, parsed_options)
all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? }
parsed_options_with_defaults = command.default_params.merge(parsed_options)
parsed_params = match_arguments(command.arguments, arguments, parsed_options_with_defaults)
parsed_required_params = match_arguments(command.required_arguments, arguments, parsed_options_with_defaults)

all_required_params_satisfied =
command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } &&
command.required_options.all? { |option| !parsed_options_with_defaults[option.name].nil? }

unused_arguments = arguments.drop(command.required_arguments.length)

unless all_required_params_satisfied
parsed_required_params_values = parsed_required_params.values.compact

usage = "\nUsage: \"#{prog_name} #{command.required_arguments.map(&:description_name).join(" ")}"

usage += " | #{prog_name} SUBCOMMAND" if command.subcommands.any?

usage += '"'

if parsed_required_params_values.empty?
return Result.failure("ERROR: \"#{prog_name}\" was called with no arguments#{usage}")
else
return Result.failure("ERROR: \"#{prog_name}\" was called with arguments #{parsed_required_params_values}#{usage}")
end
return error_message(
command, prog_name, parsed_required_params, parsed_options, parsed_options_with_defaults
)
end

parsed_params.reject! { |_key, value| value.nil? }
parsed_options = parsed_options.merge(parsed_params)
parsed_options = parsed_options_with_defaults.merge(parsed_params)
parsed_options = parsed_options.merge(args: unused_arguments) if unused_arguments.any?
Result.success(parsed_options)
end
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Layout/LineLength
# rubocop:enable Metrics/AbcSize

def self.short_usage(command, prog_name)
usage = "\nUsage: \"#{prog_name} #{command.required_arguments.map(&:description_name).join(" ")}"
usage += " | #{prog_name} SUBCOMMAND" if command.subcommands.any?
if command.required_options.any?
usage += " #{command.required_options.map { |opt|
Banner.simple_option(opt)
}.join(" ")}"
end
usage += '"'
usage
end

def self.error_message(command, prog_name, parsed_required_params, parsed_options, parsed_options_with_defaults)
parsed_required_params_values = parsed_required_params.values.compact

missing_options = command.required_options.select { |option|
parsed_options_with_defaults[option.name].nil?
}

error_msg = "ERROR: \"#{prog_name}\" was called with "
error_msg += if parsed_required_params_values.empty?
"no arguments"
else
"arguments #{parsed_required_params_values}"
end
error_msg += " and options #{parsed_options}" if parsed_options.any?
error_msg += short_usage(command, prog_name)

if missing_options.any?
error_msg += "\nMissing required options:"
missing_options.each do |missing_option|
error_msg += "\n #{Banner.extended_option(missing_option)}"
end
end

Result.failure(error_msg)
end

def self.match_arguments(command_arguments, arguments, default_values)
result = {}
Expand Down
44 changes: 37 additions & 7 deletions spec/integration/single_command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
it "shows usage" do
_, stderr, = Open3.capture3("baz")
expect(stderr).to eq(
"ERROR: \"#{cmd}\" was called with no arguments\nUsage: \"#{cmd} MANDATORY_ARG\"\n"
"ERROR: \"#{cmd}\" was called with no arguments\n"\
"Usage: \"#{cmd} MANDATORY_ARG --mandatory-option=VALUE --mandatory-option-with-default=VALUE\"\n"\
"Missing required options:\n --mandatory-option=VALUE # REQUIRED Mandatory option\n"
)
end

Expand All @@ -33,39 +35,67 @@
--option-one=VALUE, -1 VALUE # Option one
--[no-]boolean-option, -b # Option boolean
--option-with-default=VALUE, -d VALUE # Option default, default: "test"
--mandatory-option=VALUE # REQUIRED Mandatory option
--mandatory-option-with-default=VALUE # REQUIRED Mandatory option, default: "mandatory default"
--help, -h # Print this help
OUTPUT
expect(output).to eq(expected_output)
end

it "with mandatory arg and non-required option" do
_, stderr, = Open3.capture3("baz first_arg --option_one=test2")

if RUBY_VERSION < "3.4"
expect(stderr).to eq(
"ERROR: \"#{cmd}\" was called with arguments [\"first_arg\"] and options {:option_one=>\"test2\"}\n" \
"Usage: \"#{cmd} MANDATORY_ARG --mandatory-option=VALUE --mandatory-option-with-default=VALUE\"\n"\
"Missing required options:\n --mandatory-option=VALUE # REQUIRED Mandatory option\n"
)
else
expect(stderr).to eq(
"ERROR: \"#{cmd}\" was called with arguments [\"first_arg\"] and options {option_one: \"test2\"}\n" \
"Usage: \"#{cmd} MANDATORY_ARG --mandatory-option=VALUE --mandatory-option-with-default=VALUE\"\n"\
"Missing required options:\n --mandatory-option=VALUE # REQUIRED Mandatory option\n"
)
end
end

it "with option_one" do
output = `baz first_arg --option-one=test2`
output = `baz first_arg --option-one=test2 --mandatory-option=required`

if RUBY_VERSION < "3.4"
expect(output).to eq(
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
"Options: {:option_with_default=>\"test\", :option_one=>\"test2\"}\n"
"mandatory_option: required. " \
"Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \
":option_one=>\"test2\", :mandatory_option=>\"required\"}\n"
)
else
expect(output).to eq(
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
"Options: {option_with_default: \"test\", option_one: \"test2\"}\n"
"mandatory_option: required. " \
"Options: {option_with_default: \"test\", mandatory_option_with_default: \"mandatory default\", " \
"option_one: \"test2\", mandatory_option: \"required\"}\n"
)
end
end

it "with combination of aliases" do
output = `baz first_arg -bd test3`
output = `baz first_arg -bd test3 --mandatory-option=required`

if RUBY_VERSION < "3.4"
expect(output).to eq(
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
"Options: {:option_with_default=>\"test3\", :boolean_option=>true}\n"
"mandatory_option: required. " \
"Options: {:option_with_default=>\"test3\", :mandatory_option_with_default=>\"mandatory default\", " \
":boolean_option=>true, :mandatory_option=>\"required\"}\n"
)
else
expect(output).to eq(
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
"Options: {option_with_default: \"test3\", boolean_option: true}\n"
"mandatory_option: required. " \
"Options: {option_with_default: \"test3\", mandatory_option_with_default: \"mandatory default\", " \
"boolean_option: true, mandatory_option: \"required\"}\n"
)
end
end
Expand Down
3 changes: 3 additions & 0 deletions spec/support/fixtures/baz_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ class CLI < Dry::CLI::Command
option :option_one, aliases: %w[1], desc: "Option one"
option :boolean_option, aliases: %w[b], desc: "Option boolean", type: :boolean
option :option_with_default, aliases: %w[d], desc: "Option default", default: "test"
option :mandatory_option, desc: "Mandatory option", required: true
option :mandatory_option_with_default, desc: "Mandatory option", required: true, default: "mandatory default"

def call(mandatory_arg:, optional_arg: "optional_arg", **options)
puts "mandatory_arg: #{mandatory_arg}. " \
"optional_arg: #{optional_arg}. " \
"mandatory_option: #{options[:mandatory_option]}. "\
"Options: #{options.inspect}"
end
end
Expand Down
Loading