lib/rails/command/base.rb



# frozen_string_literal: true

require "thor"
require "erb"

require "active_support/core_ext/string/filters"
require "active_support/core_ext/string/inflections"

require "rails/command/actions"

module Rails
  module Command
    class Base < Thor
      class Error < Thor::Error # :nodoc:
      end

      class CorrectableError < Error # :nodoc:
        attr_reader :key, :options

        def initialize(message, key, options)
          @key     = key
          @options = options
          super(message)
        end

        if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable)
          include DidYouMean::Correctable

          def corrections
            @corrections ||= DidYouMean::SpellChecker.new(dictionary: options).correct(key)
          end
        end
      end

      include Actions

      class << self
        def exit_on_failure? # :nodoc:
          false
        end

        # Returns true when the app is a Rails engine.
        def engine?
          defined?(ENGINE_ROOT)
        end

        # Tries to get the description from a USAGE file one folder above the command
        # root.
        def desc(usage = nil, description = nil, options = {})
          if usage
            super
          else
            @desc ||= ERB.new(File.read(usage_path), trim_mode: "-").result(binding) if usage_path
          end
        end

        # Convenience method to get the namespace from the class name. It's the
        # same as Thor default except that the Command at the end of the class
        # is removed.
        def namespace(name = nil)
          if name
            super
          else
            @namespace ||= super.chomp("_command").sub(/:command:/, ":")
          end
        end

        # Convenience method to hide this command from the available ones when
        # running rails command.
        def hide_command!
          Rails::Command.hidden_commands << self
        end

        def inherited(base) # :nodoc:
          super

          if base.name && !base.name.end_with?("Base")
            Rails::Command.subclasses << base
          end
        end

        def perform(command, args, config) # :nodoc:
          if Rails::Command::HELP_MAPPINGS.include?(args.first)
            command, args = "help", []
          end

          dispatch(command, args.dup, nil, config)
        end

        def printing_commands
          namespaced_commands
        end

        def executable
          "rails #{command_name}"
        end

        # Use Rails' default banner.
        def banner(*)
          "#{executable} #{arguments.map(&:usage).join(' ')} [options]".squish
        end

        # Sets the base_name taking into account the current class namespace.
        #
        #   Rails::Command::TestCommand.base_name # => 'rails'
        def base_name
          @base_name ||= if base = name.to_s.split("::").first
            base.underscore
          end
        end

        # Return command name without namespaces.
        #
        #   Rails::Command::TestCommand.command_name # => 'test'
        def command_name
          @command_name ||= if command = name.to_s.split("::").last
            command.chomp!("Command")
            command.underscore
          end
        end

        # Path to lookup a USAGE description in a file.
        def usage_path
          if default_command_root
            path = File.join(default_command_root, "USAGE")
            path if File.exist?(path)
          end
        end

        # Default file root to place extra files a command might need, placed
        # one folder above the command file.
        #
        # For a Rails::Command::TestCommand placed in <tt>rails/command/test_command.rb</tt>
        # would return <tt>rails/test</tt>.
        def default_command_root
          path = File.expand_path(relative_command_path, __dir__)
          path if File.exist?(path)
        end

        private
          # Allow the command method to be called perform.
          def create_command(meth)
            if meth == "perform"
              alias_method command_name, meth
            else
              # Prevent exception about command without usage.
              # Some commands define their documentation differently.
              @usage ||= ""
              @desc  ||= ""

              super
            end
          end

          def command_root_namespace
            (namespace.split(":") - %w(rails)).join(":")
          end

          def relative_command_path
            File.join("../commands", *command_root_namespace.split(":"))
          end

          def namespaced_commands
            commands.keys.map do |key|
              if command_root_namespace.match?(/(\A|:)#{key}\z/)
                command_root_namespace
              else
                "#{command_root_namespace}:#{key}"
              end
            end
          end
      end

      def help
        if command_name = self.class.command_name
          self.class.command_help(shell, command_name)
        else
          super
        end
      end
    end
  end
end