From 4eba166dfe42aae5338101a859f14b515c28f0a6 Mon Sep 17 00:00:00 2001 From: Ronan Potage Date: Wed, 12 Oct 2022 13:25:13 -0700 Subject: [PATCH 1/6] 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 --- lib/dry/cli/banner.rb | 37 +++++--- lib/dry/cli/command.rb | 13 ++- lib/dry/cli/parser.rb | 51 ++++++----- spec/integration/single_command_spec.rb | 94 +++++++++++++++------ spec/support/fixtures/baz_command.rb | 3 + spec/support/shared_examples/commands.rb | 6 +- spec/support/shared_examples/subcommands.rb | 10 +-- spec/unit/dry/cli/cli_spec.rb | 54 ++++++++---- 8 files changed, 179 insertions(+), 89 deletions(-) diff --git a/lib/dry/cli/banner.rb b/lib/dry/cli/banner.rb index a70a89a..8f8f172 100644 --- a/lib/dry/cli/banner.rb +++ b/lib/dry/cli/banner.rb @@ -101,24 +101,35 @@ def self.extended_command_arguments(command) end.join("\n") end + # @since 0.8.0 + # @api private + def self.simple_option(option) + name = Inflector.dasherize(option.name) + name = if option.boolean? + "[no-]#{name}" + elsif option.array? + "#{name}=VALUE1,VALUE2,.." + else + "#{name}=VALUE" + end + name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any? + "--#{name}" + end + + # @since 0.8.0 + # @api private + def self.extended_option(option) + name = " #{simple_option(option).ljust(32)} # #{"REQUIRED " if option.required?}#{option.desc}" # rubocop:disable Metrics/LineLength + 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.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" diff --git a/lib/dry/cli/command.rb b/lib/dry/cli/command.rb index 4fbbaf6..cc9340e 100644 --- a/lib/dry/cli/command.rb +++ b/lib/dry/cli/command.rb @@ -344,6 +344,12 @@ def self.optional_arguments arguments.reject(&:required?) end + # @since 0.8.0 + # @api private + def self.required_options + options.select(&:required?) + end + # @since 0.7.0 # @api private def self.subcommands @@ -373,14 +379,15 @@ def self.superclass_options extend Forwardable delegate %i[ + arguments + default_params description examples - arguments + optional_arguments options params - default_params required_arguments - optional_arguments + required_options subcommands ] => "self.class" end diff --git a/lib/dry/cli/parser.rb b/lib/dry/cli/parser.rb index b7801bd..8080893 100644 --- a/lib/dry/cli/parser.rb +++ b/lib/dry/cli/parser.rb @@ -29,10 +29,9 @@ 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 - Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"") # rubocop:disable Metrics/LineLength + Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"") # rubocop:disable Layout/LineLength end # @since 0.1.0 @@ -40,41 +39,55 @@ def self.call(command, arguments, prog_name) # # rubocop:disable Metrics/AbcSize def self.parse_required_params(command, arguments, prog_name, parsed_options) - parsed_params = match_arguments(command.arguments, arguments) + parsed_params = match_arguments(command.arguments, arguments) parsed_required_params = match_arguments(command.required_arguments, arguments) - all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } # rubocop:disable Metrics/LineLength + parsed_options_with_defaults = command.default_params.merge(parsed_options) + + all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } && # rubocop:disable Layout/LineLength + command.required_options.all? { |option| !parsed_options_with_defaults[option.name].nil? } # rubocop:disable Layout/LineLength 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(" ")}" # rubocop:disable Metrics/LineLength - - 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}") # rubocop:disable Metrics/LineLength - end + return error_message(command, prog_name, parsed_required_params, parsed_options) 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 + def self.short_usage(command, prog_name) + usage = "\nUsage: \"#{prog_name} #{command.required_arguments.map(&:description_name).join(" ")}" # rubocop:disable Layout/LineLength + usage += " | #{prog_name} SUBCOMMAND" if command.subcommands.any? + usage += " #{command.required_options.map { |opt| Banner.simple_option(opt) }.join(" ")}" if command.required_options.any? # rubocop:disable Layout/LineLength + usage += '"' + usage + end + + def self.error_message(command, prog_name, parsed_required_params, parsed_options) + parsed_required_params_values = parsed_required_params.values.compact + + 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) + + Result.failure(error_msg) + end + def self.match_arguments(command_arguments, arguments) result = {} command_arguments.each_with_index do |cmd_arg, index| if cmd_arg.array? - result[cmd_arg.name] = arguments[index..-1] + result[cmd_arg.name] = arguments[index..] break else result[cmd_arg.name] = arguments.at(index) diff --git a/spec/integration/single_command_spec.rb b/spec/integration/single_command_spec.rb index c55fa5f..525ef47 100644 --- a/spec/integration/single_command_spec.rb +++ b/spec/integration/single_command_spec.rb @@ -9,7 +9,8 @@ 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" ) end @@ -33,49 +34,88 @@ --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 option_one" do - output = `baz first_arg --option-one=test2` - expect(output).to eq( - "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test\", :option_one=>\"test2\"}\n" - ) + context "with mandatory arg and non-required option" do + it "errors out and shows usage" do + _, stderr, = Open3.capture3("baz first_arg --option_one=test2") + 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" + ) + end end - it "with combination of aliases" do - output = `baz first_arg -bd test3` - expect(output).to eq( - "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test3\", :boolean_option=>true}\n" - ) + context "with mandatory arg and mandatory_option" do + it "works" do + output = `baz first_arg --mandatory-option=test1` + expect(output).to eq( + "mandatory_arg: first_arg. optional_arg: optional_arg. " \ + "mandatory_option: test1. " \ + "Options: {:option_with_default=>\"test\", " \ + ":mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"test1\"}\n" + ) + end + end + + context "with mandatory arg, option_one and mandatory_option" do + it "works" do + output = `baz first_arg --mandatory-option=test1 --option_one=test2` + expect(output).to eq( + "mandatory_arg: first_arg. optional_arg: optional_arg. " \ + "mandatory_option: test1. " \ + "Options: {:option_with_default=>\"test\", " \ + ":mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"test1\", :option_one=>\"test2\"}\n" + ) + end + end + + context "with combination of aliases" do + it "works" do + output = `baz first_arg --mandatory-option test1 -bd test3` + expect(output).to eq( + "mandatory_arg: first_arg. optional_arg: optional_arg. " \ + "mandatory_option: test1. " \ + "Options: {:option_with_default=>\"test3\", " \ + ":mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"test1\", :boolean_option=>true}\n" + ) + end end end context "root command with arguments and subcommands" do - it "with arguments" do - output = `foo root-command "hello world"` + context "with arguments" do + it "works" do + output = `foo root-command "hello world"` - expected = <<~DESC - I'm a root-command argument:hello world - I'm a root-command option: - DESC + expected = <<~DESC + I'm a root-command argument:hello world + I'm a root-command option: + DESC - expect(output).to eq(expected) + expect(output).to eq(expected) + end end - it "with options" do - output = `foo root-command "hello world" --root-command-option="bye world"` + context "with options" do + it "works" do + output = `foo root-command "hello world" --root-command-option="bye world"` - expected = <<~DESC - I'm a root-command argument:hello world - I'm a root-command option:bye world - DESC + expected = <<~DESC + I'm a root-command argument:hello world + I'm a root-command option:bye world + DESC - expect(output).to eq(expected) + expect(output).to eq(expected) + end end end end diff --git a/spec/support/fixtures/baz_command.rb b/spec/support/fixtures/baz_command.rb index bc1923f..aa67e3b 100644 --- a/spec/support/fixtures/baz_command.rb +++ b/spec/support/fixtures/baz_command.rb @@ -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 diff --git a/spec/support/shared_examples/commands.rb b/spec/support/shared_examples/commands.rb index 67a99b2..9a90d9a 100644 --- a/spec/support/shared_examples/commands.rb +++ b/spec/support/shared_examples/commands.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Metrics/LineLength RSpec.shared_examples "Commands" do |cli| let(:cli) { cli } @@ -96,7 +95,7 @@ context "and with an unknown value passed" do it "prints error" do error = capture_error { cli.call(arguments: %w[console --engine=unknown]) } - expect(error).to eq("ERROR: \"rspec console\" was called with arguments \"--engine=unknown\"\n") # rubocop:disable Metrics/LineLength + expect(error).to eq("ERROR: \"rspec console\" was called with arguments \"--engine=unknown\"\n") end end end @@ -145,7 +144,7 @@ it "with unknown param" do error = capture_error { cli.call(arguments: %w[new bookshelf --unknown 1234]) } - expect(error).to eq("ERROR: \"rspec new\" was called with arguments \"bookshelf --unknown 1234\"\n") # rubocop:disable Metrics/LineLength + expect(error).to eq("ERROR: \"rspec new\" was called with arguments \"bookshelf --unknown 1234\"\n") end it "no required" do @@ -297,4 +296,3 @@ end end end -# rubocop:enable Metrics/LineLength diff --git a/spec/support/shared_examples/subcommands.rb b/spec/support/shared_examples/subcommands.rb index b0a0b7c..36aac8c 100644 --- a/spec/support/shared_examples/subcommands.rb +++ b/spec/support/shared_examples/subcommands.rb @@ -87,17 +87,17 @@ end it "more than one param and with optional params" do - output = capture_output { cli.call(arguments: %w[generate action web users#index --url=/signin]) } # rubocop:disable Metrics/LineLength - expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>false, :url=>\"/signin\"}\n") # rubocop:disable Metrics/LineLength + output = capture_output { cli.call(arguments: %w[generate action web users#index --url=/signin]) } + expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>false, :url=>\"/signin\"}\n") end it "more than one param and with boolean params" do - output = capture_output { cli.call(arguments: %w[generate action web users#index --skip-view --url=/signin]) } # rubocop:disable Metrics/LineLength - expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>true, :url=>\"/signin\"}\n") # rubocop:disable Metrics/LineLength + output = capture_output { cli.call(arguments: %w[generate action web users#index --skip-view --url=/signin]) } + expect(output).to eq("generate action - app: web, action: users#index, options: {:skip_view=>true, :url=>\"/signin\"}\n") end it "more than required params" do - output = capture_output { cli.call(arguments: %w[destroy action web users#index unexpected_param]) } # rubocop:disable Metrics/LineLength + output = capture_output { cli.call(arguments: %w[destroy action web users#index unexpected_param]) } expect(output).to eq("destroy action - app: web, action: users#index\n") end diff --git a/spec/unit/dry/cli/cli_spec.rb b/spec/unit/dry/cli/cli_spec.rb index e0cdbe4..4fb0b5f 100644 --- a/spec/unit/dry/cli/cli_spec.rb +++ b/spec/unit/dry/cli/cli_spec.rb @@ -51,72 +51,90 @@ --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 required_argument" do - output = capture_output { cli.call(arguments: ["first_arg"]) } + it "with mandatory_arg and mandatory_option" do + output = capture_output { cli.call(arguments: %w[first_arg --mandatory-option=mandatory_opt_val]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test\"}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\"}\n" ) end it "with optional_arg" do - output = capture_output { cli.call(arguments: %w[first_arg opt_arg]) } + output = capture_output { cli.call(arguments: %w[first_arg opt_arg --mandatory_option=mandatory_opt_val]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: opt_arg. " \ - "Options: {:option_with_default=>\"test\", :args=>[\"opt_arg\"]}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\", :args=>\[\"opt_arg\"]}\n" ) end it "with underscored option_one" do - output = capture_output { cli.call(arguments: %w[first_arg --option_one=test2]) } + output = capture_output { cli.call(arguments: %w[first_arg --mandatory_option=mandatory_opt_val --option_one=test2]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test\", :option_one=>\"test2\"}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\", :option_one=>\"test2\"}\n" ) end it "with option_one alias" do - output = capture_output { cli.call(arguments: %w[first_arg -1 test2]) } + output = capture_output { cli.call(arguments: %w[first_arg --mandatory-option=mandatory_opt_val -1 test2]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test\", :option_one=>\"test2\"}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\", :option_one=>\"test2\"}\n" ) end it "with underscored boolean_option" do - output = capture_output { cli.call(arguments: %w[first_arg --boolean_option]) } + output = capture_output { cli.call(arguments: %w[first_arg --mandatory_option=mandatory_opt_val --boolean_option]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test\", :boolean_option=>true}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\", :boolean_option=>true}\n" ) end it "with boolean_option alias" do - output = capture_output { cli.call(arguments: %w[first_arg -b]) } + output = capture_output { cli.call(arguments: %w[first_arg --mandatory_option=mandatory_opt_val -b]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test\", :boolean_option=>true}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\", :boolean_option=>true}\n" ) end - it "with underscoreed option_with_default alias" do - output = capture_output { cli.call(arguments: %w[first_arg --option_with_default=test3]) } + it "with underscored option_with_default alias" do + output = capture_output { cli.call(arguments: %w[first_arg --mandatory_option=mandatory_opt_val --option_with_default=test3]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test3\"}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test3\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\"}\n" ) end it "with combination of aliases" do - output = capture_output { cli.call(arguments: %w[first_arg -bd test3]) } + output = capture_output { cli.call(arguments: %w[first_arg --mandatory_option=mandatory_opt_val -bd test3]) } expect(output).to eq( "mandatory_arg: first_arg. optional_arg: optional_arg. " \ - "Options: {:option_with_default=>\"test3\", :boolean_option=>true}\n" + "mandatory_option: mandatory_opt_val. " \ + "Options: {:option_with_default=>\"test3\", :mandatory_option_with_default=>\"mandatory default\", " \ + ":mandatory_option=>\"mandatory_opt_val\", :boolean_option=>true}\n" ) end end From 8ed5b2f07d25d67785166902ba7f5877f48bc258 Mon Sep 17 00:00:00 2001 From: Ronan Potage Date: Wed, 12 Oct 2022 17:06:07 -0700 Subject: [PATCH 2/6] Fix version supported in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d49a510..4b2af9b 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ This library officially supports the following Ruby versions: -* MRI `>= 2.4.0` -* jruby `>= 9.4` (not tested on CI) +* MRI `>= 2.7.0` +* jruby `>= 9.3` (postponed until 2.7 is supported) ## License From ea6be2901206903bc7395d177ae6286c04dcdc73 Mon Sep 17 00:00:00 2001 From: Ronan Potage Date: Wed, 2 Aug 2023 13:28:45 -0700 Subject: [PATCH 3/6] Add missing required options to error message --- lib/dry/cli/parser.rb | 13 +++++++++++-- spec/integration/single_command_spec.rb | 6 ++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/dry/cli/parser.rb b/lib/dry/cli/parser.rb index 8080893..eb76731 100644 --- a/lib/dry/cli/parser.rb +++ b/lib/dry/cli/parser.rb @@ -49,7 +49,7 @@ def self.parse_required_params(command, arguments, prog_name, parsed_options) unused_arguments = arguments.drop(command.required_arguments.length) unless all_required_params_satisfied - return error_message(command, prog_name, parsed_required_params, parsed_options) + return error_message(command, prog_name, parsed_required_params, parsed_options, parsed_options_with_defaults) end parsed_params.reject! { |_key, value| value.nil? } @@ -67,9 +67,11 @@ def self.short_usage(command, prog_name) usage end - def self.error_message(command, prog_name, parsed_required_params, parsed_options) + 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" @@ -79,6 +81,13 @@ def self.error_message(command, prog_name, parsed_required_params, parsed_option 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 diff --git a/spec/integration/single_command_spec.rb b/spec/integration/single_command_spec.rb index 525ef47..f201126 100644 --- a/spec/integration/single_command_spec.rb +++ b/spec/integration/single_command_spec.rb @@ -10,7 +10,8 @@ _, stderr, = Open3.capture3("baz") expect(stderr).to eq( "ERROR: \"#{cmd}\" was called with no arguments\n"\ - "Usage: \"#{cmd} MANDATORY_ARG --mandatory-option=VALUE --mandatory-option-with-default=VALUE\"\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 @@ -46,7 +47,8 @@ _, stderr, = Open3.capture3("baz first_arg --option_one=test2") 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" + "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 From ce6c3f08d33f11a7fd094afecc372206b8a9887c Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sat, 21 Sep 2024 13:18:52 +1000 Subject: [PATCH 4/6] Use placeholder version for method docs --- lib/dry/cli/banner.rb | 4 ++-- lib/dry/cli/command.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/dry/cli/banner.rb b/lib/dry/cli/banner.rb index 8f8f172..9be5dd2 100644 --- a/lib/dry/cli/banner.rb +++ b/lib/dry/cli/banner.rb @@ -101,7 +101,7 @@ def self.extended_command_arguments(command) end.join("\n") end - # @since 0.8.0 + # @since x.x.x # @api private def self.simple_option(option) name = Inflector.dasherize(option.name) @@ -116,7 +116,7 @@ def self.simple_option(option) "--#{name}" end - # @since 0.8.0 + # @since x.x.x # @api private def self.extended_option(option) name = " #{simple_option(option).ljust(32)} # #{"REQUIRED " if option.required?}#{option.desc}" # rubocop:disable Metrics/LineLength diff --git a/lib/dry/cli/command.rb b/lib/dry/cli/command.rb index cc9340e..a932337 100644 --- a/lib/dry/cli/command.rb +++ b/lib/dry/cli/command.rb @@ -344,7 +344,7 @@ def self.optional_arguments arguments.reject(&:required?) end - # @since 0.8.0 + # @since x.x.x # @api private def self.required_options options.select(&:required?) From a5d188f5b1e7ff1d1568f2ac780d6d4844412d2d Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sat, 21 Sep 2024 13:19:08 +1000 Subject: [PATCH 5/6] Apply rubocop suggestion to merge attr reader and writer --- lib/dry/cli/command.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/dry/cli/command.rb b/lib/dry/cli/command.rb index a932337..388ae28 100644 --- a/lib/dry/cli/command.rb +++ b/lib/dry/cli/command.rb @@ -45,11 +45,10 @@ module ClassMethods # @since 0.7.0 # @api private - attr_reader :subcommands + attr_accessor :subcommands # @since 0.7.0 # @api private - attr_writer :subcommands end # Set the description of the command From 9b93931fdc1f44c70a2de50a8417ed2dd1c8a5c5 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sat, 21 Sep 2024 13:23:51 +1000 Subject: [PATCH 6/6] Reduce line width in a few places --- lib/dry/cli/parser.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/dry/cli/parser.rb b/lib/dry/cli/parser.rb index eb76731..941bcf7 100644 --- a/lib/dry/cli/parser.rb +++ b/lib/dry/cli/parser.rb @@ -43,13 +43,16 @@ def self.parse_required_params(command, arguments, prog_name, parsed_options) parsed_required_params = match_arguments(command.required_arguments, arguments) parsed_options_with_defaults = command.default_params.merge(parsed_options) - all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } && # rubocop:disable Layout/LineLength - command.required_options.all? { |option| !parsed_options_with_defaults[option.name].nil? } # rubocop:disable Layout/LineLength + 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 - return error_message(command, prog_name, parsed_required_params, parsed_options, parsed_options_with_defaults) + return error_message( + command, prog_name, parsed_required_params, parsed_options, parsed_options_with_defaults + ) end parsed_params.reject! { |_key, value| value.nil? } @@ -70,7 +73,9 @@ def self.short_usage(command, prog_name) 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? } + 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?