rlqualls/sugarcane

View on GitHub
lib/sugarcane/cli/parser.rb

Summary

Maintainability
A
35 mins
Test Coverage
require 'optparse'
require 'sugarcane/default_checks'
require 'sugarcane/cli/options'
require 'sugarcane/version'

module SugarCane
  module CLI

    # Provides a specification for the command line interface that drives
    # documentation, parsing, and default values.
    class Parser

      # Exception to indicate that no further processing is required and the
      # program can exit. This is used to handle --help and --version flags.
      class OptionsHandled < RuntimeError; end

      def self.parse(*args)
        new.parse(*args)
      end

      def initialize(stdout = $stdout)
        @stdout = stdout

        add_banner
        add_user_defined_checks

        SugarCane.default_checks.each do |check|
          add_check_options(check)
        end
        add_checks_shortcut

        add_cane_options

        add_version
        add_help
      end

      def parse(args, ret = true)
        parser.parse!(get_default_options + args)
        SugarCane::CLI.default_options.merge(options)
      rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption
        args = %w(--help)
        ret = false
        retry
      rescue OptionsHandled
        ret
      end

      def get_default_options
        # Right now, read_options_from_file can't just be called on
        # both because if the second one doesn't exist the defaults for
        # non-existent values will override what's already read
        if SugarCane::File.exists? './.sugarcane'
          read_options_from_file './.sugarcane'
        else
          read_options_from_file './.cane'
        end
      end

      def read_options_from_file(file)
        if SugarCane::File.exists?(file)
          SugarCane::File.contents(file).split(/\s+/m)
        else
          []
        end
      end

      def add_banner
        parser.banner = <<-BANNER
Usage: sugarcane [options]

Default options are loaded from a .sugarcane file in the current directory.

BANNER
      end

      def add_user_defined_checks
        description = "Load a Ruby file containing user-defined checks"
        parser.on("-r", "--require FILE", description) do |f|
          load(f)
        end

        description = "Use the given user-defined check"
        parser.on("-c", "--check CLASS", description) do |c|
          check = Kernel.const_get(c)
          options[:checks] << check
          add_check_options(check)
        end
        parser.separator ""
      end

      def add_check_options(check)
        check.options.each do |key, data|
          cli_key  = key.to_s.tr('_', '-')
          opts     = data[1] || {}
          variable = opts[:variable] || "VALUE"
          defaults = opts[:default] || []

          if opts[:type] == Array
            parser.on("--#{cli_key} #{variable}", Array, data[0]) do |opts|
              (options[key.to_sym] ||= []) << opts
            end
          else
            if [*defaults].length > 0
              add_option ["--#{cli_key}", variable], *data
            else
              add_option ["--#{cli_key}"], *data
            end
          end
        end

        parser.separator ""
      end

      def add_cane_options
        add_option %w(--max-violations VALUE),
          "Max allowed violations", default: 0, cast: :to_i

        add_option %w(--editor PROGRAM), "Text editor to use", default: nil

        add_option %w(--json),
          "output as json", default: false

        add_option %w(--report),
          "output a report", default: false

        add_option %w(--parallel),
          "Use all processors. Slower on small projects, faster on large.",
            cast: ->(x) { x }

        add_option %w(--color),
          "Colorize output", default: false

        parser.separator ""
      end

      def add_checks_shortcut
        description = "Apply all checks to given file"
        parser.on("-f", "--all FILE", description) do |f|
          # This is a bit of a hack, but provides a really useful UI for
          # dealing with single files. Let's see how it evolves.
          options[:abc_glob] = f
          options[:style_glob] = f
          options[:doc_glob] = f
        end
      end

      def add_version
        parser.on_tail("-v", "--version", "Show version") do
          stdout.puts SugarCane::VERSION
          raise OptionsHandled
        end
      end

      def add_help
        parser.on_tail("-h", "--help", "Show this message") do
          stdout.puts parser
          raise OptionsHandled
        end
      end

      def add_option(option, description, opts={})
        option_key = option[0].gsub('--', '').tr('-', '_').to_sym
        default    = opts[:default]
        cast       = opts[:cast] || ->(x) { x }

        if default
          description += " (default: %s)" % default
        end

        parser.on(option.join(' '), description) do |v|
          options[option_key] = cast.to_proc.call(v)
          options.delete(opts[:clobber])
        end
      end

      def options
        @options ||= {
          checks: SugarCane.default_checks
        }
      end

      def parser
        @parser ||= OptionParser.new
      end

      attr_reader :stdout
    end

  end
end