examples: annotate examples
[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 = false is writable
33
34 # Is this option hidden from `usage`?
35 var hidden: Bool = false is writable
36
37 # Has this option been read?
38 var read: Bool = false is writable
39
40 # Current value of this option
41 var value: VALUE is writable
42
43 # Default value of this option
44 var default_value: VALUE is writable
45
46 # Create a new option
47 init(help: String, default: VALUE, names: nullable Array[String]) is old_style_init do
48 init_opt(help, default, names)
49 end
50
51 # Init option `helptext`, `default_value` and `names`.
52 #
53 # Also set current `value` to `default`.
54 fun init_opt(help: String, default: VALUE, names: nullable Array[String])
55 do
56 if names == null then
57 self.names = new Array[String]
58 else
59 self.names = names.to_a
60 end
61 helptext = help
62 default_value = default
63 value = default
64 end
65
66 # Add new aliases for this option
67 fun add_aliases(names: String...) do names.add_all(names)
68
69 # An help text for this option with default settings
70 redef fun to_s do return pretty(2)
71
72 # A pretty print for this help
73 fun pretty(off: Int): String
74 do
75 var text = new FlatBuffer.from(" ")
76 text.append(names.join(", "))
77 text.append(" ")
78 var rest = off - text.length
79 if rest > 0 then text.append(" " * rest)
80 text.append(helptext)
81 #text.append(pretty_default)
82 return text.to_s
83 end
84
85 # Pretty print the default value.
86 fun pretty_default: String
87 do
88 var dv = default_value
89 if dv != null then return " ({dv.to_s})"
90 return ""
91 end
92
93 # Consume parameters for this option
94 protected fun read_param(opts: OptionContext, it: Iterator[String])
95 do
96 read = true
97 end
98 end
99
100 # Not really an option. Just add a line of text when displaying the usage
101 class OptionText
102 super Option
103
104 # Init a new OptionText with `text`.
105 init(text: String) is old_style_init do super(text, null, null)
106
107 redef fun pretty(off) do return to_s
108
109 redef fun to_s do return helptext
110 end
111
112 # A boolean option, `true` when present, `false` if not
113 class OptionBool
114 super Option
115 redef type VALUE: Bool
116
117 # Init a new OptionBool with a `help` message and `names`.
118 init(help: String, names: String...) is old_style_init do super(help, false, names)
119
120 redef fun read_param(opts, it)
121 do
122 super
123 value = true
124 end
125 end
126
127 # A count option. Count the number of time this option is present
128 class OptionCount
129 super Option
130 redef type VALUE: Int is fixed
131
132 # Init a new OptionCount with a `help` message and `names`.
133 init(help: String, names: String...) is old_style_init do super(help, 0, names)
134
135 redef fun read_param(opts, it)
136 do
137 super
138 value += 1
139 end
140 end
141
142 # Option with one parameter (mandatory by default)
143 abstract class OptionParameter
144 super Option
145
146 # Convert `str` to a value of type `VALUE`.
147 protected fun convert(str: String): VALUE is abstract
148
149 # Is the parameter mandatory?
150 var parameter_mandatory = true is writable
151
152 redef fun read_param(opts, it)
153 do
154 super
155
156 var ok = it.is_ok
157 if ok and not parameter_mandatory and not it.item.is_empty and it.item.chars.first == '-' then
158 # The next item may looks like a known command
159 # Only check if `not parameter_mandatory`
160 for opt in opts.options do
161 if opt.names.has(it.item) then
162 # The next item is a known command
163 ok = false
164 break
165 end
166 end
167 end
168
169 if ok then
170 value = convert(it.item)
171 it.next
172 else
173 errors.add("Parameter expected for option {names.first}.")
174 end
175 end
176 end
177
178 # An option with a `String` as parameter
179 class OptionString
180 super OptionParameter
181 redef type VALUE: nullable String
182
183 # Init a new OptionString with a `help` message and `names`.
184 init(help: String, names: String...) is old_style_init do super(help, null, names)
185
186 redef fun convert(str) do return str
187 end
188
189 # An option to choose from an enumeration
190 #
191 # Declare an enumeration option with all its possible values as an array.
192 # Once the arguments are processed, `value` is set as the index of the selected value, if any.
193 class OptionEnum
194 super OptionParameter
195 redef type VALUE: Int
196
197 # Values in the enumeration.
198 var values: Array[String]
199
200 # Init a new OptionEnum from `values` with a `help` message and `names`.
201 #
202 # `default` is the index of the default value in `values`.
203 init(values: Array[String], help: String, default: Int, names: String...) is old_style_init do
204 assert values.length > 0
205 self.values = values.to_a
206 super("{help} <{values.join(", ")}>", default, names)
207 end
208
209 redef fun convert(str)
210 do
211 var id = values.index_of(str)
212 if id == -1 then
213 var e = "Unrecognized value for option {names.join(", ")}.\n"
214 e += "Expected values are: {values.join(", ")}."
215 errors.add(e)
216 end
217 return id
218 end
219
220 # Get the value name from `values`.
221 fun value_name: String do return values[value]
222
223 redef fun pretty_default
224 do
225 return " ({values[default_value]})"
226 end
227 end
228
229 # An option with an Int as parameter
230 class OptionInt
231 super OptionParameter
232 redef type VALUE: Int
233
234 # Init a new OptionInt with a `help` message, a `default` value and `names`.
235 init(help: String, default: Int, names: String...) is old_style_init do
236 super(help, default, names)
237 end
238
239 redef fun convert(str)
240 do
241 if str.is_int then return str.to_i
242
243 errors.add "Expected an integer for option {names.join(", ")}."
244 return 0
245 end
246 end
247
248 # An option with a Float as parameter
249 class OptionFloat
250 super OptionParameter
251 redef type VALUE: Float
252
253 # Init a new OptionFloat with a `help` message, a `default` value and `names`.
254 init(help: String, default: Float, names: String...) is old_style_init do
255 super(help, default, names)
256 end
257
258 redef fun convert(str) do return str.to_f
259 end
260
261 # An option with an array as parameter
262 # `myprog -optA arg1 -optA arg2` is giving an Array `["arg1", "arg2"]`
263 class OptionArray
264 super OptionParameter
265 redef type VALUE: Array[String]
266
267 # Init a new OptionArray with a `help` message and `names`.
268 init(help: String, names: String...) is old_style_init do
269 values = new Array[String]
270 super(help, values, names)
271 end
272
273 private var values: Array[String]
274 redef fun convert(str)
275 do
276 values.add(str)
277 return values
278 end
279 end
280
281 # Context where the options process
282 class OptionContext
283 # Options present in the context
284 var options = new Array[Option]
285
286 # Rest of the options after `parse` is called
287 var rest = new Array[String]
288
289 # Errors found in the context after parsing
290 var context_errors = new Array[String]
291
292 private var optmap = new HashMap[String, Option]
293
294 # Add one or more options to the context
295 fun add_option(opts: Option...) do options.add_all(opts)
296
297 # Display all the options available
298 fun usage
299 do
300 var lmax = 1
301 for i in options do
302 var l = 3
303 for n in i.names do
304 l += n.length + 2
305 end
306 if lmax < l then lmax = l
307 end
308
309 for i in options do
310 if not i.hidden then
311 print(i.pretty(lmax))
312 end
313 end
314 end
315
316 # Parse and assign options in `argv` or `args`
317 fun parse(argv: nullable Collection[String])
318 do
319 if argv == null then argv = args
320 var it = argv.iterator
321 parse_intern(it)
322 end
323
324 # Must all option be given before the first argument?
325 #
326 # When set to `false` (the default), options of the command line are
327 # all parsed until the end of the list of arguments or until "--" is met (in this case "--" is discarded).
328 #
329 # When set to `true` options are parsed until the first non-option is met.
330 var options_before_rest = false is writable
331
332 # Parse the command line
333 protected fun parse_intern(it: Iterator[String])
334 do
335 var parseargs = true
336 build
337 var rest = rest
338
339 while parseargs and it.is_ok do
340 var str = it.item
341 if str == "--" then
342 it.next
343 rest.add_all(it.to_a)
344 parseargs = false
345 else
346 # We're looking for packed short options
347 if str.chars.last_index_of('-') == 0 and str.length > 2 then
348 var next_called = false
349 for i in [1..str.length[ do
350 var short_opt = "-" + str.chars[i].to_s
351 if optmap.has_key(short_opt) then
352 var option = optmap[short_opt]
353 if option isa OptionParameter then
354 it.next
355 next_called = true
356 end
357 option.read_param(self, it)
358 end
359 end
360 if not next_called then it.next
361 else
362 if optmap.has_key(str) then
363 var opt = optmap[str]
364 it.next
365 opt.read_param(self, it)
366 else
367 rest.add(it.item)
368 it.next
369 if options_before_rest then
370 rest.add_all(it.to_a)
371 parseargs = false
372 end
373 end
374 end
375 end
376 end
377
378 for opt in options do
379 if opt.mandatory and not opt.read then
380 context_errors.add("Mandatory option {opt.names.join(", ")} not found.")
381 end
382 end
383 end
384
385 private fun build
386 do
387 for o in options do
388 for n in o.names do
389 optmap[n] = o
390 end
391 end
392 end
393
394 # Options parsing errors.
395 fun errors: Array[String]
396 do
397 var errors = new Array[String]
398 errors.add_all context_errors
399 for o in options do
400 for e in o.errors do
401 errors.add(e)
402 end
403 end
404 return errors
405 end
406 end