脚本调试
Nushell 内置了脚本调试器。运行 nu --dap 会启动一个 Debug Adapter Protocol(DAP)服务器,VS Code、Neovim 和 Zed 等编辑器正是通过该协议来驱动调试器的。当你的编辑器配置好使用该调试器后,你可以在 .nu 文件中设置断点、逐行单步执行脚本并深入管道阶段、查看变量和 $env、在脚本暂停时评估表达式,甚至可以沿已执行路径向后单步回溯。
配置你的编辑器
编辑器会自行启动 nu --dap 并通过进程的标准输入和输出流与其通信,因此它只需知道要运行的命令(nu,附带参数 --dap)以及要启动哪个脚本。调试器是默认构建的一部分(即 dap Cargo 特性)。
例如,在 Neovim 中配合 nvim-dap 使用:
local dap = require('dap')
dap.adapters.nu = {
type = 'executable',
command = 'nu',
args = { '--dap' },
}
dap.configurations.nu = {
{
type = 'nu',
request = 'launch',
name = 'Debug this script',
program = '${file}',
},
}VS Code 则需要一个为 Nushell 注册调试器的扩展,随后从 launch.json 中读取相同的启动设置。
调试器在运行脚本前会加载你的 env.nu 和 config.nu,因此脚本能看到与你的 shell 相同的自定义命令、别名和环境。在参数中添加 -n(args = { '--dap', '-n' })可改为从默认配置启动。在解析配置文件期间,$nu.is-dap 的值为 true。
启动配置项
启动配置支持以下设置项:
| 配置项 | 默认值 | 说明 |
|---|---|---|
program | 待调试的 .nu 脚本的绝对路径。必填。 | |
args | [] | 传递给 main 或 entryPoint 命令的参数字符串列表。其解析方式与 nu <script> ...args 相同。 |
cwd | 运行脚本的工作目录。 | |
stopOnEntry | false | 是否在第一行代码运行前暂停。 |
entryPoint | 在脚本顶层执行完毕后运行的命令,适用于仅定义命令的文件。若未指定且脚本包含 main,则运行 main。 | |
timeTravel | true | 记录运行的每一行,以便能够向后单步回溯。若为 false,则仅记录脚本暂停的位置。 |
timeTravelMaxSteps | 10000 | 保留的记录步数。较早的记录将被丢弃。 |
脚本的运行方式与 nu <script> ...args 一致:先执行顶层代码,然后使用 args 中的参数执行 main,或者执行 entryPoint 中指定的命令。
断点与单步执行
在某一行设置断点,脚本就会在该行暂停。如果在无法暂停的行(例如注释行)设置断点,断点会自动移至下一行可暂停的代码处。
一行代码通常包含多个步骤。在 let doubled = ($nums | each {|n| $n * 2 }) 中,let、each 调用以及闭包体各自独立运行。普通断点会在其中的第一步暂停,随后在闭包体针对每个元素执行时再次暂停。而在特定列设置的行内断点(在 VS Code 中为 Shift+F9)则仅会在该特定步骤处暂停。
断点还可以是:
- 条件断点:条件是一个 Nushell 表达式,例如
$n == 3,脚本仅在表达式为true时暂停。 - 日志断点(Logpoints):断点不会暂停执行,而是向调试控制台输出一条消息。花括号中的表达式会被求值,例如
names has {$names | length} items会输出names has 2 items。
**运行时错误(Runtime errors)**异常过滤器默认处于开启状态,当某个命令引发错误时(包括会被 try 块捕获的错误),调试器会暂停脚本。编辑器会显示该错误的代码和消息。
脚本暂停时,你可以继续执行、单步跳过整行、单步进入命令调用或管道阶段,或者单步跳出当前命令。调用栈(Call Stack)会展示通往当前行的命令和闭包调用层级。
检查变量与值
当脚本暂停时,编辑器的变量视图会展示:
- Locals(局部变量):当前作用域内的变量、
$in,以及return(最后一步的值)。 - Pipeline(管道值):在两个管道阶段的分界处,流入下一个命令的值。
- Globals(全局变量):
$nu和$env,包含脚本对环境所做的任何更改。 - Registers(寄存器):求值器的内部工作值,用于查看 Nushell 底层执行细节。
- Process(进程输出):脚本运行的外部命令的最新输出。
嵌套的记录、列表和表格在你展开时会逐级加载,因此在查看内部结构之前,庞大的数据结构不会拖慢任何操作。数据流(Stream)仅作描述而不予读取,因此查看数据流不会消耗其内容。
监视表达式(Watch expressions)、调试控制台以及将鼠标悬停在变量上,都会使用暂停时的值来对 Nushell 表达式求值。它们在脚本状态的副本上运行,因此不会修改脚本的变量。
来自 print 和外部命令的输出会发送到编辑器的调试控制台。脚本没有附加终端,因此外部命令不会接收标准输入。
向后单步回溯
Nushell 无法反向执行代码,因此调试器会在每一行记录当时的状态(当 timeTravel 开启时,默认开启)。**单步后退(Step Back)和反向继续(Reverse Continue)**会沿这些历史记录向后回退,移动到上一行或上一个断点,并展示当时的变量和调用栈。从回退位置向前单步执行会沿着记录向前推进,直至到达最新的执行步骤,随后脚本恢复正常运行。已经执行过的命令不会被撤销:脚本已写入的文件依然保持写入状态。
**重启(Restart)**会在同一个会话中从头再次运行脚本,重新从磁盘读取脚本内容并保留你的断点。
使用限制
- 调试器目前只能启动新脚本,无法附加到已在运行的 Nushell 进程。
- 脚本暂停时无法修改变量的值。
- 暂不支持函数断点和命中计数断点。
- 变量视图始终显示最内层的命令或闭包。在调用栈中选择调用方会跳转到其所在行,但变量视图仍保持为最内层帧的状态。
input和input list需要支持调试器提示对话框的客户端。在其他编辑器中,脚本会在提示处等待,并在 10 分钟后失败。input listen在调试器中无法正常工作。