跳至内容

CLI 选项自动补全

正如你所看到的,使用Typer构建的应用程序在创建 Python 包或使用typer命令时,在你的 shell 中具有可用的补全功能。

它通常会补全CLI 选项CLI 参数和子命令(稍后你将了解)。

但你还可以为CLI 选项CLI 参数提供自动补全。我们将在本文中学习相关内容。

查看补全

在检查如何提供自定义补全之前,我们再次检查一下它是如何工作的。

在为自己的 Python 包安装补全(或使用 typer 命令)后,当你使用你的 CLI 程序并开始使用 -- 添加一个CLI 选项,然后按 TAB 时,你的 shell 将向你显示可用的CLI 选项(对于CLI 参数等也是如此)。

要快速检查它,无需创建新的 Python 包,请使用 typer 命令。

然后让我们创建一个小的示例程序

import typer
from typing_extensions import Annotated

app = typer.Typer()


@app.command()
def main(name: Annotated[str, typer.Option(help="The name to say hi to.")] = "World"):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Option("World", help="The name to say hi to.")):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

让我们使用 typer 命令来尝试它以获取补全

// Hit the TAB key in your keyboard below where you see the: [TAB]
$ typer ./main.py [TAB][TAB]

// Depending on your terminal/shell you will get some completion like this ✨
run    -- Run the provided Typer app.
utils  -- Extra utility commands for Typer apps.

// Then try with "run" and --
$ typer ./main.py run --[TAB][TAB]

// You will get completion for --name, depending on your terminal it will look something like this
--name  -- The name to say hi to.

// And you can run it as if it was with Python directly
$ typer ./main.py run --name Camila

Hello Camila

值的自定义补全

现在我们得到了CLI 选项名称的补全,但没有得到值的补全。

我们可以为值提供补全,创建一个 autocompletion 函数,类似于 CLI 选项回调和上下文 中的 callback 函数

import typer
from typing_extensions import Annotated


def complete_name():
    return ["Camila", "Carlos", "Sebastian"]


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
    ] = "World",
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

import typer


def complete_name():
    return ["Camila", "Carlos", "Sebastian"]


app = typer.Typer()


@app.command()
def main(
    name: str = typer.Option(
        "World", help="The name to say hi to.", autocompletion=complete_name
    ),
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

我们从 complete_name() 函数返回一个字符串的 list

然后我们在使用补全时得到这些值

$ typer ./main.py run --name [TAB][TAB]

// We get the values returned from the function 🎉
Camila     Carlos     Sebastian

我们让基础知识发挥作用。现在让我们改进它。

检查不完整的值

现在,我们总是返回那些值,即使用户开始输入 Sebast 然后按 TAB,他们也会得到 CamilaCarlos 的补全(取决于 shell),而我们应该只得到 Sebastian 的补全。

但我们可以修复它,以便它始终正确工作。

修改 complete_name() 函数以接收类型为 str 的参数,它将包含不完整的值。

然后,我们可以检查并仅返回从命令行开始的不完整值的那些值

import typer
from typing_extensions import Annotated

valid_names = ["Camila", "Carlos", "Sebastian"]


def complete_name(incomplete: str):
    completion = []
    for name in valid_names:
        if name.startswith(incomplete):
            completion.append(name)
    return completion


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
    ] = "World",
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

import typer

valid_names = ["Camila", "Carlos", "Sebastian"]


def complete_name(incomplete: str):
    completion = []
    for name in valid_names:
        if name.startswith(incomplete):
            completion.append(name)
    return completion


app = typer.Typer()


@app.command()
def main(
    name: str = typer.Option(
        "World", help="The name to say hi to.", autocompletion=complete_name
    ),
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

现在让我们尝试一下

$ typer ./main.py run --name Ca[TAB][TAB]

// We get the values returned from the function that start with Ca 🎉
Camila     Carlos

现在我们只返回以 Ca 开头的有效值,我们不再将 Sebastian 作为补全选项返回。

提示

你必须声明类型为 str 的不完整值,这就是你将在函数中接收的内容。

无论实际值是 int 还是其他内容,在执行完成时,您只会得到一个 str 作为不完整的值。

同样,您只能返回 str,不能返回 int 等。

为完成添加帮助

现在,我们返回一个 strlist

但是,某些 shell(Zsh、Fish、PowerShell)能够显示完成的额外帮助文本。

我们可以提供额外的帮助文本,以便这些 shell 可以显示它。

complete_name() 函数中,我们不为每个完成元素提供一个 str,而是提供一个包含 2 个项目的 tuple。第一个项目是实际完成字符串,第二个项目是帮助文本。

因此,最后,我们返回一个 strtuplelist

import typer
from typing_extensions import Annotated

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]


def complete_name(incomplete: str):
    completion = []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete):
            completion_item = (name, help_text)
            completion.append(completion_item)
    return completion


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
    ] = "World",
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

import typer

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]


def complete_name(incomplete: str):
    completion = []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete):
            completion_item = (name, help_text)
            completion.append(completion_item)
    return completion


