Skip to content

Commit d9d97f5

Browse files
committed
Implement option required: true
- Adds "# REQUIRED" to option description when required - Check is required options is passed - Refactor Usage and Banner option to reuse in short usage when erroring
1 parent 005edf6 commit d9d97f5

File tree

8 files changed

+179
-89
lines changed

8 files changed

+179
-89
lines changed

lib/dry/cli/banner.rb

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,24 +101,35 @@ def self.extended_command_arguments(command)
101101
end.join("\n")
102102
end
103103

104+
# @since 0.8.0
105+
# @api private
106+
def self.simple_option(option)
107+
name = Inflector.dasherize(option.name)
108+
name = if option.boolean?
109+
"[no-]#{name}"
110+
elsif option.array?
111+
"#{name}=VALUE1,VALUE2,.."
112+
else
113+
"#{name}=VALUE"
114+
end
115+
name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any?
116+
"--#{name}"
117+
end
118+
119+
# @since 0.8.0
120+
# @api private
121+
def self.extended_option(option)
122+
name = " #{simple_option(option).ljust(32)} # #{"REQUIRED " if option.required?}#{option.desc}" # rubocop:disable Metrics/LineLength
123+
name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
124+
name
125+
end
126+
104127
# @since 0.1.0
105128
# @api private
106129
#
107130
def self.extended_command_options(command)
108131
result = command.options.map do |option|
109-
name = Inflector.dasherize(option.name)
110-
name = if option.boolean?
111-
"[no-]#{name}"
112-
elsif option.array?
113-
"#{name}=VALUE1,VALUE2,.."
114-
else
115-
"#{name}=VALUE"
116-
end
117-
name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any?
118-
name = " --#{name.ljust(30)}"
119-
name = "#{name} # #{option.desc}"
120-
name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
121-
name
132+
extended_option(option)
122133
end
123134

124135
result << " --#{"help, -h".ljust(30)} # Print this help"

lib/dry/cli/command.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ def self.optional_arguments
344344
arguments.reject(&:required?)
345345
end
346346

347+
# @since 0.8.0
348+
# @api private
349+
def self.required_options
350+
options.select(&:required?)
351+
end
352+
347353
# @since 0.7.0
348354
# @api private
349355
def self.subcommands
@@ -373,14 +379,15 @@ def self.superclass_options
373379
extend Forwardable
374380

375381
delegate %i[
382+
arguments
383+
default_params
376384
description
377385
examples
378-
arguments
386+
optional_arguments
379387
options
380388
params
381-
default_params
382389
required_arguments
383-
optional_arguments
390+
required_options
384391
subcommands
385392
] => "self.class"
386393
end

lib/dry/cli/parser.rb

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,52 +29,65 @@ def self.call(command, arguments, prog_name)
2929
end
3030
end.parse!(arguments)
3131

32-
parsed_options = command.default_params.merge(parsed_options)
3332
parse_required_params(command, arguments, prog_name, parsed_options)
3433
rescue ::OptionParser::ParseError
35-
Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"") # rubocop:disable Metrics/LineLength
34+
Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"") # rubocop:disable Layout/LineLength
3635
end
3736

3837
# @since 0.1.0
3938
# @api private
4039
#
4140
# rubocop:disable Metrics/AbcSize
4241
def self.parse_required_params(command, arguments, prog_name, parsed_options)
43-
parsed_params = match_arguments(command.arguments, arguments)
42+
parsed_params = match_arguments(command.arguments, arguments)
4443
parsed_required_params = match_arguments(command.required_arguments, arguments)
45-
all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } # rubocop:disable Metrics/LineLength
44+
parsed_options_with_defaults = command.default_params.merge(parsed_options)
45+
46+
all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } && # rubocop:disable Layout/LineLength
47+
command.required_options.all? { |option| !parsed_options_with_defaults[option.name].nil? } # rubocop:disable Layout/LineLength
4648

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

4951
unless all_required_params_satisfied
50-
parsed_required_params_values = parsed_required_params.values.compact
51-
52-
usage = "\nUsage: \"#{prog_name} #{command.required_arguments.map(&:description_name).join(" ")}" # rubocop:disable Metrics/LineLength
53-
54-
usage += " | #{prog_name} SUBCOMMAND" if command.subcommands.any?
55-
56-
usage += '"'
57-
58-
if parsed_required_params_values.empty?
59-
return Result.failure("ERROR: \"#{prog_name}\" was called with no arguments#{usage}")
60-
else
61-
return Result.failure("ERROR: \"#{prog_name}\" was called with arguments #{parsed_required_params_values}#{usage}") # rubocop:disable Metrics/LineLength
62-
end
52+
return error_message(command, prog_name, parsed_required_params, parsed_options)
6353
end
6454

6555
parsed_params.reject! { |_key, value| value.nil? }
66-
parsed_options = parsed_options.merge(parsed_params)
56+
parsed_options = parsed_options_with_defaults.merge(parsed_params)
6757
parsed_options = parsed_options.merge(args: unused_arguments) if unused_arguments.any?
6858
Result.success(parsed_options)
6959
end
7060
# rubocop:enable Metrics/AbcSize
7161

62+
def self.short_usage(command, prog_name)
63+
usage = "\nUsage: \"#{prog_name} #{command.required_arguments.map(&:description_name).join(" ")}" # rubocop:disable Layout/LineLength
64+
usage += " | #{prog_name} SUBCOMMAND" if command.subcommands.any?
65+
usage += " #{command.required_options.map { |opt| Banner.simple_option(opt) }.join(" ")}" if command.required_options.any? # rubocop:disable Layout/LineLength
66+
usage += '"'
67+
usage
68+
end
69+
70+
def self.error_message(command, prog_name, parsed_required_params, parsed_options)
71+
parsed_required_params_values = parsed_required_params.values.compact
72+
73+
error_msg = "ERROR: \"#{prog_name}\" was called with "
74+
error_msg += if parsed_required_params_values.empty?
75+
"no arguments"
76+
else
77+
"arguments #{parsed_required_params_values}"
78+
end
79+
error_msg += " and options #{parsed_options}" if parsed_options.any?
80+
error_msg += short_usage(command, prog_name)
81+
82+
Result.failure(error_msg)
83+
end
84+
7285
def self.match_arguments(command_arguments, arguments)
7386
result = {}
7487

7588
command_arguments.each_with_index do |cmd_arg, index|
7689
if cmd_arg.array?
77-
result[cmd_arg.name] = arguments[index..-1]
90+
result[cmd_arg.name] = arguments[index..]
7891
break
7992
else
8093
result[cmd_arg.name] = arguments.at(index)

spec/integration/single_command_spec.rb

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
it "shows usage" do
1010
_, stderr, = Open3.capture3("baz")
1111
expect(stderr).to eq(
12-
"ERROR: \"#{cmd}\" was called with no arguments\nUsage: \"#{cmd} MANDATORY_ARG\"\n"
12+
"ERROR: \"#{cmd}\" was called with no arguments\n"\
13+
"Usage: \"#{cmd} MANDATORY_ARG --mandatory-option=VALUE --mandatory-option-with-default=VALUE\"\n"
1314
)
1415
end
1516

@@ -33,49 +34,88 @@
3334
--option-one=VALUE, -1 VALUE # Option one
3435
--[no-]boolean-option, -b # Option boolean
3536
--option-with-default=VALUE, -d VALUE # Option default, default: "test"
37+
--mandatory-option=VALUE # REQUIRED Mandatory option
38+
--mandatory-option-with-default=VALUE # REQUIRED Mandatory option, default: "mandatory default"
3639
--help, -h # Print this help
3740
OUTPUT
3841
expect(output).to eq(expected_output)
3942
end
4043

41-
it "with option_one" do
42-
output = `baz first_arg --option-one=test2`
43-
expect(output).to eq(
44-
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
45-
"Options: {:option_with_default=>\"test\", :option_one=>\"test2\"}\n"
46-
)
44+
context "with mandatory arg and non-required option" do
45+
it "errors out and shows usage" do
46+
_, stderr, = Open3.capture3("baz first_arg --option_one=test2")
47+
expect(stderr).to eq(
48+
"ERROR: \"#{cmd}\" was called with arguments [\"first_arg\"] and options {:option_one=>\"test2\"}\n" \
49+
"Usage: \"#{cmd} MANDATORY_ARG --mandatory-option=VALUE --mandatory-option-with-default=VALUE\"\n"
50+
)
51+
end
4752
end
4853

49-
it "with combination of aliases" do
50-
output = `baz first_arg -bd test3`
51-
expect(output).to eq(
52-
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
53-
"Options: {:option_with_default=>\"test3\", :boolean_option=>true}\n"
54-
)
54+
context "with mandatory arg and mandatory_option" do
55+
it "works" do
56+
output = `baz first_arg --mandatory-option=test1`
57+
expect(output).to eq(
58+
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
59+
"mandatory_option: test1. " \
60+
"Options: {:option_with_default=>\"test\", " \
61+
":mandatory_option_with_default=>\"mandatory default\", " \
62+
":mandatory_option=>\"test1\"}\n"
63+
)
64+
end
65+
end
66+
67+
context "with mandatory arg, option_one and mandatory_option" do
68+
it "works" do
69+
output = `baz first_arg --mandatory-option=test1 --option_one=test2`
70+
expect(output).to eq(
71+
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
72+
"mandatory_option: test1. " \
73+
"Options: {:option_with_default=>\"test\", " \
74+
":mandatory_option_with_default=>\"mandatory default\", " \
75+
":mandatory_option=>\"test1\", :option_one=>\"test2\"}\n"
76+
)
77+
end
78+
end
79+
80+
context "with combination of aliases" do
81+
it "works" do
82+
output = `baz first_arg --mandatory-option test1 -bd test3`
83+
expect(output).to eq(
84+
"mandatory_arg: first_arg. optional_arg: optional_arg. " \
85+
"mandatory_option: test1. " \
86+
"Options: {:option_with_default=>\"test3\", " \
87+
":mandatory_option_with_default=>\"mandatory default\", " \
88+
":mandatory_option=>\"test1\", :boolean_option=>true}\n"
89+
)
90+
end
5591
end
5692
end
5793

