核心设计模式
1. 两层参数结构
LowArgs (原始参数) → HiArgs (处理后的配置)
设计理念
- LowArgs:接近CLI原始输入,最小验证
- HiArgs:业务就绪的配置,包含复杂对象和计算结果
- 最小验证:最小验证的核心是将不受信任数据快速转变为可信数据
2. 字段组织策略
简单配置字段:
byte_offset: bool,
column: bool,
heading: bool,
quiet: bool,
// ... 直接从LowArgs 复制或简单计算
复杂构建对象
globs: ignore::overrides::Override,
pre_globs: ignore::overrides::Overrode,
types: ignore::types::Types,
patterns: Patterns,
paths: Paths,
环境感知字段
is_terminal_stdout: bool, // 检测输出终端
mmap_choice: grep::searcher::MmapChoice, // 内存映射策略
hyperlink_config: grep::printer::HyperLinkConfig, // 超链接配置
关键设计原则
1. 延迟构建模式
复杂对象在 from_low_args
中统一构建:
let globs = globs(&state, &low)?; // 需要所有 glob 模式
let types = types(&low)?; // 需要所有类型规则
let patterns = Patterns::from_low_args(...)?; // 需要所有模式
2. 状态依赖管理
通过 State
结构体管理环境状态
let mut state = State::new()?;
// state 包含:终端检测、stdin_cosumed、工作目录等
3. 配置计算模式
根据环境和标志动态计算最终配置:
let color = match low.color {
ColorChoice::Auto if !state.is_terminal_stdout => ColorChoice::Never,
_ => low.color,
};
let heading = match low.heading {
None => !low.vimgrep && state.is_terminal_stdout, // 智能默认值
Some(value) => value && !low.vimgrep, // 考虑标志冲突
};
通过 HiArgs
和 LowArgs
这种分层设计,将“解析”和“配置”职责分离,使得代码更加模块化,每层都有明确的职责边界。
核心构建方法
matcher() 方法的设计模式
策略模式
pub(crate) fn matcher(&self) -> anyhow::Result<PatternMatcher> {
match self.engine {
EngineChoice::Default => match self.matcher_rust() {
Ok(m) => Ok(m),
Err(err) => anyhow::bail!(suggest_other_engine(err.to_string())),
},
EngineChoice::PCRE2 => Ok(self.matcher_pcre2()?),
EngineChoice::Auto => {
// 尝试 Rust 引擎,失败则尝试 PCRE2
let rust_err = match self.matcher_rust() {
Ok(m) => return Ok(m),
Err(err) => err,
};
let pcre_err = match self.matcher_pcre2() {
Ok(m) => return Ok(m),
Err(err) => err,
};
// 两个都失败,提供详细错误信息
anyhow::bail!("regex could not be compiled with either engine...");
}
}
}
设计思路
- 根据用户选择的引擎类型,动态选择不同的匹配器实现
Auto
模式体现了优雅的降级策略:先尝试默认引擎,失败则尝试 PCRE2
建造者模式
fn matcher_rust(&self) -> anyhow::Result<PatternMatcher> {
let mut builder = grep::regex::RegexMatcherBuilder::new();
builder
.multi_line(true)
.unicode(!self.no_unicode)
.octal(false)
.fixed_strings(self.fixed_strings);
// 根据配置逐步构建
match self.case {
CaseMode::Sensitive => builder.case_insensitive(false),
CaseMode::Insensitive => builder.case_insensitive(true),
CaseMode::Smart => builder.case_smart(true),
};
// 最终构建
let m = builder.build_many(&self.patterns.patterns)?;
Ok(PatternMatcher::RustRegex(m))
}
设计思路
- 使用建造者模式逐步配置复杂对象
- 链式调用提供流畅的 API
- 最后调用
build_many()
完成构建
条件编译和特性门控
fn matcher_pcre2(&self) -> anyhow::Result<PatternMatcher> {
#[cfg(feature = "pcre2")]
{
// PCRE2 实现
let mut builder = grep::pcre2::RegexMatcherBuilder::new();
// ... 配置代码
Ok(PatternMatcher::PCRE2(m))
}
#[cfg(not(feature = "pcre2"))]
{
Err(anyhow::anyhow!("PCRE2 is not available in this build"))
}
}
设计思路
-
使用 Rust 的条件编译特性
-
特性定义(
Cargo.toml
)[features] pcre2 = ["grep/pcre2"] // 定义 pcre2 特性,依赖于 grep crate 的 pcre2 特性
-
在编译时决定是否包含 PCRE2 支持
其他方法
searcher()
、printer()
等其他构造方法基本采用建造者模式。search_worker()
方法单独说明:
组件组合模式
search_worker
体现经典的 组合模式:
SearchWorker
├── PatternMatcher (模式匹配)
├── Searcher (文件搜索)
└── Printer (结果输出)
每个组件职责:
PatternMatcher
:判断文本是否匹配模式Searcher
:读取文件内容,按行处理Printer
:格式化并输出匹配结果
设计优势
- 职责分离:每个组件专注于自己的功能
- 可测试性:可以独立测试每个组件
- 可扩展性:可以替换任何组件的实现
walk_builder()
职责分层
包含四个核心职责:
- 路径管理
let mut builder = ignore::WalkBuilder::new(&self.paths.paths[0]);
for path in self.paths.paths.iter().skip(1) {
builder.add(path)
}
- 初始化:使用第一个路径作为根路径
- 扩展:添加所有额外的搜索路径
- 忽略文件系统配置
// 用户自定义忽略文件
if !self.no_ignore_files {
for path in self.ignore_file.iter() {
if let Some(err) = builder.add_ignore(path) {
ignore_message!("{err}");
}
}
}
// Git 集成忽略规则
.git_global(!self.no_ignore_vcs && !self.no_ignore_global)
.git_ignore(!self.no_ignore_vcs)
.git_exclude(!self.no_ignore_vcs && !self.no_ignore_exclude)
// 通用忽略文件
.ignore(!self.no_ignore_dot)
.parents(!self.no_ignore_parent)
// ripgrep 专用忽略文件
if !self.no_ignore_dot {
builder.add_custom_ignore_filename(".rgignore");
}
- 遍历行为配置
builder
.max_depth(self.max_depth) // 最大递归深度
.follow_links(self.follow) // 是否跟随符号链接
.max_filesize(self.max_filesize) // 最大文件大小
.threads(self.threads) // 线程数
.same_file_system(self.one_file_system) // 是否限制在同一文件系统
.skip_stdout(matches!(self.mode, Mode::Search(_))) // 跳过标准输出
.hidden(!self.hidden) // 是否包含隐藏文件
.require_git(!self.no_require_git) // 是否要求 Git 仓库
.ignore_case_insensitive(self.ignore_file_case_insensitive); // 忽略文件大小写
- 高级功能配置
// 文件类型过滤
.overrides(self.globs.clone())
.types(self.types.clone())
// 排序优化
if let Some(ref sort) = self.sort {
assert_eq!(1, self.threads, "sorting implies single threaded");
if !sort.reverse && matches!(sort.kind, SortModeKind::Path) {
builder.sort_by_file_name(|a, b| a.cmp(b));
}
}
walk_builder
是系统集成点:
- 底层:文件系统遍历(
ignore::WalkBuilder
) - 中层:忽略规则处理(Git、自定义、类型过滤)
- 上层:用户配置映射(命令行参数到行为)
current_dir()
fn current_dir() -> anyhow::Result<PathBuf> {
let err = match std::env::current_dir() {
Err(err) => err, // 保存错误,继续尝试回退方案
Ok(cwd) => return Ok(cwd), // 成功则直接返回
};
if let Some(cwd) = std::env::var_os("PWD") {
if !cwd.is_empty() {
return Ok(PathBuf::from(cwd));
}
}
anyhow::bail!(
"failed to get current working directory: {err}\n\
did your CWD get deleted?",
)
}
此方法获取当前工作目录时的异常处理:
- 目录被删除 - 进程所在目录可能被其他进程删除
- 权限问题 - 无读取当前目录权限
- 符号链接问题 - 当前目录是损坏的符号链接
为何需要复杂处理?
- 需要 cwd 将相对路径转绝对路径(如
rg "pattern" ../other_projects/
) - 通过
PWD
环境变量回退保证健壮性
hiargs.rs
中其他方法均基于 LowArgs
完善 HiArgs
:
// 核心数据结构
let patterns = Patterns::from_low_args(&mut state, &mut low)?;
let paths = Paths::from_low_args(&mut state, &patterns, &mut low)?;
let binary = BinaryDetection::from_low_args(&state, &low);
// 辅助功能配置
let colors = take_color_specs(&mut state, &mut low);
let hyperlink_config = take_hyperlink_config(&mut state, &mut low)?;
let stats = stats(&low);
let types = types(&low)?;
let globs = globs(&state, &low)?; // 文件匹配规则
let pre_globs = preprocessor_globs(&state, &low)?; // 预处理器规则
核心结构体
Patterns
表示要匹配的内容:
struct Patterns {
patterns: Vec<String>,
}
模式来源的统一处理:
fn from_low_args(state: &mut State, low: &mut LowArgs)
-> anyhow::Result<Patterns>
三种模式来源:
- positional:
rg "pattern" file.txt
-e/--regexp
:rg -e "pattern1" -e "pattern2"
-f/--file
:rg -f patterns.txt
-e/--regexp
对应 Pattern
flag 的 update 逻辑:
fn update(&self, v: FlagValue, args: &mut LowArgs) {
let regexp = convert::string(v.unwrap_value());
args.patterns.push(PatternSource::Regexp(regexp));
Ok(())
}
-f/--file
对应 File
flag 的 update 逻辑:
fn update(&self, v: FlagValue, args: &mut LowArgs) {
let path = PathBuf::from(v.unwrap_value());
args.patterns.push(PatternSource::File(path));
Ok(())
}
Patterns::from_low_args
从三种来源构造去重(利用HashSet
)的 Patterns::patterns: Vec<String>
。-f file.txt, --file=file.txt
中file.txt(pattern文件)
存放着需要匹配的patterns
。
Paths - 路径管理系统
struct Paths {
paths: Vec<PathBuf>, // 实际路径列表
has_implicit_path: bool, // 是否有隐式路径
is_one_file: bool, // 是否只搜索单个文件
}
要么从positional
中读取文件路径,要么就是智能路径推断。
fn from_low_args(
state: &mut State,
_: &Patterns, // 虽然不使用,但强制要求存在
low: &mut LowArgs,
) -> anyhow::Result<Paths> {
let mut paths = Vec::with_capacity(low.positional.len());
for osarg in low.positional.drain(..) {
let path = PathBuf::from(osarg);
if state.stdin_consumed && path == Path::new("-") {
anyhow::bail!(
"error: attempted to read patterns from stdin \
while also searching stdin",
);
}
paths.push(path);
}
...
let use_cwd = !is_readable_stdin
|| state.stdin_consumed
|| !matches!(low.mode, Mode::Search(_));
let (path, is_one_file) = if use_cwd {
(PathBuf::from("./"), false) // 搜索当前目录
} else {
(PathBuf::from("-"), true) // 搜索 stdin
};
}
Paths::from_low_args()
函数使用了一个非常巧妙的编译时依赖约束设计,这个设计确保了
- 调用着必须先构造
Patterns
- 编译器会检查这个约束
- 无法意外地颠倒调用顺序
设计编译时依赖约束的原因是Patterns
和Paths
都使用了positional
,此数据消费顺序不可颠倒。
// Patterns::from_low_args 中:
if low.patterns.is_empty() {
let ospat = low.positional.remove(0); // 消费第一个位置参数
return Ok(Patterns { patterns: vec![pat] });
}
// Paths::from_low_args 中:
for osarg in low.positional.drain(..) { // 处理剩余的位置参数
paths.push(PathBuf::from(osarg));
}
编译时依赖约束设计优势
- 编译时安全 - 类型系统防止错误调用顺序
- 自文档化 - 函数签名清楚表达依赖关系
- 零运行时成本 - 约束在编译时检查,运行时无开销
- API 清晰性 - 强制调用者理解正确的使用方式
单文件优化检测:
let is_one_file = paths.len() == 1
&& (paths[0] == Path::new("-") || !paths[0].is_dir());
- 使用
!is_dir()
而非is_file()
更准确 - stdin (
-
) 被视为单文件 - 单文件搜索启用特定优化
BinaryDetection - 二进制文件检测系统
struct BinaryDetection {
explicit: grep::searcher::BinaryDetection // 显式指定文件的检测策略
implicit: grep::searcher::BinaryDetection // 隐式发现文件的检测策略
}
显式 vs 隐式文件处理:
let explicit = if none {
grep::searcher::BinaryDetection::none()
} else {
grep::searcher::BinaryDetection::convert(b'\x00')
};
let implicit = if none {
grep::searcher::BinaryDetection::none()
} else if convert {
grep::searcher::BinaryDetection::convert(b'\x00')
} else {
grep::searcher::BinaryDetection::quit(b'\x00') // 关键差异
};
- 显式文件:用户明确指定,必须搜索,不能“退出”
- 隐式文件:目录遍历发现,可以跳过二进制文件
三种检测模式:
none
:禁用二进制检测,当作文本处理 (--text
或--null-data
)convert(b'\x00')
:将 null 字节转换为换行符继续搜索 (--binary
)quit(b'\x00')
:遇到 null 字节立即停止搜索该文件
默认策略:
- 显式文件:
convert(b'\x00')
- 隐式文件:
quit(b'\x00')
State - 解析状态管理
struct State {
is_terminal_stdout: bool, // stdout 是否连接到终端
stdin_consumed: bool, // stdin 是否已被消费
cwd: PathBuf, // 当前工作目录
}
is_terminal_stdout
:影响颜色输出、缓冲策略等(需跨平台处理)stdin_consumed
:防止同时从 stdin 读取模式和内容
结构体设计的核心思想
1. 职责分离
Patterns
:模式收集和去重Paths
:路径管理和推断BinaryDetection
:二进制文件处理策略State
:解析状态跟踪