#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# vim: set ts=2 sw=2 sts=2 et:

# ansible-inventory. An Inventory Manager for Ansible
# Copyright (C) 2016  Diego Blanco <diego.blanco@treitos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program 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 PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


# ( IMPORTS
from __future__ import print_function
from re import fullmatch
import argparse
import cmd
import configparser
import errno
import fcntl
import json
import os
import readline
import signal
import sys
import time
import uuid
from ansible_inventory.globals import VERSION, AUTHOR_NAME, AUTHOR_MAIL, URL
# )

# ( CONFIG
if os.path.islink( __file__ ):
  # We do this here and not in __main__ so we keep
  # the same behaviour even if this is used as a module
  AI_HOME = os.path.join( os.path.dirname( os.path.abspath( __file__) ), '.ansible' )
else:
  AI_HOME = os.path.join( os.environ['HOME'], '.ansible' )
CONFIG_PATH = os.path.join( AI_HOME, 'ansible-inventory.cfg' )
HISTORY_FILE = os.path.join( AI_HOME, 'ansible-inventory_history' )
BASE_CONFIG = """[global]
use_colors = True

# backend: redis, file
backend = file


[file_backend]
path = ~/.ansible/inventory.json

[redis_backend]
host =
port =
password =
inventory_name = ansible_inventory
"""
# )


# ( AUX

def check_requirements():
  if not os.path.exists( AI_HOME ):
    os.mkdir( AI_HOME )

  if not os.path.exists( CONFIG_PATH ):
    with open( CONFIG_PATH, 'w' ) as cf:
      cf.write( BASE_CONFIG )
      cf.close()

  if not os.path.exists( HISTORY_FILE ):
    open( HISTORY_FILE, 'a').close()
  readline.read_history_file( HISTORY_FILE )
  readline.set_completer_delims(' ')


def generate_colors():
  color_list = []
  for style in range( 3 ):
    for fg in range( 31, 38):
      color = ';'.join([str(style), str(fg)])
      color_list.append(color)
  return color_list


def generate_colors256():
  DARK_COLORS = list(range( 0, 8 )) + list(range( 16, 28 )) + list(range( 52, 76 )) + list(range( 232, 250 ))
  color_list = []
  for c_256_n in range( 1, 255 ):
    if c_256_n not in DARK_COLORS:
      # Grant all colors have the same string length
      if c_256_n < 10:
        c_256_s = '00%d' % c_256_n
      elif c_256_n < 100:
        c_256_s = '0%d' % c_256_n
      else:
        c_256_s = '%d' % c_256_n
      color = '38;5;' + c_256_s
      color_list.append( color )
  return color_list


def C(word, word_to_hash=None):
  if not USE_COLORS:
    return word
  if not word_to_hash:
    word_to_hash = word
  n=0
  for c in word_to_hash:
    n+=ord(c)
  n = n*word_to_hash.__len__()**2
  color = COLORS[ n % COLORS.__len__() ]
  return '\x1b[%sm%s\x1b[0m' % (color, word)


# SimpleFlock from https://github.com/derpston/python-simpleflock
class SimpleFlock:
   """Provides the simplest possible interface to flock-based file locking. Intended for use with the `with` syntax. It will create/truncate/delete the lock file as necessary."""

   def __init__(self, path, timeout = None):
      self._path = path
      self._timeout = timeout
      self._fd = None

   def __enter__(self):
      self._fd = os.open(self._path, os.O_CREAT)
      start_lock_search = time.time()
      while True:
         try:
            fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            # Lock acquired!
            return
         except (OSError, IOError) as ex:
            if ex.errno != errno.EAGAIN:  # Resource temporarily unavailable
               raise
            elif self._timeout is not None and time.time() > (start_lock_search + self._timeout):
               # Exceeded the user-specified timeout.
               raise

         # TODO It would be nice to avoid an arbitrary sleep here, but spinning
         # without a delay is also undesirable.
         time.sleep(0.1)

   def __exit__(self, *args):
      fcntl.flock(self._fd, fcntl.LOCK_UN)
      os.close(self._fd)
      self._fd = None

      # Try to remove the lock file, but don't try too hard because it is
      # unnecessary. This is mostly to help the user see whether a lock
      # exists by examining the filesystem.
      try:
         os.unlink(self._path)
      except:
         pass
# )


# ( GLOBALS
USE_COLORS = True
COLORS = generate_colors256()

C_BASE = '\x1b[1;37m'
C_FAIL = '\x1b[1;31m'
C_GOOD = '\x1b[1;32m'
C_WARN = '\x1b[1;33m'
C_INFO = '\x1b[1;34m'
C_RESET = '\x1b[0;0m'
# )


# ( CLASSES
class file_backend:
  "Backend class for ansible-inventory that uses a json file for storage"

  def __init__( self, backend_parameters ):
    self.json_path = backend_parameters
    self.lockfile = '/tmp/.ansible-inventory_file_backend.lock'
    self.__lock = SimpleFlock('/tmp/.ansible-inventory_file_backend_main.lock', timeout=3)

  def load_inventory(self):
    "Returns a dictionary with the inventory contents as required by Inventory class"
    if os.path.exists( self.json_path ):
      with SimpleFlock( self.lockfile, timeout=3 ):
        with open( self.json_path ) as inv_file:
          return json.loads( inv_file.read() )
    else:
        return {}

  def save_inventory(self, inventory):
    "Saves the inventory from a dictionary with the inventory contents from the Inventory class"
    with SimpleFlock( self.lockfile, timeout=3 ):
      with open( self.json_path, 'w' ) as inv_file:
        inv_file.write( json.dumps( inventory ) )

  def lock(self):
    "Locks the backend for reading and writting"
    self.__lock.__enter__()

  def unlock(self):
    "Unlocks the backend"
    self.__lock.__exit__()