app = typer.Typer()


@app.command()
def main(
    name: str = typer.Option(
        "World", help="The name to say hi to.", autocompletion=complete_name
    ),
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

提示

如果您希望每个项目都有帮助文本,请确保列表中的每个项目都是一个 tuple。而不是一个 list

Click 在提取帮助文本时专门检查 tuple

因此,最后,返回值将是 2 个 strtuplelist(或其他可迭代对象)。

信息

帮助文本将在 Zsh、Fish 和 PowerShell 中显示。

Bash 不支持显示帮助文本,但完成仍然可以正常工作。

如果您有 Zsh 等 shell,它将如下所示

$ typer ./main.py run --name [TAB][TAB]

// We get the completion items with their help text 🎉
Camila     -- The reader of books.
Carlos     -- The writer of scripts.
Sebastian  -- The type hints guy.

使用 yield 简化

我们不必创建并返回包含值(strtuple)的列表,而是可以在完成中使用每个所需值来使用 yield

这样,我们的函数将成为一个 Typer(实际上是 Click)可以迭代的 生成器

import typer
from typing_extensions import Annotated

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]


def complete_name(incomplete: str):
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete):
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
    ] = "World",
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

import typer

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]


def complete_name(incomplete: str):
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete):
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: str = typer.Option(
        "World", help="The name to say hi to.", autocompletion=complete_name
    ),
):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

这简化了我们的代码,并且工作方式相同。

提示

如果您觉得所有 yield 部分很复杂,请不要担心,您只需使用上面带有 list 的版本即可。

最终,这只是为了节省几行代码。

信息

该函数可以使用 yield,因此它不必严格返回一个 list,它只需要是 可迭代的

但是,每个完成元素都必须是 strtuple(当包含帮助文本时)。

使用 Context 访问其他CLI 参数

假设现在我们想要修改程序,以便能够同时向多个人“打招呼”。

因此,我们将允许多个 --name CLI 选项

提示

您将在本教程的后面部分了解有关具有多个值的CLI 参数的更多信息。

因此,现在可以将其视为抢先预览 😉。

为此,我们使用 strList

from typing import List

import typer
from typing_extensions import Annotated

app = typer.Typer()


@app.command()
def main(
    name: Annotated[List[str], typer.Option(help="The name to say hi to.")] = ["World"],
):
    for each_name in name:
        print(f"Hello {each_name}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

from typing import List

import typer

app = typer.Typer()


@app.command()
def main(name: List[str] = typer.Option(["World"], help="The name to say hi to.")):
    for each_name in name:
        print(f"Hello {each_name}")


if __name__ == "__main__":
    app()

然后我们可以像这样使用它

$ typer ./main.py run --name Camila --name Sebastian

Hello Camila
Hello Sebastian

获取多个值的补全

与之前相同,我们希望为这些名称提供补全。但是,如果这些名称已在前面的参数中给出,我们不希望为补全提供相同的名称

为此,我们将访问并使用“Context”。当您创建Typer应用程序时,它在下面使用 Click。每个 Click 应用程序都有一个称为 "Context" 的特殊对象,该对象通常是隐藏的。

但是,您可以通过声明类型为 typer.Context 的函数参数来访问上下文。

您可以从该上下文中获取每个参数的当前值。

from typing import List

import typer
from typing_extensions import Annotated

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]


def complete_name(ctx: typer.Context, incomplete: str):
    names = ctx.params.get("name") or []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete) and name not in names:
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        List[str],
        typer.Option(help="The name to say hi to.", autocompletion=complete_name),
    ] = ["World"],
):
    for n in name:
        print(f"Hello {n}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

from typing import List

import typer

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]


def complete_name(ctx: typer.Context, incomplete: str):
    names = ctx.params.get("name") or []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete) and name not in names:
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: List[str] = typer.Option(
        ["World"], help="The name to say hi to.", autocompletion=complete_name
    ),
):
    for n in name:
        print(f"Hello {n}")


if __name__ == "__main__":
    app()

我们正在获取在触发此补全之前在命令行中使用 --name 提供的 names

如果命令行中没有 --name,它将为 None,因此我们使用 or [] 确保我们有一个 list(即使为空)以稍后检查其内容。

然后,当我们有补全候选时,我们通过检查它是否在 namesname not in names 列表中,来检查是否已使用 --name 提供了每个 name

然后,我们yield尚未使用的每个项目。

检查它

$ typer ./main.py run --name [TAB][TAB]

// The first time we trigger completion, we get all the names
Camila     -- The reader of books.
Carlos     -- The writer of scripts.
Sebastian  -- The type hints guy.

// Add a name and trigger completion again
$ typer ./main.py run --name Sebastian --name Ca[TAB][TAB]

// Now we get completion only for the names we haven't used 🎉
Camila  -- The reader of books.
Carlos  -- The writer of scripts.

// And if we add another of the available names:
$ typer ./main.py run --name Sebastian --name Camila --name [TAB][TAB]

// We get completion for the only available one
Carlos  -- The writer of scripts.

提示

