[bat-cli] 输出处理 | `OutputType` 和 `OutputHandle`
第三章:输出处理
在上一章输入管理中,我们学习了 bat
如何巧妙地收集所有内容——无论是来自文件、标准输入还是字节字符串——并为其处理做好准备。
现在 bat
已经获取了我们的代码,控制器正准备将其美化,接下来的关键问题是:这些格式化的文本最终会去哪里
这就是输出处理的职责所在~
输出处理解决了什么问题?
想象一下,我们刚刚烹饪完一道美味佳肴(即高亮显示的代码)。我们不会把它留在厨房台面上,对吧?我们需要将其端给客人(也就是用户!)。但如何端上呢?
- 是直接放在盘子里立即享用(直接显示到终端)?
- 或者,如果是一道大餐,是否应该使用特殊的餐具,让客人可以轻松浏览并选择他们想要的部分(通过分页程序如
less
发送)?
输出处理就是 bat
的“上菜服务”。它的任务是决定如何将 bat
格式化的输出最佳地呈现给我们。它会判断是直接打印到屏幕还是使用外部工具来管理长输出,同时确保颜色和格式保持完美。
本章的目标是理解 bat
如何做出这一决策并管理输出的传递。
OutputType
和 OutputHandle
:输出选项
bat
提供了几种输出方式,每种适用于不同场景:
- 直接输出到终端(标准输出):对于较短的文件,
bat
直接将所有内容打印到终端屏幕。这种方式快速且简单。 - 通过分页程序:对于较长的文件,或者如果我们有偏好,
bat
可以将输出发送到一个称为“分页程序”的特殊程序。分页程序如less
(在 Linux/macOS 上非常常见)允许我们滚动浏览输出、搜索和轻松导航,而不会淹没终端历史记录。 - 写入自定义写入器/缓冲区:如果我们在自己的 Rust 程序中将
bat
作为库使用,可能希望将输出捕获到字符串或其他自定义数据结构中,而不是直接显示。
这里的核心概念是 OutputType
和 OutputHandle
。
OutputType
:这是一个内部组件,用于决定输出应该去哪里(例如直接到stdout
还是到Pager
)。如果需要,它会管理分页程序的启动。OutputHandle
:这是一个通用接口,bat
的打印机使用它来实际写入格式化文本,无论最终传递到哪里。它可以是写入终端、分页程序的输入或字符串缓冲区。
让我们看看如何控制这一点。
PrettyPrinter
控制输出
我们可以通过 PrettyPrinter
的配置选项来影响输出处理。
use bat::{PrettyPrinter, PagingMode, WrappingMode};
use std::path::Path;fn main() {PrettyPrinter::new()// 1. 直接输出到终端(不使用分页程序).paging_mode(PagingMode::Never).input_file(Path::new("small_file.rs")).print().unwrap();PrettyPrinter::new()// 2. 始终使用分页程序,并专门配置 'less'.paging_mode(PagingMode::Always).pager("less -R -F -X") // 自定义分页程序命令.wrapping_mode(WrappingMode::NoWrapping(true)) // 告诉 less 不要换行.input_file(Path::new("long_file.rs")).print().unwrap();// 默认行为通常会智能地为大文件使用分页程序PrettyPrinter::new().input_file(Path::new("large_file.py")).print() // 可能会自动使用分页程序!.unwrap();
}
说明:
paging_mode(PagingMode::Never)
:明确告诉bat
不要使用分页程序,无论文件多长。输出将直接发送到终端。paging_mode(PagingMode::Always)
:强制bat
使用分页程序,即使是短文件。pager("less -R -F -X")
:设置bat
用于启动分页程序的具体命令。-R
对颜色很重要,-F
如果文件适合一屏则退出,-X
禁用less
的 termcap 初始化,有时可以解决问题。wrapping_mode(WrappingMode::NoWrapping(true))
:配置bat
本身,但也会影响bat
如何设置分页程序(less
)来处理长行。
运行这些命令时,bat
会:
- 对于
small_file.rs
:将其内容直接打印到终端。 - 对于
long_file.rs
:使用指定选项启动less
,并将高亮内容通过管道传递给它。然后我们可以使用less
滚动浏览。 - 对于
large_file.py
:bat
的默认paging_mode
是QuitIfOneScreen
,意味着只有在内容超出终端屏幕时才会启动分页程序。
输出到字符串缓冲区
如果我们将 bat
集成到另一个程序中,可能希望将其输出捕获到 String
而不是直接打印。OutputHandle::FmtWrite
选项允许这样做。
use bat::{assets::HighlightingAssets,config::Config,controller::Controller,output::OutputHandle, // 我们需要这个特定的 OutputHandleInput,
};fn main() {let mut buffer = String::new(); // 我们的目标字符串缓冲区let config = Config {colored_output: false, // 为了简化字符串输出..Default::default()};let assets = HighlightingAssets::from_binary();let controller = Controller::new(&config, &assets);let input = Input::from_bytes(b"fn test() { /* ... */ }").name("test.rs");controller.run(vec![input.into()],Some(OutputHandle::FmtWrite(&mut buffer)), // 将输出定向到缓冲区!).unwrap();println!("--- 捕获的输出 ---\n{buffer}--- 捕获结束 ---");
}
说明:
这里,我们向 controller.run()
方法提供了 OutputHandle::FmtWrite(&mut buffer)
。
这告诉控制器将所有格式化输出定向到我们的 buffer
字符串,而不是终端或分页程序。
这对于测试或基于 bat
构建工具非常有用。
输出处理的内部工作原理
当控制器准备显示内容时,它需要设置输出通道。
以下是简化的步骤分解:
- 控制器询问输出类型:控制器首先从配置中获取
PagingMode
(例如Always
、Never
、QuitIfOneScreen
)。 OutputType
决策:基于PagingMode
和pager
命令(如果指定),OutputType
模块决定是使用分页程序还是stdout
。它会检查终端是否支持分页程序、指定的分页程序是否存在,以及传递哪些参数(如less
的-R
颜色支持)。- 分页程序设置(如果选择):如果选择了分页程序,
OutputType
会实际启动分页程序(如less
)作为子进程。然后将bat
的输出重定向到分页程序的标准输入。这使得分页程序显示bat
的内容。 - 创建
OutputHandle
:无论是否使用分页程序,控制器都会创建一个OutputHandle
。这个句柄要么是直接到stdout
或分页程序stdin
的io::Write
,要么是如果我们提供了自定义缓冲区时的fmt::Write
。 - 打印机写入:打印机模块然后使用这个
OutputHandle
写入所有格式化行,而无需知道数据最终去哪里。它只是写入,OutputHandle
确保它到达正确的目的地。
以下是描述这一流程的序列图:
深入代码:src/output.rs
和 src/pager.rs
让我们看看 src/output.rs
和 src/pager.rs
中管理这一功能的核心结构。
首先,src/output.rs
中的 OutputType
枚举定义了两个主要目的地:
// src/output.rs
#[derive(Debug)]
pub enum OutputType {#[cfg(feature = "paging")]Pager(Child), // 使用分页程序时,保存子进程Stdout(io::Stdout), // 输出直接到标准输出
}impl OutputType {#[cfg(feature = "paging")]pub fn from_mode(paging_mode: PagingMode,wrapping_mode: WrappingMode,pager: Option<&str>,) -> Result<Self> {// ... 决定使用分页程序还是标准输出的逻辑 ...// 调用 try_pager 或 stdout()}/// 尝试启动分页程序。如果出错则回退到标准输出。#[cfg(feature = "paging")]fn try_pager( /* ... */ ) -> Result<Self> {// ... 构造并启动分页程序 Command 的逻辑 ...// 返回 OutputType::Pager(child_process) 或出错时返回 OutputType::stdout()}pub(crate) fn stdout() -> Self {OutputType::Stdout(io::stdout())}pub fn handle(&mut self) -> Result<&mut dyn Write> {// 返回对底层写入器的可变引用(分页程序的 stdin 或标准输出)match *self {#[cfg(feature = "paging")]OutputType::Pager(ref mut command) => command.stdin.as_mut().ok_or("无法为分页程序打开 stdin")?,OutputType::Stdout(ref mut handle) => handle,}}
}
说明:
OutputType
是一个enum
,捕获状态:bat
要么启动了Pager
(并保存其Child
进程句柄),要么正在写入Stdout
。from_mode
:控制器调用此函数,根据配置中的paging_mode
、wrapping_mode
和特定pager
命令决定输出类型。try_pager
:这个私有辅助函数尝试启动分页程序进程。它处理设置less
特定参数如-R
(颜色)和-F
(一屏退出)。如果启动分页程序失败,则回退到OutputType::stdout()
。stdout()
:一个简单的构造函数,用于输出直接到终端的情况。handle()
:这是关键方法,提供通用&mut dyn Write
接口。打印机使用它来写入,而无需知道是写入stdout
还是分页程序的stdin
。
接下来,OutputHandle
提供了写入的最终抽象层:
// src/output.rs
pub enum OutputHandle<'a> {IoWrite(&'a mut dyn io::Write), // 用于实际 I/O 流写入FmtWrite(&'a mut dyn fmt::Write), // 用于实现 fmt::Write 的任何对象(如 String)
}impl OutputHandle<'_> {pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> Result<()> {match self {Self::IoWrite(handle) => handle.write_fmt(args).map_err(Into::into),Self::FmtWrite(handle) => handle.write_fmt(args).map_err(Into::into),}}
}
说明:
OutputHandle
是另一个enum
,包装了io::Write
(用于实际 I/O,如文件或网络流,包括stdout
和分页程序stdin
)或fmt::Write
(用于写入String
缓冲区等)。write_fmt
:此方法允许bat
将格式化字符串(如writeln!
创建的)写入任一类型的句柄。
最后,src/pager.rs
模块包含查找和配置分页程序的逻辑:
// src/pager.rs
#[derive(Debug)]
pub(crate) struct Pager {pub bin: String, // 分页程序二进制文件(如 "less")pub args: Vec<String>, // 分页程序参数(如 ["-R", "-F"])pub kind: PagerKind, // 已知分页程序的枚举(Less、More、Unknown 等)pub source: PagerSource, // 分页程序配置来源(Config、EnvVarPager)
}#[derive(Debug, PartialEq)]
pub(crate) enum PagerKind
{Bat, // 防止递归如果 PAGER 设置为 'bat'Less,More,Most,Unknown,
}#[derive(Debug, PartialEq)]
pub(crate) enum PagerSource
{Config,EnvVarBatPager, // 来自 BAT_PAGER 环境变量EnvVarPager, // 来自 PAGER 环境变量Default,
}pub(crate) fn get_pager(config_pager: Option<&str>) -> Result<Option<Pager>, ParseError>
{// ... 检查 config_pager、BAT_PAGER、PAGER 环境变量的逻辑 ...// ... 然后解析命令并创建 Pager 结构体 ...// ... 有时将 'more' 或 'most' 替换为 'less' 以支持颜色 ...// 返回 Option<Pager>
}
说明:
Pager
:此结构体保存所选分页程序的所有详细信息,包括其二进制路径、参数以及bat
找到其配置的位置。PagerKind
:枚举用于识别常用分页程序,允许bat
应用特定配置(如为less
添加-R
)。PagerSource
:枚举跟踪分页程序如何被指定(通过--pager
选项、BAT_PAGER
环境变量、PAGER
环境变量或bat
默认值)。这帮助bat
决定何时覆盖参数。get_pager
:此函数由OutputType::try_pager
调用。- 它负责从各种来源(
--pager
命令行选项、BAT_PAGER
环境变量、PAGER
环境变量)解析分页程序命令,并创建Pager
结构体。 - 它还包含逻辑,如果分页程序来自通用
PAGER
环境变量且不支持颜色(如more
),则替换为less
。
- 它负责从各种来源(
这些组件共同确保 bat
精美的输出总是传递到正确的地方,以正确的方式,无论是直接到终端、通过智能分页程序,还是到自定义缓冲区。
结论
在本章中,我们揭示了 bat
的输出处理机制。
我们学习了 bat
如何智能地决定将其格式化文本发送到哪里——直接到终端或通过强大的分页程序如 less
。
-
我们还看到了如何通过
PrettyPrinter
选项显式控制此行为或将输出重定向到String
缓冲区。 -
在内部,
OutputType
做出关键决策并管理分页程序进程,而OutputHandle
为打印机提供统一的方式来写入内容,确保为我们的代码提供流畅灵活的输出服务。
现在输出已经处理完毕,下一步自然是深入了解 bat
本身的配置。在下一章中,我们将探索配置,学习所有这些偏好和设置如何存储和管理。
下一章:配置