在Python生态系统中,有许多用于CLI应用程序的软件包,两个都是流行的(例如Click)软件包,而没有那么多。 在
上一篇文章中考虑了最常见的那些,但此处将显示鲜为人知但同样有趣的内容。

与第一部分一样,将为Python 3.7中的每个库编写todolib库的控制台脚本。 除此之外,将为每种实现编写使用这些固定装置的简单测试:
@pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch , """ value = {"tasks": []} monkeypatch.setattr(todolib.TodoApp, "save", lambda _: ...) monkeypatch.setattr(todolib.TodoApp, "get_db", lambda _: value) return value @pytest.yield_fixture(autouse=True) def check(db): """ """ yield assert db["tasks"] and db["tasks"][0]["title"] == "test" # , EXPECTED = "Task 'test' created with number 1.\n"
该库中提供了所有源代码。
悬崖
Github该文件许多人听说过OpenStack,它是IaaS的开源平台。 它的大多数内容都是用Python编写的,其中包括长期以来一直在重复CLI功能的控制台实用程序。 这种情况一直持续到悬崖或命令行界面制定框架作为通用框架出现为止。 借助它,Openstack开发人员将python-novaclient,python-swiftclient和python-keystoneclient等软件包组合到一个
openstack程序中。
队伍声明命令的方法类似于水泥和老套:argparse作为参数解析器,并且命令本身是通过继承Command类而创建的。 同时,Command类有一些小的扩展,例如Lister,它们独立地格式化数据。
源代码 from cliff import command from cliff.lister import Lister class Command(command.Command): """Command with a parser shortcut.""" def get_parser(self, prog_name): parser = super().get_parser(prog_name) self.extend_parser(parser) return parser def extend_parser(self, parser): ... class Add(Command): """Add new task.""" def extend_parser(self, parser): parser.add_argument("title", help="Task title") def take_action(self, parsed_args): task = self.app.todoapp.add_task(parsed_args.title) print(task, "created with number", task.number, end=".\n") class Show(Lister, Command): """Show current tasks.""" def extend_parser(self, parser): parser.add_argument( "--show-done", action="store_true", help="Include done tasks" ) def take_action(self, parsed_args): tasks = self.app.todoapp.list_tasks(show_done=parsed_args.show_done)
应用与主要应用程序类具有
initialize_app和
clean_up方法 ,在我们的示例中,它们初始化应用程序并保存数据。
源代码 from cliff import app from cliff.commandmanager import CommandManager from todolib import TodoApp, __version__ class App(app.App): def __init__(self):
工作实例 igor$ ./todo_cliff.py add "sell the old laptop" Using database file /home/igor/.local/share/todoapp/db.json Task 'sell the old laptop' created with number 0. Saving database to a file /home/igor/.local/share/todoapp/db.json
开箱即用! 而且,如果您仔细查看,您会发现它是通过巧妙的方式完成的:stdout中的信息,stderr中的警告/错误,并在必要时通过
--quiet标志禁用。
igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | ✘ | +--------+----------------------+--------+
如前所述,Lister格式化数据,但表不限于:
igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}]
除了json和table外,yaml和csv也可用。
还有一个默认的隐藏跟踪:
igor$ ./todo_cliff.py -q remove 3 No such task.
另一个可用的REPL和模糊搜索又名
模糊搜索 :
igor$ ./todo_cliff.py -q (todo_cliff) help Shell commands (type help %topic%): =================================== alias exit history py quit shell unalias edit help load pyscript set shortcuts Application commands (type help %topic%): ========================================= add complete done help remove show (todo_cliff) whow todo_cliff: 'whow' is not a todo_cliff command. See 'todo_cliff --help'. Did you mean one of these? show
测试中很简单:创建一个App对象并调用run(),它返回退出代码。
def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED
利弊优点:
- 开箱即用的各种便利设施;
- 由OpenStack开发;
- 互动模式;
- 通过setuptools入口点和CommandHook进行扩展;
- Sphinx插件,用于向CLI自动记录文档;
- 命令完成(仅bash);
缺点:
注意另一个错误:在隐藏堆栈跟踪时发生错误的情况下,退出代码始终为零。
普拉克
Github该文件乍看之下,Plac看起来像是Fire,但
实际上它就像Fire,它隐藏了相同的argparse,并且在引擎盖下隐藏了更多内容。
普拉克(Plac)引用了文档,“计算机世界的古老原则:
程序应只解决普通情况,简单应保持简单,而复杂可同时实现” 。 该框架的作者使用Python已有9年以上的时间,并以解决“ 99.9%的任务”的期望编写它。
命令和主要注意show和done方法中的注释:这是Plac分别解析参数和参数帮助的方式。
源代码 import plac import todolib class TodoInterface: commands = "add", "show", "done", "remove" def __init__(self): self.app = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.app.save() def add(self, task): """ Add new task. """ task = self.app.add_task(title=task) print(task, "created with number", task.number, end=".\n") def show(self, show_done: plac.Annotation("Include done tasks", kind="flag")): """ Show current tasks. """ self.app.print_tasks(show_done=show_done) def done(self, number: "Task number"): """ Mark task as done. """ task = self.app.task_done(number=int(number)) print(task, "marked as done.") def remove(self, number: "Task number"): """ Remove task from the list. """ task = self.app.remove_task(number=int(number)) print(task, "removed from the list.") if __name__ == "__main__": plac.Interpreter.call(TodoInterface)
测试中不幸的是,测试退出代码Plac不允许。 但是测试本身是真实的:
def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
利弊优点:
- 使用简单;
- 带有阅读线支持的交互模式;
- 稳定的API
- 优秀的文档;
但是Plac最有趣的是隐藏在
高级用法中 :
- 在线程和子进程中执行几个命令;
- 并行计算;
- telnet服务器;
缺点:
铅球
Github该文件CLI文档实际上,Plumbum并不是一个鲜为人知的框架-差不多有2000颗星,并且值得一看,因为它可以实现UNIX Shell语法。 好吧,加上添加剂:
>>> from plumbum import local >>> output = local["ls"]() >>> output.split("\n")[:3] ['console_examples.egg-info', '__pycache__', 'readme.md']
命令和主要Plumbum还具有用于CLI的工具,并不是说它们只是一种补充:有nargs,命令和颜色:
源代码 from plumbum import cli, colors class App(cli.Application): """Todo notes on plumbum.""" VERSION = todolib.__version__ verbosity = cli.CountOf("-v", help="Increase verbosity") def main(self, *args): if args: print(colors.red | f"Unknown command: {args[0]!r}.") return 1 if not self.nested_command:
测试中在Plumbum上测试应用程序与其他应用程序不同,除了需要同时传递应用程序的名称(即 第一个参数:
def test_plumbum(capsys): _, code = todo_plumbum.App.run(["todo_plumbum", "add", "test"], exit=False) assert code == 0 out, _ = capsys.readouterr() assert out == "Task test created with number 0.\n"
利弊优点:
- 与外部团队合作的优秀工具包;
- 支持样式和颜色;
- 稳定的API
- 项目的活跃寿命;
没有发现缺陷。
命令2
Github该文件首先,cmd2是标准库中
cmd的扩展,
即 它旨在用于交互式应用程序。 不过,由于它也可以配置为常规CLI模式,因此它已包含在该评论中。
命令和主要cmd2要求遵守特定规则:命令应以
do_前缀开头,但其他所有内容都很清楚:
互动模式 import cmd2 import todolib class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, title): """Add new task.""" task = self.todoapp.add_task(str(title)) self.poutput(f"{task} created with number {task.number}.") def do_show(self, show_done): """Show current tasks.""" self.todoapp.print_tasks(bool(show_done)) def do_done(self, number): """Mark task as done.""" task = self.todoapp.task_done(int(number)) self.poutput(f"{task} marked as done.") def do_remove(self, number): """Remove task from the list.""" task = self.todoapp.remove_task(int(number)) self.poutput(f"{task} removed from the list.") def main(**kwargs): with App(**kwargs) as app: app.cmdloop() if __name__ == '__main__': main()
非互动模式普通模式需要一点额外的运动。
例如,您必须返回argparse并为没有参数的情况下调用脚本编写逻辑。 现在,命令获取
argparse.Namespace 。
该解析器取自argparse示例,并带有少量附加内容-现在,子解析器是main
ArgumentParser的属性。
import cmd2 from todo_argparse import get_parser parser = get_parser(progname="todo_cmd2_cli") class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, args): """Add new task.""" task = self.todoapp.add_task(args.title) self.poutput(f"{task} created with number {task.number}.") def do_show(self, args): """Show current tasks.""" self.todoapp.print_tasks(args.show_done) def do_done(self, args): """Mark task as done.""" task = self.todoapp.task_done(args.number) self.poutput(f"{task} marked as done.") def do_remove(self, args): """Remove task from the list.""" task = self.todoapp.remove_task(args.number) self.poutput(f"{task} removed from the list.") parser.add.set_defaults(func=do_add) parser.show.set_defaults(func=do_show) parser.done.set_defaults(func=do_done) parser.remove.set_defaults(func=do_remove) @cmd2.with_argparser(parser) def do_base(self, args): func = getattr(args, "func", None) if func: func(self, args) else: print("No command provided.") print("Call with --help to get available commands.") def main(argv=None): with App() as app: app.do_base(argv or sys.argv[1:]) if __name__ == '__main__': main()
测试中仅交互式脚本将被测试。由于测试交互式应用程序需要大量的人力和时间资源,因此cmd2开发人员试图在
转录的帮助下解决此问题-带有输入和预期输出示例的文本文件。 例如:
(Cmd) add test Task 'test' created with number 0.
因此,所需要做的就是将带有转录的文件列表传输到
App :
def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"])
利弊优点:
- 交互式应用程序的良好API;
- 近年来积极发展;
- 一种原始的测试方法;
- 好的文档。
缺点:
- 编写非交互式应用程序时需要argparse和其他代码;
- API不稳定;
- 在某些地方,文档为空。
奖励:乌尔维德
Github该文件讲解程序范例Urwid是一个略有不同的框架-来自curses和npyscreen,即来自控制台/终端用户界面。 不过,我认为它是奖金,因为它值得关注。
应用程式和团队Urwid有大量的小部件,但没有诸如窗口或用于访问相邻小部件的简单工具之类的概念。 因此,如果要获得漂亮的结果,则需要进行周密的设计和/或使用其他程序包,否则,您将不得不在按钮的属性中传输数据,如下所示:
源代码 import urwid from urwid import Button import todolib class App(urwid.WidgetPlaceholder): max_box_levels = 4 def __init__(self): super().__init__(urwid.SolidFill()) self.todoapp = None self.box_level = 0 def __enter__(self): self.todoapp = todolib.TodoApp.fromenv() self.new_menu( "Todo notes on urwid",
团队 app = App() def menu(title, *items) -> urwid.ListBox: body = [urwid.Text(title), urwid.Divider()] body.extend(items) return urwid.ListBox(urwid.SimpleFocusListWalker(body)) def add(button): edit = urwid.Edit("Title: ") def handle(button): text = edit.edit_text app.todoapp.add_task(text) app.popup("Task added") app.new_menu("New task", edit, Button("Add", on_press=handle)) def list_tasks(button): tasks = app.todoapp.list_tasks(show_done=True) buttons = [] for task in tasks: status = "done" if task.done else "not done" text = f"{task.title} [{status}]"
主要的 if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass
利弊优点:
- 伟大的API,用于编写不同的TUI应用程序;
- 悠久的开发历史(自2010年起)和稳定的API;
- 主管架构;
- 好的文档,有示例。
缺点:
- 如何测试尚不清楚。 只想到tmux发送键 ;
- 小部件未正确排列时的错误消息。
* * *
Cliff与Cleo和Cement非常相似,通常适合大型项目。
我个人不敢使用Plac,但我建议阅读源代码。
Plumbum有一个方便的CLI工具包和一个用于执行其他命令的出色API,因此,如果您要用Python重写Shell脚本,那么这就是您所需要的。
cmd2非常适合作为交互式应用程序以及希望从标准cmd迁移的应用程序的基础。
Urwid具有漂亮且用户友好的控制台应用程序。
以下软件包未包含在评价中: