Nushell
安装 Nu !
快速开始
  • Nushell 之书
  • 命令参考列表
  • 实战指南
  • 语言参考指南
  • 贡献指南
博客
  • English
  • 中文
  • Deutsch
  • Français
  • Español
  • 日本語
  • Português do Brasil
  • Русский язык
  • 한국어
GitHub
安装 Nu !
快速开始
  • Nushell 之书
  • 命令参考列表
  • 实战指南
  • 语言参考指南
  • 贡献指南
博客
  • English
  • 中文
  • Deutsch
  • Français
  • Español
  • 日本語
  • Português do Brasil
  • Русский язык
  • 한국어
GitHub
  • 简介
  • 安装
    • 默认 Shell
  • 快速入门
    • 快速入门
    • 在系统中四处移动
    • 用 Nu 的方式思考
    • Nushell 速查表
  • Nu 基础篇
    • 数据类型
    • 加载数据
    • 管道
    • 处理字符串
    • 处理列表
    • 处理记录
    • 处理表格
    • 导航和访问结构化数据
    • 特殊变量
  • Nushell 编程
    • 自定义命令
    • 别名
    • 运算符
    • 变量
    • 控制流
    • 脚本
    • 模块
      • 使用模块
      • 创建模块
    • 覆层
    • 排序
    • 测试你的 Nushell 代码
    • 脚本调试
    • 最佳实践
  • Nu 作为 Shell 使用
    • 配置
    • 环境
    • 标准输出、标准错误和退出码
    • 运行系统(外部)命令
    • 如何配置第三方提示
    • 目录栈
    • Reedline,Nu 的行编辑器
    • 自定义补全
    • 外部命令
    • Nu 的配色和主题
    • 钩子
    • 后台任务
  • 迁移到 Nu
    • 从 Bash 到 Nu
    • 从 CMD.EXE 到 Nu
    • 从 PowerShell 到 Nu
    • 从其他 Shell 或 DSL 到 Nu
    • 从命令式语言到 Nu
    • 从函数式语言到 Nu
    • Nushell 运算符对照
  • 设计说明
    • Nushell代码执行原理
  • (不怎么)高级篇
    • 标准库 (预览版)
    • Dataframes
    • 元数据
    • 创建你自己的错误
    • 并行
    • 插件
    • explore
    • 使用 tui 构建终端用户界面
    • MCP 服务器

自定义命令

与任何编程语言一样,你很快就会想要保存较长的管道和表达式,以便在需要时可以轻松地再次调用它们。

这就是自定义命令的作用。

注意

自定义命令与许多语言中的函数类似,但在 Nushell 中,自定义命令本身就是一等公民。正如你将在下面看到的,它们与内置命令一起包含在帮助系统中,可以成为管道的一部分,会实时进行类型错误解析等等。

  • 创建和运行自定义命令
  • 从命令返回值
    • 管道输出
    • 管道输入
  • 命名命令
    • 子命令
    • 遮蔽内置命令
  • 参数
    • 多个参数
    • 必需的位置参数
    • 可选的位置参数
    • 参数类型
    • 类型别名
    • 标志
    • 剩余参数
    • 用 -- 结束标志解析
    • 带包装的外部命令的剩余参数
  • 管道输入输出签名
  • 为你的命令添加文档
  • 通过 attr 为自定义命令添加属性
  • 在自定义命令中更改环境
    • 在自定义命令中更改目录 (cd)
  • 持久化

创建和运行自定义命令

让我们从一个简单的 greet 自定义命令开始:

def greet [name] {
  $"Hello, ($name)!"
}

在这里,我们定义了 greet 命令,它接受一个参数 name。此参数后面是表示自定义命令运行时将发生什么的块。调用时,自定义命令会将为 name 传递的值设置为 $name 变量,该变量将在块内可用。

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

greet "World"
# => Hello, World!

从命令返回值

你可能会注意到上面的示例中没有 return 或 echo 语句。

像 PowerShell 和 JavaScript(使用箭头函数)等其他一些语言一样,Nushell 具有隐式返回的特性,即命令中最后一个表达式的值成为其返回值。

在上面的示例中,只有一个表达式——字符串。此字符串成为命令的返回值。

greet "World" | describe
# => string

当然,一个典型的命令将由多个表达式组成。为了演示,这里有一个包含 3 个表达式的无意义命令:

def eight [] {
  1 + 1
  2 + 2
  4 + 4
}

eight
# => 8

返回值同样只是命令中最后一个表达式的结果,即 4 + 4 (8)。

其他示例:

提前返回

由于某种情况需要提前退出的命令仍然可以使用 return 语句返回值。

def process-list [] {
  let input_length = length
  if $input_length > 10_000 {
    print "Input list is too long"
    return null
  }

  $in | each {|i|
    # Process the list
    $i * 4.25
  }
}
抑制返回值

你通常会希望创建一个作为语句而不是表达式的自定义命令,并且不返回值。

在这种情况下,你可以使用 ignore 关键字:

def create-three-files [] {
  [ file1 file2 file3 ] | each {|filename|
    touch $filename
  } | ignore
}

如果没有管道末尾的 ignore,该命令将从 each 语句返回一个空列表。

