何时为命令行界面选择 C 或 Python

从 C 的历史中吸取教训,学习如何使用 Python 编写有用的 CLI 程序。
96 位读者喜欢这篇文章。
Searching for code

Opensource.com

本文的目标很简单:帮助新的 Python 开发者了解命令行界面 (CLI) 的一些历史和术语,并探讨如何使用 Python 编写这些有用的程序。

开始...

首先,从 Unix 的角度来看命令行界面的设计。

Unix 是一种计算机操作系统,是 Linux 和 macOS(以及许多其他操作系统)的鼻祖。在图形用户界面出现之前,用户通过命令行提示符与计算机交互(可以想象今天的 Bash 环境)。在 Unix 下开发这些程序的主要语言是 C,它非常 强大

因此,我们至少应该了解 C 程序的基础知识。

假设你没有阅读该链接,C 程序的基本架构是一个名为 main 的函数,其签名如下所示:

   int main(int argc, char **argv)
   {
   ...
   }

这对 Python 程序员来说应该不会太陌生。C 函数首先具有返回类型,函数名称,然后在括号内包含类型化的参数。最后,函数的主体位于花括号之间。函数名 main运行时链接器(构造和运行程序的程序)决定从哪里开始执行程序的方式。如果你编写的 C 程序不包含名为 main 的函数,它将不会执行任何操作。可悲。

函数参数变量 argcargv 共同描述了一个字符串列表,这些字符串是用户在调用程序时在命令行上键入的。按照典型的简洁 Unix 命名传统,argc 表示*参数计数*,argv 表示*参数向量*。向量听起来比列表更酷,而 argl 听起来像是被勒住脖子的求救声。我们是 Unix 系统程序员,我们不会求救。我们让*其他人*求救。

继续

$ ./myprog foo bar -x baz

如果 myprog 是用 C 实现的,那么 argc 的值为 5,而 argv 将是指向具有五个条目的字符的指针数组。(如果这听起来非常技术性,请不要担心;它是一个包含五个字符串的列表。)向量中的第一个条目 argv[0] 是程序的名称。argv 的其余部分包含参数

   argv[0] == "./myprog"
   argv[1] == "foo"
   argv[2] == "bar"
   argv[3] == "-x"
   argv[4] == "baz"
   
   /* Note: not valid C */

在 C 中,你可以选择多种方式来处理 argv 中的字符串。你可以手动循环访问数组 argv,并根据程序的需求解释每个字符串。这相对容易,但会导致程序的界面差异很大,因为不同的程序员对什么是“好”有不同的想法。

include <stdio.h>

/* A simple C program that prints the contents of argv */

int main(int argc, char **argv) {
    int i;
    
    for(i=0; i<argc; i++)
      printf("%s\n", argv[i]);
}

标准化命令行的早期尝试

命令行武器库中的下一个武器是一个名为 getoptC 标准库函数。此函数允许程序员解析开关,即前面带有短划线的参数,例如 -x,并可以选择将后续参数与其开关配对。想想像“/bin/ls -alSh"getopt 是最初用于解析该参数字符串的函数。使用 getopt 可以使解析命令行非常容易,并改善用户体验 (UX)。

#include <stdio.h>
#include <getopt.h>

#define OPTSTR "b:f:"

extern char *optarg;

int main(int argc, char **argv) {
    int opt;
    char *bar = NULL;
    char *foo = NULL;
    
    while((opt=getopt(argc, argv, OPTSTR)) != EOF)
       switch(opt) {
          case 'b':
              bar = optarg;
              break;
          case 'f':
              foo = optarg;
              break;
          case 'h':
          default':
              fprintf(stderr, "Huh? try again.");
              exit(-1);
              /* NOTREACHED */
       }
    printf("%s\n", foo ? foo : "Empty foo");
    printf("%s\n", bar ? bar : "Empty bar");
}

个人而言,我希望Python 有 switch 语句,但这永远不会发生

GNU 世代

GNU 项目出现并为其传统 Unix 命令行工具的实现引入了更长的格式参数,例如 --file-format foo。当然,我们 Unix 程序员讨厌这一点,因为键入的内容太多了,但像我们这些恐龙一样,我们失败了,因为用户喜欢更长的选项。我从未使用 GNU 风格的选项解析编写任何代码,因此此处没有代码示例。

GNU 风格的参数也接受像 -f foo 这样的短名称,也必须支持。所有这些选择导致了程序员的更多工作量,他们只想知道用户要求什么并继续进行下去。但是用户获得了更加一致的 UX:长格式和短格式选项以及自动生成的帮助,这些帮助通常可以防止用户尝试阅读出了名的难以解析的 手册页面(参见 ps 作为一个特别糟糕的例子)。

但是我们正在谈论 Python?