如果只有一个选项,你的 shell 很有可能会直接完成它,而不是显示带有帮助文本的选项,以节省你更多的输入。

获取原始CLI 参数

你还可以获取原始CLI 参数,它只是一个 list,其中包含不完整值之前在命令行中传递的所有内容的 str

例如,类似 ["typer", "main.py", "run", "--name"] 的内容。

提示

这适用于高级场景,在大多数用例中,你最好使用上下文。

但如果你需要,它仍然是可能的。

作为一个简单的示例,让我们在完成之前在屏幕上显示它。

因为补全基于你的程序打印的输出(由Typer内部处理),在补全期间,我们不能像通常那样打印其他东西。

打印到“标准错误”

提示

如果你需要复习什么是“标准输出”和“标准错误”,请查看 打印和颜色:“标准输出”和“标准错误” 中的部分。

补全系统只从“标准输出”读取,所以,打印到“标准错误”不会破坏补全。🚀

你可以使用Rich Console(stderr=True) 打印到“标准错误”。

使用 stderr=True 告诉Rich 输出应该显示在“标准错误”中。

from typing import List

import typer
from rich.console import Console
from typing_extensions import Annotated

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]

err_console = Console(stderr=True)


def complete_name(args: List[str], incomplete: str):
    err_console.print(f"{args}")
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete):
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        List[str],
        typer.Option(help="The name to say hi to.", autocompletion=complete_name),
    ] = ["World"],
):
    for n in name:
        print(f"Hello {n}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

from typing import List

import typer
from rich.console import Console

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]

err_console = Console(stderr=True)


def complete_name(args: List[str], incomplete: str):
    err_console.print(f"{args}")
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete):
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: List[str] = typer.Option(
        ["World"], help="The name to say hi to.", autocompletion=complete_name
    ),
):
    for n in name:
        print(f"Hello {n}")


if __name__ == "__main__":
    app()

信息

如果你不能安装和使用 Rich,你还可以使用 print(lastname, file=sys.stderr)typer.echo("some text", err=True)

我们通过声明类型为 List[str] 的参数来获取所有CLI 参数作为 str 的原始 list,这里它被命名为 args

提示

这里我们把所有原始CLI 参数的列表命名为 args,因为这是 Click 的惯例。

但它不只包含CLI 参数,它包含所有内容,包括CLI 选项和值,作为 str 的原始 list

然后我们只需将其打印到“标准错误”。

$ typer ./main.py run --name [TAB][TAB]

// First we see the raw CLI parameters
['./main.py', 'run', '--name']

// And then we see the actual completion
Camila     -- The reader of books.
Carlos     -- The writer of scripts.
Sebastian  -- The type hints guy.

提示

这是一个非常简单(而且相当无用的)示例,只是让你知道它是如何工作的,以及你可以如何使用它。

但它可能只在非常高级的用例中才有用。

获取上下文和原始CLI 参数

当然,如果你需要的话,你可以声明所有内容,上下文、原始CLI 参数和不完整的 str

from typing import List

import typer
from rich.console import Console
from typing_extensions import Annotated

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]

err_console = Console(stderr=True)


def complete_name(ctx: typer.Context, args: List[str], incomplete: str):
    err_console.print(f"{args}")
    names = ctx.params.get("name") or []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete) and name not in names:
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: Annotated[
        List[str],
        typer.Option(help="The name to say hi to.", autocompletion=complete_name),
    ] = ["World"],
):
    for n in name:
        print(f"Hello {n}")


if __name__ == "__main__":
    app()

提示

如果可能,最好使用 Annotated 版本。

from typing import List

import typer
from rich.console import Console

valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]

err_console = Console(stderr=True)


def complete_name(ctx: typer.Context, args: List[str], incomplete: str):
    err_console.print(f"{args}")
    names = ctx.params.get("name") or []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete) and name not in names:
            yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
    name: List[str] = typer.Option(
        ["World"], help="The name to say hi to.", autocompletion=complete_name
    ),
):
    for n in name:
        print(f"Hello {n}")


if __name__ == "__main__":
    app()

检查它

$ typer ./main.py run --name [TAB][TAB]

// First we see the raw CLI parameters
['./main.py', 'run', '--name']

// And then we see the actual completion
Camila     -- The reader of books.
Carlos     -- The writer of scripts.
Sebastian  -- The type hints guy.

$ typer ./main.py run --name Sebastian --name Ca[TAB][TAB]

// Again, we see the raw CLI parameters
['./main.py', 'run', '--name', 'Sebastian', '--name']

// And then we see the rest of the valid completion items
Camila     -- The reader of books.
Carlos     -- The writer of scripts.

类型,无处不在的类型

Typer 使用类型声明来检测它必须向你的 autocompletion 函数提供什么。

你可以声明这些类型的函数参数

  • str:用于不完整的值。
  • typer.Context:用于当前上下文。
  • List[str]:用于原始CLI 参数

无论你如何命名它们,按什么顺序命名,或者声明了 3 个选项中的哪一个都没有关系。它都会“正常工作”✨