X-Git-Url: http://nitlanguage.org diff --git a/lib/opts.nit b/lib/opts.nit index b4da3f5..fa5ce15 100644 --- a/lib/opts.nit +++ b/lib/opts.nit @@ -5,60 +5,75 @@ # # This file is free software, which comes along with NIT. This software is # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. You can modify it is you want, provided this header # is kept unaltered, and a notification of the changes is added. # You are allowed to redistribute it and sell it, alone or is a part of # another product. +# Management of options on the command line +module opts + # Super class of all option's class -class Option +abstract class Option # Names for the option (including long and short ones) - readable attr _names: Array[String] + var names: Array[String] # Type of the value of the option - type VALUE: Object + type VALUE: nullable Object # Human readable description of the option - readable attr _helptext: String + var helptext: String + + # Gathering errors during parsing + var errors: Array[String] = new Array[String] # Is this option mandatory? - readable writable attr _mandatory: Bool + var mandatory: Bool = false is writable - # context where the option is located - readable writable attr _context: OptionContext + # Is this option hidden from `usage`? + var hidden: Bool = false is writable + + # Has this option been read? + var read: Bool = false is writable # Current value of this option - readable writable attr _value: VALUE + var value: VALUE is writable # Default value of this option - readable writable attr _default_value: VALUE + var default_value: VALUE is writable # Create a new option - init init_opt(help: String, default: VALUE, names: Array[String]) + init(help: String, default: VALUE, names: nullable Array[String]) is old_style_init do + init_opt(help, default, names) + end + + # Init option `helptext`, `default_value` and `names`. + # + # Also set current `value` to `default`. + fun init_opt(help: String, default: VALUE, names: nullable Array[String]) do if names == null then - _names = new Array[String] + self.names = new Array[String] else - _names = names.to_a + self.names = names.to_a end - _helptext = help - _mandatory = false - _default_value = default - _value = default + helptext = help + default_value = default + value = default end # Add new aliases for this option - meth add_aliases(names: String...) do _names.add_all(names) - + fun add_aliases(names: String...) do names.add_all(names) + # An help text for this option with default settings - redef meth to_s do return pretty(2) - + redef fun to_s do return pretty(2) + # A pretty print for this help - meth pretty(off: Int): String + fun pretty(off: Int): String do - var text = new Buffer.from(" ") - text.append(_names.join(", ")) + var text = new FlatBuffer.from(" ") + text.append(names.join(", ")) text.append(" ") var rest = off - text.length if rest > 0 then text.append(" " * rest) @@ -67,163 +82,242 @@ class Option return text.to_s end - meth pretty_default: String + # Pretty print the default value. + fun pretty_default: String do - if default_value != null then - return " ({default_value})" - end + var dv = default_value + if dv != null then return " ({dv.to_s})" return "" end # Consume parameters for this option - protected meth read_param(it: Iterator[String]) is abstract + protected fun read_param(it: Iterator[String]) + do + read = true + end end +# Not really an option. Just add a line of text when displaying the usage class OptionText -special Option - init(text: String) do init_opt(text, null, null) + super Option - redef meth pretty(off) do return to_s + # Init a new OptionText with `text`. + init(text: String) is old_style_init do super(text, null, null) - redef meth to_s do return helptext + redef fun pretty(off) do return to_s + + redef fun to_s do return helptext end +# A boolean option, `true` when present, `false` if not class OptionBool -special Option + super Option redef type VALUE: Bool - init(help: String, names: String...) do init_opt(help, false, names) + # Init a new OptionBool with a `help` message and `names`. + init(help: String, names: String...) is old_style_init do super(help, false, names) - redef meth read_param(it) do value = true + redef fun read_param(it) + do + super + value = true + end end +# A count option. Count the number of time this option is present class OptionCount -special Option + super Option redef type VALUE: Int - init(help: String, names: String...) do init_opt(help, 0, names) + # Init a new OptionCount with a `help` message and `names`. + init(help: String, names: String...) is old_style_init do super(help, 0, names) - redef meth read_param(it) do value += 1 + redef fun read_param(it) + do + super + value += 1 + end end -# Option with one mandatory parameter -class OptionParameter -special Option - protected meth convert(str: String): VALUE is abstract +# Option with one parameter (mandatory by default) +abstract class OptionParameter + super Option + + # Convert `str` to a value of type `VALUE`. + protected fun convert(str: String): VALUE is abstract - redef meth read_param(it) + # Is the parameter mandatory? + var parameter_mandatory: Bool = true is writable + + redef fun read_param(it) do - assert context != null - if it.is_ok then + super + if it.is_ok and (it.item.is_empty or it.item.chars.first != '-') then value = convert(it.item) it.next else - # TODO: What to do? + if parameter_mandatory then + errors.add("Parameter expected for option {names.first}.") + end end end - - init init_opt(h, d, n) do super end +# An option with a String as parameter class OptionString -special OptionParameter - redef type VALUE: String + super OptionParameter + redef type VALUE: nullable String - init(help: String, names: String...) do init_opt(help, null, names) + # Init a new OptionString with a `help` message and `names`. + init(help: String, names: String...) is old_style_init do super(help, null, names) - redef meth convert(str) do return str + redef fun convert(str) do return str end +# An option with an enum as parameter +# In the code, declaring an option enum (-e) with an enum like `["zero", "one", "two"] +# In the command line, typing `myprog -e one` is giving 1 as value class OptionEnum -special OptionParameter + super OptionParameter redef type VALUE: Int - attr _enum: Array[String] - init(enum: Array[String], help: String, default: Int, names: String...) - do - assert enum != null and enum.length > 0 - _enum = enum.to_a - init_opt("{help} <{enum.join(", ")}>", default, names) + # Values in the enumeration. + var values: Array[String] + + # Init a new OptionEnum from `values` with a `help` message and `names`. + # + # `default` is the index of the default value in `values`. + init(values: Array[String], help: String, default: Int, names: String...) is old_style_init do + assert values.length > 0 + self.values = values.to_a + super("{help} <{values.join(", ")}>", default, names) end - redef meth convert(str) + redef fun convert(str) do - var id = _enum.index_of(str) + var id = values.index_of(str) + if id == -1 then + var e = "Unrecognized value for option {names.join(", ")}.\n" + e += "Expected values are: {values.join(", ")}." + errors.add(e) + end return id end - redef meth pretty_default + # Get the value name from `values`. + fun value_name: String do return values[value] + + redef fun pretty_default do - if default_value != null then - return " ({_enum[default_value]})" - else - return "" - end - end + return " ({values[default_value]})" + end end +# An option with an Int as parameter class OptionInt -special OptionParameter + super OptionParameter redef type VALUE: Int - init(help: String, default: Int, names: String...) do init_opt(help, default, names) - - redef meth convert(str) do return str.to_i + # Init a new OptionInt with a `help` message, a `default` value and `names`. + init(help: String, default: Int, names: String...) is old_style_init do + super(help, default, names) + end + + redef fun convert(str) do return str.to_i end +# An option with a Float as parameter +class OptionFloat + super OptionParameter + redef type VALUE: Float + + # Init a new OptionFloat with a `help` message, a `default` value and `names`. + init(help: String, default: Float, names: String...) is old_style_init do + super(help, default, names) + end + + redef fun convert(str) do return str.to_f +end + +# An option with an array as parameter +# `myprog -optA arg1 -optA arg2` is giving an Array `["arg1", "arg2"]` class OptionArray -special OptionParameter + super OptionParameter redef type VALUE: Array[String] - init(help: String, names: String...) - do - _values = new Array[String] - init_opt(help, _values, names) + # Init a new OptionArray with a `help` message and `names`. + init(help: String, names: String...) is old_style_init do + values = new Array[String] + super(help, values, names) end - attr _values: Array[String] - redef meth convert(str) + private var values: Array[String] + redef fun convert(str) do - _values.add(str) - return _values + values.add(str) + return values end end +# Context where the options process class OptionContext - readable attr _options: Array[Option] - readable attr _rest: Array[String] + # Options present in the context + var options = new Array[Option] + + # Rest of the options after `parse` is called + var rest = new Array[String] - attr _optmap: Map[String, Option] - - meth usage + # Errors found in the context after parsing + var errors = new Array[String] + + private var optmap = new HashMap[String, Option] + + # Add one or more options to the context + fun add_option(opts: Option...) do + options.add_all(opts) + end + + # Display all the options available + fun usage do var lmax = 1 - for i in _options do + for i in options do var l = 3 for n in i.names do l += n.length + 2 end if lmax < l then lmax = l end - - for i in _options do - print(i.pretty(lmax)) + + for i in options do + if not i.hidden then + print(i.pretty(lmax)) + end end end - # Parse ans assign options everywhere is the argument list - meth parse(argv: Collection[String]) + # Parse and assign options everywhere in the argument list + fun parse(argv: Collection[String]) do var it = argv.iterator parse_intern(it) end - protected meth parse_intern(it: Iterator[String]) + # Must all option be given before the first argument? + # + # When set to `false` (the default), options of the command line are + # all parsed until the end of the list of arguments or until "--" is met (in this case "--" is discarded). + # + # When set to `true` options are parsed until the first non-option is met. + var options_before_rest = false is writable + + # Parse the command line + protected fun parse_intern(it: Iterator[String]) do var parseargs = true build - var rest = _rest - + var rest = rest + while parseargs and it.is_ok do var str = it.item if str == "--" then @@ -231,39 +325,64 @@ class OptionContext rest.add_all(it.to_a) parseargs = false else - if _optmap.has_key(str) then - var opt = _optmap[str] - it.next - opt.read_param(it) + # We're looking for packed short options + if str.chars.last_index_of('-') == 0 and str.length > 2 then + var next_called = false + for i in [1..str.length[ do + var short_opt = "-" + str.chars[i].to_s + if optmap.has_key(short_opt) then + var option = optmap[short_opt] + if option isa OptionParameter then + it.next + next_called = true + end + option.read_param(it) + end + end + if not next_called then it.next else - rest.add(it.item) - it.next + if optmap.has_key(str) then + var opt = optmap[str] + it.next + opt.read_param(it) + else + rest.add(it.item) + it.next + if options_before_rest then + rest.add_all(it.to_a) + parseargs = false + end + end end end end - end - meth add_option(opts: Option...) - do - for opt in opts do - opt.context = self - _options.add(opt) + for opt in options do + if opt.mandatory and not opt.read then + errors.add("Mandatory option {opt.names.join(", ")} not found.") + end end end - init + private fun build do - _options = new Array[Option] - _optmap = new HashMap[String, Option] - _rest = new Array[String] + for o in options do + for n in o.names do + optmap[n] = o + end + end end - private meth build + # Options parsing errors. + fun get_errors: Array[String] do - for o in _options do - for n in o.names do - _optmap[n] = o + var errors = new Array[String] + errors.add_all(errors) + for o in options do + for e in o.errors do + errors.add(e) end end + return errors end end