misc/vim: inform the user when no results are found
[nit.git] / lib / opts.nit
index 5f409e8..fa5ce15 100644 (file)
@@ -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: 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
 
-       # Current value of this option
-       writable attr _value: nullable VALUE
+       # 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
-       meth value: VALUE do return _value.as(VALUE)
+       var value: VALUE is writable
 
        # Default value of this option
-       readable writable attr _default_value: nullable VALUE
+       var default_value: VALUE is writable
 
        # Create a new option
-       init init_opt(help: String, default: nullable VALUE, names: nullable 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,162 +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
+
+       # Init a new OptionText with `text`.
+       init(text: String) is old_style_init do super(text, null, null)
 
-       redef meth pretty(off) do return to_s
+       redef fun pretty(off) do return to_s
 
-       redef meth to_s do return helptext
+       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
-               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
+       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.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.as(not null)]})"
-               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
@@ -230,38 +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
-                       _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