class redis_backend:
  "Backend class for ansible-inventory that uses redis for storage"

  def __init__( self, backend_parameters ):
    host = backend_parameters['host']
    port=6379
    inventory_name = 'ansible_inventory'
    password = None
    if 'port' in backend_parameters:
      port = backend_parameters['port']
    if 'password' in backend_parameters:
      password = backend_parameters['password']
    if 'inventory_name' in backend_parameters:
      inventory_name = backend_parameters['inventory_name']

    self.r = redis.Redis( host=host, port=port, password=password )
    self.i = inventory_name
    self.uuid = str( uuid.uuid4() )
    self.__lock_name = inventory_name + '_redis_backend_lock'
    self.__timeout = 3  # seconds

  def load_inventory( self ):
    "Returns a dictionary with the inventory contents as required by Inventory class"
    i = self.r.get( self.i )
    if i:
      return json.loads( i.decode("utf-8") )
    else:
      return {}

  def save_inventory(self, inventory):
    "Saves the inventory from a dictionary with the inventory contents from the Inventory class"
    self.r.set( self.i, json.dumps( inventory ) )

  def lock(self):
    "Locks the backend for reading and writting"
    # Try to get the lock or raise exception on timeout
    t=0
    while not self.r.set( self.__lock_name, self.uuid, nx=True, px=self.__timeout*1000 ):
      if t >= self.__timeout:
        raise BlockingIOError
      time.sleep( 0.5 )
      t+=0.5

  def unlock(self):
    "Unlocks the backend"
    if self.r.get( self.__lock_name ) == self.uuid:
      self.r.delete( self.__lock_name )


class InventoryException( Exception ):

  def __init__(self, message, targets=()):
    super().__init__( message )
    if type( targets ) == str:
      self.targets = ( targets, )
    else:
      self.targets = tuple( targets )


class InventoryInfoException( InventoryException ):
  pass


class InventoryWarnException( InventoryException ):
  pass


class Inventory:

  # Inventory variable
  I = None