你也可以返回一个 null 作为最后一个表达式。或者,在这个人为的例子中,使用一个不返回值的 for 语句(见下一个例子)。

不返回值的语句

Nushell 中的一些关键字是语句,不返回值。如果你使用这些语句之一作为自定义命令的最后一个表达式,则返回值将为 null。在某些情况下,这可能是意料之外的。例如:

def exponents-of-three [] {
  for x in [ 0 1 2 3 4 5 ] {
    3 ** $x
  }
}
exponents-of-three

上面的命令将不显示任何内容,返回值为 null,因为 for 是一个不返回值的语句。

要从输入列表中返回值,请使用诸如 each 命令之类的过滤器:

def exponents-of-three [] {
  [ 0 1 2 3 4 5 ] | each {|x|
    3 ** $x
  }
}

exponents-of-three

# => ╭───┬─────╮
# => │ 0 │   1 │
# => │ 1 │   3 │
# => │ 2 │   9 │
# => │ 3 │  27 │
# => │ 4 │  81 │
# => │ 5 │ 243 │
# => ╰───┴─────╯
Match 表达式
# 返回当前目录中的一个随机文件
def "random file" [] {
  let files = (ls)
  let num_files = ($files | length)

  match $num_files {
    0 => null  # 对于空目录返回 null
    _ => {
      let random_file = (random int 0..($num_files - 1))
      ($files | get $random_file)
    }
  }
}

在这种情况下,最后一个表达式是 match 语句,它可以返回:

  • 如果目录为空,则返回 null
  • 否则,返回一个表示随机选择的文件的 record :::

自定义命令和管道

与内置命令一样,自定义命令的返回值可以传递到管道中的下一个命令。自定义命令也可以接受管道输入。此外,只要有可能,管道输入和输出都会在可用时进行流式处理。

重要!

另请参阅:管道

管道输出

ls | get name

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

def my-ls [] { ls }

我们可以像使用 ls 一样使用此命令的输出。

my-ls | get name
# => ╭───┬───────────────────────╮
# => │ 0 │ commands              │
# => │ 1 │ myscript.nu           │
# => │ 2 │ myscript2.nu          │
# => │ 3 │ welcome_to_nushell.md │
# => ╰───┴───────────────────────╯

这使我们可以轻松地构建自定义命令并处理其输出。请记住,我们不像其他语言那样使用 return 语句。相反,隐式返回允许我们构建输出数据流的管道,这些数据流可以连接到其他管道。

注意

在这种情况下,ls 内容仍然是流式的,即使它在一个单独的命令中。在慢速(例如,网络)文件系统上对长目录运行此命令将会在行可用时返回它们。

管道输入

自定义命令也可以从管道中获取输入,就像其他命令一样。此输入会自动传递给自定义命令的块。

让我们创建自己的命令,将接收到的每个值都加倍:

def double [] {
  each { |num| 2 * $num }
}

现在,如果我们在管道中稍后调用上述命令,我们可以看到它对输入的作用:

[1 2 3] | double
# => ╭───┬───╮
# => │ 0 │ 2 │
# => │ 1 │ 4 │
# => │ 2 │ 6 │
# => ╰───┴───╯

酷!

此命令演示了输入和输出流式处理。尝试使用无限输入运行它:

1.. | each {||} | double | first 3
# => ╭───┬───╮
# => │ 0 │ 2 │
# => │ 1 │ 4 │
# => │ 2 │ 6 │
# => ╰───┴───╯

即使输入命令永远不会结束,double 命令仍然可以在值可用时接收和输出它们,因此 first 取得它的三个值后管道就会停止。

如果没有 first 3,管道将永远运行下去。按 Ctrl+C 停止它。

我们还可以使用 $in 变量存储输入以供以后使用:

def nullify [...cols] {
  let start = $in
  $cols | reduce --fold $start { |col, table|
    $table | upsert $col null
  }
}

ls | nullify name size
# => ╭───┬──────┬──────┬──────┬───────────────╮
# => │ # │ name │ type │ size │   modified    │
# => ├───┼──────┼──────┼──────┼───────────────┤
# => │ 0 │      │ dir  │      │ 8 minutes ago │
# => │ 1 │      │ file │      │ 8 minutes ago │
# => │ 2 │      │ file │      │ 8 minutes ago │
# => │ 3 │      │ file │      │ 8 minutes ago │
# => ╰───┴──────┴──────┴──────┴───────────────╯

命名命令

在 Nushell 中,命令名可以是一串字符。以下是一些有效命令名的示例:greet、get-size、mycommand123、my command、命令,甚至 😊。

应避免可能与其他解析器模式混淆的字符串。例如,以下命令名可能无法调用:

  • 1、"1" 或 "1.5":Nushell 不允许使用数字作为命令名
  • 4MiB 或 "4MiB":Nushell 不允许使用文件大小作为命令名
  • "number#four"、"number^four" 或 "number%four":命令名中不允许使用插入符号、哈希符号和百分号
  • "foo bar"、" foo" 或 "foo ":命令名中的单词必须用单个空格分隔,不能有前导或尾随空白。这样的名称永远无法被调用,因此 def、extern 和 alias 会拒绝它。
  • -a、"{foo}"、"(bar)":将无法调用,因为 Nushell 会将它们解释为标志、闭包或表达式。

