diff --git a/Gemfile b/Gemfile index e653285..116f528 100644 --- a/Gemfile +++ b/Gemfile @@ -9,5 +9,5 @@ gemspec gem "backports", "~> 3.15.0", require: false unless ENV["CI"] - gem "yard", require: false + gem "yard", require: false end diff --git a/lib/dry/cli.rb b/lib/dry/cli.rb index d0b8c13..5f93860 100644 --- a/lib/dry/cli.rb +++ b/lib/dry/cli.rb @@ -14,6 +14,7 @@ class CLI require "dry/cli/registry" require "dry/cli/parser" require "dry/cli/usage" + require "dry/cli/spell_checker" require "dry/cli/banner" require "dry/cli/inflector" @@ -108,7 +109,7 @@ def perform_command(arguments) # @api private def perform_registry(arguments) result = registry.get(arguments) - return usage(result) unless result.found? + return spell_checker(result, arguments) unless result.found? command, args = parse(result.command, result.arguments, result.names) @@ -161,9 +162,11 @@ def error(result) exit(1) end - # @since 0.1.0 - # @api private - def usage(result) + # @since 1.1.1 + def spell_checker(result, arguments) + spell_checker = SpellChecker.call(result, arguments) + err.puts spell_checker if spell_checker + puts err.puts Usage.call(result) exit(1) end diff --git a/lib/dry/cli/spell_checker.rb b/lib/dry/cli/spell_checker.rb new file mode 100644 index 0000000..8080d5c --- /dev/null +++ b/lib/dry/cli/spell_checker.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "dry/cli/program_name" +require "did_you_mean" + +module Dry + class CLI + # Command(s) usage + # + # @since 1.1.1 + # @api private + module SpellChecker + # @since 1.1.1 + # @api private + def self.call(result, arguments) + commands = result.children.keys + cmd = cmd_to_spell(arguments, result.names) + + suggestions = DidYouMean::SpellChecker.new(dictionary: commands).correct(cmd.first) + if suggestions.any? + "I don't know how to '#{cmd.join(" ")}'. Did you mean: '#{suggestions.first}' ?" + end + end + + # @since 1.1.1 + # @api private + def self.cmd_to_spell(arguments, result_names) + arguments - result_names + end + + # @since 1.1.1 + # @api private + def self.ignore?(cmd) + cmd.empty? || cmd.first.start_with?("-") + end + end + end +end diff --git a/spec/integration/inherited_commands_spec.rb b/spec/integration/inherited_commands_spec.rb index c872df8..9ed9621 100644 --- a/spec/integration/inherited_commands_spec.rb +++ b/spec/integration/inherited_commands_spec.rb @@ -10,6 +10,7 @@ based logs APP # Display recent log output based run APP CMD # Run a one-off process inside your app based subrun APP CMD + OUT expect(output).to eq(expected) end diff --git a/spec/integration/spell_checker_spec.rb b/spec/integration/spell_checker_spec.rb new file mode 100644 index 0000000..30ab94a --- /dev/null +++ b/spec/integration/spell_checker_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "open3" + +RSpec.describe "Spell checker" do + it "print similar command when there is a command with a typo" do + _, stderr, = Open3.capture3("foo routs") + + expected = <<~DESC + I don't know how to 'routs'. Did you mean: 'routes' ? + Commands: + foo assets [SUBCOMMAND] + foo callbacks DIR # Command with callbacks + foo console # Starts Foo console + foo db [SUBCOMMAND] + foo destroy [SUBCOMMAND] + foo exec TASK [DIRS] # Execute a task + foo generate [SUBCOMMAND] + foo greeting [RESPONSE] + foo hello # Print a greeting + foo new PROJECT # Generate a new Foo project + foo root-command [ARGUMENT|SUBCOMMAND] # Root command with arguments and subcommands + foo routes # Print routes + foo server # Start Foo server (only for development) + foo sub [SUBCOMMAND] + foo variadic [SUBCOMMAND] + foo version # Print Foo version + DESC + + expect(stderr).to eq(expected) + end + + it "handles typos in subcommands" do + _, stderr, = Open3.capture3("foo sub comand") + + expected = <<~DESC + I don't know how to 'comand'. Did you mean: 'command' ? + Commands: + foo sub command # Override a subcommand + DESC + + expect(stderr).to eq(expected) + end +end