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,他们也会得到 Camila
和 Carlos
的补全(取决于 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
等。
为完成添加帮助¶
现在,我们返回一个 str
的 list
。
但是,某些 shell(Zsh、Fish、PowerShell)能够显示完成的额外帮助文本。
我们可以提供额外的帮助文本,以便这些 shell 可以显示它。
在 complete_name()
函数中,我们不为每个完成元素提供一个 str
,而是提供一个包含 2 个项目的 tuple
。第一个项目是实际完成字符串,第二个项目是帮助文本。
因此,最后,我们返回一个 str
的 tuple
的 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(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 个 str
的 tuple
的 list
(或其他可迭代对象)。
信息
帮助文本将在 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
简化¶
我们不必创建并返回包含值(str
或 tuple
)的列表,而是可以在完成中使用每个所需值来使用 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
的版本即可。
最终,这只是为了节省几行代码。
使用 Context 访问其他CLI 参数¶
假设现在我们想要修改程序,以便能够同时向多个人“打招呼”。
因此,我们将允许多个 --name
CLI 选项。
提示
您将在本教程的后面部分了解有关具有多个值的CLI 参数的更多信息。
因此,现在可以将其视为抢先预览 😉。
为此,我们使用 str
的 List
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
(即使为空)以稍后检查其内容。
然后,当我们有补全候选时,我们通过检查它是否在 names
的 name 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 个选项中的哪一个都没有关系。它都会“正常工作”✨