Skip to content
This repository has been archived by the owner on Jan 19, 2024. It is now read-only.

Result of small research about the DUNE CLI parser

Alexander Molchevsky edited this page Apr 3, 2023 · 1 revision

Problems with current CLI and parser.

  1. Set of options is flat. Most options have a set of positioned parameters. It means that it is impossible to combine two or more of such options in one command line because the parser is unable to distinguish the next option from a parameter of the previous option.

  2. The parser has no mechanism to parse automatically an option which has several required and several optional parameters. It needs to parse them manually in the application code.

  3. The parser is unable to generate a correct description of syntax of its options following BNF notation, especially for options which have several required and several optional parameters. Generally the parser doesn't support an exact number of optional parameters (from 0 to 5 for example) but only infinite.

  4. The parser does not support a number of required parameters before the optional parameters greater than one.

  5. Current implementation of the parser in DUNE is a long set of "if elif elif" constructions. Generally it is bad software design which is impossible to decompose to independent parts.

Idea to improve the parser by using subparsers.

Argparse allows linking parsers in chains by declaration of parent parser for a new parse or by creating a parser with a set of its subparsers.

I had hoped that this mechanism would solve the problems above by the following way:

  1. Add a hierarchy to the options by grouping them under commands. Make syntax of the CLI like:

    dune [global options] <command> [command specific options]

For example group node specific options under "node" command.

import argparse
parser = argparse.ArgumentParser(prog="DUNE", add_help=True)
subparsers = parser.add_subparsers(help='List of commands. Call dune <command> -h for command specific help.')

# A node command
node_parser = subparsers.add_parser('node', help='Handling nodes')

node_parser.add_argument('--print', action='store_true', help='Print list of nodes')

node_parser.add_argument('-s', '--start', nargs=1, metavar="<NODE>", help='start a new node with a given name')

node_parser.add_argument('-c', '--config', nargs=1, metavar="<CONFIG_DIR>",
                          help='optionally used with --start, a path containing the config.ini file to use')
node_parser.add_argument('--stop', metavar="<NODE>", help='stop a node with a given name')
node_parser.add_argument('--remove', metavar="<NODE>",
                          help='a node with a given name, will stop the node if running')
node_parser.add_argument('--list', action='store_true',
                          help='list all nodes available and their statuses')
node_parser.add_argument('--simple-list', action='store_true',
                          help='list all nodes available and their statuses without '
                               'formatting and unicode')
node_parser.add_argument('--set-active', metavar="<NODE>",
                          help='set a node to active status')
node_parser.add_argument('--get-active', action='store_true',
                          help='get the name of the node that is currently active')
node_parser.add_argument('--export-node', metavar=("<NODE>", "<PATH>"), nargs=2,
                          help='export state and blocks log for the given node. '
                               'PATH may be a directory or a filename with `.tgz` extension.')
node_parser.add_argument('--import-node', metavar=("<NODE>", "<PATH>"), nargs=2,
                          help='import state and blocks log to a given node'
                               'PATH *must* be a previously exported node ending in `.tgz`.')
node_parser.add_argument('--monitor', action='store_true',
                          help='monitor the currently active node')


# An account command
account_parser = subparsers.add_parser('account', help='Description of account command')

account_parser.add_argument('--create-account', nargs='+',
                          metavar='',
                          help='create an EOSIO account and an optional creator (the default is eosio)')
account_parser.add_argument('--system-newaccount', nargs='+',
                          help='create an EOSIO account with initial resources using '
                               '"cleos system newaccount" command. Optional flags are of the form: '
                               '"-- --buy-ram-bytes 3000"')

account_parser.add_argument('--opt', help='Description of opt')
account_parser.add_argument('--opt2', help='Description of opt2')

args = parser.parse_args()
parser.print_help()

Argparse generates the following help text:

dune -h
usage: DUNE [-h] {node,account} ...

positional arguments:
  {node,account}  List of commands. Call dune <command> -h for command specific help.
    node          Handling nodes
    account       Description of command2

optional arguments:
  -h, --help      show this help message and exit

