opts: fix crash on grouped single options
[nit.git] / lib / opts.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2008 Floréal Morandat <morandat@lirmm.fr>
4 # Copyright 2008 Jean Privat <jean@pryen.org>
5 #
6 # This file is free software, which comes along with NIT. This software is
7 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
8 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
9 # PARTICULAR PURPOSE. You can modify it is you want, provided this header
10 # is kept unaltered, and a notification of the changes is added.
11 # You are allowed to redistribute it and sell it, alone or is a part of
12 # another product.
13
14 # Management of options on the command line
15 module opts
16
17 # Super class of all option's class
18 abstract class Option
19 # Names for the option (including long and short ones)
20 var names: Array[String]
21
22 # Type of the value of the option
23 type VALUE: nullable Object
24
25 # Human readable description of the option
26 var helptext: String
27
28 # Gathering errors during parsing
29 var errors: Array[String] = new Array[String]
30
31 # Is this option mandatory?
32 var mandatory: Bool writable = false
33
34 # Is this option hidden from `usage`?
35 var hidden: Bool writable = false
36
37 # Has this option been read?
38 var read: Bool writable = false
39
40 # Current value of this option
41 var value: VALUE writable
42
43 # Default value of this option
44 var default_value: VALUE writable
45
46 # Create a new option
47 init(help: String, default: VALUE, names: nullable Array[String])
48 do
49 init_opt(help, default, names)
50 end
51
52 fun init_opt(help: String, default: VALUE, names: nullable Array[String])
53 do
54 if names == null then
55 self.names = new Array[String]
56 else
57 self.names = names.to_a
58 end
59 helptext = help
60 default_value = default
61 value = default
62 end
63
64 # Add new aliases for this option
65 fun add_aliases(names: String...) do names.add_all(names)
66
67 # An help text for this option with default settings
68 redef fun to_s do return pretty(2)
69
70 # A pretty print for this help
71 fun pretty(off: Int): String
72 do
73 var text = new FlatBuffer.from(" ")
74 text.append(names.join(", "))
75 text.append(" ")
76 var rest = off - text.length
77 if rest > 0 then text.append(" " * rest)
78 text.append(helptext)
79 #text.append(pretty_default)
80 return text.to_s
81 end
82
83 fun pretty_default: String
84 do
85 var dv = default_value
86 if dv != null then return " ({dv})"
87 return ""
88 end
89
90 # Consume parameters for this option
91 protected fun read_param(it: Iterator[String])
92 do
93 read = true
94 end
95 end
96
97 # Not really an option. Just add a line of text when displaying the usage
98 class OptionText
99 super Option
100 init(text: String) do super(text, null, null)
101
102 redef fun pretty(off) do return to_s
103
104 redef fun to_s do return helptext
105 end
106
107 # A boolean option, `true` when present, `false` if not
108 class OptionBool
109 super Option
110 redef type VALUE: Bool
111
112 init(help: String, names: String...) do super(help, false, names)
113
114 redef fun read_param(it)
115 do
116 super
117 value = true
118 end
119 end
120
121 # A count option. Count the number of time this option is present
122 class OptionCount
123 super Option
124 redef type VALUE: Int
125
126 init(help: String, names: String...) do super(help, 0, names)
127
128 redef fun read_param(it)
129 do
130 super
131 value += 1
132 end
133 end
134
135 # Option with one parameter (mandatory by default)
136 abstract class OptionParameter
137 super Option
138 protected fun convert(str: String): VALUE is abstract
139
140 # Is the parameter mandatory?
141 var parameter_mandatory: Bool writable = true
142
143 redef fun read_param(it)
144 do
145 super
146 if it.is_ok and it.item.chars.first != '-' then
147 value = convert(it.item)
148 it.next
149 else
150 if parameter_mandatory then
151 errors.add("Parameter expected for option {names.first}.")
152 end
153 end
154 end
155 end
156
157 # An option with a String as parameter
158 class OptionString
159 super OptionParameter
160 redef type VALUE: nullable String
161
162 init(help: String, names: String...) do super(help, null, names)
163
164 redef fun convert(str) do return str
165 end
166
167 # An option with an enum as parameter
168 # In the code, declaring an option enum (-e) with an enum like `["zero", "one", "two"]
169 # In the command line, typing `myprog -e one` is giving 1 as value
170 class OptionEnum
171 super OptionParameter
172 redef type VALUE: Int
173 var values: Array[String]
174
175 init(values: Array[String], help: String, default: Int, names: String...)
176 do
177 assert values.length > 0
178 self.values = values.to_a
179 super("{help} <{values.join(", ")}>", default, names)
180 end
181
182 redef fun convert(str)
183 do
184 var id = values.index_of(str)
185 if id == -1 then
186 var e = "Unrecognized value for option {names.join(", ")}.\n"
187 e += "Expected values are: {values.join(", ")}."
188 errors.add(e)
189 end
190 return id
191 end
192
193 fun value_name: String do return values[value]
194
195 redef fun pretty_default
196 do
197 if default_value != null then
198 return " ({values[default_value.as(not null)]})"
199 else
200 return ""
201 end
202 end
203 end
204
205 # An option with an Int as parameter
206 class OptionInt
207 super OptionParameter
208 redef type VALUE: Int
209
210 init(help: String, default: Int, names: String...) do super(help, default, names)
211
212 redef fun convert(str) do return str.to_i
213 end
214
215 # An option with a Float as parameter
216 class OptionFloat
217 super OptionParameter
218 redef type VALUE: Float
219
220 init(help: String, default: Float, names: String...) do super(help, default, names)
221
222 redef fun convert(str) do return str.to_f
223 end
224
225 # An option with an array as parameter
226 # `myprog -optA arg1 -optA arg2` is giving an Array `["arg1", "arg2"]`
227 class OptionArray
228 super OptionParameter
229 redef type VALUE: Array[String]
230
231 init(help: String, names: String...)
232 do
233 values = new Array[String]
234 super(help, values, names)
235 end
236
237 private var values: Array[String]
238 redef fun convert(str)
239 do
240 values.add(str)
241 return values
242 end
243 end
244
245 # Context where the options process
246 class OptionContext
247 # Options present in the context
248 var options: Array[Option]
249
250 # Rest of the options after `parse` is called
251 var rest: Array[String]
252
253 # Errors found in the context after parsing
254 var errors: Array[String]
255
256 private var optmap: Map[String, Option]
257
258 init
259 do
260 options = new Array[Option]
261 optmap = new HashMap[String, Option]
262 rest = new Array[String]
263 errors = new Array[String]
264 end
265
266 # Add one or more options to the context
267 fun add_option(opts: Option...) do
268 options.add_all(opts)
269 end
270
271 # Display all the options available
272 fun usage
273 do
274 var lmax = 1
275 for i in options do
276 var l = 3
277 for n in i.names do
278 l += n.length + 2
279 end
280 if lmax < l then lmax = l
281 end
282
283 for i in options do
284 if not i.hidden then
285 print(i.pretty(lmax))
286 end
287 end
288 end
289
290 # Parse and assign options everywhere in the argument list
291 fun parse(argv: Collection[String])
292 do
293 var it = argv.iterator
294 parse_intern(it)
295 end
296
297 # Parse the command line
298 protected fun parse_intern(it: Iterator[String])
299 do
300 var parseargs = true
301 build
302 var rest = rest
303
304 while parseargs and it.is_ok do
305 var str = it.item
306 if str == "--" then
307 it.next
308 rest.add_all(it.to_a)
309 parseargs = false
310 else
311 # We're looking for packed short options
312 if str.chars.last_index_of('-') == 0 and str.length > 2 then
313 var next_called = false
314 for i in [1..str.length[ do
315 var short_opt = "-" + str.chars[i].to_s
316 if optmap.has_key(short_opt) then
317 var option = optmap[short_opt]
318 if option isa OptionParameter then
319 it.next
320 next_called = true
321 end
322 option.read_param(it)
323 end
324 end
325 if not next_called then it.next
326 else
327 if optmap.has_key(str) then
328 var opt = optmap[str]
329 it.next
330 opt.read_param(it)
331 else
332 rest.add(it.item)
333 it.next
334 end
335 end
336 end
337 end
338
339 for opt in options do
340 if opt.mandatory and not opt.read then
341 errors.add("Mandatory option {opt.names.join(", ")} not found.")
342 end
343 end
344 end
345
346 private fun build
347 do
348 for o in options do
349 for n in o.names do
350 optmap[n] = o
351 end
352 end
353 end
354
355 fun get_errors: Array[String]
356 do
357 var errors: Array[String] = new Array[String]
358 errors.add_all(errors)
359 for o in options do
360 for e in o.errors do
361 errors.add(e)
362 end
363 end
364 return errors
365 end
366 end