日期:2026-11-07 GitHub Trending 日榜第 13 位;榜单采集时间:北京时间 16:00:24。
从构建结果里筛出成功的模块,再按耗时倒序排列,是一个很小的需求。如果输入原本就是 JSON,最后却要靠空格分列、正则截取和字符串转数字完成统计,脚本就把大部分精力花在了修补数据格式上。
Nushell 的做法是让管道直接传递带类型的值:记录仍然是记录,整数仍然是整数,文件大小也不必先拆掉单位。它保留了 Shell 一步步组合命令的体验,却改变了步骤之间传什么。
这件事听起来像给终端加一层表格界面。读到运行时后,会发现表格只是输出方式;真正影响脚本行为的是值、流和外部进程之间的转换,以及那些不得不把流完整收进内存的地方。
📋 项目概览
| 项目 | 内容 |
|---|---|
| 主仓库 | nushell/nushell |
| 定位 | 跨平台 Shell 与脚本语言,使用结构化数据管道 |
| 主要实现语言 | Rust |
| 许可证 | MIT,分发代码时需保留许可及版权声明 |
| 常见输入 | 文件系统信息、JSON、CSV、TOML、外部进程输出 |
| 数据通路 | 单值、结构化值流、字节流 |
| 运行方式 | 交互式 nu、nu -c、执行 .nu 脚本 |
| 本文示例版本 | 官方发行版 0.116.1 |
| 阅读重点 | 类型如何保留、流何时收集、外部命令如何报告失败 |
下面的实现分析涉及开发分支 0.116.2 的代码组织,上手例子使用 0.116.1。两者不能仅凭版本相近就视为完全相同;文中的示例输出对应后者,不把开发分支的新行为当成发行版保证。
🔥 为什么值得关注
看一段处理构建记录的管道就容易理解 Nu 的取向:
nu
[
{module: app, duration_ms: 810, ok: true}
{module: core, duration_ms: 120, ok: true}
{module: feature, duration_ms: 1200, ok: false}
]
| where ok == true
| sort-by duration_ms --reverse
| select module duration_ms
where 按布尔字段筛选,sort-by 按数值字段排序,select 裁剪列。这里的 810 没有经过终端输出再被重新解析,true 也不是需要约定大小写的文本标记。字段名表达了脚本想做什么,列宽和空格数量不再影响正确性。
对经常处理接口响应、构建报告和目录信息的开发者,这种差别很实际。数据源加一列,按字段访问的脚本通常不必跟着修改;命令换了一套展示宽度,内部数据也不会因此改变。
不过,结构化 Shell 并不会让所有命令突然遵守同一个协议。git、adb、编译器等外部程序仍然输出字节。Nu 能运行它们,但必须有明确的解析环节,才能把文字变成记录。这个边界值得比终端截图看得更仔细。
🏗️ 核心特性
数据有类型,管道操作围绕字段展开
nu-protocol 的 Value 定义了布尔、整数、浮点数、字符串、文件大小、时长、日期等值,还包括记录、列表与错误值。目录列表因此可以支持 where size > 2kb 这样的条件,而不要求用户从一行文本里找出第三列。
这里也有一个容易误会的地方:表格不等于数据库表。来自 JSON 的一组记录可能缺字段、字段类型也可能不一致。漂亮的列布局不会替上游补全数据契约。把缺字段当作空值,还是直接报错,需要脚本明确决定。
文件解析与序列化可以放在清晰的位置
README 用 open Cargo.toml | get package.version 展示字段访问。open 会尝试依据格式调用相应转换命令;如果想保留原始输入,可以使用 --raw。
这个设计把格式处理从正则技巧变成了命令。读取 JSON 后,可以用普通管道访问嵌套字段;要交给其他程序时,再明确选择 to json 或 to csv。输入解析和输出编码各有位置,比让每一步猜测数据是不是文本更容易维护。
值流与字节流并存
过滤不必先拿到整个输入,排序通常需要。Nu 的运行时允许两种情况同时存在:能逐条消费时使用流,需要完整值时再收集。
因此不能把"支持流"理解成"所有命令都用常量内存"。sort-by 的实现会先收集全部元素,普通 from json 也先收集完整字符串。数据量大时,命令组合方式比管道写得短不短更重要。
外部工具继续可用,但失败结果要单独处理
^ 可以明确指定外部命令,避免与同名 Nu 命令混淆。complete 则能把外部进程的输出和退出码变成一个记录,适合写需要判断成功与否的构建包装脚本。
这不是沙箱。外部工具的文件写入、网络访问和权限仍由当前用户及操作系统决定,结构化结果也不会撤销已经发生的副作用。
🔬 技术架构深度解析
1. 从源代码到执行:先解析、再合并状态、最后运行
Nu 不会把每段管道简单改写成一条 Bash 命令。nu-cli 中的 evaluate_source 会创建 StateWorkingSet,交给解析器生成代码块;遇到解析错误或编译错误就返回失败,成功后才把工作集增量合并进 EngineState。
解析阶段会构造 AST,编译器把代码块编译成 IR,并附加到 block.ir_block。运行时的 eval_block 再进入 eval_ir_block。这是一条解释执行 IR 的路径,不应把它描述成生成本机机器码的 JIT。
text
交互输入 / -c 命令 / .nu 文件
│
▼
nu-cli / StateWorkingSet
│
▼
nu-parser:词法、调用解析、代码块
│
▼
nu-engine::compile → IrBlock
│
解析/编译错误 ──→ 诊断并停止该次求值
│ 成功
▼
EngineState.merge_delta + Stack
│
▼
eval_block → eval_ir_block
│
▼
内建命令 / 闭包 / 外部进程
│
▼
PipelineData + 执行状态
│
▼
下一命令 / 显式序列化 / 终端显示
EngineState 可以理解为命令声明、代码块等引擎信息所在的位置;Stack 承担执行时变量与调用环境等状态。它们与 StateWorkingSet 的职责不同:解析临时产生的新增声明,不应在解析还没成功时就被当作正常执行结果。
脚本入口还有额外处理。evaluate_file 会查找 main,若存在,就先求值文件里的顶层定义和语句,再调用入口。它不是"只取最后一行执行",也不能据此推断顶层语句没有副作用。
2. 为什么流没有直接塞进所有 Value
PipelineData 的核心分支很少。下面省略元数据,用示意写法保留它的结构:
rust
// 结构示意,不是可直接替换项目源码的声明
PipelineData = Empty
| Value(Value)
| ListStream(ListStream)
| ByteStream(ByteStream)
这区分了三个常被混为一谈的对象:一个已经得到的值、一串尚未全部消费的值、尚未全部读取的字节。
源码注释说明了把流和普通值分开的原因。假如一个可克隆的值内部持有同一个可消费流,变量 A 读取几条后,变量 B 看到的剩余内容也会改变。看似只是复制变量,实际却共享了读取进度。为它加锁只能保护并发,不能消除这种让人困惑的语义。
Nu 选择让流保留在管道层,需要稳定、可重复访问的数据时再变成具体值。代价是收集行为必须被认真对待:把一个大流转成列表,意味着等待和内存占用,而不只是换一个类型名字。
这也解释了"结构化"与"流式"为什么不能互相代替。一个完整 JSON 数组是结构化数据,但解析器可能得先读完整段文本;一条字节流可以持续到来,却还没有记录字段供 where 使用。
3. where 如何保留流,以及错误为什么不等于 false
where 的签名要求一个条件,其语法形状是 RowCondition。where duration_ms > 500 是面向当前行的简写;显式闭包则写成 where {|row| $row.duration_ms > 500 }。
运行时的主要步骤是:
text
输入 PipelineData
│
├─取出元数据
▼
into_iter_strict
│
▼
逐元素调用 ClosureEval
│
├─条件结果为 true:保留原元素
├─条件结果不是 true:不输出该元素
└─条件求值失败:输出 Value::Error
│
▼
带元数据的输出流
实现使用迭代器的 filter_map,并不先创建包含所有输入的向量。但闭包求值过程中,每个输入值会被克隆后交给条件判断,因而"流式"也不等于零分配或零复制。
更重要的是错误分支。若某行缺少要访问的字段,Nu 不会自动把它判成"不匹配"并悄悄丢掉。where 的实现把求值失败包装为错误值,后续消费者可能把错误升级为整条管道失败。普通 Result 返回路径和流里的 Value::Error 是两个不同层次,不能只看命令函数已经返回 Ok 就认定所有行都处理成功。
例如:
nu
[{a: 1}, {b: 2}] | where a > 0 | to json --raw
在 0.116.1 中,这段命令会以非零状态退出并报告缺少 a 列。若数据允许缺字段,应先确定缺失值的业务含义,再做可选访问或补默认值,而不是用忽略错误掩盖输入格式变化。
仓库中的 where 测试覆盖了文件大小比较、闭包条件、表格过滤等场景;这些测试表达的是维护者承诺要守住的行为,不代表每一种数据源都天然符合相同字段约束。
4. 一个 sort-by 就能改变整条管道的内存模型
sort-by 的实现里有一行非常直白:
rust
let mut vec: Vec<_> = input.into_iter_strict(head)?.collect();
它拿到全部元素后,解析比较字段或比较闭包,调用排序逻辑;若有 --reverse 再反转结果,最后输出列表。这个顺序意味着它是完整收集点。
因此下面两种思路并不等价:先排序再过滤,和先过滤再排序。只要过滤不依赖全局顺序,先减少记录数量,通常就能降低后续排序所需的工作量与存储量。这里说的是算法上的成本变化,不是某台机器上的速度倍数。
对持续产生的日志流,追加一个 sort-by 还可能让结果迟迟不能出现,因为排序得等输入结束。first 放在全局排序之后,也不能让排序跳过读取剩余输入;从排序实现看,元素已经被收齐了。
同样需要注意排序键的类型。数字 120 和字符串 "120" 表达的业务含义可能相同,比较行为却不能靠直觉假定一致。源头把耗时写成数值,比在多个消费脚本里分别修补格式更可靠。
5. JSON 数组与 JSON Lines 走不同的数据路径
from json 默认调用 collect_string_strict,拿到整段字符串后解析成一个值。一个文件只有一份 JSON 文档时,这条路径很自然,但它不是增量读取超大数组的承诺。
--objects 切换到按行解析:输入为字符串时用 Cursor,输入为适合文本处理的 ByteStream 时拿到 reader,然后通过 read_json_lines 产生 ListStream。空行会被跳过,每个非空行单独解析。
text
普通 JSON 文档
字节/字符串 → 收集完整字符串 → 解析一个 JSON 值
JSON Lines
字节 reader → 按行读取 → 每行解析 → 记录流 → where
名字叫 --objects,但实际约定是一行一个 JSON 值,不是只准出现对象;把一个对象漂亮地缩进成多行,则不满足这条输入约定。--strict 负责严格 JSON 语法,例如拒绝尾随逗号,和是否按行读取是两个独立开关。
按行模式仍有边界:单行本身很大时,这一行也要占内存;后面接 sort-by,照样会把所有解析结果收集起来。坏行会成为错误值进入流,也不能假定系统自动跳过并保留一份旁路错误日志。
6. 到外部程序,结构化协议就在这里结束
run-external 负责环境、参数、工作目录、重定向和子进程连接。其 write_pipeline_data 能直接传递字节流,二进制值也有专门路径;其他结构化数据可能经过 table 转成展示文本再写给外部进程。
这意味着把记录直接管道给某个外部程序,并不等于发送了 JSON。面向人看的表格包含布局约定,外部程序需要的却可能是逐行文本或固定编码格式。
更稳妥的规则是:在跨进程边界前显式编码。
nu
[{module: app, duration_ms: 810}]
| to json --raw
输出才是一段明确的 JSON 文本。接收端还需要自己解析它;若其协议要求一行一个对象,也要按对方协议组织,不能用"总归是 JSON"来替代格式约定。
反方向也一样。外部程序输出 JSON,应先 from json 再按字段过滤;普通日志应先 lines、parse 或其他合适命令变成记录。Nu 没有从任意人类日志中自动推断可靠业务模式的保证。
7. complete 收集结果,不替业务判断成功
complete 接受来自外部子进程的字节流,通过 wait_with_output 等待并收集输出,再构造结果记录。标准输出、标准错误在存在相应捕获数据时加入记录;有效 UTF-8 转成字符串,否则保留为二进制值。退出码以整数加入。
它的重定向声明为 stdout 和 stderr 分别设置捕获通路,避免把两者混为一段。涉及复合外部命令时,可以按帮助建议用 do 包住,再交给 complete。
nu
do { ^sh -c 'printf ok; printf problem >&2; exit 7' }
| complete
| to json --raw
在 macOS 上用 0.116.1 执行,结果是:
json
{"stdout":"ok","stderr":"problem","exit_code":7}
这里 Nu 命令本身可以成功返回结果记录,而记录里的子进程退出码是 7。自动化系统如果只检查"成功得到 JSON",就会误判。应当明确检查 exit_code,再决定是否继续解析 stdout、返回失败或启动重试。
开发分支实现还会把已经由 complete 处理的子进程状态标记为已处理,避免全局管道失败机制再次把同一状态当未处理错误;这不表示错误已经修复,更不表示应该忽略结果字段。
complete 也不是任意流的收集器。[1 2 3] | complete 会报错。它绑定的是外部进程结果语义;如果目标只是拿到内部列表,应使用对应的数据操作,而不是借一个名字相似的命令。
此外,完整捕获日志意味着内存成本。长时间运行且输出没有上限的任务,不宜仅为方便判断退出码就把全部内容长期攒在一个记录里。仓库有大量 stdout、stderr 以及两者同时产生数据时的测试,这些用例说明维护者关注管道阻塞问题,但不能代替对实际日志规模的评估。
📖 README 核心内容摘要
README 把 Nu 定义为一种新的 Shell,并强调 Windows、macOS、Linux 上的跨平台体验。最有用的例子并不是炫目的表格,而是同一套 where 能用于目录信息,也能用于进程信息:命令间复用的是数据操作,不是输出文本格式。
另一个适合马上借鉴的例子是 open Cargo.toml | get package.version。格式解析、字段访问和最终展示各自独立;一旦养成这个习惯,脚本里"先打印、再从打印结果反向提取数据"的代码会少很多。
安装方面,README 给出 Homebrew 与 Windows 包管理器入口。正式发行包也适合放在独立目录中试用,不必立即更换系统登录 Shell。项目仍提醒部分命令可能不稳定、语言设计会演进,团队脚本应固定版本并覆盖关键输入。
README 还介绍插件扩展,但阅读这部分应区分概念与协议细节:插件可以提供命令并参与结构化数据处理,不代表随便一个打印 JSON 的程序就成了 Nu 插件。插件通信和兼容性有专门实现,不能拿 README 里的简短概述代替当前协议规范。
对于第一次接触 Nu 的开发者,先把现有工作流的一小段数据处理迁入,比同时迁移配置文件、提示符和所有 Bash 脚本更容易判断收益。MIT 许可证允许复用和修改代码,但二进制依赖与插件仍应按各自许可处理。
🚀 快速上手
先独立运行,不改登录 Shell
macOS 或 Linux 可按照 README 使用 Homebrew:
bash
brew install nushell
nu --version
nu --no-config-file
包管理器安装的是当时可用版本,不保证固定在 0.116.1。需要与下文一致时,可从主仓库的 Releases 页面选择 0.116.1 以及匹配 CPU、操作系统的官方压缩包,解压后使用其中的 nu 可执行文件;Windows 对应 nu.exe。不必把它写入系统默认 Shell 配置。
--no-config-file 会跳过用户的 config 和 env 文件,适合让示例少受个人设置影响,但它不是隔离执行环境,不能防止命令访问当前用户的文件或网络。
下面代码块写的是 Nu 语法,请在 nu 中执行。示例构建数据是手写样例,不代表某个真实项目的性能。
处理一组构建结果
nu
let builds = [
{module: app, duration_ms: 810, ok: true}
{module: core, duration_ms: 120, ok: true}
{module: feature, duration_ms: 1200, ok: false}
]
$builds
| where ok == true
| sort-by duration_ms --reverse
| select module duration_ms
| to json --raw
0.116.1 的输出为:
json
[{"module":"app","duration_ms":810},{"module":"core","duration_ms":120}]
如果需要写入工程脚本,可以把上述内容保存为 build-report.nu,再从保存文件的目录执行:
bash
nu --no-config-file build-report.nu
这里不需要克隆 Nu 仓库;它处理的是你自己保存的脚本。正式流水线应把版本检查放进环境准备,而不是把某台电脑上碰巧能运行当作兼容性证明。
读取 JSON Lines,并主动选择严格语法
nu
'{"a":1}
{"a":3}'
| from json --objects --strict
| where a > 1
| to json --raw
对应结果:
json
[{"a":3}]
面对文件,可以用 open --raw 避开自动格式判断,再显式接 from json --objects --strict。这种写法把格式契约放在命令里,便于别人接手时判断文件究竟是一份 JSON 文档还是逐行记录。
把外部失败转成明确分支
下面是 macOS、Linux 上具备 sh 时可运行的例子:
nu
let result = (do { ^sh -c 'printf ok; printf problem >&2; exit 7' } | complete)
if $result.exit_code != 0 {
error make {msg: $"外部任务失败,退出码:($result.exit_code)"}
}
$result.stdout
此例故意制造退出码 7,因此应该报错,而不是打印成功信息。它只演示错误分支,没有自动重试:若真实命令涉及上传、发布或数据库写入,应先判断副作用是否已经发生,再决定重试是否安全。
help from json、help sort-by、help complete 可以直接查看当前二进制支持的参数。把这些接口与脚本版本一起确认,比复制一段未标版本的博客命令稳妥。
📊 增长速度与社区热度
Nushell 在这次日榜采集中位列第 13。它与编辑器、框架、编译器和基础工具出现在同一张榜单,说明当天的关注对象并不局限于 AI 应用包装。对 Nu 来说,值得持续观察的是命令行为与脚本兼容性,而不只是终端演示是否好看。
日榜位置是关注信号,不能直接换算成采用规模或增长率。判断是否引入团队,更应看关键命令在固定版本下的行为、对缺失字段的处理,以及维护者对回归问题的响应。仓库里围绕过滤、排序、JSON 转换和外部进程捕获的测试,为这些判断提供了具体入口。
下表是本次采集的完整 GitHub Trending 日榜,按原始顺序列出:
🎯 适用场景
构建产物与接口返回值的整理
Android 构建脚本、测试结果和发布清单经常已经有 JSON 或 CSV 输出。Nu 适合负责筛选、字段重命名、排序和导出。若 Gradle 任务只输出面向人的日志,则最好先让任务生成机器可读报告,再进入 Nu;换一个 Shell 并不能消除上游格式不稳定的问题。
本地运维与目录查询
按文件大小、类型、修改时间筛选目录,比反复解析 ls 展示文本更贴近意图。同一套字段操作也适用于其他结构化数据源。涉及删除和覆盖时,仍应把"得到候选列表"和"执行写操作"分开,先确认数据,再施加副作用。
跨平台数据脚本的一个选择
团队在 Windows、macOS 和 Linux 上处理相同报告时,Nu 的内建命令与数据模型有助于减少平台文本差异。不过,一旦调用 sh、adb 或某个系统工具,平台可用性和外部参数差异仍然存在。文章里的 sh 示例就不能原样承诺适用于所有 Windows 环境。
不适合直接照搬的情况
已有大量稳定 POSIX Shell 脚本时,Nu 不是语法兼容替身。条件表达式、变量、循环与命令替换都需要按 Nu 语言理解,不能仅替换解释器路径。
需要处理巨大、持续不断的数据流时,也要留意完整收集点。sort-by、普通 JSON 文档解析、complete 捕获各有成本;若任务本身需要数据库的索引、事务或磁盘外排序,不应因为 Nu 有表格操作就把它当作数据库替代品。
💡 总结
Nushell 把字段和类型留在管道里,让一批原本围绕文本格式打补丁的脚本变得直接。实现上最值得读的是 PipelineData 对值与流的分离,以及命令自己选择逐项消费还是完整收集的方式。
实际使用时,把输入格式、完整收集点和外部进程退出码说清楚,收益就比较容易落到具体脚本上。先迁移一个报告处理步骤,用固定版本守住输出,再决定要不要把它扩展成日常 Shell,比一次性替换全部环境更合适。