解析器关键字,例如 if、for、let、def、type、use 或 where,根本不能用作命令名,因为遮蔽它们会破坏其所实现的语言结构:

def if [] { "my if" }
# => Error: nu::parser::name_is_keyword
# =>
# =>   × Can't use parser keyword `if` as command name.
# =>    ╭─[repl_entry #1:1:5]
# =>  1 │ def if [] { "my if" }
# =>    ·     ─┬
# =>    ·      ╰── 'if' is a parser keyword
# =>    ╰────
# =>   help: Parser keywords cannot be shadowed (including via module exports and `use *`). Choose a different command name so language constructs keep working.

虽然像 "+foo" 这样的名称可能有效,但最好避免使用,因为解析器规则可能会随着时间的推移而改变。如有疑问,请使命令名尽可能简单。

提示

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

提示

因为 def 是一个解析器关键字,所以命令名必须在解析时已知。这意味着命令名不能是变量或常量。例如,以下是不允许的:

let name = "foo"
def $name [] { foo }
# => Error: nu::parser::unknown_state
# =>
# =>   × Unknown state.
# =>    ╭─[repl_entry #1:2:5]
# =>  1 │ let name = "foo"
# =>  2 │ def $name [] { foo }
# =>    ·     ──┬──
# =>    ·       ╰── Could not get string from string expression
# =>    ╰────

子命令

你还可以使用空格定义命令的子命令。例如,如果我们想向 str 添加一个新子命令,我们可以通过以“str ”开头命名我们的子命令来创建它。例如:

def "str mycommand" [] {
  "hello"
}

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

str mycommand
# => hello

当然,名称中带空格的命令也以同样的方式定义:

def "custom command" [] {
  "This is a custom command with a space in the name!"
}

遮蔽内置命令

自定义命令可以与内置命令同名。此时该自定义命令会遮蔽内置命令:调用这个名称会运行你的版本。要仍然调用内置命令,可以在其名称前加上 % 符号,这与 ^ 强制调用外部命令很像:

def echo [...args] { "echo is turned off" }

echo hello
# => echo is turned off

%echo hello
# => hello

当内置命令的名称存储在变量中(%$cmd)或在子表达式中计算(%($cmd))时,% 符号同样有效:

let cmd = "echo"
%$cmd hello
# => hello

只有内置命令可以这样调用。对自定义命令、别名或未知名称使用 % 是一个解析错误:

def greet [] { "hi" }
%greet
# => Error: nu::parser::error
# =>
# =>   × percent sigil requires a built-in command
# =>    ╭─[repl_entry #1:2:2]
# =>  1 │ def greet [] { "hi" }
# =>  2 │ %greet
# =>    ·  ──┬──
# =>    ·    ╰── unknown built-in command
# =>    ╰────
# =>   help: remove `%` to use normal resolution, or use `^` to run an external command explicitly

提示

在遮蔽了内置命令的命令内部,% 非常方便,因为命令体可以调用原始命令而不会递归调用自身。替换 ls 的完整示例参见别名。

参数

多个参数

在 def 命令中,参数在 list 中定义。这意味着多个参数可以用空格、逗号或换行符分隔。

例如,这里是一个接受两个名称的 greet 版本。这三个定义中的任何一个都可以工作:

# 空格
def greet [name1 name2] {
  $"Hello, ($name1) and ($name2)!"
}
# 逗号
def greet [name1, name2] {
  $"Hello, ($name1) and ($name2)!"
}
# 换行符
def greet [
  name1
  name2
] {
  $"Hello, ($name1) and ($name2)!"
}

必需的位置参数

上面使用的基本参数定义是位置的。传递给上面 greet 命令的第一个参数被分配给 name1 参数(并且,如上所述,是 $name1 变量)。第二个参数成为 name2 参数和 $name2 变量。

默认情况下,位置参数是必需的。使用我们之前定义的带有两个必需位置参数的 greet:

def greet [name1, name2] {
  $"Hello, ($name1) and ($name2)!"
}

greet Wei Mei
# => Hello, Wei and Mei!

greet Wei
# => Error: nu::parser::missing_positional
# =>
# =>   × Missing required positional argument.
# =>    ╭─[repl_entry #3:1:10]
# =>  1 │ greet Wei
# =>    ╰────
# =>   help: Usage: greet <name1> <name2> . Use `--help` for more information.

提示

尝试在此版本的 greet 之后键入第三个名称。请注意,解析器会自动检测到错误,并在执行前将第三个参数突出显示为错误。

可选的位置参数

我们可以通过在位置参数的名称后加上问号 (?) 来将其定义为可选。例如:

def greet [name?: string] {
  $"Hello, ($name | default 'You')"
}

greet
# => Hello, You

提示

请注意,用于访问变量的名称不包括 ?;只有其在命令签名中的定义才包括。

当未传递可选参数时,其在命令主体中的值为 null。上面的示例使用 default 命令在 name 为 null 时提供默认值“You”。

你也可以直接比较该值:

