Class: Getopt::Long

Inherits:
Object
  • Object
show all
Includes:
Version
Defined in:
lib/getopt/long.rb

Overview

The Getopt::Long class encapsulates longhanded parameter parsing options

Defined Under Namespace

Classes: Error

Constant Summary

Constants included from Version

Version::VERSION

Class Method Summary collapse

Class Method Details

.getopts(*switches) ⇒ Object

Takes an array of switches. Each array consists of up to three elements that indicate the name and type of switch. Returns a hash containing each switch name, minus the ‘-’, as a key. The value for each key depends on the type of switch and/or the value provided by the user.

The long switch must be provided. The short switch defaults to the first letter of the short switch. The default type is BOOLEAN.

Example:

opts = Getopt::Long.getopts(
   ['--debug'                   ],
   ['--verbose', '-v'           ],
   ['--level',   '-l', INCREMENT]
)

See the README file for more information.



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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/getopt/long.rb', line 42

def self.getopts(*switches)
  if switches.empty?
    raise ArgumentError, 'no switches provided'
  end

  hash  = {} # Hash returned to user
  valid = [] # Tracks valid switches
  types = {} # Tracks argument types
  syns  = {} # Tracks long and short arguments, or multiple shorts
  negs  = {} # Tracks negated switches (maps --no-foo to --foo)

  # If a string is passed, split it and convert it to an array of arrays
  if switches.first.is_a?(String)
    switches = switches.join.split
    switches.map!{ |switch| [switch] }
  end

  # Set our list of valid switches, and proper types for each switch
  switches.each do |switch|
    valid.push(switch[0]) # Set valid long switches

    # Set type for long switch, default to BOOLEAN.
    if switch[1].is_a?(Integer)
      switch[2] = switch[1]
      types[switch[0]] = switch[2]
      switch[1] = switch[0][1..2]
    else
      switch[2] ||= BOOLEAN
      types[switch[0]] = switch[2]
      switch[1] ||= switch[0][1..2]
    end

    # Create synonym hash.  Default to first char of long switch for
    # short switch, e.g. "--verbose" creates a "-v" synonym.  The same
    # synonym can only be used once - first one wins.
    syns[switch[0]] = switch[1] unless syns[switch[1]]
    syns[switch[1]] = switch[0] unless syns[switch[1]]

    switch[1] = [switch[1]]

    switch[1].each do |char|
      types[char] = switch[2]  # Set type for short switch
      valid.push(char)         # Set valid short switches
    end

    # For NEGATABLE switches, register the --no- variant
    if switch[2] == NEGATABLE
      negated = switch[0].sub(/^--/, '--no-')
      valid.push(negated)
      types[negated] = NEGATABLE
      negs[negated] = switch[0] # Map --no-foo back to --foo
    end
  end

  re_long     = /^(--\w+[-\w+]*)?$/
  re_short    = /^(-[^\s-])$/
  re_long_eq  = /^(--\w+[-\w+]*)?=(.*?)$|(-[^\s-])=(.*?)$/
  re_short_sq = /^(-[^\s-])(\S+?)$/

  ARGV.each_with_index do |opt, index|
    # Allow either -x -v or -xv style for single char args
    if re_short_sq.match(opt)
      chars = opt.chars[1..].map{ |s| "-#{s}" }

      chars.each_with_index do |char, i|
        unless valid.include?(char)
          raise Error, "invalid switch '#{char}'"
        end

        # Grab the next arg if the switch takes a required arg
        if types[char] == REQUIRED
          # Deal with a argument squished up against switch
          if chars[i + 1]
            arg = chars[i + 1..].join.tr('-', '')
            ARGV.push(char, arg)
            break
          else
            arg = ARGV.delete_at(index + 1)
            if arg.nil? || valid.include?(arg) # Minor cheat here
              err = "no value provided for required argument '#{char}'"
              raise Error, err
            end
            ARGV.push(char, arg)
          end
        elsif types[char] == OPTIONAL
          if chars[i + 1] && !valid.include?(chars[i + 1])
            arg = chars[i + 1..].join.tr('-', '')
            ARGV.push(char, arg)
            break
          elsif ARGV[index + 1] && !valid.include?(ARGV[index + 1])
            arg = ARGV.delete_at(index + 1)
            ARGV.push(char, arg)
          else
            ARGV.push(char)
          end
        else
          ARGV.push(char)
        end
      end
      next
    end

    match = re_long.match(opt) || re_short.match(opt)
    switch = match.captures.first if match

    if match = re_long_eq.match(opt)
      switch, value = match.captures.compact
      ARGV.push(switch, value)
      next
    end

    # Make sure that all the switches are valid.  If 'switch' isn't
    # defined at this point, it means an option was passed without
    # a preceding switch, e.g. --option foo bar.
    unless valid.include?(switch)
      switch ||= opt
      raise Error, "invalid switch '#{switch}'"
    end

    # Required arguments
    if types[switch] == REQUIRED
      nextval = ARGV[index + 1]

      # Make sure there's a value for mandatory arguments
      if nextval.nil?
        err = "no value provided for required argument '#{switch}'"
        raise Error, err
      end

      # If there is a value, make sure it's not another switch
      if valid.include?(nextval)
        err = "cannot pass switch '#{nextval}' as an argument"
        raise Error, err
      end

      # If the same option appears more than once, put the values in array.
      if hash[switch]
        hash[switch] = [hash[switch], nextval].flatten
      else
        hash[switch] = nextval
      end

      ARGV.delete_at(index + 1)
    end

    # For boolean arguments set the switch's value to true.
    if types[switch] == BOOLEAN
      if hash.key?(switch)
        raise Error, 'boolean switch already set'
      end
      hash[switch] = true
    end

    # For increment arguments, set the switch's value to 0, or
    # increment it by one if it already exists.
    if types[switch] == INCREMENT
      if hash.key?(switch)
        hash[switch] += 1
      else
        hash[switch] = 1
      end
    end

    # For negatable arguments, --foo sets true, --no-foo sets false.
    # The value is stored under the base switch name (without --no-).
    if types[switch] == NEGATABLE
      base_switch = negs[switch] || switch
      if hash.key?(base_switch)
        raise Error, 'negatable switch already set'
      end
      hash[base_switch] = !negs.key?(switch) # true unless it's a --no- variant
    end

    # For optional argument, there may be an argument.  If so, it
    # cannot be another switch.  If not, it is set to true.
    if types[switch] == OPTIONAL
      nextval = ARGV[index + 1]
      if valid.include?(nextval)
        hash[switch] = true
      else
        hash[switch] = nextval
        ARGV.delete_at(index + 1)
      end
    end
  end

  # rubocop:disable Style/CombinableLoops

  # Set synonymous switches to the same value, e.g. if -t is a synonym
  # for --test, and the user passes "--test", then set "-t" to the same
  # value that "--test" was set to.
  #
  # This allows users to refer to the long or short switch and get
  # the same value
  hash.dup.each do |switch, val|
    # Skip negated switches - they've already been mapped to base switch
    next if negs.key?(switch)

    if syns.keys.include?(switch)
      syns[switch] = [syns[switch]]
      syns[switch].each do |key|
        hash[key] = val
      end
    end
  end

  # Get rid of leading "--" and "-" to make it easier to reference
  hash.dup.each do |key, value|
    if key =~ /^-/
      if key[0, 2] == '--'
        nkey = key.sub('--', '')
      else
        nkey = key.sub('-', '')
      end
      hash.delete(key)
      hash[nkey] = value
    end
  end

  # rubocop:enable Style/CombinableLoops

  hash
end