Skip to content

Commit

Permalink
1. improve showing better hints for errors
Browse files Browse the repository at this point in the history
- show relative commands (when there're more than one)
- show detail of parameters
- support parameter types are mixed as traditional and class parameter combination e.g. string|GetLogRequest
- better doc parsing
  • Loading branch information
wjo1212 committed Nov 30, 2017
1 parent 111425f commit 6b8f9be
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 42 deletions.
30 changes: 23 additions & 7 deletions aliyunlogcli/cli_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _sort_str_dict(obj, enclosed=False):
return _get_str(obj, enclosed)


def docopt_ex(doc, usage, help=True, version=None):
def docopt_ex(doc, usage, method_param_usage, help=True, version=None):
argv = sys.argv[1:]

# support customized help
Expand All @@ -88,22 +88,38 @@ def docopt_ex(doc, usage, help=True, version=None):
except DocoptExit as ex:
# show customized error
if first_cmd == "configure":
print("Invalid parameters.\n")
print("Usage:\n" + MORE_DOCOPT_CMD)
return
elif first_cmd == "log" and len(argv) > 1:
second_cmd = argv[1]
header_printed = False
for cmd in doc.split("\n"):
if "aliyun log " + second_cmd in cmd:
print("Usage:\n" + cmd)
return
if "aliyun log " + second_cmd + " " in cmd:
if not header_printed:
print("Invalid parameters.\n")
print("Usage:")
header_printed = True

print(cmd)

if header_printed and second_cmd in method_param_usage:
print("\nOptions:")
print(method_param_usage[second_cmd])
print(GLOBAL_OPTIONS_STR)
else:
print("Unknown subcommand.")
print(usage)

print(usage)
else:
print("Unknown command.\n")
print(usage)


def main():
method_types, optdoc, usage = parse_method_types_optdoc_from_class(LogClient, LOG_CLIENT_METHOD_BLACK_LIST)
method_types, method_param_usage, optdoc, usage = parse_method_types_optdoc_from_class(LogClient, LOG_CLIENT_METHOD_BLACK_LIST)

arguments = docopt_ex(optdoc, usage, help=False, version=__version__)
arguments = docopt_ex(optdoc, usage, method_param_usage, help=False, version=__version__)
if arguments is None:
return

Expand Down
22 changes: 11 additions & 11 deletions aliyunlogcli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
('logtail_config', "Logtail Config"), ('machine', "Machine Group"), 'shard',
'cursor', ('logs|histogram', "Logs"), ('consumer|check_point', "Consumer Group"), 'shipper']

GLOBAL_OPTIONS_STR = """
Global Options:
[--access-id=<value>] : use this access id in this command
[--access-key=<value>] : use this access key in this command
[--region-endpoint=<value>] : use this endpoint in this command
[--client-name=<value>] : use this client name in configured accounts
[--jmes-filter=<value>] : filter results using JMES syntax
Refer to http://aliyun-log-cli.readthedocs.io/ for more info.
"""

