diff --git a/bin/__init__.py b/bin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bin/fkCli.py b/bin/fkCli.py index 4d3271a..1780ebc 100755 --- a/bin/fkCli.py +++ b/bin/fkCli.py @@ -50,6 +50,42 @@ "close": "stop", "connect": "start", } +# argument should be a string +ARGS = { + "path": "/path:str", + "data": "data:str", + "version": "version:int", +} + +# flag should be a set of its representations (strings) +KWARGS = { + "ephemeral": {"-e", "--ephemeral"}, + "sequence": {"-s", "--sequence"}, + "watch": {"-w", "--watch"}, + "includeData": {"-i", "--includeData"}, +} + +# program can distinguish kwarg and arg by their type +CMD = { + # create /test 0x0 False False + "create": [ARGS["path"], ARGS["data"], KWARGS["ephemeral"], KWARGS["sequence"]], + "get": [ARGS["path"], KWARGS["watch"]], + "set": [ARGS["path"], ARGS["data"], ARGS["version"]], + "delete": [ARGS["path"], ARGS["version"]], + "exists": [ARGS["path"]], + "help": [], + "getChildren": [ARGS["path"], KWARGS["includeData"]], +} + +INPUT_FORMAT = { + "create": "create -e -s /test 0x0", + "get": "get -w /test", + "set": "set /test 0x0 -1", + "delete": "delete /test -1", + "exists": "exists /test", + "help": "help", + "getChildren": "getChildren -i /test", +} fkCompleter = WordCompleter(keywords, ignore_case=True) @@ -72,18 +108,26 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): function = getattr(client, clientAPIMapping[cmd]) sig = signature(function) params_count = len(sig.parameters) - # incorrect number of parameters - if params_count != len(args): - msg = f"{cmd} arguments:" - for param in sig.parameters.values(): - # "watch" requires conversion - API uses a callback - # the CLI is a boolean switch if callback should be use or not - if param.name == "watch": - msg += f" watch:bool" - else: - msg += f" {param.name}:{param.annotation.__name__}" - click.echo(msg) - return client.session_status, client.session_id + # check incorrect number of parameters + if cmd in CMD: # check parsed arguments + # in this case, number of parsed arguments is always correct, so we need to check it type + # if an argument is missing, it will be a boolean value + for idx, param in enumerate(sig.parameters.values()): + if param.annotation != bool and isinstance(args[idx], bool) and param.name != "watch": + click.echo(f"Command Example: {INPUT_FORMAT[cmd]}") + return client.session_status, client.session_id + else: + if params_count != len(args): + msg = f"{cmd} arguments:" + for param in sig.parameters.values(): + # "watch" requires conversion - API uses a callback + # the CLI is a boolean switch if callback should be use or not + if param.name == "watch": + msg += f" watch:bool" + else: + msg += f" {param.name}:{param.annotation.__name__}" + click.echo(msg) + return client.session_status, client.session_id # convert arguments converted_arguments = [] @@ -91,7 +135,7 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): # "watch" requires conversion - API uses a callback # the CLI is a boolean switch if callback should be use or not if param.name == "watch": - if args[idx].lower() == "true": + if args[idx]: converted_arguments.append(watch_callback) else: converted_arguments.append(None) @@ -99,8 +143,6 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): if bytes == param.annotation: converted_arguments.append(args[idx].encode()) - elif bool == param.annotation: - converted_arguments.append(bool(args[idx])) else: converted_arguments.append(args[idx]) try: @@ -137,6 +179,40 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): return client.session_status, client.session_id +# find the position of the argument in the command (function call like), return -1 if the argument is not a flag +def kwarg_pos(arg: str, kwargs_array: List[str]): + for idx, kwarg in enumerate(kwargs_array): + if isinstance(kwarg, set) and (arg in kwarg): + return idx + return -1 + +PARSE_SUCCESS = 1 +PARSE_FAIL = 0 + +# parse the arguments and return the parsed arguments +def parse_args(cmd: str, args: List[str]): + if cmd not in CMD: + return PARSE_SUCCESS, args + else: + try: + assert len(args) <= len(CMD[cmd]) # check if the number of arguments is correct + parsed_args = [False] * len(CMD[cmd]) + arg_idx = parse_args_idx = 0 + while arg_idx < len(args): + idx = kwarg_pos(args[arg_idx], CMD[cmd]) + if idx != -1: + parsed_args[idx] = True + else: + while isinstance(CMD[cmd][parse_args_idx], set): # skip the positions for flags + parse_args_idx += 1 + parsed_args[parse_args_idx] = args[arg_idx] + parse_args_idx += 1 + arg_idx += 1 + except Exception as e: + return PARSE_FAIL, None + return PARSE_SUCCESS, parsed_args + + @click.command() @click.argument("config", type=click.File("r")) @click.option("--port", type=int, default=-1) @@ -190,7 +266,12 @@ def cli(config, port: int, verbose: str): elif cmd not in keywords: click.echo(f"Unknown command {text}") else: - status, session_id = process_cmd(client, cmd, cmds[1:]) + parse_status, parsed_args = parse_args(cmd, cmds[1:]) + if parse_status == PARSE_FAIL: + click.echo(f"Command Example: {INPUT_FORMAT[cmd]}") + status, session_id = client.session_status, client.session_id + else: + status, session_id = process_cmd(client, cmd, parsed_args) counter += 1 print("Closing...") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli_parser_test.py b/tests/cli_parser_test.py new file mode 100644 index 0000000..0bbd399 --- /dev/null +++ b/tests/cli_parser_test.py @@ -0,0 +1,19 @@ +import pytest + +from bin.fkCli import parse_args + +def test_cli(): + print("test create") + assert ['/test', '0x0', True, True] == parse_args("create", ["-e", "-s", "/test", "0x0"])[1] + assert ['/test', '0x0', True, True] == parse_args("create", ["/test", "0x0", "-e", "-s"])[1] + assert ['/test', '0x0', False, False] == parse_args("create", ["/test", "0x0"])[1] + assert ['/test', '0x0', True, False] == parse_args("create", ["/test", "0x0", "-e"])[1] + assert ['/test', '0x0', False, True] == parse_args("create", ["/test", "0x0", "-s"])[1] + print("test get") + assert ['/test', True] == parse_args("get", ["/test", "-w"])[1] + assert ['/test', True] == parse_args("get", ["-w", "/test"])[1] + assert ['/test', False] == parse_args("get", ["/test"])[1] + print("test getChildren") + assert ['/test', True] == parse_args("getChildren", ["/test", "-i"])[1] + assert ['/test', True] == parse_args("getChildren", ["-i", "/test"])[1] + assert ['/test', False] == parse_args("getChildren", ["/test"])[1] \ No newline at end of file