def greet [name?: string] {
  match $name {
    null => "Hello! I don't know your name!"
    _ => $"Hello, ($name)!"
  }
}

greet
# => Hello! I don't know your name!

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

带默认值的参数

你还可以为缺少的参数设置默认值。带默认值的参数在调用命令时也是可选的。

def greet [name = "Nushell"] {
  $"Hello, ($name)!"
}

你可以不带参数调用此命令,也可以带一个值来覆盖默认值:

greet
# => Hello, Nushell!

greet world
# => Hello, world!

你还可以将默认值与类型注解结合起来:

def congratulate [age: int = 18] {
  $"Happy birthday! You are ($age) years old now!"
}

参数类型

对于每个参数,你可以选择性地定义其类型。例如,你可以将基本的 greet 命令写成:

def greet [name: string] {
  $"Hello, ($name)"
}

如果参数没有类型注解,Nushell 会将其视为 any 类型。如果你在参数上注解了类型,Nushell 会在你调用函数时检查其类型。

例如,假设你只想接受一个 int 而不是 string:

def greet [name: int] {
  $"hello ($name)"
}

如果我们用字符串调用它,Nushell 会告诉我们类型不匹配:

greet World
# => Error: nu::parser::parse_mismatch
# =>
# =>   × Parse mismatch: expected int.
# =>    ╭─[repl_entry #2:1:7]
# =>  1 │ greet World
# =>    ·       ──┬──
# =>    ·         ╰── expected int
# =>    ╰────
# =>   help: Check the syntax around this position — a typo, missing delimiter, or wrong separator is common.

酷!

类型检查是解析器的一项功能。在命令行中输入自定义命令时,Nushell 解析器甚至可以实时检测到无效的参数类型,并在执行命令前将其突出显示。

突出显示样式可以使用主题或手动使用 $env.config.color_config.shape_garbage 进行更改。

类型注解列表

大多数类型都可以用作类型注解。此外,还有一些“形状”可以使用。例如:

  • number: 接受 int 或 float
  • path:被视为文件系统路径的字符串。前导 ~ 会展开为主目录,而诸如 ... 这样的“n 点”简写会展开为 ../..。相对路径保持相对:. 仍然是 .,而 foo/../bar 变成 bar。有关示例用法,请参阅语言参考指南中的路径。
  • directory: path(上文)的子集。使用制表符补全参数时,只会提供目录。扩展方式与 path 相同。
  • external_arg:以外部命令解析参数的方式解析该参数。裸词会保持输入时的原样,因此 0001 或 true 不会被转换为 int 或 bool,而带引号的字符串、变量和子表达式则保留各自的值。这主要用于从另一个 shell 接收参数的脚本的 main 命令。
  • oneof<...>:接受所列类型中的任意一个,例如 oneof<int, string>。字面量参数会取它匹配的第一个所列类型,而文本类类型(string、path、directory、glob、cell-path、external_arg 和 any)无论其位置如何都会最后尝试。因此对于 oneof<string, list<string>>,[foo bar] 是一个列表;而对于 oneof<string, int>,5(以及 007)是一个 int。如果你需要文本,请给参数加引号。
  • error:可用,但目前没有已知的有效用法。有关更多信息,请参阅语言参考指南中的错误。

例如,比较一个无类型参数和一个 external_arg 参数:

def as-any [value] { $value }
def as-external [value: external_arg] { $value }

as-any 0001
# => 1
as-external 0001
# => 0001

以下类型可用于参数注解:

  • any
  • binary
  • bool
  • cell-path
  • closure
  • datetime
  • duration
  • filesize
  • float
  • glob
  • int
  • list
  • nothing
  • range
  • record
  • string
  • table

类型别名

当多个命令接收或返回相同的结构化数据时,每次都写出 record<x: float, y: float> 这样的类型会很长,而且容易写错。type 关键字可以为类型命名:

type Point = record<x: float, y: float>
type Polygon = list<Point>

def first-x [poly: Polygon]: nothing -> float { $poly.0.x }

first-x [{x: 1.0, y: 2.0} {x: 3.0, y: 4.0}]
# => 1.0

类型别名只是其类型的简短写法。解析器会在使用名称的地方把名称替换为完整类型,因此 help、describe 和错误消息显示的都是完整类型:

type Point = record<x: float, y: float>
def length-of [p: Point]: nothing -> float { ($p.x ** 2 + $p.y ** 2) | math sqrt }

length-of {x: 1, y: 2}
# => Error: nu::parser::type_mismatch
# =>
# =>   × Type mismatch.
# =>    ╭─[repl_entry #3:1:11]
# =>  1 │ length-of {x: 1, y: 2}
# =>    ·           ──────┬─────
# =>    ·                 ╰── expected record<x: float, y: float>, found record<x: int, y: int>
# =>    ╰────

类型别名可以用在任何需要类型注解的地方:位置参数、标志、剩余参数、闭包参数、输入/输出类型、let、mut 和 const 的注解、其它类型内部(例如 list<Point>),以及另一个 type 的右侧。

