opts: added possibility to hide an option from the usage (easter egg?)
[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 # FIXME: avoir crashing on a command line like : `myprog -foo` (more than one letter after a single `-`)
299 protected fun parse_intern(it: Iterator[String])
300 do
301 var parseargs = true
302 build
303 var rest = rest
304
305 while parseargs and it.is_ok do
306 var str = it.item
307 if str == "--" then
308 it.next
309 rest.add_all(it.to_a)
310 parseargs = false
311 else
312 # We're looking for packed short options
313 if str.chars.last_index_of('-') == 0 and str.length > 2 then
314 var next_called = false
315 for i in [1..str.length] do
316 var short_opt = "-" + str.chars[i].to_s
317 if optmap.has_key(short_opt) then
318 var option = optmap[short_opt]
319 if option isa OptionParameter then
320 it.next
321 next_called = true
322 end
323 option.read_param(it)
324 end
325 end
326 if not next_called then it.next
327 else
328 if optmap.has_key(str) then
329 var opt = optmap[str]
330 it.next
331 opt.read_param(it)
332 else
333 rest.add(it.item)
334 it.next
335 end
336 end
337 end
338 end
339
340 for opt in options do
341 if opt.mandatory and not opt.read then
342 errors.add("Mandatory option {opt.names.join(", ")} not found.")
343 end
344 end
345 end
346
347 private fun build
348 do
349 for o in options do
350 for n in o.names do
351 optmap[n] = o
352 end
353 end
354 end
355
356 fun get_errors: Array[String]
357 do
358 var errors: Array[String] = new Array[String]
359 errors.add_all(errors)
360 for o in options do
361 for e in o.errors do
362 errors.add(e)
363 end
364 end
365 return errors
366 end
367 end