summaryrefslogtreecommitdiff
path: root/lib/slop/parser.rb
blob: 553c2ff8c67a5b6b10e4318bff614578f21c1d32 (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
module Slop
  class Parser

    # Our Options instance.
    attr_reader :options

    # A Hash of configuration options.
    attr_reader :config

    # Returns an Array of String arguments that were not parsed.
    attr_reader :arguments

    def initialize(options, **config)
      @options = options
      @config  = config
      reset
    end

    # Reset the parser, useful to use the same instance to parse a second
    # time without duplicating state.
    def reset
      @arguments = []
      @options.each(&:reset)
      self
    end

    # Traverse `strings` and process options one by one. Anything after
    # `--` is ignored. If a flag includes a equals (=) it will be split
    # so that `flag, argument = s.split('=')`.
    #
    # The `call` method will be executed immediately for each option found.
    # Once all options have been executed, any found options will have
    # the `finish` method called on them.
    #
    # Returns a Slop::Result.
    def parse(strings)
      reset # reset before every parse

      pairs = strings.each_cons(2).to_a
      # this ensures we still support the last string being a flag,
      # otherwise it'll only be used as an argument.
      pairs << [strings.last, nil]

      @arguments = strings.dup

      pairs.each do |flag, arg|
        break if !flag

        # ignore everything after '--', flag or not
        if flag == '--'
          arguments.delete(flag)
          break
        end

        # support `foo=bar`
        if flag.include?("=")
          flag, arg = flag.split("=")
        end

        if opt = try_process(flag, arg)
          # since the option was parsed, we remove it from our
          # arguments (plus the arg if necessary)
          # delete argument first so that it doesn't mess up the index
          if opt.expects_argument?
            arguments.each_with_index do |argument, i|
              arguments.delete_at(i + 1) if argument == flag
            end
          end
          arguments.delete(flag)
        end
      end

      Result.new(self).tap do |result|
        used_options.each { |o| o.finish(result) }
      end
    end

    # Returns an Array of Option instances that were used.
    def used_options
      options.select { |o| o.count > 0 }
    end

    # Returns an Array of Option instances that were not used.
    def unused_options
      options.to_a - used_options
    end

    private

    # We've found an option, process and return it
    def process(option, arg)
      option.ensure_call(arg)
      option
    end

    # Try and find an option to process
    def try_process(flag, arg)
      if option = matching_option(flag)
        process(option, arg)
      elsif flag.start_with?("--no-") && option = matching_option(flag.sub("no-", ""))
        process(option, false)
      elsif flag =~ /\A-[^-]{2,}/
        # try and process as a set of grouped short flags. drop(1) removes
        # the prefixed -, then we add them back to each flag separately.
        flags = flag.split("").drop(1).map { |f| "-#{f}" }
        last  = flags.pop

        flags.each { |f| try_process(f, nil) }
        try_process(last, arg) # send the argument to the last flag
      else
        if flag.start_with?("-") && !suppress_errors?
          raise UnknownOption.new("unknown option `#{flag}'", "#{flag}")
        end
      end
    end

    def suppress_errors?
      config[:suppress_errors]
    end

    def matching_option(flag)
      options.find { |o| o.flags.include?(flag) }
    end
  end
end