自定义命令

Nu 具备组合长管道的能力使你对数据和系统有很强的控制力,但它的代价是需要大量的键盘输入。不过理想情况下,你可以保存精心设计的管道以便反复使用。

这就是自定义命令(Custom Commands)的作用。

下面看一个自定义命令的例子:

  1. def greet [name] {
  2. echo "hello" $name
  3. }

在这个定义中,我们定义了greet命令,它需要一个参数name。在这个参数后面是自定义命令运行时将执行的代码块。当被调用时,自定义命令将把传递给name的值设置为$name变量,该变量在块内是可用的。

要运行上述命令,我们可以像调用内置命令一样调用它:

  1. > greet "world"

当我们这样做的时候,就会得到输出,如同我们使用内置命令一样:

  1. ───┬───────
  2. 0 hello
  3. 1 world
  4. ───┴───────

TIP

echo将其参数分别返回给管道。如果你想用它来生成一个单一的字符串,请在管道中添加| str collect

  1. def greet [name] {
  2. echo "hello " $name | str collect
  3. }
  4. greet nushell

返回 hello nushell

命令名称

在 Nushell 中,命令名是一串字符或一个带引号的字符串。下面是一些有效命令名的例子:greet, get-size, mycommand123, "mycommand", 😊, 和123

注意:在 Nushell 中,通常的做法是用-来分隔命令的多个单词,以提高可读性。 例如,使用 get-size 而不是 getsize 或者 get_size

子命令

你也可以使用空格来定义命令的子命令(Subcommand)。例如,如果我们想给str添加一个新的子命令,可以通过命名我们的子命令以 “str” 开头来做到。比如:

  1. def "str mycommand" [] {
  2. echo hello
  3. }

现在我们可以像调用str的内置子命令一样调用我们的自定义命令:

  1. > str mycommand

参数类型

在定义自定义命令时,你可以为每个参数命名并选择性地设置其类型。例如,你可以把上面的内容写成:

  1. def greet [name: string] {
  2. echo "hello " $name | str collect
  3. }

参数的类型是可选的。Nushell 支持不添加类型,此时会把参数类型当作any。如果你在参数上标注了一个类型,Nushell 将在你调用函数的时候检查该类型。

例如,假设你需要输入一个int类型:

  1. def greet [name: int] {
  2. echo "hello " $name | str collect
  3. }
  4. greet world

如果我们尝试运行上述内容,Nushell 会告诉我们,类型不匹配:

  1. error: Type Error
  2. ┌─ shell:6:7
  3. 5 greet world
  4. ^^^^^ Expected int, found world

这可以帮助指导你的用户只使用支持的类型来调用你所定义的命令。

目前可以支持的类型是(从 0.59.0 版本开始):

  • any
  • block
  • cell-path
  • duration
  • path
  • expr
  • filesize
  • glob
  • int
  • math
  • number
  • operator
  • range
  • cond
  • bool
  • signature
  • string
  • variable

具有默认值的参数

若要使一个参数成为可选的,并具有默认值,你可以在命令定义中指定该默认值:

  1. def greet [name = "nushell"] {
  2. echo "hello " $name | str collect
  3. }

你可以在没有参数的情况下调用这个命令,也可以指定一个值来覆盖默认值:

  1. > greet
  2. hello nushell
  3. > greet world
  4. hello world

你也可以将默认值与类型要求相结合:

  1. def congratulate [age: int = 18] {
  2. echo "Happy birthday! Wow you are " $age " years old now!" | str collect
  3. }

如果你想检查一个可选参数是否存在,而不是仅仅依赖一个默认值,请使用可选位置参数代替。

可选位置参数

