-#!/usr/bin/env python
-
-# Copyright 2011-2012 GRNET S.A. All rights reserved.
+# Copyright 2012-2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
-# or implied, of GRNET S.A.
-
-from __future__ import print_function
-
-import gevent.monkey
-#Monkey-patch everything for gevent early on
-gevent.monkey.patch_all()
+# or implied, of GRNET S.A.command
import logging
-
-from inspect import getargspec
-from argparse import ArgumentParser, ArgumentError
+from sys import argv, exit, stdout
from os.path import basename
-from sys import exit, stdout, stderr, argv
-
-try:
- from collections import OrderedDict
-except ImportError:
- from ordereddict import OrderedDict
+from inspect import getargspec
-#from kamaki import clients
-from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
-from .config import Config #TO BE REMOVED
-from .utils import bold, magenta, red, yellow, print_list, print_dict
-from .command_tree import CommandTree
-from argument import _arguments, parse_known_args
-from .history import History
+from kamaki.cli.argument import _arguments, parse_known_args, update_arguments
+# init_parser,
+from kamaki.cli.history import History
+from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
+from kamaki.cli.errors import CLIError
-cmd_spec_locations = [
- 'kamaki.cli.commands',
- 'kamaki.commands',
- 'kamaki.cli',
- 'kamaki',
- '']
-_commands = CommandTree(name='kamaki', description='A command line tool for poking clouds')
-
-#If empty, all commands are loaded, if not empty, only commands in this list
-#e.g. [store, lele, list, lolo] is good to load store_list but not list_store
-#First arg should always refer to a group
-candidate_command_terms = []
-allow_no_commands = False
-allow_all_commands = False
-allow_subclass_signatures = False
-
-def _allow_class_in_cmd_tree(cls):
- global allow_all_commands
- if allow_all_commands:
- return True
- global allow_no_commands
- if allow_no_commands:
- return False
-
- term_list = cls.__name__.split('_')
- global candidate_command_terms
- index = 0
- for term in candidate_command_terms:
- try:
- index += 1 if term_list[index] == term else 0
- except IndexError: #Whole term list matched!
- return True
- if allow_subclass_signatures:
- if index == len(candidate_command_terms) and len(term_list) > index:
- try: #is subterm already in _commands?
- _commands.get_command('_'.join(term_list[:index+1]))
- except KeyError: #No, so it must be placed there
- return True
- return False
-
- return True if index == len(term_list) else False
-
-def command():
- """Class decorator that registers a class as a CLI command"""
-
- def decorator(cls):
- """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
-
- if not _allow_class_in_cmd_tree(cls):
- return cls
+_help = False
+_debug = False
+_verbose = False
+_colors = False
- cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
- # Generate a syntax string based on main's arguments
+def _construct_command_syntax(cls):
spec = getargspec(cls.main.im_func)
args = spec.args[1:]
n = len(args) - len(spec.defaults or ())
- required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
+ required = ' '.join('<%s>' % x\
+ .replace('____', '[:')\
+ .replace('___', ':')\
+ .replace('__', ']').\
replace('_', ' ') for x in args[:n])
- optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
+ optional = ' '.join('[%s]' % x\
+ .replace('____', '[:')\
+ .replace('___', ':')\
+ .replace('__', ']').\
replace('_', ' ') for x in args[n:])
cls.syntax = ' '.join(x for x in [required, optional] if x)
if spec.varargs:
cls.syntax += ' <%s ...>' % spec.varargs
- #store each term, one by one, first
- _commands.add_command(cls.__name__, cls.description, cls)
- return cls
- return decorator
-def _update_parser(parser, arguments):
- for name, argument in arguments.items():
- try:
- argument.update_parser(parser, name)
- except ArgumentError:
- pass
-
-def _init_parser(exe):
- parser = ArgumentParser(add_help=False)
- parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
- _update_parser(parser, _arguments)
- return parser
-
-def _print_error_message(cli_err, verbose=False):
- errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
- if cli_err.importance == 1:
- errmsg = magenta(errmsg)
- elif cli_err.importance == 2:
- errmsg = yellow(errmsg)
- elif cli_err.importance > 2:
- errmsg = red(errmsg)
- stdout.write(errmsg)
- if verbose and cli_err.details is not None and len(cli_err.details) > 0:
- print(': %s'%cli_err.details)
- else:
- print()
-
-def get_command_group(unparsed):
- groups = _arguments['config'].get_groups()
- for grp_candidate in unparsed:
- if grp_candidate in groups:
- unparsed.remove(grp_candidate)
- return grp_candidate
+def _get_cmd_tree_from_spec(spec, cmd_tree_list):
+ for tree in cmd_tree_list:
+ if tree.name == spec:
+ return tree
return None
-def load_command(group, unparsed, reload_package=False):
- global candidate_command_terms
- candidate_command_terms = [group] + unparsed
- pkg = load_group_package(group, reload_package)
- #From all possible parsed commands, chose the first match in user string
- final_cmd = _commands.get_command(group)
- for term in unparsed:
- cmd = final_cmd.get_subcmd(term)
- if cmd is not None:
- final_cmd = cmd
- unparsed.remove(cmd.name)
- return final_cmd
-
-def shallow_load():
- """Load only group names and descriptions"""
- global allow_no_commands
- allow_no_commands = True#load only descriptions
- for grp in _arguments['config'].get_groups():
- load_group_package(grp)
- allow_no_commands = False
-
-def load_group_package(group, reload_package=False):
- spec_pkg = _arguments['config'].value.get(group, 'cli')
- if spec_pkg is None:
- return None
- for location in cmd_spec_locations:
- location += spec_pkg if location == '' else ('.'+spec_pkg)
+_best_match = []
+
+
+def _num_of_matching_terms(basic_list, attack_list):
+ if not attack_list:
+ return len(basic_list)
+
+ matching_terms = 0
+ for i, term in enumerate(basic_list):
try:
- package = __import__(location, fromlist=['API_DESCRIPTION'])
- except ImportError:
- continue
- if reload_package:
- reload(package)
- for grp, descr in package.API_DESCRIPTION.items():
- _commands.add_command(grp, descr)
- return package
- raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
-
-def print_commands(prefix=None, full_depth=False):
- cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
- cmds = {}
- for subcmd in cmd_list:
- if subcmd.sublen() > 0:
- sublen_str = '( %s more terms ... )'%subcmd.sublen()
- cmds[subcmd.name] = [subcmd.help, sublen_str] if subcmd.has_description else subcmd_str
- else:
- cmds[subcmd.name] = subcmd.help
- if len(cmds) > 0:
- print('\nOptions:')
- print_dict(cmds, ident=12)
- if full_depth:
- _commands.pretty_print()
-
-def setup_logging(silent=False, debug=False, verbose=False, include=False):
+ if term != attack_list[i]:
+ break
+ except IndexError:
+ break
+ matching_terms += 1
+ return matching_terms
+
+
+def _update_best_match(name_terms, prefix=[]):
+ if prefix:
+ pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
+ else:
+ pref_list = []
+
+ num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
+ global _best_match
+ if not prefix:
+ _best_match = []
+
+ if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
+ if len(_best_match) < num_of_matching_terms:
+ _best_match = name_terms[:num_of_matching_terms]
+ return True
+ return False
+
+
+def command(cmd_tree, prefix='', descedants_depth=1):
+ """Load a class as a command
+ spec_cmd0_cmd1 will be command spec cmd0
+ @cmd_tree is initialized in cmd_spec file and is the structure
+ where commands are loaded. Var name should be _commands
+ @param prefix if given, load only commands prefixed with prefix,
+ @param descedants_depth is the depth of the tree descedants of the
+ prefix command. It is used ONLY if prefix and if prefix is not
+ a terminal command
+ """
+
+ def wrap(cls):
+ cls_name = cls.__name__
+
+ if not cmd_tree:
+ if _debug:
+ print('Warning: command %s found but not loaded' % cls_name)
+ return cls
+
+ name_terms = cls_name.split('_')
+ if not _update_best_match(name_terms, prefix):
+ if _debug:
+ print('Warning: %s failed to update_best_match' % cls_name)
+ return None
+
+ global _best_match
+ max_len = len(_best_match) + descedants_depth
+ if len(name_terms) > max_len:
+ partial = '_'.join(name_terms[:max_len])
+ if not cmd_tree.has_command(partial): # add partial path
+ cmd_tree.add_command(partial)
+ if _debug:
+ print('Warning: %s failed max_len test' % cls_name)
+ return None
+
+ cls.description, sep, cls.long_description\
+ = cls.__doc__.partition('\n')
+ _construct_command_syntax(cls)
+
+ cmd_tree.add_command(cls_name, cls.description, cls)
+ return cls
+ return wrap
+
+
+def get_cmd_terms():
+ global command
+ return [term for term in command.func_defaults[0]\
+ if not term.startswith('-')]
+
+cmd_spec_locations = [
+ 'kamaki.cli.commands',
+ 'kamaki.commands',
+ 'kamaki.cli',
+ 'kamaki',
+ '']
+
+
+def _setup_logging(silent=False, debug=False, verbose=False, include=False):
"""handle logging for clients package"""
def add_handler(name, level, prefix=''):
else:
add_handler('', logging.WARNING)
-def one_command():
- _debug = False
- _help = False
- _verbose = False
+
+def _init_session(arguments):
+ global _help
+ _help = arguments['help'].value
+ global _debug
+ _debug = arguments['debug'].value
+ global _verbose
+ _verbose = arguments['verbose'].value
+ global _colors
+ _colors = arguments['config'].get('global', 'colors')
+ if not (stdout.isatty() and _colors == 'on'):
+ from kamaki.cli.utils import remove_colors
+ remove_colors()
+ _silent = arguments['silent'].value
+ _include = arguments['include'].value
+ _setup_logging(_silent, _debug, _verbose, _include)
+
+
+def get_command_group(unparsed, arguments):
+ groups = arguments['config'].get_groups()
+ for term in unparsed:
+ if term.startswith('-'):
+ continue
+ if term in groups:
+ unparsed.remove(term)
+ return term
+ return None
+ return None
+
+
+def _load_spec_module(spec, arguments, module):
+ spec_name = arguments['config'].get(spec, 'cli')
+ if spec_name is None:
+ return None
+ pkg = None
+ for location in cmd_spec_locations:
+ location += spec_name if location == '' else '.%s' % spec_name
+ try:
+ pkg = __import__(location, fromlist=[module])
+ return pkg
+ except ImportError:
+ continue
+ return pkg
+
+
+def _groups_help(arguments):
+ global _debug
+ descriptions = {}
+ for spec in arguments['config'].get_groups():
+ pkg = _load_spec_module(spec, arguments, '_commands')
+ if pkg:
+ cmds = None
+ try:
+ cmds = [
+ cmd for cmd in getattr(pkg, '_commands')\
+ if arguments['config'].get(cmd.name, 'cli')
+ ]
+ except AttributeError:
+ if _debug:
+ print('Warning: No description for %s' % spec)
+ try:
+ for cmd in cmds:
+ descriptions[cmd.name] = cmd.description
+ except TypeError:
+ if _debug:
+ print('Warning: no cmd specs in module %s' % spec)
+ elif _debug:
+ print('Warning: Loading of %s cmd spec failed' % spec)
+ print('\nOptions:\n - - - -')
+ print_dict(descriptions)
+
+
+def _print_subcommands_help(cmd):
+ printout = {}
+ for subcmd in cmd.get_subcommands():
+ spec, sep, print_path = subcmd.path.partition('_')
+ printout[print_path.replace('_', ' ')] = subcmd.description
+ if printout:
+ print('\nOptions:\n - - - -')
+ print_dict(printout)
+
+
+def _update_parser_help(parser, cmd):
+ global _best_match
+ parser.prog = parser.prog.split('<')[0]
+ parser.prog += ' '.join(_best_match)
+
+ if cmd.is_command:
+ cls = cmd.get_class()
+ parser.prog += ' ' + cls.syntax
+ arguments = cls().arguments
+ update_arguments(parser, arguments)
+ else:
+ parser.prog += ' <...>'
+ if cmd.has_description:
+ parser.description = cmd.help
+
+
+def _print_error_message(cli_err):
+ errmsg = '%s' % cli_err
+ if cli_err.importance == 1:
+ errmsg = magenta(errmsg)
+ elif cli_err.importance == 2:
+ errmsg = yellow(errmsg)
+ elif cli_err.importance > 2:
+ errmsg = red(errmsg)
+ stdout.write(errmsg)
+ print_list(cli_err.details)
+
+
+def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
+ matched = [term for term in unparsed if not term.startswith('-')]
+ while matched:
+ try:
+ return cmd_tree.get_command('_'.join(matched))
+ except KeyError:
+ matched = matched[:-1]
+ return None
+
+
+def _exec_cmd(instance, cmd_args, help_method):
+ try:
+ return instance.main(*cmd_args)
+ except TypeError as err:
+ if err.args and err.args[0].startswith('main()'):
+ print(magenta('Syntax error'))
+ if _debug:
+ raise err
+ if _verbose:
+ print(unicode(err))
+ help_method()
+ else:
+ raise
+ return 1
+
+
+def set_command_param(param, value):
+ if param == 'prefix':
+ pos = 0
+ elif param == 'descedants_depth':
+ pos = 1
+ else:
+ return
+ global command
+ def_params = list(command.func_defaults)
+ def_params[pos] = value
+ command.func_defaults = tuple(def_params)
+
+
+def one_cmd(parser, unparsed, arguments):
+ group = get_command_group(list(unparsed), arguments)
+ if not group:
+ parser.print_help()
+ _groups_help(arguments)
+ exit(0)
+
+ set_command_param(
+ 'prefix',
+ [term for term in unparsed if not term.startswith('-')]
+ )
+ global _best_match
+ _best_match = []
+
+ spec_module = _load_spec_module(group, arguments, '_commands')
+
+ cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
+
+ if _best_match:
+ cmd = cmd_tree.get_command('_'.join(_best_match))
+ else:
+ cmd = _get_best_match_from_cmd_tree(cmd_tree, unparsed)
+ _best_match = cmd.path.split('_')
+ if cmd is None:
+ if _debug or _verbose:
+ print('Unexpected error: failed to load command')
+ exit(1)
+
+ _update_parser_help(parser, cmd)
+
+ if _help or not cmd.is_command:
+ parser.print_help()
+ _print_subcommands_help(cmd)
+ exit(0)
+
+ cls = cmd.get_class()
+ executable = cls(arguments)
+ parsed, unparsed = parse_known_args(parser, executable.arguments)
+ for term in _best_match:
+ unparsed.remove(term)
+ _exec_cmd(executable, unparsed, parser.print_help)
+
+
+def _load_all_commands(cmd_tree, arguments):
+ _config = arguments['config']
+ for spec in [spec for spec in _config.get_groups()\
+ if _config.get(spec, 'cli')]:
+ try:
+ spec_module = _load_spec_module(spec, arguments, '_commands')
+ spec_commands = getattr(spec_module, '_commands')
+ except AttributeError:
+ if _debug:
+ print('Warning: No valid description for %s' % spec)
+ continue
+ for spec_tree in spec_commands:
+ if spec_tree.name == spec:
+ cmd_tree.add_tree(spec_tree)
+ break
+
+
+def run_shell(exe_string, arguments):
+ from command_shell import _init_shell
+ shell = _init_shell(exe_string, arguments)
+ _load_all_commands(shell.cmd_tree, arguments)
+ shell.run(arguments)
+
+
+from kamaki.cli.argument import ArgumentParseManager
+
+
+def main():
try:
exe = basename(argv[0])
- parser = _init_parser(exe)
- parsed, unparsed = parse_known_args(parser)
- _history = History(_arguments['config'].get('history', 'file'))
- _history.add(' '.join([exe]+argv[1:]))
- _debug = _arguments['debug'].value
- _help = _arguments['help'].value
- _verbose = _arguments['verbose'].value
- if _arguments['version'].value:
- exit(0)
+ #parser = init_parser(exe, _arguments)
+ parser = ArgumentParseManager(exe)
+ parsed, unparsed = parse_known_args(parser.parser, parser.arguments)
- group = get_command_group(unparsed)
- if group is None:
- parser.print_help()
- shallow_load()
- print_commands(full_depth=_debug)
+ if _arguments['version'].value:
exit(0)
- cmd = load_command(group, unparsed)
- if _help or not cmd.is_command:
- if cmd.has_description:
- parser.description = cmd.help
- else:
- try:
- parser.description = _commands.get_closest_ancestor_command(cmd.path).help
- except KeyError:
- parser.description = ' '
- parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
- if cmd.is_command:
- cli = cmd.get_class()
- parser.prog += cli.syntax
- _update_parser(parser, cli().arguments)
- else:
- parser.prog += '[...]'
- parser.print_help()
-
- #Shuuuut, we now have to load one more level just to see what is missing
- global allow_subclass_signatures
- allow_subclass_signatures = True
- load_command(group, cmd.path.split('_')[1:], reload_package=True)
-
- print_commands(cmd.path, full_depth=_debug)
- exit(0)
+ _init_session(_arguments)
- setup_logging(silent=_arguments['silent'].value, debug=_debug, verbose=_verbose,
- include=_arguments['include'].value)
- cli = cmd.get_class()
- executable = cli(_arguments)
- _update_parser(parser, executable.arguments)
- parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
- parsed, new_unparsed = parse_known_args(parser)
- unparsed = [term for term in unparsed if term in new_unparsed]
- try:
- ret = executable.main(*unparsed)
- exit(ret)
- except TypeError as e:
- if e.args and e.args[0].startswith('main()'):
- parser.print_help()
- exit(1)
- else:
- raise
+ if unparsed:
+ _history = History(_arguments['config'].get('history', 'file'))
+ _history.add(' '.join([exe] + argv[1:]))
+ one_cmd(parser.parser, unparsed, parser.arguments)
+ elif _help:
+ parser.parser.print_help()
+ _groups_help(_arguments)
+ else:
+ run_shell(exe, _arguments)
except CLIError as err:
if _debug:
- raise
- _print_error_message(err, verbose=_verbose)
+ raise err
+ _print_error_message(err)
exit(1)
-
+ except Exception as err:
+ if _debug:
+ raise err
+ print('Unknown Error: %s' % err)