Creating a shell command line application with Python and Click
Asked Answered
P

4

10

I'm using click (http://click.pocoo.org/3/) to create a command line application, but I don't know how to create a shell for this application.
Suppose I'm writing a program called test and I have commands called subtest1 and subtest2

I was able to make it work from terminal like:

$ test subtest1
$ test subtest2

But what I was thinking about is a shell, so I could do:

$ test  
>> subtest1  
>> subtest2

Is this possible with click?

Photometer answered 11/3, 2015 at 15:18 Comment(1)
Maybe you can put together something using the prompt function.Suntan
V
16

This is not impossible with click, but there's no built-in support for that either. The first you would have to do is making your group callback invokable without a subcommand by passing invoke_without_command=True into the group decorator (as described here). Then your group callback would have to implement a REPL. Python has the cmd framework for doing this in the standard library. Making the click subcommands available there involves overriding cmd.Cmd.default, like in the code snippet below. Getting all the details right, like help, should be doable in a few lines.

import click
import cmd

class REPL(cmd.Cmd):
    def __init__(self, ctx):
        cmd.Cmd.__init__(self)
        self.ctx = ctx

    def default(self, line):
        subcommand = cli.commands.get(line)
        if subcommand:
            self.ctx.invoke(subcommand)
        else:
            return cmd.Cmd.default(self, line)

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        repl = REPL(ctx)
        repl.cmdloop()

@cli.command()
def a():
    """The `a` command prints an 'a'."""
    print "a"

@cli.command()
def b():
    """The `b` command prints a 'b'."""
    print "b"

if __name__ == "__main__":
    cli()
Verduzco answered 11/3, 2015 at 19:22 Comment(2)
How would I go about passing additional arguments to the subcommand, and have them parsed by Click?Nevski
I know I'm pretty late to this game, but check out my answer for how you might handle additional args to the subcommands. (It's based on this answer, no way I would have gotten this far without it).Matamoros
M
3

I know this is super old, but I've been working on fpbhb's solution to support options as well. I'm sure this could use some more work, but here is a basic example of how it could be done:

import click
import cmd
import sys

from click import BaseCommand, UsageError


class REPL(cmd.Cmd):
    def __init__(self, ctx):
        cmd.Cmd.__init__(self)
        self.ctx = ctx

    def default(self, line):
        subcommand = line.split()[0]
        args = line.split()[1:]

        subcommand = cli.commands.get(subcommand)
        if subcommand:
            try:
                subcommand.parse_args(self.ctx, args)
                self.ctx.forward(subcommand)
            except UsageError as e:
                print(e.format_message())
        else:
            return cmd.Cmd.default(self, line)


@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        repl = REPL(ctx)
        repl.cmdloop()


@cli.command()
@click.option('--foo', required=True)
def a(foo):
    print("a")
    print(foo)
    return 'banana'


@cli.command()
@click.option('--foo', required=True)
def b(foo):
    print("b")
    print(foo)

if __name__ == "__main__":
    cli()
Matamoros answered 7/7, 2017 at 0:31 Comment(1)
It is really helpful in creating interactive cli using click but i am stuck when i am trying to pass argument at group level. I tried adding one at group level like this @click.argument('username') but it throws error "EXCEPTION of type 'IndexError' occurred with message: 'list index out of range'" But could not understand the reason for. Can anyone help me in this.Aculeate
S
3

There is now a library called click_repl that does most of the work for you. Thought I'd share my efforts in getting this to work.

The one difficulty is that you have to make a specific command the repl command, but we can repurpose @fpbhb's approach to allow calling that command by default if another one isn't provided.

This is a fully working example that supports all click options, with command history, as well as being able to call commands directly without entering the REPL:

import click
import click_repl
import os
from prompt_toolkit.history import FileHistory

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    """Pleasantries CLI"""
    if ctx.invoked_subcommand is None:
        ctx.invoke(repl)

@cli.command()
@click.option('--name', default='world')
def hello(name):
    """Say hello"""
    click.echo('Hello, {}!'.format(name))

@cli.command()
@click.option('--name', default='moon')
def goodnight(name):
    """Say goodnight"""
    click.echo('Goodnight, {}.'.format(name))

@cli.command()
def repl():
    """Start an interactive session"""
    prompt_kwargs = {
        'history': FileHistory(os.path.expanduser('~/.repl_history'))
    }
    click_repl.repl(click.get_current_context(), prompt_kwargs=prompt_kwargs)

if __name__ == '__main__':
    cli(obj={})

Here's what it looks like to use the REPL:

$ python pleasantries.py
> hello
Hello, world!
> goodnight --name fpbhb
Goodnight, fpbhb.

And to use the command line subcommands directly:

$ python pleasntries.py goodnight
Goodnight, moon.
Stryker answered 16/5, 2018 at 16:51 Comment(0)
C
1

I was trying to do something similar to the OP, but with additional options / nested sub-sub-commands. The first answer using the builtin cmd module did not work in my case; maybe with some more fiddling.. But I did just run across click-shell. Haven't had a chance to test it extensively, but so far, it seems to work exactly as expected.

Coontie answered 1/2, 2017 at 7:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.