默认情况下,位置参数(Positional Parameters)是必须的。如果没有传递位置参数,我们将遇到一个报错:

  1. × Missing required positional argument.
  2. ╭─[entry #23:1:1]
  3. 1 greet
  4. ·
  5. · ╰── missing name
  6. ╰────
  7. help: Usage: greet <name>

我们可以在一个位置参数的名字后面加上一个问号(?),将其标记为可选参数。比如:

  1. def greet [name?: string] {
  2. echo "hello" $name | str collect
  3. }
  4. greet

使一个位置参数成为可选参数并不改变它在命令体中被访问的名称。如上例所示,尽管参数列表中有?的后缀,但它仍然以$name的形式被访问。

当一个可选参数没有被传递,它在命令体中的值等于null$nothing。我们可以利用这一点来对没有传递参数的情况进行处理:

  1. def greet [name?: string] {
  2. if ($name == null) {
  3. echo "hello, I don't know your name!"
  4. } else {
  5. echo "hello " $name | str collect
  6. }
  7. }
  8. greet

如果你只是想在参数缺失时设置一个默认值,那么使用默认值来代替就更简单了。

如果必需的和可选的位置参数一起使用,那么必需的参数必须先出现在定义中。

标志

除了传递位置参数之外, 你还可以通过为自定义命令定义标志(Flags)来传递命名参数。

比如:

  1. def greet [
  2. name: string
  3. --age: int
  4. ] {
  5. echo $name $age
  6. }

在上面的greet定义中,我们定义了name位置参数以及age标志。这允许greet的调用者也可以选择传递age参数。

你可以用以下方法调用上述内容:

  1. > greet world --age 10

或者:

  1. > greet --age 10 world

或者甚至完全不使用标志:

  1. > greet world

标志也可以指定一个缩写版本,这允许你传递一个更简单的标志,如同传递一个更容易阅读的全写标志那样。

让我们扩展前面的例子,为age添加一个缩写标志:

  1. def greet [
  2. name: string
  3. --age (-a): int
  4. ] {
  5. echo $name $age
  6. }

注意: 标志是以其全称命名的,所以上面的例子的命令体内需要使用$age而不是$a

现在,我们可以使用缩写标志来调用这个新的定义:

  1. > greet -a 10 hello

剩余参数

在某些情况下, 你可能想定义一个需要任意数量的位置参数的命令。我们可以用一个剩余参数(Rest Parameter)来实现这一点,通过下面的...语法:

  1. def greet [...name: string] {
  2. echo "hello all:"
  3. for $n in $name {
  4. echo $n
  5. }
  6. }
  7. greet earth mars jupiter venus

我们可以使用任意数量的参数来调用上述greet命令的定义,包括完全没有参数,所有的参数都将被收集到$name列表中。

剩余参数可以和位置参数一起使用:

  1. def greet [vip: string, ...name: string] {
  2. echo "hello to our VIP " $vip | str collect
  3. echo "and hello to everybody else:"
  4. for $n in $name {
  5. echo $n
  6. }
  7. }
  8. # $vip $name
  9. # ---- ------------------------
  10. greet moon earth mars jupiter venus

为命令添加文档

为了更好地帮助用户使用你的自定义命令,也可以为其添加额外的命令和参数描述。

以我们之前的例子为例:

  1. def greet [
  2. name: string
  3. --age (-a): int
  4. ] {
  5. echo $name $age
  6. }

一旦定义完毕,我们可以运行help greet来获得该命令的帮助信息:

  1. Usage:
  2. > greet <name> {flags}
  3. Parameters:
  4. <name>
  5. Flags:
  6. -h, --help: Display this help message
  7. -a, --age <integer>

你可以看到我们定义的参数和标志,以及所有命令都会得到的-h帮助标志。

为了改进这个帮助,我们可以在定义中加入描述,这些描述将在帮助中显示出来:

  1. # A greeting command that can greet the caller
  2. def greet [
  3. name: string # The name of the person to greet
  4. --age (-a): int # The age of the person
  5. ] {
  6. echo $name $age
  7. }

我们给定义和参数添加的注释会作为描述出现在命令的help中。

现在,如果我们运行help greet,就会得到一些更友好的帮助文本:

  1. A greeting command that can greet the caller
  2. Usage:
  3. > greet <name> {flags}
  4. Parameters:
  5. <name> The name of the person to greet
  6. Flags:
  7. -h, --help: Display this help message
  8. -a, --age <integer>: The age of the person

管道输出

自定义命令会像内置命令一样流式输出。例如,假设我们想重构这个管道:

  1. > ls | get name

让我们把ls移到我们编写的命令中:

  1. def my-ls [] { ls }

我们就可以像使用ls一样使用这个命令的输出:

  1. > my-ls | get name
  2. ───┬───────────────────────
  3. 0 myscript.nu
  4. 1 myscript2.nu
  5. 2 welcome_to_nushell.md
  6. ───┴───────────────────────

这让我们可以很容易地创建自定义命令并处理它们的输出。注意,我们不像其他语言那样使用返回语句,取而代之的是我们创建管道,而其输出数据流可以连接到其他管道。

管道输入

如同其他命令一样,自定义命令也可以从管道中获取输入,这个输入会自动传递给自定义命令所使用的代码块。

让我们创建一个把所有接收值都加倍的命令:

  1. def double [] {
  2. each { |it| 2 * $it }
  3. }

现在,如果我们在一个管道中调用上述命令,就可以看到它对输入的处理结果:

  1. > [1 2 3] | double
  2. ───┬─────
  3. 0 2
  4. 1 4
  5. 2 6
  6. ───┴─────

我们还可以使用$in变量来存储输入,以便在后面使用:

  1. def nullify [...cols] {
  2. let start = $in
  3. $cols | reduce --fold $start { |col, df|
  4. $df | upsert $col null
  5. }
  6. }

持久化

关于如何持久化自定义命令,以便在你启动 Nushell 时它们是可用的,请参阅 配置 部分并添加你的启动脚本。