### Internal inventory format
#{
#          "all"   : { 'hosts':[ all hosts here ], 'children':[], 'vars',{} },
#    "databases"   : {
#            "hosts" : [ "host1.example.com", "host2.example.com" ],
#            "vars"  : {
#                  "a" : true
#            }
#    },
#    "webservers"  : [ "host2.example.com", "host3.example.com" ],
#    "atlanta"     : {
#          "hosts"   : [ "host1.example.com", "host4.example.com", "host5.example.com" ],
#          "vars"    : {
#                "b"   : false
#          },
#          "children": [ "marietta", "5points" ]
#    },
#    "marietta"    : [ "host6.example.com" ],
#    "5points"     : [ "host7.example.com" ]
#       "_meta"    : {
#         "hostvars" : {
#           "moocow.example.com"     : { "asdf" : 1234 },
#           "llama.example.com"      : { "asdf" : 5678 },
#           "xeira"                  : { "ansible_host" : xeira.example.com },
#         }
#    }
#}
  def write(f):
    "Decorator for functions that change the inventory. This should grant inventory integrity when several concurrent ansible-inventory sessions"
    def wrapper(s, *args, **kwargs):
      try:
        s.backend.lock()
        s.reload()
        r = f(s, *args, **kwargs)
        s.save()
        return r
      except BlockingIOError:
        raise InventoryException("Backend temporally unavailable. Please try again.")
      except InventoryException:
        raise
      finally:
        s.backend.unlock()

    return wrapper

  def read(f):
    "Decorator for functions that read, so they use they have the most updated information in case of several concurrent ansible-inventory sessions"
    def wrapper(s, *kargs, **kwargs):
      try:
        s.reload()
        return f(s, *kargs, **kwargs)
      except Exception as e:
        raise InventoryException( e.__str__() )
    return wrapper

  def __init__(self, backend):
    self.backend = backend
    self.reload()

  def __ensure_inventory_skel(self):
    'Ensures the basic structure of the inventory is pressent'
    if '_meta' not in self.I:
      self.I['_meta'] = {}
    if 'all' not in self.I:
      self.I['all'] = {
        'children': [],
        'hosts': [],
        'vars': {}
      }
    if 'hostvars' not in self.I['_meta']:
      self.I['_meta']['hostvars'] = {}

  def save(self):
    "Saves the inventory to persistence backend"
    self.backend.save_inventory( self.I )

  def reload(self):
    "Loads the inventory from the persistence backend"
    self.I = self.backend.load_inventory()
    self.__ensure_inventory_skel()

  def __list_hosts(self, h_regex='.*'):
    'Internal: Returns a list of known hosts in the inventory. If regex specified only matching hosts will be returned.'
    hosts = []
    for g in self.I:
      if g == '_meta':
        for h in self.I['_meta']['hostvars']:
          if h not in hosts and fullmatch( h_regex, h ):
            hosts.append( h )
      else:
        for h in self.__get_group_hosts( g ):
          if h not in hosts and fullmatch( h_regex, h ):
            hosts.append( h )
    return hosts

  def __list_groups(self, g_regex='.*'):
    'Internal: Returns a list of available groups. If g_regex is specified, only matching groups will be returned'
    groups = []
    for g in self.I:
      if g == '_meta':
        continue
      if fullmatch( g_regex, g ):
        groups.append(g)
    return groups

  def __get_group_hosts(self, group):
    'Internal: Returns a list of hosts in a group'
    if group in self.I:
      if isinstance( self.I[group], list):
        return self.I[group]
      if isinstance( self.I[group], dict ):
        return self.I[group]['hosts']
    else:
      return []

  def __get_host_groups(self, host):
    'Internal: Returns a list of groups where a host belongs'
    groups = []
    for g in self.I:
      if g == '_meta':
        continue
      if host in self.__get_group_hosts(g):
        groups.append(g)
    return groups

  def __get_group_children(self, group):
    'Internal: Returns the list of subgroups in a group'
    if group in self.I:
      if isinstance( self.I[group], dict ) and 'children' in self.I[group]:
        return self.I[group]['children']
    return []

  def __set_host_host(self, h_name, host):
    'Sets or replaces a host address for a host'
    if h_name in self.I['_meta']['hostvars']:
      self.I['_meta']['hostvars'][h_name]['ansible_host'] = host
    else:
      self.I['_meta']['hostvars'][h_name] = { 'ansible_host': host }

  def __set_host_port(self, h_name, port):
    'Sets or replaces a host address for a host'
    if h_name in self.I['_meta']['hostvars']:
      self.I['_meta']['hostvars'][h_name]['ansible_port'] = port
    else:
      self.I['_meta']['hostvars'][h_name] = { 'ansible_port': port }

  def local_list_vars( self, v_regex='.*' ):
    'Returns a list of variables in the inventory. If regex specified only matching variables will be returned. This only checks the local memory copy of the inventory.'
    i_vars = []
    for g in self.I:
      if g == '_meta':
        for h in self.I['_meta']['hostvars']:
          for v in self.I['_meta']['hostvars'][ h ]:
            if v not in i_vars and fullmatch( v_regex, v ):
              i_vars.append( v )
      else:
        for v in self.I[ g ]['vars']:
          if v not in i_vars and fullmatch( v_regex, v ):
            i_vars.append( v )
    return i_vars

  def local_list_hosts(self, h_regex='.*'):
    'Returns a list of known hosts in the inventory. If regex specified only matching hosts will be returned. This only checks the local memory copy of the inventory.'
    return self.__list_hosts( h_regex )

  def local_list_groups(self, g_regex='.*'):
    'Returns a list of available groups. If g_regex is specified, only matching groups will be returned. This only checks the local memory copy of the inventory.'
    return self.__list_groups( g_regex )

  @read
  def get_ansible_json(self):
    'Returns the ansible json'
    return json.dumps( self.I )

  @read
  def get_ansible_host_json(self, host):
    'Returns the ansible json for a host'
    if host in self.I['_meta']['hostvars']:
      return json.dumps( self.I['_meta']['hostvars'][ host ] )
    return json.dumps( {} )

  @read
  def list_hosts(self, h_regex='.*'):
    'Returns a list of known hosts in the inventory. If regex specified only matching hosts will be returned'
    return self.__list_hosts( h_regex )

  @read
  def list_groups(self, g_regex='.*'):
    'Returns a list of available groups. If g_regex is specified, only matching groups will be returned'
    return self.__list_groups( g_regex )

  @read
  def get_group_vars(self, group):
    'Returns a dict with the group vars'
    if group in self.I:
      if isinstance( self.I[group], dict):
        if 'vars' in self.I[group]:
          return self.I[group]['vars']
    return {}

  @read
  def get_group_hosts(self, group):
    'Returns a list of hosts in a group'
    return self.__get_group_hosts( group )

  @read
  def get_group_children(self, group):
    'Returns the list of subgroups in a group'
    return self.__get_group_children( group )

  @read
  def get_group_parent(self, group):
    'Returns the name of a group parent or None if no parent is found'
    for g in self.I:
      if isinstance( self.I[g], dict ) and 'children' in self.I[g] and group in self.I[g]['children']:
        return g
    return None

  @read
  def get_host_vars(self, host):
    'Returns a dict with the host vars'
    if host in self.I['_meta']['hostvars']:
      return self.I['_meta']['hostvars'][host]
    else:
      return {}

  @read
  def get_host_groups(self, host):
    'Returns a list of groups where a host belongs'
    return self.__get_host_groups( host )

  @write
  def add_hosts_to_groups(self, h_regex, g_regex):
    'Adds a hosts matching h_regex to groups matching g_regex'
    matching_hosts = self.__list_hosts( h_regex )
    if not matching_hosts:
      raise InventoryException('No host matches your selection')

    matching_groups = self.__list_groups( g_regex )
    if not matching_groups:
      raise InventoryException('No group matches your selection')

    for h_name in matching_hosts:
      for g_name in matching_groups:
        if isinstance( self.I[ g_name ], list ):
          if h_name not in self.I[ g_name ]:
            self.I[ g_name ].append( h_name )
        elif isinstance( self.I[ g_name ], dict ):
          if h_name not in self.I[ g_name ]['hosts']:
            self.I[ g_name ]['hosts'].append( h_name )

  @write
  def add_host(self, h_name, h_host=None, h_port=None ):
    'Adds a host'
    if h_name in self.__list_hosts():
      raise InventoryException('Host %s already exists', h_name)
    else:
      self.I['all']['hosts'].append( h_name )
      self.I['_meta']['hostvars'][ h_name ] = {}

    if h_host:
      self.__set_host_host( h_name, h_host )
    if h_port:
      self.__set_host_port( h_name, h_port )

  @write
  def add_group(self, group):
    'Adds a group'
    if group not in self.I:
      self.I[ group ] = {
        'hosts': [],
        'vars': {},
        'children': []
      }
    else:
      raise InventoryException('Group %s already exists', group)

  @write
  def add_group_to_groups(self, group, g_regex):
    'Adds a single group to groups matching g_regex'
    if group not in self.__list_groups():
      raise InventoryException('Group %s does not exist', targets=group)

    matching_groups = self.__list_groups( g_regex )
    if not matching_groups:
      raise InventoryException('No group matches your selection')

    for g in matching_groups:
      if isinstance( self.I[g], list):
        hosts = self.I[g]
        self.I[g] = {
          'hosts': hosts,
          'vars': {},
          'children': []
        }
      elif isinstance( self.I[g], dict) and 'children' not in self.I[g]:
        self.I[g]['children'] = []

      if group not in self.I[g]['children']:
        self.I[g]['children'].append( group )

  @write
  def add_var_to_groups(self, v_name, v_value, g_regex):
    'Adds a variable a to groups matching g_regex'
    matching_groups = self.__list_groups( g_regex )
    if not matching_groups:
      raise InventoryException('No group matches your selection')

    exist_in = []
    for g in matching_groups:
      if isinstance( self.I[g], list):
        hosts = self.I[g]
        self.I[g] = {
          'hosts': hosts,
          'vars': {},
          'children': []
        }
      elif isinstance( self.I[g], dict) and 'vars' not in self.I[g]:
        self.I[g]['vars'] = {}

      if v_name not in self.I[g]['vars']:
        self.I[g]['vars'][v_name] = v_value
      else:
        exist_in.append( g )

    if exist_in:
      str_list = '%s ' * exist_in.__len__()
      raise InventoryException('Var %s already exist in these groups: '+str_list, targets=(v_name,)+tuple( exist_in ) )

  @write
  def add_var_to_hosts(self, v_name, v_value, h_regex):
    'Adds a variable to hosts matching h_regex'
    matching_hosts = self.__list_hosts( h_regex )
    if not matching_hosts:
      raise InventoryException('No host matches your selection')

    exist_in = []
    for h in matching_hosts:
      if h not in self.I['_meta']['hostvars']:
        self.I['_meta']['hostvars'][h] = {v_name : v_value}
      elif v_name not in self.I['_meta']['hostvars'][h]:
        self.I['_meta']['hostvars'][h][v_name] = v_value
      else:
        exist_in.append( h )
    if exist_in:
      str_list = '%s ' * exist_in.__len__()
      raise InventoryException('Var %s already exist in these hosts: '+str_list, targets=(v_name,)+tuple( exist_in ) )

  @write
  def rename_host(self, h_name, new_name):
    'Renames a host'
    hosts = self.__list_hosts()
    if new_name in hosts:
      raise InventoryException('Host %s already exists', new_name)
    if h_name not in hosts:
      raise InventoryException('Host %s does not exist', h_name)

    if h_name in self.I['_meta']['hostvars']:
      hvars = self.I['_meta']['hostvars'].pop(h_name)
      self.I['_meta']['hostvars'][new_name] = hvars
    for g in self.__get_host_groups( h_name ):
      if isinstance( self.I[g], list ):
        self.I[g].remove( h_name )
        self.I[g].append( new_name )
      elif isinstance( self.I[g], dict):
        self.I[g]['hosts'].remove( h_name )
        self.I[g]['hosts'].append( new_name )

  @write
  def change_host(self, h_name, h_host=None, h_port=None):
    'Changes the host address or port of a host'
    if h_name not in self.__list_hosts():
      raise InventoryException('Host %s does not exist', h_name)
    if h_host:
      self.__set_host_host( h_name, h_host )
    if h_port:
      self.__set_host_port( h_name, h_port )

  @write
  def rename_host_var(self, v_name, new_name, h_regex):
    'Renames a variable in a set of hosts matching a regular expression'
    for h in self.__list_hosts():
      if fullmatch( h_regex, h ) and h in self.I['_meta']['hostvars'] and v_name in self.I['_meta']['hostvars'][h]:
        v_value = self.I['_meta']['hostvars'][h].pop(v_name)
        self.I['_meta']['hostvars'][h][new_name] = v_value

  @write
  def change_host_var(self, v_name, v_value, h_regex):
    'Changes the value of a variable in the hosts matching a regular expression in case it is defined'
    for h in self.__list_hosts():
      if fullmatch( h_regex, h ) and h in self.I['_meta']['hostvars'] and v_name in self.I['_meta']['hostvars'][h]:
        self.I['_meta']['hostvars'][h][v_name] = v_value

  @write
  def rename_group(self, g_name, new_name):
    'Renames a group'
    if new_name in self.I:
      raise InventoryException('Group %s already exists', new_name)
    if g_name not in self.I:
      raise InventoryException('Group %s does not exist', g_name)
    g_data = self.I.pop(g_name)
    self.I[new_name] = g_data

  @write
  def rename_group_var(self, v_name, new_name, g_regex):
    'Renames a variable in a set of groups matching a regular expression'
    for g in self.I:
      if g == '_meta':
        continue
      if fullmatch( g_regex, g ) and isinstance( self.I[g], dict) and 'vars' in self.I[g]:
        if v_name in self.I[g]['vars']:
          v_value = self.I[g]['vars'].pop(v_name)
          self.I[g]['vars'][new_name] = v_value

  @write
  def change_group_var(self, v_name, v_value, g_regex):
    'Changes the value of a variable in the groups matching a regular expression in case it is defined'
    for g in self.I:
      if g == '_meta':
        continue
      if fullmatch( g_regex, g ) and isinstance( self.I[g], dict) and 'vars' in self.I[g]:
        if v_name in self.I[g]['vars']:
          self.I[g]['vars'][v_name] = v_value

  @write
  def remove_host(self, h_name, from_groups=[]):
    'Removes the selected host. If from_groups is provided, the host will only removed from those groups.'
    if from_groups:
      groups = from_groups
    else:
      groups = self.__get_host_groups( h_name )
      if h_name in self.I['_meta']['hostvars']:
        self.I['_meta']['hostvars'].pop(h_name)
    for g in groups:
      g_hosts = self.__get_group_hosts( g )
      if g_hosts and h_name in g_hosts:
        g_hosts.remove( h_name )

  @write
  def remove_group(self, g_name, from_groups=[]):
    'Removes the selected group. If from_groups is provided, the group will only removed from those groups.'
    if from_groups:
      for g in from_groups:
        g_child = self.__get_group_children( g )
        if g_name in g_child:
          g_child.remove( g_name )
    else:
      if g_name == 'all':
        raise InventoryException('Group %s cannot be removed', 'all')
      for g in self.I:
        g_child = self.__get_group_children( g )
        if g_name in g_child:
          g_child.remove( g_name )
      if g_name in self.I:
        self.I.pop( g_name )
      else:
        raise InventoryException('Group %s does not exist.', g_name)

  @write
  def remove_host_var(self, v_name, h_name ):
    'Removes a variable from a host'
    if h_name in self.I['_meta']['hostvars'] and v_name in self.I['_meta']['hostvars'][h_name]:
      self.I['_meta']['hostvars'][h_name].pop( v_name )

  @write
  def remove_group_var(self, v_name, g_name):
    'Removes a variable from a group'
    if g_name in self.I and 'vars' in self.I[g_name] and v_name in self.I[g_name]['vars']:
      self.I[g_name]['vars'].pop( v_name )


