postmodern/command_kit.rb

View on GitHub
lib/command_kit/arguments.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require_relative 'arguments/argument'
require_relative 'usage'
require_relative 'main'
require_relative 'help'
require_relative 'printing'

module CommandKit
  #
  # Provides a thin DSL for defining arguments as attributes.
  #
  # ## Examples
  #
  #     include CommandKit::Arguments
  #     
  #     argument :output, desc: 'The output file'
  #     
  #     argument :input, desc: 'The input file(s)'
  #     
  #     def run(output,input)
  #     end
  #
  # ### Optional Arguments
  #
  #     argument :dir, required: false,
  #                    desc:     'Can be omitted'
  #
  #     def run(dir=nil)
  #     end
  #
  # ### Repeating Arguments
  #
  #     argument :files, repeats: true,
  #                      desc:    'Can be repeated one or more times'
  #
  #     def run(*files)
  #     end
  #
  # ### Optional Repeating Arguments
  #
  #     argument :files, required: true,
  #                      repeats:  true,
  #                      desc:     'Can be repeated one or more times'
  #
  #     def run(*files)
  #     end
  #
  # ### Multi-line descriptions
  #
  #     argument :arg, desc: [
  #                            'line1',
  #                            'line2',
  #                            '...'
  #                          ]
  #
  module Arguments
    include Usage
    include Main
    include Help
    include Printing

    #
    # @api private
    #
    module ModuleMethods
      #
      # Extends {ClassMethods} or {ModuleMethods}, depending on whether
      # {Arguments} is being included into a class or module.
      #
      # @param [Class, Module] context
      #   The class or module which is including {Arguments}.
      #
      def included(context)
        super(context)

        if context.class == Module
          context.extend ModuleMethods
        else
          context.extend ClassMethods
        end
      end
    end

    extend ModuleMethods

    #
    # Defines class-level methods.
    #
    module ClassMethods
      #
      # All defined arguments for the class.
      #
      # @return [Hash{Symbol => Argument}]
      #   The defined argument for the class and it's superclass.
      #
      # @api semipublic
      #
      def arguments
        @arguments ||= if superclass.kind_of?(ClassMethods)
                         superclass.arguments.dup
                       else
                         {}
                       end
      end

      #
      # Defines an argument for the class.
      #
      # @param [Symbol] name
      #   The name of the argument.
      #
      # @param [Hash{Symbol => Object}] kwargs
      #   Keyword arguments.
      #
      # @option kwargs [String, nil] usage
      #   The usage string for the argument. Defaults to the argument's name.
      #
      # @option kwargs [Boolean] required
      #   Specifies whether the argument is required or optional.
      #
      # @option kwargs [Boolean] repeats
      #   Specifies whether the argument can be repeated multiple times.
      #
      # @option kwargs [String, Array<String>] desc
      #   The description for the argument.
      #
      # @return [Argument]
      #   The newly defined argument.
      #
      # @example Define an argument:
      #     argument :bar, desc: "Bar argument"
      #
      # @example Defines an argument with a multi-line description:
      #     argument :bar, desc: [
      #                            "Line 1 ...",
      #                            "Line 2 ..."
      #                          ]
      #
      # @example With a custom usage string:
      #     argument :bar, usage: 'BAR',
      #                    desc: "Bar argument"
      #
      # @example With a custom type:
      #     argument :bar, desc: "Bar argument"
      #
      # @example With a default value:
      #     argument :bar, default: "bar.txt",
      #                    desc: "Bar argument"
      #
      # @example An optional argument:
      #     argument :bar, required: true,
      #                    desc: "Bar argument"
      #
      # @example A repeating argument:
      #     argument :bar, repeats: true,
      #                    desc: "Bar argument"
      #
      # @api public
      #
      def argument(name,**kwargs)
        arguments[name] = Argument.new(name,**kwargs)
      end
    end

    #
    # Checks the minimum/maximum number of arguments, then calls the
    # superclass'es `#main`.
    #
    # @param [Array<String>] argv
    #   The arguments passed to the program.
    #
    # @return [Integer]
    #   The exit status code. If too few or too many arguments are given, then
    #   an error message is printed and `1` is returned.
    #
    # @api public
    #
    def main(argv=[])
      required_args   = self.class.arguments.each_value.count(&:required?)
      optional_args   = self.class.arguments.each_value.count(&:optional?)
      has_repeats_arg = self.class.arguments.each_value.any?(&:repeats?)

      if argv.length < required_args
        print_error("insufficient number of arguments.")
        help_usage
        return 1
      elsif argv.length > (required_args + optional_args) && !has_repeats_arg
        print_error("too many arguments given.")
        help_usage
        return 1
      end

      super(argv)
    end

    #
    # Prints any defined arguments, along with the usual `--help` information.
    #
    # @api semipublic
    #
    def help_arguments
      unless (arguments = self.class.arguments).empty?
        puts
        puts 'Arguments:'

        arguments.each_value do |arg|
          case arg.desc
          when Array
            arg.desc.each_with_index do |line,index|
              if index == 0
                puts "    #{arg.usage.ljust(33)}#{line}"
              else
                puts "    #{' '.ljust(33)}#{line}"
              end
            end
          else
            puts "    #{arg.usage.ljust(33)}#{arg.desc}"
          end
        end
      end
    end

    #
    # Calls the superclass'es `#help` method, if it's defined, then calls
    # {#help_arguments}.
    #
    # @api public
    #
    def help
      super

      help_arguments
    end
  end
end