moigagoo/cliar

How to create nested commands?

LexSong opened this issue · 4 comments

Take conda for example. We have commands like this:

conda create
conda list

conda env create
conda env list
conda env export

or git:

git init
git commit

git stack push
git stack pop

git remote add
git remote show

I think one way to handle this is to create subcommand class and explicitly pass args to it's parse function:

from cliar import Cliar

class StackCommand(Cliar):
    def push(self, ...):
       ...
    def pop(self, ...):
       ...
 
class MainCommand(Cliar):
    def __init__(self):
        self.stack_command = StackCommand()
    def stack(self, args):
        self.stack_command.parse(args)

Currently, the Cliar class impliedly take args from the command line.
However, I think taking args from another command may be useful to build complex CLI.

Thanks for opening the issue. The problem of multilevel subcommands has been bugging me since the very start, but I haven't come up with a clever solution so far. I'll investigate closer into your proposal, thanks!

How about something like the following?

class Env(Cliar):
    def create(self):
        ...

    def list(self):
        ...

    def export(self):
        ...


class Conda(Cliar):
    env: Env

    def create(self):
        ...

    def list(self):
        ...

Cliar's metaclass will read the class definition of Conda. It will see that it has a class variable annotated as Env, and that Env is itself a subclass of Cliar. The metaclass will then add a function to the class Conda that will do the work of directing any nested command like conda env create to an object of the Env class instead.

Of course, the nesting could be made more explicit by using descriptors.

class Conda(Cliar):
  env = Env()
  ...

For this, the Cliar class should itself support the descriptor protocol.

OK, I got it :-) I've implemented this feature in https://github.com/moigagoo/cliar/tree/feature/nested_commands

Have to fix the tests and add a new test.

The syntax will be as follows:

class Env(Cliar):
    def create(self):
        ...

    def list(self):
        ...

    def export(self):
        ...


class Conda(Cliar):
    env = Env

    def create(self):
        ...

    def list(self):
        ...