class Console(cmd.Cmd):

  intro = """
Welcome to %s.

 Author: %s %s
Project: %s

Type 'help' for help.""" % ( C_BASE+'Ansible Inventory ' + VERSION,
                             C_RESET+AUTHOR_NAME, '<'+AUTHOR_MAIL+'>'+C_BASE,
                             C_RESET+URL )

  if USE_COLORS:
    # We use \001 and \002 to delimit non printable characters so the history completion is not messed up
    prompt = '\n\001'+C_BASE+"\002¬¬ \001"+C_RESET+'\002'
  else:
    prompt = '\n¬¬ '
  file = None

  allowed_commands = ['add', 'show', 'edit', 'del', 'help', 'exit' ]

  def console_handler( f ):
    def wrapper(s, arg ):
      # parse args
      args={ 0: f.__name__.split('do_')[1] }
      i=0
      for a in arg.split():
        i+=1
        if i>0 and '=' in a:
          args[i] = tuple(a.split('=', 1))
        else:
          args[i] = a

      # handle exceptions
      try:
        return f(s, args)
      except InventoryException as e:
        targets = tuple( C(x) for x in e.targets )
        s.__error( e.__str__() % targets )
      except InventoryWarnException as e:
        targets = tuple( C(x) for x in e.targets )
        s.__warn( e.__str__() % targets )
      except InventoryInfoException as e:
        targets = tuple( C(x) for x in e.targets )
        s.__info( e.__str__() % targets )
    wrapper.__doc__ = f.__doc__
    return wrapper

  def __init__(self, inventory):
    super(Console, self).__init__()
    self.inventory = inventory
    signal.signal(signal.SIGINT, self.__signal_sigint)

  def emptyline(self):
    "Override the default from cmd.Cmd that executes last command instead"
    pass

  def __close(self):
    "Close shell file"
    if self.file:
      self.file.close()
      self.file = None

  def __signal_sigint(self, signal, frame):
    "Catch sigint and exit cleanly"
    print('')
    self.do_exit(None)
    sys.exit(0)

  def __validate_args(self, args):
    "Validates the commands and args sent to the shell"

    # Get number of arguments
    n_args = args.keys().__len__()

    # Create initial structure to clasify positional and optional arguments
    args_dict = {
      'positional': {},
      'optional': {}
    }

    pos_i=0
    for i in range( 0, n_args ):
      if isinstance(args[i], tuple):
        args_dict['optional'][ args[i][0] ] = args[i][1]
      else:
        args_dict['positional'][ pos_i ] = args[i]
        pos_i+=1

    # Main command
    cmd = args_dict['positional'][0]

    # Check number of arguments
    if cmd == 'show':
      if n_args < 2:
        return 'Not enough arguments'
    else:
      if n_args < 3:
        return 'Not enough arguments'

    # Get subcommand (l2cmd) and check it
    if args_dict['positional'].__len__() < 2:
        return 'No subcommand provided'
    l2cmd = args_dict['positional'][1]
    if not isinstance(l2cmd, str) or l2cmd not in ('host', 'group', 'var'):
      if cmd == 'show' and l2cmd not in ('host', 'group', 'hosts', 'groups', 'tree'):
        return 'Wrong subcommand: %s' % l2cmd

    # Convert the 3rd positional argument in the "name" optional argument
    if args_dict['positional'].__len__() == 3 and cmd+l2cmd != 'addvar':
      args[2] = ('name', args_dict['positional'][2])
      return self.__validate_args( args )

    # Check the number of positional arguments
    if args_dict['positional'].__len__() > 3:
      return 'Too many positional arguments'

    # Initialize the posible options per command+subcommand and the posible option combinations
    l2cmd_opts = []
    l2cmd_combs = []
    if cmd == 'add':
      if l2cmd == 'host':
        l2cmd_opts = ['name', 'host', 'to_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'host'],
          ['name', 'to_groups'],
        ]
      elif l2cmd == 'group':
        l2cmd_opts = ['name', 'to_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'to_groups']
        ]
      elif l2cmd == 'var':
        l2cmd_opts = [ args[2][0], 'to_hosts', 'to_groups']
        l2cmd_combs = [
          [ args[2][0], 'to_hosts'],
          [ args[2][0], 'to_groups']
        ]

    elif cmd == 'edit':
      if l2cmd == 'host':
        l2cmd_opts = ['name', 'new_name', 'new_host']
        l2cmd_combs = [
          ['name', 'new_name'],
          ['name', 'new_host']
        ]
      elif l2cmd == 'group':
        l2cmd_opts = ['name', 'new_name']
        l2cmd_combs = [
          ['name', 'new_name']
        ]
      elif l2cmd == 'var':
        l2cmd_opts = ['name', 'new_name', 'new_value', 'in_hosts', 'in_groups']
        l2cmd_combs = [
          ['name', 'new_name', 'in_hosts'],
          ['name', 'new_name', 'in_groups'],
          ['name', 'new_value', 'in_hosts'],
          ['name', 'new_value', 'in_groups']
        ]

    elif cmd == 'del':
      if l2cmd == 'host':
        l2cmd_opts = ['name', 'from_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'from_groups'],
        ]
      elif l2cmd == 'group':
        l2cmd_opts = ['name', 'from_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'from_groups']
        ]
      elif l2cmd == 'var':
        l2cmd_opts = ['name', 'from_hosts', 'from_groups']
        l2cmd_combs = [
          ['name', 'from_hosts'],
          ['name', 'from_groups']
        ]

    elif cmd == 'show':
      if l2cmd in ['host', 'hosts']:
        l2cmd_opts = ['name']
        l2cmd_combs = [
          [],
          ['name'],
        ]
      elif l2cmd in ['group', 'groups']:
        l2cmd_opts = ['name']
        l2cmd_combs = [
          [],
          ['name']
        ]
      elif l2cmd in ['tree']:
        l2cmd_opts = ['name']
        l2cmd_combs = [
          ['name']
        ]

    else:
      return 'Wrong command: %s' % cmd

    if not l2cmd_opts and not l2cmd_combs:
      return 'Wrong subcommand %s' % l2cmd

    # Check optional arguments
    for a in args_dict['optional']:
      if a not in l2cmd_opts:
        return 'Invalid argument %s' % a
    valid=False
    pos_args = list(args_dict['optional'].keys())
    pos_args.sort()
    for c in l2cmd_combs:
      c.sort()
      if pos_args == c:
        valid=True
    if not valid:
      return 'Invalid arguments'

    # Return the args_dict with the optional and positional arguments
    return args_dict

  def precmd(self, line):
    if line == 'EOF':
      print('')
      self.do_exit(None)
      sys.exit(0)
    if line and line.split() and line.split()[0] not in self.allowed_commands:
      self.__error('Invalid command')
      return ''
    return line

  def __ok(self, msg):
    if USE_COLORS:
      print(C_BASE+"v   "+C_GOOD+'ok '+C_BASE+msg+C_RESET)
    else:
      print("v   ok "+msg)
    print('')

  def __info(self, msg):
    if USE_COLORS:
      print(C_BASE+"-   "+C_INFO+'info '+C_BASE+msg+C_RESET)
    else:
      print("-   info "+msg)
    print('')

  def __warn(self, msg):
    if USE_COLORS:
      print(C_BASE+"~   "+C_WARN+'warning '+C_BASE+msg+C_RESET)
    else:
      print("~   warning "+msg)
    print('')

  def __error(self, msg):
    if USE_COLORS:
      print(C_BASE+"^   "+C_FAIL+'error '+C_BASE+msg+C_RESET)
    else:
      print("^   error "+msg)
    print('')

  def __confirm(self, msg):
    if USE_COLORS:
      print(C_BASE+"·   "+C_INFO+'confirm '+C_BASE+msg+' [N/y]: '+C_RESET, end='')
    else:
      print("·   confirm "+msg+' [N/y]: ', end='')
    sys.stdout.flush()
    answer = sys.stdin.readline()
    if answer.lower().strip( '\n' ) in ('y', 'yes'):
      return True
    return False

  def do_exit(self, arg):
    'Exists Ansible Inventory console'
    readline.write_history_file( HISTORY_FILE )
    self.__close()
    self.__info('Deica logo!')
    return True

  def __print_group_tree(self, group, level=0, preline='', last_node=False):
      hosts = self.inventory.get_group_hosts( group )
      hosts.sort()
      if hosts:
        last_host = hosts[-1]
      child = self.inventory.get_group_children( group )
      child.sort()
      if child:
        last_child = child[-1]

      if hosts or child:
        g_fork = '┬'
      else:
        g_fork = '─'

      if last_node:
        g_link = '╰'
      else:
        g_link = '├'

      if level == 0:
        print('  ╭─'+'─'*group.__len__()+'─╮')
        print(' ╭┤ %s │' % C( group ))
        print(' │╰─'+'─'*group.__len__()+'─╯')
      else:
        if last_node:
          print( preline +'\b\b │' )
        else:
          print( preline )
        print( preline, end='' )
        print('\b\b %s' % g_link, end='')
        print('─%s─[%s]' % (g_fork, C( group )) )

      for h in hosts:
        print( preline, end='')
        if h == last_host and not child:
          print(' ╰', end='' )
        else:
          print(' ├', end='' )
        print('╴%s' % C( h ))

      for g in child:
        if last_child == g:
          prel = preline + '  '
        else:
          prel = preline + ' │'
        self.__print_group_tree( g, level+1, preline=prel, last_node=last_child==g)

  @console_handler
  def do_show(self, args):
    """
    show host(s) [[name=]HOST_REGEX]
    show group(s) [[name=]GROUPS_REGEX]
    show tree <[name=]GROUP>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('show')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    name = None
    if 'name' in args_opt:
      name = args_opt['name']

    # SHOW HOST
    if args_pos[1] in ['host', 'hosts']:
      if name:
        hosts = self.inventory.list_hosts( name )
      else:
        hosts = self.inventory.list_hosts()

      if not hosts:
        self.__warn('No host matched')
        return False

      max_n_len=0
      for n in hosts:
        cn = C(n)
        if cn.__len__() > max_n_len:
          max_n_len = cn.__len__()
      hosts.sort()

      host_line = ' ╭╴%%-%ds ' % max_n_len

      self.__info('Here is your hosts list')
      for host in hosts:
        print( host_line % C(host), end='' )

        print('[ ', end='')
        h_vars =  self.inventory.get_host_vars( host )
        h_vars_keys = []
        for v in h_vars:
          h_vars_keys.append( v )
        h_vars_keys.sort()
        for v in h_vars_keys:
          print( '%s=%s ' % (C(v), h_vars[v]), end='')
        print(']')

        print(' ╰──╴groups╶( ', end='')
        groups = self.inventory.get_host_groups( host )
        groups.sort()
        for g in groups:
          print( C(g)+' ', end='')
        print(')\n')

    # SHOW GROUP
    if args_pos[1] in ['group', 'groups']:
      if name:
        groups = self.inventory.list_groups(name)
      else:
        groups = self.inventory.list_groups()

      if not groups:
        self.__warn('No group matched')
        return False

      max_n_len=0
      for n in groups:
        cn = C(n)
        if cn.__len__() > max_n_len:
          max_n_len = cn.__len__()
      groups.sort()

      group_line = ' ╭╴%%-%ds [ ' % max_n_len

      self.__info('Here is your groups list')
      for n in groups:
        print( group_line % C(n), end='' )
        g_vars =  self.inventory.get_group_vars( n )
        for v in g_vars:
          print( '%s=%s ' % (C(v), g_vars[v]), end='')
        print(']')

        print(' ├──╴hosts╶( ', end='')
        for c in self.inventory.get_group_hosts( n ):
          print( C(c)+' ', end='')
        print(')')

        print(' ╰──╴child╶{ ', end='')
        for h in self.inventory.get_group_children( n ):
          print( C(h)+' ', end='')
        print('}\n')

    # SHOW TREE
    if args_pos[1] == 'tree':
      if name not in self.inventory.list_groups(name):
        self.__warn('No group matched')
        return False
      self.__info('Here is the group tree for %s' % C( name ))
      self.__print_group_tree( name )

  @console_handler
  def do_add(self, args):
    """
    add host  <[name=]HOST> [host=<host>[:<host_port>]]
    add host  <[name=]HOST_REGEX> <to_groups=GROUP_REGEX_LIST>
    add group <[name=]GROUP> [to_groups=GROUP_REGEX_LIST]
    add var   <VAR_NAME=VAR_VALUE> <to_hosts=HOST_REGEX_LIST>
    add var   <VAR_NAME=VAR_VALUE> <to_groups=GROUP_REGEX_LIST>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('add')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    if 'to_groups' in args_opt:
      to_groups = args_opt['to_groups'].split(',')
    else:
      to_groups = []

    if 'to_hosts' in args_opt:
      to_hosts = args_opt['to_hosts'].split(',')
    else:
      to_hosts = []

    # ADD HOST
    if args_pos[1] == 'host':
      name = args_opt['name']
      host = None
      port = None

      if 'host' in args_opt:
        if ':' in args_opt['host']:
          host, port = args_opt['host'].split(':')
        else:
          host = args_opt['host']

      if not to_groups:
        self.inventory.add_host( name, host, port )
        self.__ok('Host %s added' % C(name))

      # ADD HOST: TO_GROUPS
      else:
        for g_regex in to_groups:
          self.inventory.add_hosts_to_groups( name, g_regex )
        self.__ok('Host %s added to groups' % C(name))

    # ADD GROUP
    elif args_pos[1] == 'group':
      group = args_opt['name']

      if to_groups:
        for g_regex in to_groups:
          self.inventory.add_group_to_groups( group, g_regex )
        self.__ok('Group %s added to groups' % C(group))
      else:
        self.inventory.add_group( group )
        self.__ok('Group %s added' % C(group))

    # ADD VAR
    elif args_pos[1] == 'var':
      for v in args_opt:
        if v not in ['to_hosts', 'to_groups']:
          v_name = v
          v_value = args_opt[v]

      if to_groups:
        for g_regex in to_groups:
          self.inventory.add_var_to_groups( v_name, v_value, g_regex )
        self.__ok('Var %s added to groups with value %s' % ( C(v_name), C(v_value) ) )

      if to_hosts:
        for h_regex in to_hosts:
          self.inventory.add_var_to_hosts( v_name, v_value, h_regex )
        self.__ok('Var %s added to hosts with value %s' % ( C(v_name), C(v_value) ) )

  @console_handler
  def do_edit(self, args):
    """
    edit host  <[name=]HOST> <new_name=NEW_NAME>
    edit host  <[name=]HOST> <new_host=NEW_HOST>
    edit group <[name=]GROUP> <new_name=NEW_NAME>
    edit var   <[name=]VAR_NAME> <new_name=NEW_NAME> <[in_hosts=HOST_REGEX_LIST | in_groups=GROUP_REGEX_LIST]>
    edit var   <[name=]VAR_NAME> <new_value=NEW_VALUE> <[in_hosts=HOST_REGEX_LIST | in_groups=GROUP_REGEX_LIST]>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('edit')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    if 'in_groups' in args_opt:
      in_groups = args_opt['in_groups'].split(',')
    else:
      in_groups = []

    if 'in_hosts' in args_opt:
      in_hosts = args_opt['in_hosts'].split(',')
    else:
      in_hosts = []

    # EDIT HOST
    if args_pos[1] == 'host':
      name = args_opt['name']

      if 'new_name' in args_opt:
        msg = self.inventory.rename_host( name, args_opt['new_name'] )
        if msg:
          self.__warn(msg)
        else:
          self.__ok('Host %s renamed to %s' % ( C(name), C(args_opt['new_name'])))

      if 'new_host' in args_opt:
        if ':' in args_opt['new_host']:
          host, port = args_opt['new_host'].split(':')
        else:
          host = args_opt['new_host']
          port = None

        self.inventory.change_host( name, host, port )
        self.__ok('Host address for %s changed to %s' % ( C(name), C(args_opt['new_host'])))

    # EDIT GROUP
    if args_pos[1] == 'group':
      g = args_opt['name']
      msg = self.inventory.rename_group( g, args_opt['new_name'] )
      if msg:
        self.__info(msg)
      else:
        self.__ok('Group %s renamed to %s' % (g, args_opt['new_name']))

    # EDIT VAR
    if args_pos[1] == 'var':
      v_name = args_opt['name']
      new_name = None
      new_value = None
      if 'new_name' in args_opt:
        new_name = args_opt['new_name']
      if 'new_value' in args_opt:
        new_value = args_opt['new_value']

      if in_groups:
        if new_name:
          for g_regex in in_groups:
            self.inventory.rename_group_var(v_name, new_name, g_regex)
          self.__ok('Variable %s renamed to %s in selected groups' % (C(v_name), C(new_name)))
        if new_value:
          for g_regex in in_groups:
            self.inventory.change_group_var(v_name, new_value, g_regex)
          self.__ok('Variable %s changed to %s in selected groups' % (C(v_name), C(new_value)))

      if in_hosts:
        if new_name:
          for h_regex in in_hosts:
            self.inventory.rename_host_var(v_name, new_name, h_regex)
          self.__ok('Variable %s renamed to %s in selected hosts' % (C(v_name), C(new_name)))
        if new_value:
          for h_regex in in_hosts:
            self.inventory.change_host_var(v_name, new_value, h_regex)
          self.__ok('Variable %s changed to %s in selected hosts' % (C(v_name), C(new_value)))

  @console_handler
  def do_del(self, args):
    """
    del host  <[name=]HOST_REGEX> [from_groups=GROUP_REGEX_LIST]
    del group <[name=]GROUP_REGEX> [from_groups=GROUP_REGEX_LIST]
    del var   <[name=]VAR_NAME> <[from_hosts=HOST_REGEX_LIST | from_groups=GROUP_REGEX_LIST]>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('del')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    if 'from_groups' in args_opt:
      from_groups = args_opt['from_groups'].split(',')
    else:
      from_groups = []

    if 'from_hosts' in args_opt:
      from_hosts = args_opt['from_hosts'].split(',')
    else:
      from_hosts = []

    name = args_opt['name']

    # DEL HOST
    if args_pos[1] == 'host':
      hosts = self.inventory.list_hosts( name )
      hosts.sort()
      c_hosts = []
      for h in hosts:
        c_hosts.append( C( h ) )
      if not from_groups:
        if not hosts:
          self.__warn('Host pattern %s does not match any host' % C(name))
        elif self.__confirm('The following hosts will be permanently removed: %s.\n            Do you want to proceed?' % ', '.join( c_hosts ) ):
          for h in hosts:
            self.inventory.remove_host( h )
          self.__ok('The hosts have been removed')
        else:
          return False
      else:
        del_groups = []
        for g_regex in from_groups:
          gs = self.inventory.list_groups( g_regex )
          del_groups += gs

        self.__info('The host %s would be removed from the following groups:' % C(name))
        for g in del_groups:
          print( ' '+C(g), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          self.inventory.remove_host( name, from_groups=del_groups )
          self.__ok('Host %s removed from groups' % C(name))

    # DEL GROUP
    if args_pos[1] == 'group':
      if not from_groups:
        groups = self.inventory.list_groups( name )
        groups.sort()
        c_groups = []
        for g in groups:
          c_groups.append( C( g ) )
        if not groups:
          self.__warn('Group pattern %s does not match any group' % C(name))
        elif self.__confirm('The following groups will be permanently removed: %s.\n            Do you want to proceed?' % ', '.join( c_groups ) ):
          some_error = False
          for g in groups:
            msg = self.inventory.remove_group( g )
            if msg:
              some_error = True
              self.__warn( msg )
          if not some_error:
            self.__ok('The groups have been removed')
        else:
          return False
      else:
        del_groups = []
        for g_regex in from_groups:
          gs = self.inventory.list_groups( g_regex )
          del_groups += gs

        self.__info('The group %s would be removed from the following groups:' % C(name))
        for g in del_groups:
          print( ' '+C(g), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          self.inventory.remove_group( name, from_groups=del_groups )
          self.__ok('Group %s removed from groups' % C(name))

    # DEL VAR
    if args_pos[1] == 'var':
      if from_groups:
        del_groups = []
        for g_regex in from_groups:
          gs = self.inventory.list_groups( g_regex )
          del_groups += gs
        self.__info('The variable %s would be removed from the following groups:' % C(name))
        for g in del_groups:
          print( ' '+C(g), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          for g in del_groups:
            self.inventory.remove_group_var( name, g )
          self.__ok('Variable %s removed from groups' % C(name))
      elif from_hosts:
        del_hosts = []
        for h_regex in from_hosts:
          hs = self.inventory.list_hosts( h_regex )
          del_hosts += hs
        self.__info('The variable %s would be removed from the following hosts:' % C(name))
        for h in del_hosts:
          print( ' '+C(h), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          for h in del_hosts:
            self.inventory.remove_host_var( name, h )
          self.__ok('Variable %s removed from hosts' % C(name))

  def completedefault(self, text, line, begidx, endidx):
    current_line=line.split()
    cmd = current_line[ 0 ]
    cmd_map = {
      'add' : {
        'host' : { 'host': None, 'to_groups': None },
        'group': { 'to_groups': None },
        'var'  : { 'to_hosts': None, 'to_groups': None }
      },
      'del' : {
        'host' : { 'from_groups': None },
        'group': { 'from_groups': None },
        'var'  : { 'from_hosts': None, 'from_groups': None }
      },
      'edit': {
        'host' : { 'new_name': None, 'new_host': None },
        'group': { 'new_name': None },
        'var'  : {
          'new_name' : { 'in_hosts': None, 'in_groups': None },
          'new_value': { 'in_hosts': None, 'in_groups': None },
        }
      },
      'show': { 'host': None, 'group': None, 'tree': None },
    }

    def __comp( _kind, _text ):
      if _kind == 'host':
        return self.inventory.local_list_hosts( _text+'.*' )
      elif _kind == 'var':
        return self.inventory.local_list_vars( _text+'.*' )
      else:
        return self.inventory.local_list_groups( _text+'.*' )

    if cmd in cmd_map:
      if current_line.__len__() > 1:
        kind = current_line[ 1 ]
        if kind not in cmd_map[ cmd ].keys():
          for sc in cmd_map[ cmd ]:
            if sc.startswith( kind ):
              return [ sc+' ' ]
        else:
          if cmd != 'show':
            options = list(cmd_map[ cmd ][ kind ].keys())

          if current_line.__len__() > 2:
            target = current_line[ 2 ]
          else:
            target = None

          if current_line.__len__() > 3:
            subtarget = current_line[ 3 ]
            options = list(cmd_map[ cmd ][ kind ].keys())
          else:
            subtarget = None

          if current_line.__len__() > 4 or (current_line.__len__() == 4 and not text):
            if cmd == 'edit' and kind == 'var' and subtarget.startswith('new_'):
              options = list(cmd_map[ cmd ][ kind ][ subtarget.split('=')[0] ].keys())
              if current_line.__len__() > 4:
                subtarget = current_line[ 4 ]
              else:
                subtarget = None

          if subtarget is not None:
            if 'groups=' in text or 'hosts=' in text:
              part = text.split('=')[1].split(',')[ -1 ]
              prepart = text.rpartition(',')[0]
              if prepart:
                prepart+=','
              else:
                prepart = text.split('=')[0]+'='
              if 'hosts=' in text:
                return [ prepart+x for x in __comp('host', part) ]
              return [ prepart+x for x in __comp('group', part) ]
            else:
              opt = []
              for o in [ op+'=' for op in options ]:
                if o.startswith( text ):
                  opt.append( o )
              return opt or options

          if target is not None:
            if text:
              return __comp( kind, text )
            return options
          return __comp( kind, text )
      else:
        return list(cmd_map[ cmd ].keys())
    return []
# )


# Create needed directories and files
check_requirements()

# Read configuration
config = configparser.ConfigParser()
config.read( CONFIG_PATH )

# Parse configuration
USE_COLORS = config['global'].getboolean('use_colors')
if config['global']['backend'].lower() == 'file':
  BACKEND = file_backend
  inv_path = os.path.expanduser( config['file_backend']['path'].strip('"\'') )
  if os.path.isabs( inv_path ):
    BACKEND_PARAMS = inv_path
  else:
    BACKEND_PARAMS = os.path.join( AI_HOME, inv_path )
elif config['global']['backend'].lower() == 'redis':
  import redis
  BACKEND = redis_backend
  BACKEND_PARAMS = { 'host': config['redis_backend']['host'] }
  port = config['redis_backend']['port']
  password = config['redis_backend']['password']
  if 'inventory_name' in config['redis_backend']:
    inventory_name = config['redis_backend']['inventory_name']
  else:
    inventory_name = None
  if port:
    BACKEND_PARAMS['port'] = port
  if password:
    BACKEND_PARAMS['password'] = password
  if inventory_name:
    BACKEND_PARAMS['inventory_name'] = inventory_name


if __name__ == '__main__':
  # Parse command line arguments
  parser = argparse.ArgumentParser( description='Ansible Inventory manager' )
  group = parser.add_mutually_exclusive_group()
  group.add_argument('--import', dest='inventory', action='store',
                      help='Import an existing ansible inventory. It must be in the ansible dynamic inventory JSON format.')
  group.add_argument('--batch', dest='command', action='store',
                     help='Execute an Ansible Inventory command in batch mode. (i.e. "add host test host=1.2.3.4" ).')
  group.add_argument('--list', dest='list', action='store_true',
                     help='Used by Ansible. Dumps the current inventory.')
  group.add_argument('--host', dest='host', action='store',
                     help='Used by Ansible. Dumps the inventory information of a known host.')
  args = parser.parse_args()

  # Import
  if args.inventory:
    print(" This will overwrite the current inventory. Are you sure you want to continue? [N/y]: ", end="")
    sys.stdout.flush()
    ans = sys.stdin.readline()
    if ans.replace('\n', '').lower() in ('y', 'yes'):
      json_file_import = args.inventory
      if not os.path.exists( json_file_import ):
        print("File %s does not exist." % json_file_import)
        sys.exit( 1 )
    else:
      sys.exit( 1 )

    inventory = Inventory( file_backend( json_file_import ) )
    inventory.backend = BACKEND( BACKEND_PARAMS )
    inventory.save()
    sys.exit( 0 )

  # Load inventory and create console
  inventory = Inventory( BACKEND( BACKEND_PARAMS ) )
  console = Console( inventory )

  # Import
  if args.command:
    USE_COLORS = False
    cmd = args.command
    console.onecmd( cmd )
    sys.exit( 0 )

  # Ansible requests
  if args.list:
    print( inventory.get_ansible_json() )
    sys.exit( 0 )

  if args.host:
    host = args.host
    print( inventory.get_ansible_host_json(host) )
    sys.exit( 0 )

  # Main console loop
  console.cmdloop()