以下几点规则适用:

  • 与 alias 和 const 一样,类型别名从定义处到所在块结束处可见。它必须定义在首次使用之前,且不能引用自身。
  • 自定义补全器属于参数,而不属于类型。应写在参数上,例如 def paint [color: Color@[red green blue]] { ... }。type Color = string@[red green blue] 是错误的。
  • 在模块中,类型别名默认是私有的,除非用 export type 定义。如何导出参见模块,如何导入参见使用模块。

注意

type 是一个解析器关键字,因此自定义命令、别名或 extern 不能命名为 type。要运行名为 type 的外部命令,请以 ^type 调用,或使用 Nushell 的 which 查看某个命令名指向什么。

标志

除了位置参数,你还可以定义命名标志。

例如:

def greet [
  name: string
  --age: int
] {
    {
      name: $name
      age: $age
    }
}

在此版本的 greet 中,我们定义了 name 位置参数以及一个 age 标志。位置参数(因为它没有 ?)是必需的。命名标志是可选的。不带 --age 标志调用命令会将 $age 设置为 null。

--age 标志可以放在位置 name 之前或之后。示例:

greet Lucia --age 23
# => ╭──────┬───────╮
# => │ name │ Lucia │
# => │ age  │ 23    │
# => ╰──────┴───────╯

greet --age 39 Ali
# => ╭──────┬─────╮
# => │ name │ Ali │
# => │ age  │ 39  │
# => ╰──────┴─────╯

greet World
# => ╭──────┬───────╮
# => │ name │ World │
# => │ age  │       │
# => ╰──────┴───────╯

标志也可以用简写版本定义。这允许你传递一个更简单的标志以及一个更易于阅读的长标志。

让我们扩展前面的示例,为 age 值使用一个简写标志:

def greet [
  name: string
  --age (-a): int
] {
    {
      name: $name
      age: $age
    }
  }

提示

结果变量始终基于长标志名称。在上面的示例中,变量仍然是 $age。$a 将无效。

现在,我们可以使用简写标志调用此更新后的定义:

greet Akosua -a 35
# => ╭──────┬────────╮
# => │ name │ Akosua │
# => │ age  │ 35     │
# => ╰──────┴────────╯

标志也可以用作基本开关。存在时,基于开关的变量为 true。不存在时,为 false。

def greet [
  name: string
  --caps
] {
    let greeting = $"Hello, ($name)!"
    if $caps {
      $greeting | str uppercase
    } else {
      $greeting
    }
}

greet Miguel --caps
# => HELLO, MIGUEL!

greet Chukwuemeka
# => Hello, Chukwuemeka!

你还可以将其分配给 true/false 以启用/禁用该标志:

greet Giulia --caps=false
# => Hello, Giulia!

greet Hiroshi --caps=true
# => HELLO, HIROSHI!

提示

请注意以下错误:

greet Gabriel --caps true
# => Error: nu::parser::extra_positional
# =>
# =>   × Extra positional argument.
# =>    ╭─[repl_entry #1:1:22]
# =>  1 │ greet Gabriel --caps true
# =>    ·                      ──┬─
# =>    ·                        ╰── extra positional argument
# =>    ╰────
# =>   help: Usage: greet {flags} <name>

键入空格而不是等号会将 true 作为位置参数传递,这可能不是期望的结果!这里 greet 只接受一个位置参数,因此 Nushell 会报错。

为避免混淆,不允许在标志上注解布尔类型:

def greet [
    --caps: bool   # Not allowed
] { $caps }
# => Error: nu::parser::error
# =>
# =>   × Type annotations are not allowed for boolean switches.
# =>    ╭─[repl_entry #1:2:13]
# =>  1 │ def greet [
# =>  2 │     --caps: bool   # Not allowed
# =>    ·             ──┬─
# =>    ·               ╰── Remove the `: bool` type annotation.
# =>  3 │ ] { $caps }
# =>    ╰────

标志可以包含破折号。可以通过在结果变量名中用下划线替换破折号来访问它们:

def greet [
  name: string
  --all-caps
] {
    let greeting = $"Hello, ($name)!"
    if $all_caps {
      $greeting | str uppercase
    } else {
      $greeting
    }
}

值为 null 的标志

当标志被赋予 null 作为其值时,结果取决于该标志的类型:

  • 如果类型不接受 nothing(例如 --age: int),Nushell 会将其视为未传入该标志,因此使用其默认值(或 null)。
  • 如果类型接受 nothing(例如 --age: any 或 --age: oneof<int, nothing>),null 会被原样传递给命令。

这让编写一个把可选标志转发给另一个命令的命令变得容易。在下面的示例中,我们将使用这个版本的 greet:

def greet [
  name: string
  --age: int = 18
  --caps
] {
  let greeting = $"Hello, ($name)! You are ($age)."
  if $caps { $greeting | str uppercase } else { $greeting }
}

party 转发它自己的 --age 标志。当调用者省略它时,$age 为 null,因此 greet 回退到其默认值:

def party [name: string, --age: int] {
  greet $name --age=$age
}

party Kai
# => Hello, Kai! You are 18.

party Kai --age 30
# => Hello, Kai! You are 30.

从记录传递标志

你还可以使用展开运算符(...)把记录作为命名标志传递。每个字段名就是一个标志名(不带 --),每个值成为该标志的值。对于开关,true 会设置该标志,而 false 或 null 则不设置:

