summaryrefslogtreecommitdiff
path: root/lib/slop/options.rb
blob: c1873d08efa21fe5ac479413e943891b0661b89f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
module Slop
  class Options
    include Enumerable

    DEFAULT_CONFIG = {
      suppress_errors: false,
      type:            "null",
      banner:          true,
    }

    # The Array of Option instances we've created.
    attr_reader :options

    # An Array of separators used for the help text.
    attr_reader :separators

    # Our Parser instance.
    attr_reader :parser

    # A Hash of configuration options.
    attr_reader :config

    # The String banner prefixed to the help string.
    attr_accessor :banner

    def initialize(**config)
      @options    = []
      @separators = []
      @banner     = config[:banner].is_a?(String) ? config[:banner] : config.fetch(:banner, "usage: #{$0} [options]")
      @config     = DEFAULT_CONFIG.merge(config)
      @parser     = Parser.new(self, @config)

      yield self if block_given?
    end

    # Add a new option. This method is an alias for adding a NullOption
    # (i.e an option with an ignored return value).
    #
    # Example:
    #
    #   opts = Slop.parse do |o|
    #     o.on '--version' do
    #       puts Slop::VERSION
    #     end
    #   end
    #
    #   opts.to_hash #=> {}
    #
    # Returns the newly created Option subclass.
    def on(*flags, **config, &block)
      desc   = flags.pop unless flags.last.start_with?('-')
      config = self.config.merge(config)
      klass  = Slop.string_to_option_class(config[:type].to_s)
      option = klass.new(flags, desc, config, &block)

      add_option option
    end

    # Add a separator between options. Used when displaying
    # the help text.
    def separator(string)
      if separators[options.size]
        separators.last << "\n#{string}"
      else
        separators[options.size] = string
      end
    end

    # Sugar to avoid `options.parser.parse(x)`.
    def parse(strings)
      parser.parse(strings)
    end

    # Implements the Enumerable interface.
    def each(&block)
      options.each(&block)
    end

    # Handle custom option types. Will fall back to raising an
    # exception if an option is not defined.
    def method_missing(name, *args, **config, &block)
      if respond_to_missing?(name)
        config[:type] = name
        on(*args, config, &block)
      else
        super
      end
    end

    def respond_to_missing?(name, include_private = false)
      Slop.option_defined?(name) || super
    end

    # Return a copy of our options Array.
    def to_a
      options.dup
    end

    # Returns the help text for this options. Used by Result#to_s.
    def to_s(prefix: " " * 4)
      str = config[:banner] ? "#{banner}\n" : ""
      len = longest_flag_length

      options.select(&:help?).sort_by(&:tail).each_with_index do |opt, i|
        # use the index to fetch an associated separator
        if sep = separators[i]
          str << "#{sep}\n"
        end

        str << "#{prefix}#{opt.to_s(offset: len)}\n"
      end

      str
    end

    private

    def longest_flag_length
      (o = longest_option) && o.flag.length || 0
    end

    def longest_option
      options.max { |a, b| a.flag.length <=> b.flag.length }
    end

    def add_option(option)
      options.each do |o|
        flags = o.flags & option.flags

        # Raise an error if we found an existing option with the same
        # flags. I can't immediately see a use case for this..
        if flags.any?
          raise ArgumentError, "duplicate flags: #{flags}"
        end
      end

      options << option
      option
    end
  end
end