5894
context "root command with arguments and subcommands" do
59-
it "with arguments" do
60-
output = `foo root-command "hello world"`
95+
context "with arguments" do
96+
it "works" do
97+
output = `foo root-command "hello world"`
6198

62-
expected = <<~DESC
63-
I'm a root-command argument:hello world
64-
I'm a root-command option:
65-
DESC
99+
expected = <<~DESC
100+
I'm a root-command argument:hello world
101+
I'm a root-command option:
102+
DESC
66103

67-
expect(output).to eq(expected)
104+
expect(output).to eq(expected)
105+
end
68106
end
69107

70-
it "with options" do
71-
output = `foo root-command "hello world" --root-command-option="bye world"`
108+
context "with options" do
109+
it "works" do
110+
output = `foo root-command "hello world" --root-command-option="bye world"`
72111

73-
expected = <<~DESC
74-
I'm a root-command argument:hello world
75-
I'm a root-command option:bye world
76-
DESC
112+
expected = <<~DESC
113+
I'm a root-command argument:hello world
114+
I'm a root-command option:bye world
115+
DESC
77116

78-
expect(output).to eq(expected)
117+
expect(output).to eq(expected)
118+
end
79119
end
80120
end
81121
end

spec/support/fixtures/baz_command.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ class CLI < Dry::CLI::Command
99
option :option_one, aliases: %w[1], desc: "Option one"
1010
option :boolean_option, aliases: %w[b], desc: "Option boolean", type: :boolean
1111
option :option_with_default, aliases: %w[d], desc: "Option default", default: "test"
12+
option :mandatory_option, desc: "Mandatory option", required: true
13+
option :mandatory_option_with_default, desc: "Mandatory option", required: true, default: "mandatory default"
1214

1315
def call(mandatory_arg:, optional_arg: "optional_arg", **options)
1416
puts "mandatory_arg: #{mandatory_arg}. " \
1517
"optional_arg: #{optional_arg}. " \
18+
"mandatory_option: #{options[:mandatory_option]}. "\
1619
"Options: #{options.inspect}"
1720
end
1821
end

spec/support/shared_examples/commands.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
# rubocop:disable Metrics/LineLength
43
RSpec.shared_examples "Commands" do |cli|
54
let(:cli) { cli }
65

@@ -96,7 +95,7 @@
9695
context "and with an unknown value passed" do
9796
it "prints error" do
9897
error = capture_error { cli.call(arguments: %w[console --engine=unknown]) }
99-
expect(error).to eq("ERROR: \"rspec console\" was called with arguments \"--engine=unknown\"\n") # rubocop:disable Metrics/LineLength
98+
expect(error).to eq("ERROR: \"rspec console\" was called with arguments \"--engine=unknown\"\n")
10099
end
101100
end
102101
end
@@ -145,7 +144,7 @@
145144

146145
it "with unknown param" do
147146
error = capture_error { cli.call(arguments: %w[new bookshelf --unknown 1234]) }
148-
expect(error).to eq("ERROR: \"rspec new\" was called with arguments \"bookshelf --unknown 1234\"\n") # rubocop:disable Metrics/LineLength
147+
expect(error).to eq("ERROR: \"rspec new\" was called with arguments \"bookshelf --unknown 1234\"\n")
149148
end
150149

151150
it "no required" do
@@ -297,4 +296,3 @@
297296
end
298297
end
299298
end
300-
# rubocop:enable Metrics/LineLength

spec/support/shared_examples/subcommands.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,17 @@
8787
end
8888

8989
it "more than one param and with optional params" do
90-
output = capture_output { cli.call(arguments: %w[generate action web users#index --url=/signin]) } # rubocop:disable Metrics/LineLength
91-
expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>false, :url=>\"/signin\"}\n") # rubocop:disable Metrics/LineLength
90+
output = capture_output { cli.call(arguments: %w[generate action web users#index --url=/signin]) }
91+
expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>false, :url=>\"/signin\"}\n")
9292
end
9393

9494
it "more than one param and with boolean params" do
95-
output = capture_output { cli.call(arguments: %w[generate action web users#index --skip-view --url=/signin]) } # rubocop:disable Metrics/LineLength
96-
expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>true, :url=>\"/signin\"}\n") # rubocop:disable Metrics/LineLength
95+
output = capture_output { cli.call(arguments: %w[generate action web users#index --skip-view --url=/signin]) }
96+
expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>true, :url=>\"/signin\"}\n")
9797
end
9898

9999
it "more than required params" do
100-
output = capture_output { cli.call(arguments: %w[destroy action web users#index unexpected_param]) } # rubocop:disable Metrics/LineLength
100+
output = capture_output { cli.call(arguments: %w[destroy action web users#index unexpected_param]) }
101101
expect(output).to eq("destroy action - app: web, action: users#index\n")
102102
end
103103

0 commit comments

Comments
 (0)