深入解析 nushell 的 nu-pretty-hex:可定制、带颜色与 ASCII 列的 Rust 十六进制转储库
【免费下载链接】nushellA new type of shell项目地址: https://gitcode.com/GitHub_Trending/nu/nushell
nu-pretty-hex是 Nushell 仓库(/data/web/disk1/git_repo/GitHub_Trending/nu/nushell)中维护的一个纯 Rust 十六进制转储(hex dump)库,它是在社区库pretty-hex基础上的重新打磨版本,目标是"让它更漂亮"。本文将以 crates/nu-pretty-hex/README.md 为主线,结合 crate 源码与它在 Nushell 内部的实际调用点,系统讲解三种渲染入口、HexConfig的每个配置字段、ANSI 颜色分类规则,以及fmt::Display/fmt::Debug集成等进阶用法。读完后,你将掌握如何把任意字节数据渲染成带地址、分组、ASCII 列、甚至带色彩的一行或多行 hex dump,并理解 Nushell 命令行工具中的二进制展示为什么看起来那么整齐。
一个 crate,三种渲染风格
nu-pretty-hex对外提供"一条 API,三种口味"的设计,全部通过 src/lib.rs 的mod pretty_hex; pub use pretty_hex::*;统一导出:
| 函数 | 输出形态 | 默认配置 | 等价的可格式化调用 |
|---|---|---|---|
simple_hex(&source) | 单行、无标题、无 ASCII 列 | HexConfig::simple() | format!("{}", source.hex_dump()) |
pretty_hex(&source) | 多行、含标题、地址、ASCII 列 | HexConfig::default() | format!("{:?}", source.hex_dump()) |
config_hex(&source, cfg) | 完全按HexConfig自定义 | 传入的cfg | format!("{:?}", source.hex_conf(cfg)) |
从 src/pretty_hex.rs 的实现可以看到,三个函数最终都汇聚到同一个底层函数hex_write,只是传入的配置和with_color开关不同:
pub fn simple_hex<T: AsRef<[u8]>>(source: &T) -> String { let mut writer = String::new(); hex_write(&mut writer, source, HexConfig::simple(), None).unwrap_or(()); writer } pub fn pretty_hex<T: AsRef<[u8]>>(source: &T) -> String { let mut writer = String::new(); hex_write(&mut writer, source, HexConfig::default(), Some(true)).unwrap_or(()); writer } pub fn config_hex<T: AsRef<[u8]>>(source: &T, cfg: HexConfig) -> String { let mut writer = String::new(); hex_write(&mut writer, source, cfg, Some(true)).unwrap_or(()); writer }同时 crate 还提供了一批面向std::fmt::Write的"写者"版本,适合不经过中间String、直接写入任意缓冲区的场景(src/pretty_hex.rs):
simple_hex_write(writer, source):等价于simple_hex的写者版;pretty_hex_write(writer, source):等价于pretty_hex的写者版;hex_write(writer, source, cfg, with_color):一切格式化的终极底层函数,with_color传Some(true)开启 ANSI 颜色,Some(false)/None关闭。
tests/tests.rs中的test_hex_write_with_simple_config甚至用它配合heapless::Vec验证了在不依赖alloc的嵌入式/no_std场景下也能正常工作——这是 fork 掉上游pretty-hex后新增的关键能力之一。
simple_hex:一行搞定小数据
simple_hex渲染单行十六进制,无标题、无地址、无 ASCII 列。README 给出的标准示例:
use nu_pretty_hex::*; let v = vec![222, 173, 190, 239, 202, 254, 32, 24]; assert_eq!(simple_hex(&v), format!("{}", v.hex_dump())); println!("{}", v.hex_dump());输出:
de ad be ef ca fe 20 18注意默认的 4 字节分组:前 4 个字节de ad be ef之间用单空格,跨组交界处(第 4、第 8 字节之后)用双空格分隔。这个视觉分组正是源码头文件注释里"grouped in default format without header and ASCII column"所说的默认格式,HexConfig::default()中width为 0(不换行、无地址)即是该风格。
pretty_hex:经典的多行"标准 hexdump"
pretty_hex使用HexConfig::default(),即 16 字节一行、4 字节一组、带标题头与 ASCII 列。README 示例:
use nu_pretty_hex::*; let v: &[u8] = &random::<[u8;30]>(); assert_eq!(pretty_hex(&v), format!("{:?}", v.hex_dump())); println!("{:?}", v.hex_dump());输出:
Length: 30 (0x1e) bytes 0000: 6b 4e 1a c3 af 03 d2 1e 7e 73 ba c8 bd 84 0f 83 kN......~s...... 0010: 89 d5 cf 90 23 67 4b 48 db b1 bc 35 bf ee ....#gKH...5..输出结构分四段:首行是长度标题(十进制 + 十六进制),随后每一行依次是 8 位十六进制地址、按 4 字节分组排列的十六进制字节、以及对应的 ASCII 可读列。末行不足 16 字节时,十六进制区以空格补齐、ASCII 列截断到实际字节数,使各行的可读列仍能左右对齐。
config_hex:一切皆可配置
当需要精确控制版面(例如去掉标题、改列宽、取消分组、指定起止位置)时,使用config_hex,配合结构体更新语法只覆盖需要的字段:
use nu_pretty_hex::*; let cfg = HexConfig {title: false, width: 8, group: 0, ..HexConfig::default() }; let v = &include_bytes!("data"); assert_eq!(config_hex(&v, cfg), format!("{:?}", v.hex_conf(cfg))); println!("{:?}", v.hex_conf(cfg));输出(8 字节/行、不分组、无标题头):
0000: 6b 4e 1a c3 af 03 d2 1e kN...... 0008: 7e 73 ba c8 bd 84 0f 83 ~s...... 0010: 89 d5 cf 90 23 67 4b 48 ....#gKH 0018: db b1 bc 35 bf ee ...5..这一示例同时演示了 trait 方法hex_conf(cfg)的用法——它和hex_dump()一样返回Hex<'_, T>包装器(见下文"与 Rust 格式化体系深度集成"小节),两者的区别仅是携带的配置不同。
HexConfig:把版面的控制权交给你
HexConfig是这套库的"控制面板",所有布局参数都集中在这里(定义见 src/pretty_hex.rs),配合#[derive(Clone, Copy, Debug)]可以放心复制传递。
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
title | bool | true | 是否在首行输出数据长度标题头 |
ascii | bool | true | 是否在每行末尾附加 ASCII 可读列 |
width | usize | 16 | 每行显示的源字节数;为 0 时表示整段不换行且不打印地址前缀(对应simple风格) |
group | usize | 4 | 每多少个字节插入一个较宽的组间距(双空格);0 表示整行不分组 |
chunk | usize | 1 | 每个"词"内连续堆叠的字节数(用于把字节两两/四四合并,如6b4e);0 表示不合并 |
address_offset | usize | 0 | 地址计数的起始偏移量 |
skip | Option<usize> | None | 跳过源数据开头多少个字节不显示 |
length | Option<usize> | None | 只显示源数据的前多少字节;None即完整长度 |
styles | HexStyles | default() | 各类字节的 ANSI 颜色/字体样式 |
Default的实现定义在 src/pretty_hex.rs:默认即 16 字节/行、4 字节/组、带标题与 ASCII 列,等价于pretty_hex的输出。而HexConfig::simple()则返回title: false, ascii: false, width: 0并保留其余字段,直接支撑simple_hex。
字节布局的三个联动参数:width、group、chunk
理解版面要先弄清width、group、chunk三者如何协同,它们在 hex_write 与 delimiter 中实现。核心规则:
width > 0:数据按width字节切行,每行地址为行号 * width + skip + address_offset(格式{:08x}占满 8 位十六进制);width == 0:整段作为一行,不打印地址。delimiter(i):字节i与下一个字节之间的分隔符。当chunk > 0且i恰为chunk的整数倍时插入分隔——如果group > 0且i同时是group * chunk的整数倍,则插入" "(组间距,即一个更宽的列间隙),否则插入单个" "。README 各示例中每 4 字节一组的双空格即由此而来。chunk > 1:把连续多个字节"黏"成一个字,如示例examples/hex_demo.rs注释所示——chunk: 2时输出461 7272 656e 2053 ...(每 2 字节拼一个 4 位十六进制字);chunk: 4时输出44617272 656e2053 6368726f 65646572(每 4 字节拼一个 8 位字)。
切片参数:skip、length、address_offset
skip与length在 hex_write 中作用于源数据上:先用.skip(skip).take(amount)截出真正需要渲染的子序列,再进入分行循环。address_offset则只参与地址打印,不改变字节内容。
examples/hex_demo.rs给出一个综合用法,对字符串"Darren Schroeder 😉"(含多字节 emoji)做切片与自定义样式:
let config = HexConfig { title: true, ascii: true, width: 16, group: 4, chunk: 1, address_offset: 0, skip: Some(10), // 跳过前 10 个字节 // length: Some(5), // 可选:只显示 5 个字节 length: Some(50), // 最多显示 50 个字节 styles: HexStyles::default(), }; println!("ConfigHex\n{}\n", config_hex(&my_string, config));值得留意的是,若源数据为空,hex_write 会直接返回,不输出任何内容;而tests/tests.rs的注释用例还验证了pretty_hex(&vec![])会输出"Length: 0 (0x0) bytes\n"这一标题行为——空数据到底该不该有标题,取决于走的是哪条渲染路径。
面向AsRef<[u8]>的泛型设计:字符串与字节数组一视同仁
库的所有公开函数都使用T: AsRef<[u8]>泛型约束,意味着Vec<u8>、&[u8]、[u8; N]、String、&str、&[T](当元素为u8)都能直接传入。tests/tests.rs(早期注释用例)明确验证了以下等价关系:
let str = "string"; let string: String = String::from("string"); let slice: &[u8] = &[0x73, 0x74, 0x72, 0x69, 0x6e, 0x67]; assert_eq!(simple_hex(&str), "73 74 72 69 6e 67"); assert_eq!(simple_hex(&str), simple_hex(&string)); assert_eq!(simple_hex(&str), simple_hex(&slice));也就是说,&str、String与等价字节切片会产生完全一致的输出。examples/hex_demo.rs中甚至直接把一个String(UTF-8 字符串)传给pretty_hex/config_hex,打印时自然带上 UTF-8 的多字节字符占用——在 ASCII 列中,非 ASCII 字节会根据字节类别被替换为占位符(见下节)。因此调试网络包、文件头、编码字节、内存布局时,你几乎不需要先做类型转换。
ANSI 颜色与字节分类:被 Nushell 复用的"好看"来源
"让它更漂亮"的核心卖点在于颜色。库在字节分类的基础上为每一类字节提供独立的Style(来自 Nushell 自研的nu-ansi-term),十六进制区与 ASCII 区共用同一套分类逻辑,函数 categorize_byte 的判定顺序是:
- 值为
0x00的 NUL 字节 →null_char; is_ascii_graphic()的可打印图形字符(字母、数字、标点)→printable;is_ascii_whitespace()的空白类:0x20空格在 ASCII 列显示为真实空格,\t/\n/\r等其它空白显示为_→whitespace;- 其余 ASCII 控制字符(如
0x7f等)→ascii_other,ASCII 列中显示为•; - 非 ASCII(
>= 0x80)→non_ascii,ASCII 列中显示为×。
HexStyles(src/pretty_hex.rs)封装这五类样式,其Default定义了与 Nushell 终端主题一致的配色(见 src/pretty_hex.rs):
| 类别 | 样式 |
|---|---|
null_char(NUL) | Color::Fixed(242)(近灰) |
printable(可打印 ASCII) | 青色(Cyan)+ 加粗 |
whitespace(空白) | 绿色(Green)+ 加粗 |
ascii_other(其它 ASCII/控制字符) | 紫色(Purple)+ 加粗 |
non_ascii(非 ASCII) | 黄色(Yellow)+ 加粗 |
tests/tests.rs的test_hex_colors是对这一套分类的"快照式"验证:它构造了[0,1,2,3,b'a',b'b',b'c',空格,\t,\n,\r,'[','?',0xF0,0xFE,0xFF]覆盖全部五类字节,再断言完整输出。从期望串里可以看到 NUL 的00被包上38;5;242m、可打印字符61被包上1;36m(青加粗)、空白09/0a/0d被包上1;32m(绿加粗)、非 ASCII 的f0 fe ff被包上1;33m(黄加粗);ASCII 列则相应出现0、真实空格、_、•与×的组合。
开启颜色的方式有两种:一是config_hex/pretty_hex返回的字符串本身默认就带 ANSI 转义(它们内部传Some(true));二是调用底层hex_write(..., Some(true))。若目标输出不支持颜色(如写日志文件、做断言比较),可走simple_hex_write/hex_write(..., None),或用simple_hex/simple_hex_write。
另外注意:标题行的颜色内容稍有不同。启用颜色时 write_title 输出的标题会在Length: N (0xN) bytes之后追加一行null_char printable whitespace ascii_other non_ascii图例,各词用对应类别的样式染色,方便读者即时看懂颜色约定;关闭颜色时标题只有Length: N (0xN) bytes(对应tests/256.txt的期望输出)。这也是pretty_hex_stream在 Nushell 中渲染时的行为来源。
与 Rust 格式化体系深度集成:hex_dump 与 hex_conf
除了自由函数,库还通过PrettyHextrait 与fmt::Display/fmt::Debug打通了 Rust 原生的格式化机制,这是"能用{}/{:?}直接打印"的关键:
PrettyHextrait(src/pretty_hex.rs)对一切T: AsRef<[u8]>自动实现:hex_dump()→ 包装为使用HexConfig::default()的Hex<'_, T>;hex_conf(cfg)→ 包装为使用指定cfg的Hex<'_, T>。
- 包装器
Hex<'a, T>(src/pretty_hex.rs)同时实现了两个 trait,行为恰好对应两种风格:fmt::Display→ 走hex_write(..., cfg.to_simple(), None),即单行 simple 风格;fmt::Debug→ 走hex_write(..., cfg, None),即完整 pretty 风格。
所以 README 中出现的两个assert_eq!其实是同一结果的不同写法:
assert_eq!(simple_hex(&v), format!("{}", v.hex_dump())); // Display = simple assert_eq!(pretty_hex(&v), format!("{:?}", v.hex_dump())); // Debug = pretty assert_eq!(config_hex(&v, cfg), format!("{:?}", v.hex_conf(cfg)));而tests/tests.rs(早期注释用例)进一步验证了v.hex_conf(cfg).to_string()会输出没有地址的单行十六进制,format!("{:?}", v.hex_conf(cfg))则输出带地址与 ASCII 的多行版本。注意调试输出在{:?}场景下默认不带颜色(with_color为None),如果希望终端上既有 Debug 结构又有颜色,应改用显式的config_hex或pretty_hex。
在 Nushell 中的真实战场:终端二进制数据展示
脱离 Nushell 语境去理解这个 crate 是不完整的——它的 fork 动机恰恰来自 Nushell 的实际渲染需求。在仓库中,nu-pretty-hex被两类关键 UI 场景复用:
1. Nushell 表格命令的字节流渲染。crates/nu-command/src/viewers/table.rs 导入nu_pretty_hex::{HexConfig, HexStyles},在handle_table_command中对ByteStreamType::Binary数据走pretty_hex_stream,把二进制输出为带标题与 ASCII 的 hex dump 文本流。该函数(table.rs)演示了本库在真实工程中的用法:以cfg.width为步长增量读取字节流,第一块先由write_title手动写标题,此后每块调用hex_write(..., Some(use_ansi_coloring))输出一行并累加cfg.address_offset。同时它还借用hex_styles字段接入用户配置的 ANSI 主题——这正是HexStyles从pretty_hex.rs中被 Nushell 表渲染直接消费的证据。
2. Nushell explore(交互式探索器)的二进制视图。crates/nu-explore/src/explore/views/binary/mod.rs 与 crates/nu-explore/src/explore/views/binary/binary_widget.rs 导入HexStyles与categorize_byte,用同一套字节分类与配色逻辑实现二进制视图的着色。
也就是说,在 Nushell 终端里对二进制数据执行table或进入 explore 二进制视图时所见到的地址列、分组、ASCII 列和颜色,底层都来自本 crate。若你想在 Nushell 之外快速体验其效果,可参考 examples/hex_demo.rs 运行:
cargo run --example hex_demo --manifest-path crates/nu-pretty-hex/Cargo.toml它会依次打印 "Darren Schroeder 😉" 的ConfigHex/SimpleHex/PrettyHex三种形态、0–127 号字符的完整渲染,以及一串随机字节的 pretty 输出,是理解各配置字段最直观的现场演示(examples目录位于 crates/nu-pretty-hex/examples)。
从上游 pretty-hex 到 nu-pretty-hex:改动要点一览
README 的 Inspiration 列表与本 crate 的自我介绍点明了它的血统与差异:它 fork 自 wolandr/pretty-hex(设计灵感最初来自 Haskell 生态的pretty-hex),目标是把原始实现打磨得更美观。从源码对比可以确认以下增强点(这些均可在本仓库源码中直接核验,并非上游行为):
- 面向
no_std/无alloc场景:库根模块不强制依赖alloc,hex_write等写者类 API 可工作在heapless等外部缓冲区上(tests/tests.rs的test_hex_write_with_simple_config专门覆盖此路径;Cargo.toml以[lib] doctest = false关闭文档测试并声明最小依赖); - 内置 ANSI 配色与字节分类:新增
HexStyles/categorize_byte,颜色取自nu-ansi-term,同时服务于 Nushell 的终端主题联动; - 流式渲染支持:Nushell 的
pretty_hex_stream通过write_title+ 分段hex_write的方式对未知长度字节流增量输出——这一用法在原始返回整串String的 API 形态上并不直接成立。
作为依赖它被声明在 crates/nu-pretty-hex/Cargo.toml 中,运行时仅依赖nu-ansi-term.workspace,开发期(示例与测试)额外使用rand与heapless。
应用速查:三种场景的推荐 API
把前面内容收敛成可直接照抄的实践模板:
场景一:想在日志/终端快速确认一段字节内容,只要单行紧凑输出:
use nu_pretty_hex::*; let bytes = b"nushell"; println!("{}", simple_hex(&bytes)); // 6e 75 73 68 65 6c 6c场景二:做文件头/网络包等二进制分析,需要地址、分组、ASCII 完整呈现:
use nu_pretty_hex::*; let data = std::fs::read("path/to/file.bin")?; println!("{}", pretty_hex(&data));场景三:按表格宽度精确排版——每行 8 字节、去掉标题、从第 4 字节起只打 20 字节:
use nu_pretty_hex::*; let cfg = HexConfig { title: false, width: 8, group: 2, skip: Some(4), length: Some(20), ..HexConfig::default() }; println!("{}", config_hex(&data, cfg));场景四:嵌入format!/println!,让对象的 Display 输出十六进制可读列:
use nu_pretty_hex::*; println!("{:#?}", blob.hex_dump()); // Debug 走完整多行 pretty 风格 println!("{}", blob.hex_dump()); // Display 走单行 simple 风格 println!("{:?}", blob.hex_conf(my_cfg)); // 用自定义配置的 pretty 风格需要指出,以上是 crate 作为库的使用方式;若你身处 Nushell 脚本环境,期望的其实是上节所述的table/explore 对二进制数据的自动 hex 渲染,两者共享同一实现。
小结与参考索引
nu-pretty-hex的定位可以概括为:在pretty-hex的基础上,为 Nushell 生态提供了一个带颜色的、可流式的、且保持AsRef<[u8]>泛型便利性的十六进制渲染引擎。从simple_hex的一行速览、到pretty_hex的标准分栏、再到HexConfig逐字段掌控版面,配合hex_dump()/hex_conf()与Display/Debug的双重集成,这套 API 已经覆盖了从调试打印到产品级表格渲染的全部需求。
想进一步阅读源码,推荐按此顺序浏览:
- 文档主入口:crates/nu-pretty-hex/README.md;
- 模块公开头与文档示例:crates/nu-pretty-hex/src/lib.rs;
- 全部实现(三个入口 +
HexConfig+ 颜色分类 + 格式化 trait):crates/nu-pretty-hex/src/pretty_hex.rs; - 可运行示例:crates/nu-pretty-hex/examples/hex_demo.rs;
- 集成测试(颜色快照、
no_std写者测试):crates/nu-pretty-hex/tests/tests.rs 与期望输出 crates/nu-pretty-hex/tests/256.txt; - Nushell 内部消费者:crates/nu-command/src/viewers/table.rs、crates/nu-explore/src/explore/views/binary/binary_widget.rs。
【免费下载链接】nushellA new type of shell项目地址: https://gitcode.com/GitHub_Trending/nu/nushell
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考