let options = { age: 30, caps: true }
greet Kai ...$options
# => HELLO, KAI! YOU ARE 30.

greet Kai ...{ age: null, caps: false }
# => Hello, Kai! You are 18.

字段值会按标志定义进行类型检查,不匹配任何标志的字段是一个错误:

greet Kai ...{ colour: red }
# => Error: nu::shell::error
# =>
# =>   × Unknown flag `colour` in spread record
# =>    ╭─[repl_entry #1:1:14]
# =>  1 │ greet Kai ...{ colour: red }
# =>    ·              ───────┬───────
# =>    ·                     ╰── `colour` is not a named argument of this command
# =>    ╰────

剩余参数

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

def multi-greet [...names: string] {
  for $name in $names {
    print $"Hello, ($name)!"
  }
}

multi-greet Elin Lars Erik
# => Hello, Elin!
# => Hello, Lars!
# => Hello, Erik!

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

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

def vip-greet [vip: string, ...names: string] {
  for $name in $names {
    print $"Hello, ($name)!"
  }

  print $"And a special welcome to our VIP today, ($vip)!"
}

#         $vip          $name
#         ----- -------------------------
vip-greet Rahul Priya Arjun Anjali Vikram
# => Hello, Priya!
# => Hello, Arjun!
# => Hello, Anjali!
# => Hello, Vikram!
# => And a special welcome to our VIP today, Rahul!

要将列表传递给剩余参数,你可以使用展开运算符 (...)。使用上面的 vip-greet 命令定义:

let vip = "Tanisha"
let guests = [ Dwayne, Shanice, Jerome ]
vip-greet $vip ...$guests
# => Hello, Dwayne!
# => Hello, Shanice!
# => Hello, Jerome!
# => And a special welcome to our VIP today, Tanisha!

用 -- 结束标志解析

以 - 开头的参数通常会被解析为标志,因此把 -x 这样的值作为位置参数传递是一个错误。与 POSIX shell 一样,单独的 -- 会结束标志解析。它之后的每个参数都被视为位置参数,即使以 - 开头,而 -- 本身不会传递给命令:

def show-args [...args] { $args }

show-args -- --verbose -x foo
# => ╭───┬───────────╮
# => │ 0 │ --verbose │
# => │ 1 │ -x        │
# => │ 2 │ foo       │
# => ╰───┴───────────╯

这对内置命令同样有效,例如 [a b] | str join -- -x 返回 a-xb。用 def --wrapped 定义的命令(见下文)是个例外:它们会在剩余参数中收到 --,因此可以原样传递给它们所包装的外部命令。

带包装的外部命令的剩余参数

使用 def --wrapped 定义的自定义命令会将任何未知的标志和参数收集到一个 剩余参数中,然后可以通过列表展开将其传递给外部命令。这允许 自定义命令“包装”和扩展外部命令,同时仍然接受其所有原始参数。 例如,外部 eza 命令显示目录列表。 默认情况下,它显示网格排列:

eza commands
# => categories  docs  README.md

我们可以定义一个新命令 ezal,它将始终显示长列表,并添加图标:

def --wrapped ezal [...rest] {
  eza -l ...$rest
}

注

你也可以添加 --icons。我们在此示例中省略它, 只是因为这些图标在本指南中显示效果不佳。

请注意,--wrapped 会将任何其他参数强制放入 rest 参数中,因此 可以使用 eza 支持的任何参数调用该命令。这些附加参数将通过列表展开 操作 ...$rest 进行扩展。

ezal commands
# => drwxr-xr-x   - ntd  7 Feb 11:41 categories
# => drwxr-xr-x   - ntd  7 Feb 11:41 docs
# => .rw-r--r-- 936 ntd 14 Jun  2024 README.md

ezal -d commands
# => drwxr-xr-x - ntd 14 Jun  2024 commands

自定义命令可以检查某些参数并相应地更改其行为。例如,当使用 -G 选项 强制使用网格时,我们可以省略向 eza 传递 -l:

def --wrapped ezal [...rest] {
  if '-G' in $rest {
    eza ...$rest
  } else {
    eza -l --icons ...$rest
  }
}

ezal -G commands
# => categories  docs  README.md

管道输入输出签名

默认情况下,自定义命令接受 <any> 类型作为管道输入,同样可以输出 <any> 类型。但自定义命令也可以被赋予明确的签名以缩小允许的类型范围。

例如,str stats 的签名如下所示:

def "str stats" []: string -> record { {} }

在这里,string -> record 定义了命令的管道输入和输出的允许类型:

  • 它接受一个 string 作为管道输入
  • 它输出一个 record

注意

带有输入输出签名的命令体必须产生声明的输出类型。空命令体({ })会输出它的输入,在这里是一个 string,因此 Nushell 会报错。这就是上面的占位命令体返回一个空记录({})的原因。

如果有多个输入/输出类型,它们可以放在括号内并用逗号或换行符分隔,如 str join 中所示:

def "str join" [separator?: string]: [
  list -> string
  string -> string
] { "" }

这表示 str join 可以接受 list<any> 或 string 作为管道输入。在任何一种情况下,它都将输出一个 string。

