使用Python和Click创建一个Shell命令行应用程序

10

我正在使用 click (http://click.pocoo.org/3/) 创建一个命令行应用程序,但是我不知道如何为该应用程序创建一个 shell。
假设我正在编写一个名为 test 的程序,并且我有名为 subtest1subtest2 的命令。

我已经成功在终端中运行它,例如:

$ test subtest1
$ test subtest2

但是我在想的是一个shell,这样我就可以做:

$ test  
>> subtest1  
>> subtest2

用点击操作实现这个功能,是否可行?


1
也许你可以使用prompt函数组合一些东西。 - mkrieger1
4个回答

15

使用click可以实现这一点,但click本身并没有内置支持。首先,您需要通过将invoke_without_command=True传递到组装饰器中(如此处所述),使您的组回调可在不带子命令的情况下被调用。然后,您的组回调需要实现一个REPL(交互式命令行环境)。Python标准库中有cmd框架可供使用。在其中添加click子命令需要覆盖cmd.Cmd.default,就像下面的代码片段一样。正确地处理所有细节,例如help,只需要几行代码即可完成。

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()

1
我该如何传递额外的参数给子命令,并让 Click 解析它们? - Niklas R
我知道我来晚了,但是看看我的答案,了解如何处理子命令的附加参数。(它基于这个答案,没有它我不可能做到这一步)。 - vimalloc

3
现在有一个名为click_repl的库,它可以为您完成大部分工作。我想分享一下我努力让它工作的成果。
唯一的困难是您必须将一个特定命令设置为repl命令,但我们可以重新利用@fpbhb的方法,以允许默认调用该命令,如果没有提供其他命令。
这是一个完全工作的示例,支持所有click选项,具有命令历史记录,以及能够直接调用命令而不必进入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={})

这是使用 REPL 的样子:

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

而要直接使用命令行子命令:

$ python pleasntries.py goodnight
Goodnight, moon.

2

我知道这个问题很老,但是我一直在尝试fpbhb的解决方案来支持选项。我相信这还需要更多的工作,但是这里有一个基本的示例,展示了如何实现:

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()

使用click创建交互式cli非常有帮助,但是当我尝试在组级别传递参数时遇到了困难。我尝试像这样在组级别添加一个参数:@click.argument('username')但它会抛出错误“发生类型为'IndexError'的异常,消息为:'list index out of range'”但我不明白原因。有人能帮我吗? - Raj

1
我试图做类似于OP的事情,但是带有额外的选项/嵌套子命令。使用内置的cmd模块的第一个答案在我的情况下不起作用;也许需要更多的调整.. 但是我刚刚发现click-shell。还没有机会进行全面测试,但到目前为止,它似乎正如预期地工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接