#! /usr/bin/env ruby
#encoding: utf-8

module CLI
  # A Regexp to match a command line symbol argument.
  # Examples:   key:value /OR/ -key[:=]value /OR/ --key[=:]value
  # Requires ruby 2.0+ (conditionals)
  KeyCheck = /^(--?)?([a-zA-Z_]\w*)((?(1)(?:[:=])|(?:[:])))(?(2)(.+))/
  
  # Takes a method's argument list, and transforms the given ARGV
  # based on expected parameters.
  # 
  # @param params  - Method#parameters
  # @param arglist - ARGV or similar
  def self.slurp(params, arglist = self.argv)
    args   = []
    rest   = []
    keyreq = {}
    keyset = {}
    
    return nil if !arglist || arglist.empty?
    
    dd     = arglist.include?('--')
    dcheck = proc { dd && (arglist[0] == '--') }
    
    i = 0
    loop do
      # end of list, or end of current command
      if !arglist[i] || dcheck.call
        break
        
      # key-value argument, or switch
      elsif arglist[i] =~ KeyCheck
        keyset[$2.intern] = $4
        arglist.delete_at(i)
        next
      end
      # continue through the list
      i += 1
    end
    
    params.each_with_index do |(type, key), i|
      breakout = false
      
      case type # param type
      when :keyreq, :key
        if keyset.key?(key)
          keyreq[key] = keyset.delete(key)
        else
          # if required key argument not found, stop processing
          if type == :keyreq
            breakout = true
            break
          end
        end
      # skip :keyrest, already have the rest in keyset hash
      when :req
        # if end of list or end of current command reached ('--')
        if !arglist[0] || dcheck.call
          breakout = true
          break
        end
        
        args << arglist.shift
        
      # skip :rest, already have the rest in rest array
      when :opt
        if dcheck.call
          breakout = true
          break
        end
        args << arglist.shift if arglist[0]
      end
      
      break if breakout
    end
    
    # remove trailing -- from arglist if present
    arglist.shift if dcheck.call
    
    if args.size < params.find_all {|param| param[0] == :req }.size
      return nil
    end
    
    if !params.select {|param| param[0] == :keyreq }.all? {|param| keyreq.key?(param[1]) }
      return nil
    end
    
    return [args, rest, keyreq, keyset]
  end
  
  # Run any given method with args presented from the command line
  # @param method  - A Method, Proc, or other callable object
  # @param arglist - An Array of string objects, most likely ARGV
  def self.run(method, arglist = self.argv)
    params = method.parameters
    
    a, b, c, d = CLI.slurp(params, arglist)
    
    if !a
      raise ArgumentError, "Not enough arguments given for #{method.name.to_s}"
    end
    
    rest = !!params.find {|a| a[0] == :rest }
    keyrest = !!params.find {|a| a[0] == :keyrest }
    
    a.concat(b) if rest
    c.merge!(d) if keyrest
    
    method.call(*a, **c)
  end
  
  # Preserved arglist, for checking leftover args
  def self.argv
    @argv ||= ARGV.dup
  end
end

if $0 == __FILE__

def test(cmd, key1:, key2: false, **keyrest)
  local_variables.each {|v| puts "%10s: %30s" % [v.to_s, eval(v.to_s).inspect] }
end

if ARGV.size == 0
  ARGV.concat ["command", "key1:abc", "key3:123", "key2:true"]
end

puts "-- Result: -------------"
CLI.run(method(:test))
puts "------------------------"
puts "Remainder: #{CLI.argv}"

end # if $0 == __FILE__

__END__
Results on both Windows 8 64bit and ArchLinux x86_64, with 32bit Rubies:

> uru 216 && ruby test.rb
---> Now using ruby 2.1.6-p336 tagged as `216`
---> Result: -------------
       cmd:                      "command"
      key1:                          "abc"
      key2:                         "true"
   keyrest:                 {:key3=>"123"}
------------------------
Remainder: []

> uru 221 && ruby test.rb
---> Now using ruby 2.2.1-p85 tagged as `221`
---> Result: -------------
       cmd:                      "command"
      key1:                          "abc"
      key2:                            nil
   keyrest:                 {:key3=>"123"}
------------------------
Remainder: []

> uru 223 && ruby test.rb
---> Now using ruby 2.2.3-p173 tagged as `223`
---> Result: -------------
       cmd:                      "command"
      key1:                          "abc"
      key2:                         "true"
   keyrest:                 {:key3=>"123"}
------------------------
Remainder: []