有些命令不接受或不需要数据作为管道输入。在这种情况下,输入类型将是 <nothing>。如果命令返回 null(例如,rm 或 hide),输出类型也是如此:

def xhide [module: string, members?]: nothing -> nothing { }

注意

上面的示例被重命名为 xhide,以便将其复制到 REPL 不会遮蔽内置的 hide 命令。

输入输出签名显示在命令的 help 中(包括内置和自定义命令),也可以通过以下方式进行内省:

help commands | where name == <command_name>
scope commands | where name == <command_name>

酷!

输入输出签名允许 Nushell 在解析时捕获另外两类错误:

  • 尝试从命令返回错误的类型。例如:

    def inc []: int -> int {
      $in + 1
      print "Did it!"
    }
    
    # => Error: nu::parser::output_type_mismatch
    # =>
    # =>   × Command output doesn't match int.
    # =>    ╭─[repl_entry #1:1:24]
    # =>  1 │ ╭─▶ def inc []: int -> int {
    # =>  2 │ │     $in + 1
    # =>  3 │ │     print "Did it!"
    # =>  4 │ ├─▶ }
    # =>    · ╰──── expected int, but command outputs nothing
    # =>    ╰────
  • 以及尝试将无效类型传递给命令:

    def inc []: int -> int { $in + 1 }
    "Hi" | inc
    # => Error: nu::parser::input_type_mismatch
    # =>
    # =>   × Command does not support string input.
    # =>    ╭─[repl_entry #1:2:8]
    # =>  1 │ def inc []: int -> int { $in + 1 }
    # =>  2 │ "Hi" | inc
    # =>    ·        ─┬─
    # =>    ·         ╰── command doesn't support string input
    # =>    ╰────

为你的命令添加文档

为了更好地帮助用户理解如何使用你的自定义命令,你还可以为它们添加额外的命令和参数描述。

运行 help vip-greet 来检查我们上面定义的最新命令:

Usage:
  > vip-greet <vip> ...(names)

Flags:
  -h, --help: Display the help message for this command

Command Type:
  > custom

Parameters:
  vip <string>
  ...names <string>

Input/output types:
  ╭───┬───────┬────────╮
  │ # │ input │ output │
  ├───┼───────┼────────┤
  │ 0 │ any   │ any    │
  ╰───┴───────┴────────╯

酷!

你可以看到 Nushell 仅根据我们到目前为止的定义就自动为该命令创建了一些基本帮助。Nushell 还自动向该命令添加了一个 --help/-h 标志,因此用户也可以使用 vip-greet --help 访问帮助。

我们可以通过一些简单的注释来进一步扩展帮助,描述命令及其参数:

# 问候客人和一位贵宾
#
# 用于生日、毕业派对、
# 退休,以及任何其他
# 庆祝特定人物事件的
# 活动。
def vip-greet [
  vip: string        # 特别嘉宾
   ...names: string  # 其他客人
] {
  for $name in $names {
    print $"Hello, ($name)!"
  }

  print $"And a special welcome to our VIP today, ($vip)!"
}

现在再次运行 help vip-greet 来看看有什么不同:

Greet guests along with a VIP

Use for birthdays, graduation parties,
retirements, and any other event which
celebrates an event # for a particular
person.

Usage:
  > vip-greet <vip> ...(names)

Flags:
  -h, --help: Display the help message for this command

Command Type:
  > custom

Parameters:
  vip <string>: The special guest
  ...names <string>: The other guests

Input/output types:
  ╭───┬───────┬────────╮
  │ # │ input │ output │
  ├───┼───────┼────────┤
  │ 0 │ any   │ any    │
  ╰───┴───────┴────────╯

请注意,def 语句前紧接的注释行成为帮助系统中命令的描述。可以使用多行注释。第一行(在空注释行之前)成为帮助的 description。此信息在制表符补全命令时也会显示。

其余的注释行成为其在帮助数据中的 extra_description。

提示

运行:

scope commands
| where name == 'vip-greet'
| wrap help

这将显示 Nushell 创建的帮助记录。

参数后面的注释成为它们的描述。对于参数,只允许单行注释。

注意

用于参数文档目的的同一行上的 Nushell 注释需要在 # 磅号前有一个空格。

通过 attr 为自定义命令添加属性

从 0.103.0 版本开始,Nushell 允许自定义命令的作者在命令定义之前用 attr 内置命令的子命令来丰富自己的命令,这些子命令以 @ 符号为前缀。 对于来自 Python、Java 或 JavaScript 的人来说, 这看起来类似于装饰器或注解,但用途略有不同。

让我们改进 vip-greet 自定义命令的文档。example 属性会添加到 help {command} 或 {command} -h 的输出中:

# 问候客人和一位贵宾
#
# 用于生日、毕业派对、
# 退休,以及任何其他
# 庆祝特定人物事件的
# 活动。
@example "Greet a VIP" {vip-greet "Bob"} --result "And a special welcome to our VIP today, Bob!"
@example "Greet multiple people" {vip-greet "Bob" "Alice" "Charlie"} --result "Hello, Alice!
Hello, Charlie!
And a special welcome to our VIP today, Bob!"
def vip-greet [
  vip: string        # 特别嘉宾
   ...names: string  # 其他客人
] {
  for $name in $names {
    print $"Hello, ($name)!"
  }

  print $"And a special welcome to our VIP today, ($vip)!"
}

