a1bffc1efabaf27a41186d6165e30233cb38d815
[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 if default_value != null then
198 return " ({values[default_value]})"
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 = new Array[Option]
249
250 # Rest of the options after `parse` is called
251 var rest = new Array[String]
252
253 # Errors found in the context after parsing
254 var errors = new Array[String]
255
256 private var optmap = new HashMap[String, Option]
257
258 # Add one or more options to the context
259 fun add_option(opts: Option...) do
260 options.add_all(opts)
261 end
262
263 # Display all the options available
264 fun usage
265 do
266 var lmax = 1
267 for i in options do
268 var l = 3
269 for n in i.names do
270 l += n.length + 2
271 end
272 if lmax < l then lmax = l
273 end
274
275 for i in options do
276 if not i.hidden then
277 print(i.pretty(lmax))
278 end
279 end
280 end
281
282 # Parse and assign options everywhere in the argument list
283 fun parse(argv: Collection[String])
284 do
285 var it = argv.iterator
286 parse_intern(it)
287 end
288
289 # Parse the command line
290 protected fun parse_intern(it: Iterator[String])
291 do
292 var parseargs = true
293 build
294 var rest = rest
295
296 while parseargs and it.is_ok do
297 var str = it.item
298 if str == "--" then
299 it.next
300 rest.add_all(it.to_a)
301 parseargs = false
302 else
303 # We're looking for packed short options
304 if str.chars.last_index_of('-') == 0 and str.length > 2 then
305 var next_called = false
306 for i in [1..str.length[ do
307 var short_opt = "-" + str.chars[i].to_s
308 if optmap.has_key(short_opt) then
309 var option = optmap[short_opt]
310 if option isa OptionParameter then
311 it.next
312 next_called = true
313 end
314 option.read_param(it)
315 end
316 end
317 if not next_called then it.next
318 else
319 if optmap.has_key(str) then
320 var opt = optmap[str]
321 it.next
322 opt.read_param(it)
323 else
324 rest.add(it.item)
325 it.next
326 end
327 end
328 end
329 end
330
331 for opt in options do
332 if opt.mandatory and not opt.read then
333 errors.add("Mandatory option {opt.names.join(", ")} not found.")
334 end
335 end
336 end
337
338 private fun build
339 do
340 for o in options do
341 for n in o.names do
342 optmap[n] = o
343 end
344 end
345 end
346
347 fun get_errors: Array[String]
348 do
349 var errors: Array[String] = new Array[String]
350 errors.add_all(errors)
351 for o in options do
352 for e in o.errors do
353 errors.add(e)
354 end
355 end
356 return errors
357 end
358 end