你现在已经接触了足够的(太多?)命令行历史,可以了解如何使用我们最喜欢的语言编写 CLI。Python 为命令行解析提供了类似数量的选择:自己动手,包含电池的选项以及大量的第三方选项。你选择哪一个取决于你的特定情况和需求。

首先,自己动手

你可以从 sys 模块中获取程序的参数。

import sys

if __name__ == '__main__':
   for value in sys.argv:
       print(value)

包含电池

Python 标准库中已经实现了几个参数解析模块; getoptoptparse 以及最近的 argparseArgparse 允许程序员为用户提供一致且有用的 UX,但与其 GNU 前辈一样,它需要大量的工作和 "样板代码"才能使其“良好”。

from argparse import ArgumentParser

if __name__ == "__main__":

   argparser = ArgumentParser(description='My Cool Program')
   argparser.add_argument("--foo", "-f", help="A user supplied foo")
   argparser.add_argument("--bar", "-b", help="A user supplied bar")
   
   results = argparser.parse_args()
   print(results.foo, results.bar)

回报是当用户调用 --help 时自动生成可用帮助。但是 包含电池的优势是什么?有时,你的项目的环境决定了你对第三方库的访问权限有限或根本没有,并且你必须“凑合”使用 Python 标准库。

CLI 的现代方法

然后出现了 ClickClick 框架使用 装饰器方法来构建命令行解析。突然之间,编写丰富的命令行界面变得有趣而容易。在装饰器的酷炫和未来主义的用法下,许多复杂性都消失了,并且用户惊叹于对关键字完成以及上下文帮助的自动支持。所有这些都在编写比以前的解决方案更少的代码的同时完成。任何时候你可以编写更少的代码并且仍然完成工作都是一个胜利。我们都想要胜利。

import click

@click.command()
@click.option("-f", "--foo", default="foo", help="User supplied foo.")
@click.option("-b", "--bar", default="bar", help="User supplied bar.")
def echo(foo, bar):
    """My Cool Program
    
    It does stuff. Here is the documentation for it.
    """
    print(foo, bar)
    
if __name__ == "__main__":
    echo()

你可以在 @click.option 装饰器中看到与 argparse 相同的样板代码。但是,创建和管理参数解析器的“工作”已被抽象出来。现在,在解析了命令行参数并将值分配给函数参数后,将神奇地调用函数 echo

Click 接口添加参数就像向堆栈添加另一个装饰器并将新参数添加到函数定义一样容易。

等等,还有更多!

Click 的基础上构建的 Typer 是一个更新的 CLI 框架,它将 Click 的功能与现代 Python 类型提示结合在一起。使用 Click 的缺点之一是必须添加到函数中的装饰器堆栈。CLI 参数必须在两个地方指定:装饰器和函数参数列表。Typer DRYs CLI 规范,从而使代码更易于阅读和维护。

import typer

cli = typer.Typer()

@cli.command()
def echo(foo: str = "foo", bar: str = "bar"):
    """My Cool Program
    
    It does stuff. Here is the documentation for it.
    """
    print(foo, bar)
    
if __name__ == "__main__":
    cli()

开始编写一些代码

哪种方法是正确的?这取决于的使用案例。你是要编写一个快速而肮脏的脚本,只有你才会使用吗?直接使用 sys.argv 并继续前进。你需要更强大的命令行解析吗?也许 argparse 足够了。你有大量的子命令和复杂的选项,你的团队每天都会使用它吗?现在你绝对应该考虑 ClickTyper。作为程序员的乐趣之一是编写替代实现,看看哪一个最适合你。

最后,有许多第三方软件包可用于在 Python 中解析命令行参数。我只展示了我喜欢或使用过的软件包。你喜欢和/或使用不同的软件包完全没问题并且可以预料。我的建议是从这些开始,看看你最终会得到什么。

去写一些很酷的东西。


本文最初发表于 PyBites,并经授权转载。

接下来阅读
XENON coated avatar will glow red in the presence of aliens.
Erik O'Shaughnessy 是一位固执但友好的 UNIX 系统程序员,在德克萨斯州过着美好的生活。 在过去的二十多年(或更长时间!)里,他曾为 IBM、Sun Microsystems、Oracle 以及最近的 Intel 工作,从事计算机系统性能相关的工作。

4 条评论

你忘记了 docopt,它可以解析文档并从中生成解析器。

这里再次支持 docopt。 它遵循 posix 标准; 在许多语言中都有实现; 并且“正常工作”?

这是一篇写得很好且清晰的文章,感谢你的写作!

当我用 C 编写软件时,我总是使用 getopt(),因为它使用起来很合理而且非常便携。 坦率地说,我从来没有实现过长选项,因为害怕可移植性问题。

另一方面,我不知道 Python 中还有其他选项可用。 我一直使用 ArgumentParser。 感谢这些建议。

提供的信息对我的职业生涯很有用。 很高兴读完它。 非常感谢。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.