现在,运行 help vip-greet 就能看到新增的示例。

Greet guests along with a VIP

Use for birthdays, graduation parties,
retirements, and any other event which
celebrates an event # for a particular
person.

Usage:
  > vip-greet <vip> ...(names)

Flags:
  -h, --help: Display the help message for this command

Command Type:
  > custom

Parameters:
  vip <string>: The special guest
  ...names <string>: The other guests

Input/output types:
  ╭───┬───────┬────────╮
  │ # │ input │ output │
  ├───┼───────┼────────┤
  │ 0 │ any   │ any    │
  ╰───┴───────┴────────╯

Examples:
  Greet a VIP
  > vip-greet "Bob"
  And a special welcome to our VIP today, Bob!

  Greet multiple people
  > vip-greet "Bob" "Alice" "Charlie"
  Hello, Alice!
  Hello, Charlie!
  And a special welcome to our VIP today, Bob!

在维护用 nu 编写的脚本、模块、插件或库时,某些自定义命令 可能最终会被更新的版本取代或被完全移除, 但仍会临时保留一段时间, 以便用户有时间从旧功能过渡过来。

deprecated 属性会在运行该自定义命令时 向用户发出警告。

@deprecated
def greet [
  name: string
  --all-caps
] {
    let greeting = $"Hello, ($name)!"
    if $all_caps {
      $greeting | str uppercase
    } else {
      $greeting
    }
}

运行 greet {name} 就能看到该警告。

greet "bob"
# => Warning: nu::parser::deprecated
# =>
# =>   ⚠ Command deprecated.
# =>    ╭─[repl_entry #2:1:1]
# =>  1 │ greet "bob"
# =>    · ─────┬─────
# =>    ·      ╰── greet is deprecated and will be removed in a future release.
# =>    ╰────
# =>
# => Hello, bob!

默认情况下,警告只在该命令于会话中首次使用时显示。 添加 --report every 可在每次调用时都发出警告。

如果有替代命令或额外的上下文可以帮助用户更新其工作流, 可以在 @deprecated 之后添加文本。

category 属性会在使用 scope commands 或 help 命令时赋予指定的标签。

@deprecated "Use vip-greet as a replacement." --report every
@category "deprecated"
def greet [
  name: string
  --all-caps
] {
    let greeting = $"Hello, ($name)!"
    if $all_caps {
      $greeting | str uppercase
    } else {
      $greeting
    }
}
greet bob
# => Warning: nu::parser::deprecated
# =>
# =>   ⚠ Command deprecated.
# =>    ╭─[repl_entry #2:1:1]
# =>  1 │ greet bob
# =>    · ────┬────
# =>    ·     ╰── greet is deprecated and will be removed in a future release.
# =>    ╰────
# =>   help: Use vip-greet as a replacement.
# =>
# => Hello, bob!

help commands | where category == deprecated | select name category command_type
# => ╭───┬───────┬────────────┬──────────────╮
# => │ # │ name  │  category  │ command_type │
# => ├───┼───────┼────────────┼──────────────┤
# => │ 0 │ greet │ deprecated │ custom       │
# => ╰───┴───────┴────────────┴──────────────╯

属性也适用于 extern 声明。其中 @structured 仅适用于 extern。它让外部命令返回数据而不是文本。 示例参见结构化输出。

要查看 Nushell 中可用的其它属性,请查阅命令参考, 或在 Nushell REPL 中运行 help attr。

在自定义命令中更改环境

通常,环境变量定义和更改在块内是作用域的(作用域)。这意味着当它们在块结束时超出作用域时,对这些变量的更改将丢失,包括自定义命令的块。

def foo [] {
    $env.FOO = 'After'
}

$env.FOO = "Before"
foo
$env.FOO
# => Before

但是,使用 def --env 或 export def --env(对于模块)定义的命令将在调用方保留环境:

def --env foo [] {
    $env.FOO = 'After'
}

$env.FOO = "Before"
foo
$env.FOO
# => After

在自定义命令中更改目录 (cd)

同样,使用 cd 命令更改目录会导致 $env.PWD 环境变量的更改。这意味着当自定义命令结束时,目录更改($env.PWD 变量)也将被重置。如上所述,解决方案是使用 def --env 或 export def --env。

def --env go-root [] {
  cd /
}

cd ~
go-root
pwd
# => /

持久化

要使自定义命令在将来的 Nushell 会话中可用,你需要将它们添加到你的启动配置中。你可以将命令定义添加到:

  • 直接在你的 config.nu 中
  • 由你的 config.nu 加载的文件中
  • 由你的 config.nu 导入的模块中

有关更多详细信息,请参阅配置章节。

在GitHub上编辑此页面
Contributors: Justin Ma, Pedro de Matos Fedricci, Dan Davison, Hofer-Julian, Ifthel, Bruce Weirdan, 0x4D5352, Jan Klass, voyage200🍬, Jianjun Wang, passionworkeer
Next
别名