From 35b09d7a11abb91e4a7011b50fee424a191e18d0 Mon Sep 17 00:00:00 2001 From: Tianhao Date: Fri, 15 Mar 2024 18:13:24 +0000 Subject: [PATCH 1/6] parse user command to function call --- bin/fkCli.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/bin/fkCli.py b/bin/fkCli.py index 4d3271a..ed8db5e 100755 --- a/bin/fkCli.py +++ b/bin/fkCli.py @@ -50,6 +50,10 @@ "close": "stop", "connect": "start", } +CMD_KWARGS = { + "create": [{"-e", "--ephemeral"}, {"-s", "--sequence"}], + # add more commands format here for parsing +} fkCompleter = WordCompleter(keywords, ignore_case=True) @@ -137,6 +141,25 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): return client.session_status, client.session_id +def parse_args(cmd: str, args: List[str]): + if cmd not in CMD_KWARGS: + return args + else: + parsed_args = [] + parsed_kwargs = [False] * len(CMD_KWARGS[cmd]) + for arg in args: + is_kwarg = False + for idx, kwarg in enumerate(CMD_KWARGS[cmd]): + if arg in kwarg: + parsed_kwargs[idx] = True + is_kwarg = True + break + if not is_kwarg: + parsed_args.append(arg) + parsed_args.extend(parsed_kwargs) + return parsed_args + + @click.command() @click.argument("config", type=click.File("r")) @click.option("--port", type=int, default=-1) @@ -190,7 +213,8 @@ 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:]) + args = parse_args(cmd, cmds[1:]) + status, session_id = process_cmd(client, cmd, args) counter += 1 print("Closing...") From 38a1f26da24c8bf7f6d7019fd03665a30e7843e2 Mon Sep 17 00:00:00 2001 From: Tianhao Date: Tue, 19 Mar 2024 15:23:24 +0000 Subject: [PATCH 2/6] function call independent version --- bin/fkCli.py | 84 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/bin/fkCli.py b/bin/fkCli.py index ed8db5e..c06fee5 100755 --- a/bin/fkCli.py +++ b/bin/fkCli.py @@ -50,11 +50,33 @@ "close": "stop", "connect": "start", } -CMD_KWARGS = { - "create": [{"-e", "--ephemeral"}, {"-s", "--sequence"}], - # add more commands format here for parsing +# argument should be a string +ARGS = { + "path": "/path:str", + "data": "data:str", } +# flag should be a set of its representations (strings) +KWARGS = { + "ephemeral": {"-e", "--ephemeral"}, + "sequence": {"-s", "--sequence"}, + "boolean1": {"-b1", "--boolean1"}, + "boolean2": {"-b2", "--boolean2"}, +} + +# program can distinguish kwarg and arg by their type +CMD = { + # create /test 0x0 False False + "create": [ARGS["path"], ARGS["data"], KWARGS["ephemeral"], KWARGS["sequence"]], + # test /test False True 0x0 + "test": [ARGS["path"], KWARGS["boolean1"], KWARGS["boolean2"], ARGS["data"]], + + # add more commands format here for parsing... +} +# parse status code +PARSE_SUCCESS = 1 +PARSE_ERROR = 0 + fkCompleter = WordCompleter(keywords, ignore_case=True) @@ -141,23 +163,44 @@ 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 (type(kwarg) is set) and (arg in kwarg): + return idx + return -1 + +# print the command format and flags to user +def print_cmd_info(cmd: str): + args = "" + flags = "" + for arg in CMD[cmd]: + if type(arg) is str: + args += f"{arg} " + else: + flags += f"{arg} " + click.echo(f"Command: {args}| Flags: {flags}") + + +# parse the arguments and return the parsed arguments +# status code: 0 - success, 1 - not need to parse, 2 - error def parse_args(cmd: str, args: List[str]): - if cmd not in CMD_KWARGS: - return args + if cmd not in CMD: + return args, PARSE_SUCCESS else: - parsed_args = [] - parsed_kwargs = [False] * len(CMD_KWARGS[cmd]) - for arg in args: - is_kwarg = False - for idx, kwarg in enumerate(CMD_KWARGS[cmd]): - if arg in kwarg: - parsed_kwargs[idx] = True - is_kwarg = True - break - if not is_kwarg: - parsed_args.append(arg) - parsed_args.extend(parsed_kwargs) - return parsed_args + 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 + return parsed_args, PARSE_SUCCESS @click.command() @@ -213,8 +256,9 @@ def cli(config, port: int, verbose: str): elif cmd not in keywords: click.echo(f"Unknown command {text}") else: - args = parse_args(cmd, cmds[1:]) - status, session_id = process_cmd(client, cmd, args) + print(cmd, cmds[1:]) + parsed_args = parse_args(cmd, cmds[1:]) + status, session_id = process_cmd(client, cmd, parsed_args) counter += 1 print("Closing...") From 6fdc39bbd38e0817fba2a3b366c8f31c37c5dac3 Mon Sep 17 00:00:00 2001 From: Tianhao Date: Tue, 19 Mar 2024 23:36:42 +0000 Subject: [PATCH 3/6] add supporting cmds & user input check --- bin/fkCli.py | 115 +++++++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 49 deletions(-) diff --git a/bin/fkCli.py b/bin/fkCli.py index c06fee5..9f418fd 100755 --- a/bin/fkCli.py +++ b/bin/fkCli.py @@ -54,28 +54,38 @@ 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"}, - "boolean1": {"-b1", "--boolean1"}, - "boolean2": {"-b2", "--boolean2"}, + "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"]], - # test /test False True 0x0 - "test": [ARGS["path"], KWARGS["boolean1"], KWARGS["boolean2"], ARGS["data"]], + "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"]], +} - # add more commands format here for parsing... +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", } -# parse status code -PARSE_SUCCESS = 1 -PARSE_ERROR = 0 fkCompleter = WordCompleter(keywords, ignore_case=True) @@ -98,18 +108,28 @@ 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 + if isinstance(args[idx], bool): # convert boolean to string + args[idx] = str(args[idx]) + 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 = [] @@ -166,41 +186,35 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): # 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 (type(kwarg) is set) and (arg in kwarg): + if isinstance(kwarg, set) and (arg in kwarg): return idx return -1 -# print the command format and flags to user -def print_cmd_info(cmd: str): - args = "" - flags = "" - for arg in CMD[cmd]: - if type(arg) is str: - args += f"{arg} " - else: - flags += f"{arg} " - click.echo(f"Command: {args}| Flags: {flags}") - +PARSE_SUCCESS = 1 +PARSE_FAIL = 0 # parse the arguments and return the parsed arguments -# status code: 0 - success, 1 - not need to parse, 2 - error def parse_args(cmd: str, args: List[str]): if cmd not in CMD: - return args, PARSE_SUCCESS + return PARSE_SUCCESS, args else: - 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 + 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 - parsed_args[parse_args_idx] = args[arg_idx] - parse_args_idx += 1 - arg_idx += 1 - return parsed_args, PARSE_SUCCESS + arg_idx += 1 + except Exception as e: + return PARSE_FAIL, None + return PARSE_SUCCESS, parsed_args @click.command() @@ -256,9 +270,12 @@ def cli(config, port: int, verbose: str): elif cmd not in keywords: click.echo(f"Unknown command {text}") else: - print(cmd, cmds[1:]) - parsed_args = parse_args(cmd, cmds[1:]) - status, session_id = process_cmd(client, cmd, parsed_args) + status, parsed_args = parse_args(cmd, cmds[1:]) + if 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...") From ea897571d457927cbe8fc8af61ef3b472b9d76cc Mon Sep 17 00:00:00 2001 From: Tianhao Date: Wed, 20 Mar 2024 00:13:57 +0000 Subject: [PATCH 4/6] resolve variable name conflict --- bin/fkCli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fkCli.py b/bin/fkCli.py index 9f418fd..60f4861 100755 --- a/bin/fkCli.py +++ b/bin/fkCli.py @@ -270,8 +270,8 @@ def cli(config, port: int, verbose: str): elif cmd not in keywords: click.echo(f"Unknown command {text}") else: - status, parsed_args = parse_args(cmd, cmds[1:]) - if status == PARSE_FAIL: + 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: From 2efc3412ee157664fc8d0f852d8cf55b1308ccdd Mon Sep 17 00:00:00 2001 From: Tianhao Date: Wed, 20 Mar 2024 02:27:53 +0000 Subject: [PATCH 5/6] remove the code to convert bool to str --- bin/fkCli.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bin/fkCli.py b/bin/fkCli.py index 60f4861..1780ebc 100755 --- a/bin/fkCli.py +++ b/bin/fkCli.py @@ -116,8 +116,6 @@ def process_cmd(client: FaaSKeeperClient, cmd: str, args: List[str]): 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 - if isinstance(args[idx], bool): # convert boolean to string - args[idx] = str(args[idx]) else: if params_count != len(args): msg = f"{cmd} arguments:" @@ -137,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) @@ -145,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: From adc732bafefc6e940138631c89c84407e4cb0fb3 Mon Sep 17 00:00:00 2001 From: Tianhao Date: Wed, 20 Mar 2024 10:00:40 +0000 Subject: [PATCH 6/6] CLI parser tests --- bin/__init__.py | 0 tests/__init__.py | 0 tests/cli_parser_test.py | 19 +++++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 bin/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/cli_parser_test.py diff --git a/bin/__init__.py b/bin/__init__.py new file mode 100644 index 0000000..e69de29 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