1

This code:

#!/usr/bin env python3

import click


def f(*a, **kw):
    print(a, kw)


commands = [click.Command("cmd1", callback=f), click.Command("cmd2", callback=f)]


cli = click.Group(commands={c.name: c for c in commands})

if __name__ == "__main__":
    cli()

generates this help:

# Usage: cli.py [OPTIONS] COMMAND [ARGS]...

# Options:
#   --help  Show this message and exit.

# Commands:
#   cmd1
#   cmd2

I have a lot of subcommands, so I want to divide them into sections in the help like this:

# Usage: cli.py [OPTIONS] COMMAND [ARGS]...

# Options:
#   --help  Show this message and exit.

# Commands:
#   cmd1
#   cmd2
# 
# Extra other commands:
#   cmd3
#   cmd4

How can I split the commands into sections in the help like that, without affecting the functionality?

2 Answers 2

3

If you define your own group class you can overide the help generation like:

Custom Class:

class SectionedHelpGroup(click.Group):
    """Sections commands into help groups"""

    def __init__(self, *args, **kwargs):
        self.grouped_commands = kwargs.pop('grouped_commands', {})
        commands = {}
        for group, command_list in self.grouped_commands.items():
            for cmd in command_list:
                cmd.help_group = group
                commands[cmd.name] = cmd

        super(SectionedHelpGroup, self).__init__(
            *args, commands=commands, **kwargs)

    def command(self, *args, **kwargs):
        help_group = kwargs.pop('help_group')
        decorator = super(SectionedHelpGroup, self).command(*args, **kwargs)

        def new_decorator(f):
            cmd = decorator(f)
            cmd.help_group = help_group
            self.grouped_commands.setdefault(help_group, []).append(cmd)
            return cmd

        return new_decorator

    def format_commands(self, ctx, formatter):
        for group, cmds in self.grouped_commands.items():
            rows = []
            for subcommand in self.list_commands(ctx):
                cmd = self.get_command(ctx, subcommand)
                if cmd is None or cmd.help_group != group:
                    continue
                rows.append((subcommand, cmd.short_help or ''))

            if rows:
                with formatter.section(group):
                    formatter.write_dl(rows)

Using the Custom Class:

Pass the Custom Class to click.group() using cls parameter like:

@click.group(cls=SectionedHelpGroup)
def cli():
    """"""

when defining commands, pass the help group the command belongs to like:

@cli.command(help_group='my help group')
def a_command(*args, **kwargs):
    ....        

How does this work?

This works because click is a well designed OO framework. The @click.group() decorator usually instantiates a click.Group object but allows this behavior to be over-ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride the desired methods.

In this case, we hook the command() decorator to allow the help_group to be identified. We also override the format_commands() method to print the commands help into the groups.

Test Code:

import click

def f(*args, **kwargs):
    click.echo(args, kwargs)

commands = {
    'help group 1': [
        click.Command("cmd1", callback=f),
        click.Command("cmd2", callback=f)
    ],
    'help group 2': [
        click.Command("cmd3", callback=f),
        click.Command("cmd4", callback=f)
    ]
}

cli = SectionedHelpGroup(grouped_commands=commands)

@cli.command(help_group='help group 3')
def a_command(*args, **kwargs):
    """My command"""
    click.echo(args, kwargs)


if __name__ == "__main__":
    cli(['--help'])

Results:

Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

help group 1:
  cmd1
  cmd2

help group 2:
  cmd3
  cmd4

help group 3:
  a_command  My command
Sign up to request clarification or add additional context in comments.

Comments

0

Stephen's answer above gives the general principle. If you only add new commands to a group using add_command, it can be simplified slightly:

import click
import collections


class SectionedHelpGroup(click.Group):
    """Organize commands as sections"""

    def __init__(self, *args, **kwargs):
        self.section_commands = collections.defaultdict(list)
        super().__init__(*args, **kwargs)

    def add_command(self, cmd, name=None, section=None):
        self.section_commands[section].append(cmd)
        super().add_command(cmd, name=name)

    def format_commands(self, ctx, formatter):
        for group, cmds in self.section_commands.items():
            with formatter.section(group):
                formatter.write_dl(
                    [(cmd.name, cmd.get_short_help_str() or "") for cmd in cmds]
                )

Example

def f(*args, **kwargs):
    click.echo(args, kwargs)

commands = {
    'help group 1': [
        click.Command("cmd1", callback=f),
        click.Command("cmd2", callback=f)
    ],
    'help group 2': [
        click.Command("cmd3", callback=f),
        click.Command("cmd4", callback=f)
    ]
}

@click.group(
    help=f"Sectioned Commands CLI",
    cls=SectionedHelpGroup
)
def cli():
    pass

for (section, cmds) in commands.items():
    for cmd in cmds:
        cli.add_command(cmd, section=section)

if __name__ == "__main__":
    cli()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.