dune node -h
usage: DUNE node [-h] [--print] [-s <NODE>] [-c <CONFIG_DIR>] [--stop <NODE>] [--remove <NODE>] [--list] [--simple-list] [--set-active <NODE>] [--get-active] [--export-node <NODE> <PATH>] [--import-node <NODE> <PATH>] [--monitor]

optional arguments:
  -h, --help            show this help message and exit
  --print               Print list of nodes
  -s <NODE>, --start <NODE>
                        start a new node with a given name
  -c <CONFIG_DIR>, --config <CONFIG_DIR>
                        optionally used with --start, a path containing the config.ini file to use
  --stop <NODE>         stop a node with a given name
  --remove <NODE>       a node with a given name, will stop the node if running
  --list                list all nodes available and their statuses
  --simple-list         list all nodes available and their statuses without formatting and unicode
  --set-active <NODE>   set a node to active status
  --get-active          get the name of the node that is currently active
  --export-node <NODE> <PATH>
                        export state and blocks log for the given node. PATH may be a directory or a filename with `.tgz` extension.
  --import-node <NODE> <PATH>
                        import state and blocks log to a given node PATH *must* be a previously exported node ending in `.tgz`.
  --monitor             monitor the currently active node

  1. Subparsers allows to parse their commands independently so it will allow to use the options with the same names but with different behavior in different commands.

  2. The parser will generates the correct help text and command line syntax.

After a short research I found that this approach will not work because of following reasons:

  1. Adding of the commands doesn't solve the problem with fact that The parser doesn't support several required parameters before the optional parameters.

  2. The parser doesn't generate the correct help text. Especially for options which has several required and several optional parameters.

  3. The parsed unable to generate the CLI syntax following the BNF notation.

Example:

The code:

# An account command
account_parser = subparsers.add_parser('account', help='Description of account command')
account_parser.add_argument('--create-account', nargs='+',
                          metavar='',
                          help='create an EOSIO account and an optional creator (the default is eosio)')
account_parser.add_argument('--system-newaccount', nargs='+',
                          help='create an EOSIO account with initial resources using '
                               '"cleos system newaccount" command. Optional flags are of the form: '
                               '"-- --buy-ram-bytes 3000"')
account_parser.add_argument('--opt', help='Description of opt')
account_parser.add_argument('--opt2', help='Description of opt2')

The help text:

`dune account -h`
usage: DUNE account [-h] [--create-account  [...]] [--system-newaccount SYSTEM_NEWACCOUNT [SYSTEM_NEWACCOUNT ...]] [--opt OPT] [--opt2 OPT2]

optional arguments:
  -h, --help            show this help message and exit
  --create-account  [ ...]
                        create an EOSIO account and an optional creator (the default is eosio)
  --system-newaccount SYSTEM_NEWACCOUNT [SYSTEM_NEWACCOUNT ...]
                        create an EOSIO account with initial resources using "cleos system newaccount" command. Optional flags are of the form: "-- --buy-ram-bytes 3000"
  --opt OPT             Description of opt
  --opt2 OPT2           Description of opt2

In addition calling of method parse_args() of the root parser doesn't allow to parse whole command line as expected.

Example:

import argparse

parser = argparse.ArgumentParser(prog='PROG') # root parser

parser.add_argument('--foo', action='store_true', help='foo help')

subparsers = parser.add_subparsers(help='sub-command help')

parser_a = subparsers.add_parser('a', help='a help') 
parser_a.add_argument('-a', help='arg -a help')

parser_b = subparsers.add_parser('b', help='b help')
parser_b.add_argument('-b', help='arg -b help')

parser.parse_known_args(['a', '-a', '11', 'b', '-b', '14', '--foo', '-d', '42']) # Call the root parser. I was expected to get a result of completely parsed command line, including the subparsers. 
>>> (Namespace(a='11', foo=False), ['b', '-b', '14', '--foo', '-d', '42']) # But actually we have a namespace with result of the call of the first subparser and global options,rest of the arguments are left unparsed.

Therefore IMHO the current implementation of the CLI is the best which is possible to get from argparse and adding of commands and rewriting the CLI with subparsers don't solve the actual problems.