USAGE_STR_TEMPLATE = """
Usage:
Expand All @@ -36,17 +46,7 @@
Subcommand:
{grouped_api}
Global Options:
[--access-id=<value>] : use this access id in this command
[--access-key=<value>] : use this access key in this command
[--region-endpoint=<value>] : use this endpoint in this command
[--client-name=<value>] : use this client name in configured accounts
[--jmes-filter=<value>] : filter results using JMES syntax
Refer to http://aliyun-log-cli.readthedocs.io/ for more info.
"""
""" + GLOBAL_OPTIONS_STR

MORE_DOCOPT_CMD = """aliyun configure <secure_id> <secure_key> <endpoint> [<client_name>]
"""
Expand Down
87 changes: 63 additions & 24 deletions aliyunlogcli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,23 @@ def normalize_system_options(arguments):
def _parse_method_cli(func):
args, option_arg_pos = _parse_method(func)

ptn = r'^\s*\:param[ \t]+(\w+)[ \t]*\:[ \t]*([^\n]*?)\s*$'
param_docs = dict(re.findall(ptn, func.__doc__ or '', re.MULTILINE))
param_usage_doc = ''

args_list = ''
for i, arg in enumerate(args):
if i >= option_arg_pos:
args_list += '[--' + arg + '=<value>] '
arg_doc = '[--' + arg + '=<value>] '
else:
args_list += '--' + arg + '=<value> '
arg_doc = '--' + arg + '=<value> '

args_list += arg_doc
param_usage_doc += arg_doc + '\t\t: ' + param_docs.get(arg, arg) + "\n"

doc = 'aliyun log ' + func.__name__ + ' ' + args_list + SYSTEM_OPTIONS_STR + '\n'
opt_doc = 'aliyun log ' + func.__name__ + ' ' + args_list + SYSTEM_OPTIONS_STR + '\n'

return doc
return opt_doc, param_usage_doc


def _to_bool(s):
Expand Down Expand Up @@ -169,12 +176,11 @@ def to_logitem_list(s):

def _request_maker(cls):
def maker(json_str):
args_list, option_arg_pos = _parse_method(cls.__init__)

if json_str.startswith('file://'):
with open(json_str[7:], "r") as f:
json_str = f.read()

args_list, option_arg_pos = _parse_method(cls.__init__)
if option_arg_pos == 0 and hasattr(cls, 'from_json'):
# there's a from json method, try to use it
try:
Expand Down Expand Up @@ -222,12 +228,27 @@ def maker(json_str):
except Exception as ex:
continue

print("*** cannot construct relative object for json {0} with cls list {1}".format(json_str, cls_args))
print("*** cannot construct relative object for json '{0}' with cls list {1}".format(json_str, cls_args))
return json_str

return maker


def _chained_method_maker(*method_list):
def maker(value):
for method in method_list:
try:
obj = method(value)
return obj
except Exception as ex:
continue

print("*** cannot construct relative object '{0}' with value '{1}'".format(str(method_list), value))
return value

return maker


def _make_log_client(to_client):
if to_client:
if to_client == LOG_CONFIG_SECTION:
Expand Down Expand Up @@ -260,14 +281,23 @@ def _make_log_client(to_client):
def _find_multiple_cls(t):
results = re.split(r"\s+or\s+|/|\||,", t.strip(), re.IGNORECASE)
find_cls = []
find_method_list = []
for t in results:
t = t.strip()
if t and t in dir(log) and inspect.isclass(getattr(log, t)):
find_cls.append(getattr(log, t))
elif t.lower() in types_maps:
find_method_list.append(types_maps.get(t.lower()))

if find_cls:
if find_cls and find_method_list:
# combine two kind of caller together
handler = _requests_maker(*find_cls)
return handler
find_method_list.append(handler)
return _chained_method_maker(*find_method_list)
elif find_cls:
return _requests_maker(*find_cls)
elif find_method_list:
return _chained_method_maker(*find_method_list)

return None

Expand All @@ -276,26 +306,26 @@ def _parse_method_params_from_doc(doc):
if not doc:
return None

ptn = r'\:type\s+(\w+)\s*\:\s*([\w\. <>]+)\s*.+?(?:\:param\s+\1\:\s*([^\n]+)\s*)?'
m = re.findall(ptn, doc, re.MULTILINE)
ptn = r'^\s*\:type[ \t]+(\w+)[ \t]*\:[ \t]*([^\n]*?)\s*$'
key_type_list = re.findall(ptn, doc, re.MULTILINE)

params = dict((k, types_maps.get(t.lower().strip(), None)) for k, t, d in m)
param_handlers = dict((k, types_maps.get(t.lower().strip(), None)) for k, t in key_type_list)

unsupported_types = [(k, t, d) for k, t, d in m if params.get(k, None) is None]
unsupported_types = [(k, t) for k, t in key_type_list if param_handlers.get(k, None) is None]
if unsupported_types:
for k, t, d in unsupported_types:
for k, t in unsupported_types:
handler = _find_multiple_cls(t)
if handler is not None:
# add to type maps
types_maps[t] = handler

# add to returned value
params[k] = handler
param_handlers[k] = handler
continue

print("***** unknown types: ", k, t, d)
print("***** unknown types: ", k, t)

return params
return param_handlers


def _match_black_list(method_name, black_list):
Expand Down Expand Up @@ -339,7 +369,7 @@ def _get_grouped_usage(method_list):
return usage.getvalue()


def parse_method_types_optdoc_from_class(cls, black_list=None):
def _get_method_list(cls, black_list=None):
if black_list is None:
black_list = (r'^_.+',)

Expand All @@ -350,23 +380,32 @@ def parse_method_types_optdoc_from_class(cls, black_list=None):
and (inspect.isfunction(m) or inspect.ismethod(m)):
method_list.append(k)

return method_list


def parse_method_types_optdoc_from_class(cls, black_list=None):
method_list = _get_method_list(cls, black_list)
params_types = {}
params_doc = {}

usage = USAGE_STR_TEMPLATE.format(grouped_api=_get_grouped_usage(method_list))
cli_usage_doc = USAGE_STR_TEMPLATE.format(grouped_api=_get_grouped_usage(method_list))

doc = 'Usage:\n'
doc += MORE_DOCOPT_CMD
opt_doc = 'Usage:\n'
opt_doc += MORE_DOCOPT_CMD

for m in method_list:
method = getattr(cls, m, None)
if method:
p = _parse_method_params_from_doc(method.__doc__)
params_types[m] = p

doc += _parse_method_cli(method)
method_opt_doc, method_usage_doc = _parse_method_cli(method)
opt_doc += method_opt_doc

params_doc[m] = method_usage_doc

doc += '\n'
return params_types, doc, usage
opt_doc += '\n'
return params_types, params_doc, opt_doc, cli_usage_doc


def _convert_args(args_values, method_types):
Expand Down

0 comments on commit 6b8f9be

Please sign in to comment.