5e361423a5020a9d485064decabbbee85a6bb9f1
[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])
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.to_s})"
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 = true is writable
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 return " ({values[default_value]})"
198 end
199 end
200
201 # An option with an Int as parameter
202 class OptionInt
203 super OptionParameter
204 redef type VALUE: Int
205
206 init(help: String, default: Int, names: String...) do super(help, default, names)
207
208 redef fun convert(str) do return str.to_i
209 end
210
211 # An option with a Float as parameter
212 class OptionFloat
213 super OptionParameter
214 redef type VALUE: Float
215
216 init(help: String, default: Float, names: String...) do super(help, default, names)
217
218 redef fun convert(str) do return str.to_f
219 end
220
221 # An option with an array as parameter
222 # `myprog -optA arg1 -optA arg2` is giving an Array `["arg1", "arg2"]`
223 class OptionArray
224 super OptionParameter
225 redef type VALUE: Array[String]
226
227 init(help: String, names: String...)
228 do
229 values = new Array[String]
230 super(help, values, names)
231 end
232
233 private var values: Array[String]
234 redef fun convert(str)
235 do
236 values.add(str)
237 return values
238 end
239 end
240
241 # Context where the options process
242 class OptionContext
243 # Options present in the context
244 var options = new Array[Option]
245
246 # Rest of the options after `parse` is called
247 var rest = new Array[String]
248
249 # Errors found in the context after parsing
250 var errors = new Array[String]
251
252 private var optmap = new HashMap[String, Option]
253
254 # Add one or more options to the context
255 fun add_option(opts: Option...) do
256 options.add_all(opts)
257 end
258
259 # Display all the options available
260 fun usage
261 do
262 var lmax = 1
263 for i in options do
264 var l = 3
265 for n in i.names do
266 l += n.length + 2
267 end
268 if lmax < l then lmax = l
269 end
270
271 for i in options do
272 if not i.hidden then
273 print(i.pretty(lmax))
274 end
275 end
276 end
277
278 # Parse and assign options everywhere in the argument list
279 fun parse(argv: Collection[String])
280 do
281 var it = argv.iterator
282 parse_intern(it)
283 end
284
285 # Must all option be given before the first argument?
286 #
287 # When set to `false` (the default), options of the command line are
288 # all parsed until the end of the list of arguments or until "--" is met (in this case "--" is discarded).
289 #
290 # When set to `true` options are parsed until the first non-option is met.
291 var options_before_rest = false is writable
292
293 # Parse the command line
294 protected fun parse_intern(it: Iterator[String])
295 do
296 var parseargs = true
297 build
298 var rest = rest
299
300 while parseargs and it.is_ok do
301 var str = it.item
302 if str == "--" then
303 it.next
304 rest.add_all(it.to_a)
305 parseargs = false
306 else
307 # We're looking for packed short options
308 if str.chars.last_index_of('-') == 0 and str.length > 2 then
309 var next_called = false
310 for i in [1..str.length[ do
311 var short_opt = "-" + str.chars[i].to_s
312 if optmap.has_key(short_opt) then
313 var option = optmap[short_opt]
314 if option isa OptionParameter then
315 it.next
316 next_called = true
317 end
318 option.read_param(it)
319 end
320 end
321 if not next_called then it.next
322 else
323 if optmap.has_key(str) then
324 var opt = optmap[str]
325 it.next
326 opt.read_param(it)
327 else
328 rest.add(it.item)
329 it.next
330 if options_before_rest then
331 rest.add_all(it.to_a)
332 parseargs = false
333 end
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