
文章目录
- [第21章 构建命令行工具](#第21章 构建命令行工具)
第21章 构建命令行工具
命令行工具是系统编程和日常开发中不可或缺的一部分。Rust凭借其出色的性能、内存安全性和强大的生态系统,成为了构建命令行工具的绝佳选择。本章将全面介绍如何使用Rust构建功能完整、用户友好的命令行应用程序。
21.1 接受命令行参数
命令行参数是用户与工具交互的主要方式。Rust提供了多种处理命令行参数的方法,从标准库的基础功能到功能丰富的第三方库。
使用标准库处理参数
Rust标准库提供了基本的命令行参数处理功能,适合简单的用例。
rust
use std::env;
// 基本的命令行参数解析
fn basic_args() {
let args: Vec<String> = env::args().collect();
println!("程序名称: {}", args[0]);
match args.len() {
1 => println!("没有提供参数"),
2 => println!("有一个参数: {}", args[1]),
_ => {
println!("有多个参数:");
for (i, arg) in args.iter().skip(1).enumerate() {
println!(" {}: {}", i + 1, arg);
}
}
}
}
// 更结构化的参数解析
struct CliConfig {
input_file: String,
output_file: Option<String>,
verbose: bool,
count: usize,
}
impl CliConfig {
fn from_args() -> Result<Self, String> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
return Err("用法: program <输入文件> [输出文件] [-v|--verbose] [-c|--count N]".to_string());
}
let mut config = CliConfig {
input_file: args[1].clone(),
output_file: None,
verbose: false,
count: 1,
};
let mut i = 2;
while i < args.len() {
match args[i].as_str() {
"-v" | "--verbose" => {
config.verbose = true;
i += 1;
}
"-c" | "--count" => {
if i + 1 >= args.len() {
return Err("--count 参数需要提供一个数值".to_string());
}
config.count = args[i + 1].parse().map_err(|_| "无效的计数值".to_string())?;
i += 2;
}
_ => {
if config.output_file.is_none() {
config.output_file = Some(args[i].clone());
} else {
return Err(format!("未知参数: {}", args[i]));
}
i += 1;
}
}
}
Ok(config)
}
}
// 演示标准库参数解析
fn demonstrate_std_args() {
println!("=== 基本参数解析 ===");
basic_args();
println!("\n=== 结构化参数解析 ===");
match CliConfig::from_args() {
Ok(config) => {
println!("输入文件: {}", config.input_file);
if let Some(output) = config.output_file {
println!("输出文件: {}", output);
}
println!("详细模式: {}", config.verbose);
println!("计数: {}", config.count);
}
Err(e) => {
eprintln!("错误: {}", e);
}
}
}
fn main() {
demonstrate_std_args();
}
使用 clap 库进行高级参数解析
clap 是 Rust 生态系统中最流行的命令行参数解析库,提供了声明式和编程式两种 API。
首先在 Cargo.toml 中添加依赖:
toml
[dependencies]
clap = { version = "4.0", features = ["derive"] }
rust
use clap::{Parser, Subcommand, Arg, ArgAction, value_parser};
// 使用派生宏的声明式 API
#[derive(Parser, Debug)]
#[command(name = "rcli", version = "1.0", about = "一个强大的 Rust CLI 工具", long_about = None)]
struct Cli {
/// 输入文件路径
#[arg(short, long, value_name = "FILE")]
input: String,
/// 输出文件路径
#[arg(short, long, value_name = "FILE")]
output: Option<String>,
/// 启用详细输出
#[arg(short, long, action = ArgAction::SetTrue)]
verbose: bool,
/// 处理次数
#[arg(short, long, default_value_t = 1, value_parser = value_parser!(u8).range(1..=100))]
count: u8,
/// 子命令
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// 处理配置文件
Config {
/// 配置文件路径
#[arg(short, long)]
file: String,
/// 操作类型
#[arg(value_enum)]
action: ConfigAction,
},
/// 网络相关操作
Network {
/// 主机地址
#[arg(short, long)]
host: String,
/// 端口号
#[arg(short, long, default_value_t = 8080)]
port: u16,
},
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum ConfigAction {
/// 验证配置
Validate,
/// 生成配置
Generate,
/// 显示配置
Show,
}
// 编程式 API
fn build_clap_app() -> clap::Command {
clap::Command::new("rcli")
.version("1.0")
.about("一个强大的 Rust CLI 工具")
.arg(
Arg::new("input")
.short('i')
.long("input")
.value_name("FILE")
.help("输入文件路径")
.required(true)
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("输出文件路径")
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.help("启用详细输出")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("count")
.short('c')
.long("count")
.value_name("COUNT")
.help("处理次数")
.default_value("1")
.value_parser(value_parser!(u8).range(1..=100))
)
.subcommand(
clap::Command::new("config")
.about("处理配置文件")
.arg(
Arg::new("file")
.short('f')
.long("file")
.value_name("FILE")
.help("配置文件路径")
.required(true)
)
.arg(
Arg::new("action")
.value_name("ACTION")
.help("操作类型")
.value_parser(["validate", "generate", "show"])
.required(true)
)
)
}
// 演示 clap 功能
fn demonstrate_clap() {
println!("=== 使用派生宏 API ===");
let cli = Cli::parse();
println!("输入文件: {}", cli.input);
if let Some(output) = cli.output {
println!("输出文件: {}", output);
}
println!("详细模式: {}", cli.verbose);
println!("计数: {}", cli.count);
if let Some(command) = cli.command {
match command {
Commands::Config { file, action } => {
println!("配置命令 - 文件: {}, 操作: {:?}", file, action);
}
Commands::Network { host, port } => {
println!("网络命令 - 主机: {}, 端口: {}", host, port);
}
}
}
println!("\n=== 使用编程式 API ===");
let matches = build_clap_app().get_matches();
if let Some(input) = matches.get_one::<String>("input") {
println!("输入文件: {}", input);
}
if matches.get_flag("verbose") {
println!("详细模式已启用");
}
if let Some(count) = matches.get_one::<u8>("count") {
println!("计数: {}", count);
}
if let Some(matches) = matches.subcommand_matches("config") {
if let Some(file) = matches.get_one::<String>("file") {
println!("配置文件: {}", file);
}
if let Some(action) = matches.get_one::<String>("action") {
println!("配置操作: {}", action);
}
}
}
fn main() {
// 在实际使用时,取消注释下面的行
// demonstrate_clap();
// 演示标准库参数解析
demonstrate_std_args();
}
参数验证和转换
对命令行参数进行验证和类型转换是构建健壮 CLI 工具的重要环节。
rust
use std::path::{Path, PathBuf};
use std::net::IpAddr;
// 自定义验证器
fn validate_file_exists(s: &str) -> Result<PathBuf, String> {
let path = Path::new(s);
if path.exists() {
Ok(path.to_path_buf())
} else {
Err(format!("文件不存在: {}", s))
}
}
fn validate_port(s: &str) -> Result<u16, String> {
s.parse::<u16>()
.map_err(|_| format!("无效的端口号: {}", s))
.and_then(|port| {
if port > 0 {
Ok(port)
} else {
Err("端口号必须大于 0".to_string())
}
})
}
fn validate_ip_address(s: &str) -> Result<IpAddr, String> {
s.parse::<IpAddr>()
.map_err(|_| format!("无效的 IP 地址: {}", s))
}
// 高级参数配置
#[derive(Parser, Debug)]
struct AdvancedCli {
/// 输入文件路径
#[arg(
short,
long,
value_name = "FILE",
value_parser = validate_file_exists
)]
input: PathBuf,
/// 监听端口
#[arg(
short,
long,
value_parser = validate_port
)]
port: u16,
/// 服务器地址
#[arg(
short,
long,
value_parser = validate_ip_address
)]
host: IpAddr,
/// 日志级别
#[arg(
short,
long,
value_enum,
default_value_t = LogLevel::Info
)]
log_level: LogLevel,
/// 配置文件路径
#[arg(
short = 'C',
long,
value_name = "CONFIG_FILE",
env = "MYAPP_CONFIG" // 可以从环境变量读取
)]
config: Option<PathBuf>,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Error => write!(f, "ERROR"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Debug => write!(f, "DEBUG"),
LogLevel::Trace => write!(f, "TRACE"),
}
}
}
fn demonstrate_advanced_args() {
println!("=== 高级参数验证和转换 ===");
// 在实际使用中,我们会解析真实参数
// let cli = AdvancedCli::parse();
// 演示验证函数
match validate_file_exists("Cargo.toml") {
Ok(path) => println!("文件存在: {:?}", path),
Err(e) => println!("错误: {}", e),
}
match validate_port("8080") {
Ok(port) => println!("有效端口: {}", port),
Err(e) => println!("错误: {}", e),
}
match validate_ip_address("127.0.0.1") {
Ok(ip) => println!("有效 IP: {}", ip),
Err(e) => println!("错误: {}", e),
}
}
fn main() {
demonstrate_advanced_args();
}
21.2 读取文件和错误处理
文件操作和错误处理是 CLI 工具的核心功能。Rust 的所有权系统和错误处理机制让这些操作既安全又高效。
基本文件操作
rust
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Write, Read, Seek, SeekFrom};
use std::path::Path;
// 读取文件内容
fn read_file_contents(path: &Path) -> io::Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// 逐行读取文件
fn read_file_lines(path: &Path) -> io::Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
reader.lines().collect()
}
// 写入文件
fn write_file_contents(path: &Path, contents: &str) -> io::Result<()> {
let mut file = File::create(path)?;
file.write_all(contents.as_bytes())?;
Ok(())
}
// 追加到文件
fn append_to_file(path: &Path, content: &str) -> io::Result<()> {
let mut file = OpenOptions::new()
.append(true)
.create(true) // 如果文件不存在则创建
.open(path)?;
writeln!(file, "{}", content)?;
Ok(())
}
// 文件操作工具函数
fn demonstrate_file_operations() -> io::Result<()> {
println!("=== 基本文件操作 ===");
let test_file = Path::new("test.txt");
// 写入测试文件
write_file_contents(test_file, "Hello, World!\nThis is a test file.")?;
println!("文件写入成功");
// 读取整个文件
let contents = read_file_contents(test_file)?;
println!("文件内容:\n{}", contents);
// 逐行读取
let lines = read_file_lines(test_file)?;
println!("文件行数: {}", lines.len());
for (i, line) in lines.iter().enumerate() {
println!("{}: {}", i + 1, line);
}
// 追加内容
append_to_file(test_file, "This is appended content.")?;
println!("内容追加成功");
// 读取追加后的内容
let updated_contents = read_file_contents(test_file)?;
println!("更新后的内容:\n{}", updated_contents);
// 清理测试文件
std::fs::remove_file(test_file)?;
println!("测试文件已清理");
Ok(())
}
高级文件处理
对于大型文件或需要更复杂处理的情况,我们需要更高效的文件处理技术。
rust
use std::io::{Error, ErrorKind};
// 处理大文件的缓冲读取
fn process_large_file<P, F>(path: P, mut processor: F) -> io::Result<()>
where
P: AsRef<Path>,
F: FnMut(&str) -> io::Result<()>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
processor(&line)
.map_err(|e| Error::new(ErrorKind::Other, format!("第 {} 行处理失败: {}", line_num + 1, e)))?;
}
Ok(())
}
// 二进制文件处理
fn read_binary_file(path: &Path) -> io::Result<Vec<u8>> {
let mut file = File::open(path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
Ok(buffer)
}
fn write_binary_file(path: &Path, data: &[u8]) -> io::Result<()> {
let mut file = File::create(path)?;
file.write_all(data)?;
Ok(())
}
// 文件信息查询
fn get_file_info(path: &Path) -> io::Result<()> {
let metadata = path.metadata()?;
println!("文件: {}", path.display());
println!("大小: {} 字节", metadata.len());
println!("类型: {}", if metadata.is_dir() { "目录" } else { "文件" });
println!("权限: {}", if metadata.permissions().readonly() { "只读" } else { "可写" });
if let Ok(modified) = metadata.modified() {
println!("修改时间: {:?}", modified);
}
if let Ok(accessed) = metadata.accessed() {
println!("访问时间: {:?}", accessed);
}
Ok(())
}
// 文件搜索功能
fn search_in_file(path: &Path, pattern: &str) -> io::Result<Vec<(usize, String)>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut results = Vec::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
if line.contains(pattern) {
results.push((line_num + 1, line));
}
}
Ok(results)
}
fn demonstrate_advanced_file_ops() -> io::Result<()> {
println!("\n=== 高级文件操作 ===");
let test_file = Path::new("advanced_test.txt");
// 创建测试文件
let test_content = "这是第一行\n这是第二行\n包含关键词的行\n这是第四行";
write_file_contents(test_file, test_content)?;
// 演示大文件处理模式
println!("逐行处理文件:");
process_large_file(test_file, |line| {
println!("处理: {}", line);
Ok(())
})?;
// 演示文件搜索
println!("\n搜索包含'关键词'的行:");
let results = search_in_file(test_file, "关键词")?;
for (line_num, line) in results {
println!("第 {} 行: {}", line_num, line);
}
// 演示二进制操作
println!("\n二进制文件操作:");
let binary_data = b"Hello, Binary World!";
let binary_file = Path::new("binary_test.bin");
write_binary_file(binary_file, binary_data)?;
let read_data = read_binary_file(binary_file)?;
println!("读取的二进制数据: {:?}", String::from_utf8_lossy(&read_data));
// 文件信息
println!("\n文件信息:");
get_file_info(test_file)?;
// 清理
std::fs::remove_file(test_file)?;
std::fs::remove_file(binary_file)?;
Ok(())
}
健壮的错误处理
在 CLI 工具中,良好的错误处理对于用户体验至关重要。
rust
use std::error::Error;
use std::fmt;
use std::process;
// 自定义错误类型
#[derive(Debug)]
enum CliError {
Io(io::Error),
Parse(String),
Validation(String),
Config(String),
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::Io(e) => write!(f, "IO错误: {}", e),
CliError::Parse(s) => write!(f, "解析错误: {}", s),
CliError::Validation(s) => write!(f, "验证错误: {}", s),
CliError::Config(s) => write!(f, "配置错误: {}", s),
}
}
}
impl Error for CliError {}
impl From<io::Error> for CliError {
fn from(error: io::Error) -> Self {
CliError::Io(error)
}
}
// 文件处理结果类型
type CliResult<T> = Result<T, CliError>;
// 健壮的文件处理器
struct FileProcessor {
input_path: PathBuf,
output_path: Option<PathBuf>,
verbose: bool,
}
impl FileProcessor {
fn new(input_path: PathBuf, output_path: Option<PathBuf>, verbose: bool) -> Self {
Self {
input_path,
output_path,
verbose,
}
}
fn validate(&self) -> CliResult<()> {
if !self.input_path.exists() {
return Err(CliError::Validation(format!("输入文件不存在: {}", self.input_path.display())));
}
if let Some(ref output) = self.output_path {
if let Some(parent) = output.parent() {
if !parent.exists() {
return Err(CliError::Validation(format!("输出目录不存在: {}", parent.display())));
}
}
}
Ok(())
}
fn process(&self) -> CliResult<()> {
self.validate()?;
if self.verbose {
println!("处理文件: {}", self.input_path.display());
}
let content = read_file_contents(&self.input_path)
.map_err(|e| CliError::Io(e))?;
let processed_content = self.transform_content(&content)?;
if let Some(ref output_path) = self.output_path {
write_file_contents(output_path, &processed_content)
.map_err(|e| CliError::Io(e))?;
if self.verbose {
println!("结果写入: {}", output_path.display());
}
} else {
println!("处理后的内容:\n{}", processed_content);
}
Ok(())
}
fn transform_content(&self, content: &str) -> CliResult<String> {
// 简单的转换:转换为大写
Ok(content.to_uppercase())
}
}
// 错误处理工具函数
fn handle_error(error: &dyn Error) {
eprintln!("错误: {}", error);
// 根据错误类型提供建议
if let Some(cli_error) = error.downcast_ref::<CliError>() {
match cli_error {
CliError::Io(_) => {
eprintln!("建议: 检查文件路径和权限");
}
CliError::Parse(_) => {
eprintln!("建议: 检查输入格式");
}
CliError::Validation(_) => {
eprintln!("建议: 验证输入参数");
}
CliError::Config(_) => {
eprintln!("建议: 检查配置文件");
}
}
}
process::exit(1);
}
// 演示错误处理
fn demonstrate_error_handling() -> CliResult<()> {
println!("=== 错误处理演示 ===");
// 测试文件处理器的验证
let processor = FileProcessor::new(
PathBuf::from("nonexistent.txt"),
Some(PathBuf::from("output.txt")),
true,
);
match processor.validate() {
Ok(()) => println!("验证通过"),
Err(e) => {
println!("验证失败: {}", e);
// 在实际应用中,我们可能会在这里返回错误
}
}
// 创建有效的测试文件
let test_file = Path::new("test_input.txt");
write_file_contents(test_file, "Hello, Error Handling!")?;
// 成功的处理
let good_processor = FileProcessor::new(
test_file.to_path_buf(),
Some(PathBuf::from("test_output.txt")),
true,
);
good_processor.process()?;
println!("文件处理成功完成");
// 清理
std::fs::remove_file(test_file)?;
std::fs::remove_file("test_output.txt")?;
Ok(())
}
fn main() {
// 基本文件操作
if let Err(e) = demonstrate_file_operations() {
eprintln!("文件操作失败: {}", e);
}
// 高级文件操作
if let Err(e) = demonstrate_advanced_file_ops() {
eprintln!("高级文件操作失败: {}", e);
}
// 错误处理演示
if let Err(e) = demonstrate_error_handling() {
handle_error(&e);
}
}
21.3 使用TDD模式开发库功能
测试驱动开发(TDD)是一种先写测试再实现功能的开发方法,它能帮助设计出更清晰、更可靠的API。
设置测试环境
首先,让我们设置一个支持TDD的项目结构。
Cargo.toml:
toml
[package]
name = "cli-tool"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.0", features = ["derive"] }
anyhow = "1.0"
thiserror = "1.0"
[dev-dependencies]
tempfile = "3.3"
src/lib.rs - 核心库功能
rust
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
// 自定义错误类型
#[derive(Error, Debug)]
pub enum CliToolError {
#[error("IO错误: {0}")]
Io(#[from] std::io::Error),
#[error("解析错误: {0}")]
Parse(String),
#[error("配置错误: {0}")]
Config(String),
#[error("处理错误: {0}")]
Processing(String),
}
// 配置结构体
#[derive(Debug, Clone)]
pub struct Config {
pub input: PathBuf,
pub output: Option<PathBuf>,
pub verbose: bool,
pub settings: HashMap<String, String>,
}
impl Config {
pub fn new(input: PathBuf) -> Self {
Self {
input,
output: None,
verbose: false,
settings: HashMap::new(),
}
}
pub fn with_output(mut self, output: PathBuf) -> Self {
self.output = Some(output);
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn with_setting(mut self, key: &str, value: &str) -> Self {
self.settings.insert(key.to_string(), value.to_string());
self
}
}
// 文本处理器 trait
pub trait TextProcessor {
fn process(&self, text: &str) -> Result<String, CliToolError>;
}
// 简单的文本转换器
pub struct TextTransformer {
pub operation: TransformOperation,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TransformOperation {
UpperCase,
LowerCase,
Reverse,
WordCount,
}
impl TextProcessor for TextTransformer {
fn process(&self, text: &str) -> Result<String, CliToolError> {
match self.operation {
TransformOperation::UpperCase => Ok(text.to_uppercase()),
TransformOperation::LowerCase => Ok(text.to_lowercase()),
TransformOperation::Reverse => Ok(text.chars().rev().collect()),
TransformOperation::WordCount => Ok(text.split_whitespace().count().to_string()),
}
}
}
// 文件处理器
pub struct FileProcessor<P: TextProcessor> {
config: Config,
processor: P,
}
impl<P: TextProcessor> FileProcessor<P> {
pub fn new(config: Config, processor: P) -> Self {
Self { config, processor }
}
pub fn process(&self) -> Result<(), CliToolError> {
self.validate()?;
if self.config.verbose {
println!("处理文件: {}", self.config.input.display());
}
let content = std::fs::read_to_string(&self.config.input)?;
let processed_content = self.processor.process(&content)?;
match &self.config.output {
Some(output_path) => {
std::fs::write(output_path, &processed_content)?;
if self.config.verbose {
println!("结果写入: {}", output_path.display());
}
}
None => {
println!("{}", processed_content);
}
}
Ok(())
}
fn validate(&self) -> Result<(), CliToolError> {
if !self.config.input.exists() {
return Err(CliToolError::Config(format!(
"输入文件不存在: {}",
self.config.input.display()
)));
}
if let Some(ref output) = self.config.output {
if let Some(parent) = output.parent() {
if !parent.exists() {
return Err(CliToolError::Config(format!(
"输出目录不存在: {}",
parent.display()
)));
}
}
}
Ok(())
}
}
编写测试
tests/text_processor_tests.rs
rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uppercase_transformation() {
let processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let result = processor.process("hello world").unwrap();
assert_eq!(result, "HELLO WORLD");
}
#[test]
fn test_lowercase_transformation() {
let processor = TextTransformer {
operation: TransformOperation::LowerCase,
};
let result = processor.process("HELLO WORLD").unwrap();
assert_eq!(result, "hello world");
}
#[test]
fn test_reverse_transformation() {
let processor = TextTransformer {
operation: TransformOperation::Reverse,
};
let result = processor.process("hello").unwrap();
assert_eq!(result, "olleh");
}
#[test]
fn test_word_count() {
let processor = TextTransformer {
operation: TransformOperation::WordCount,
};
let result = processor.process("hello world from rust").unwrap();
assert_eq!(result, "4");
}
#[test]
fn test_empty_input() {
let processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let result = processor.process("").unwrap();
assert_eq!(result, "");
}
}
tests/file_processor_tests.rs
rust
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_file_processor_validation() {
let config = Config::new(PathBuf::from("nonexistent.txt"));
let processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let file_processor = FileProcessor::new(config, processor);
let result = file_processor.validate();
assert!(result.is_err());
}
#[test]
fn test_file_processor_success() {
// 创建临时输入文件
let input_file = NamedTempFile::new().unwrap();
fs::write(&input_file, "test content").unwrap();
// 创建临时输出文件
let output_file = NamedTempFile::new().unwrap();
let config = Config::new(input_file.path().to_path_buf())
.with_output(output_file.path().to_path_buf());
let processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let file_processor = FileProcessor::new(config, processor);
let result = file_processor.process();
assert!(result.is_ok());
// 验证输出文件内容
let output_content = fs::read_to_string(output_file.path()).unwrap();
assert_eq!(output_content, "TEST CONTENT");
}
#[test]
fn test_file_processor_stdout() {
let input_file = NamedTempFile::new().unwrap();
fs::write(&input_file, "test content").unwrap();
let config = Config::new(input_file.path().to_path_buf());
let processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let file_processor = FileProcessor::new(config, processor);
let result = file_processor.process();
assert!(result.is_ok());
}
}
实现功能
现在基于测试来实现和改进功能。
src/processor/mod.rs
rust
use crate::{CliToolError, TextProcessor};
use std::collections::HashMap;
// 更复杂的文本处理器:单词频率统计
pub struct WordFrequencyProcessor;
impl TextProcessor for WordFrequencyProcessor {
fn process(&self, text: &str) -> Result<String, CliToolError> {
let words: Vec<&str> = text
.split_whitespace()
.map(|word| {
// 清理单词:移除标点符号,转换为小写
word.trim_matches(|c: char| !c.is_alphabetic())
.to_lowercase()
})
.filter(|word| !word.is_empty())
.map(|word| word.into_boxed_str())
.collect::<Vec<_>>()
.into_iter()
.map(|s| s.into_string())
.collect::<Vec<String>>()
.iter()
.map(|s| s.as_str())
.collect();
let mut frequency: HashMap<&str, usize> = HashMap::new();
for &word in &words {
*frequency.entry(word).or_insert(0) += 1;
}
// 按频率排序
let mut sorted_freq: Vec<(&str, usize)> = frequency.into_iter().collect();
sorted_freq.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
// 格式化输出
let result = sorted_freq
.into_iter()
.map(|(word, count)| format!("{}: {}", word, count))
.collect::<Vec<String>>()
.join("\n");
Ok(result)
}
}
// 文本过滤器
pub struct TextFilter {
pub pattern: String,
pub case_sensitive: bool,
}
impl TextProcessor for TextFilter {
fn process(&self, text: &str) -> Result<String, CliToolError> {
let lines: Vec<&str> = text.lines().collect();
let filtered_lines: Vec<&str> = if self.case_sensitive {
lines
.into_iter()
.filter(|line| line.contains(&self.pattern))
.collect()
} else {
let pattern_lower = self.pattern.to_lowercase();
lines
.into_iter()
.filter(|line| line.to_lowercase().contains(&pattern_lower))
.collect()
};
Ok(filtered_lines.join("\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_frequency() {
let processor = WordFrequencyProcessor;
let text = "hello world hello rust world hello";
let result = processor.process(text).unwrap();
assert!(result.contains("hello: 3"));
assert!(result.contains("world: 2"));
assert!(result.contains("rust: 1"));
}
#[test]
fn test_text_filter_case_sensitive() {
let processor = TextFilter {
pattern: "Hello".to_string(),
case_sensitive: true,
};
let text = "Hello World\nhello world\nHELLO WORLD";
let result = processor.process(text).unwrap();
assert_eq!(result, "Hello World");
}
#[test]
fn test_text_filter_case_insensitive() {
let processor = TextFilter {
pattern: "hello".to_string(),
case_sensitive: false,
};
let text = "Hello World\nhello world\nHELLO WORLD\nGoodbye";
let result = processor.process(text).unwrap();
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines.contains(&"Hello World"));
assert!(lines.contains(&"hello world"));
assert!(lines.contains(&"HELLO WORLD"));
}
}
集成测试
tests/integration_tests.rs
rust
use cli_tool::{Config, FileProcessor, TextTransformer, TransformOperation};
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_end_to_end_uppercase() {
// 设置输入文件
let input_file = NamedTempFile::new().unwrap();
fs::write(&input_file, "Hello Integration Test").unwrap();
// 设置输出文件
let output_file = NamedTempFile::new().unwrap();
// 创建配置和处理器
let config = Config::new(input_file.path().to_path_buf())
.with_output(output_file.path().to_path_buf())
.with_verbose(false);
let text_processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let file_processor = FileProcessor::new(config, text_processor);
// 执行处理
let result = file_processor.process();
assert!(result.is_ok());
// 验证结果
let output_content = fs::read_to_string(output_file.path()).unwrap();
assert_eq!(output_content, "HELLO INTEGRATION TEST");
}
#[test]
fn test_end_to_end_multiple_operations() {
let input_file = NamedTempFile::new().unwrap();
fs::write(&input_file, "First line\nSecond line\nThird line").unwrap();
let config = Config::new(input_file.path().to_path_buf())
.with_verbose(false);
// 测试多个操作
let operations = vec![
TransformOperation::UpperCase,
TransformOperation::Reverse,
TransformOperation::WordCount,
];
for operation in operations {
let text_processor = TextTransformer { operation };
let file_processor = FileProcessor::new(config.clone(), text_processor);
let result = file_processor.process();
assert!(result.is_ok(), "操作 {:?} 失败", operation);
}
}
21.4 编写完整的生产级工具
现在让我们将所有部分组合起来,构建一个完整的生产级命令行工具。
完整的CLI应用
src/main.rs
rust
use clap::{Parser, Subcommand, ValueEnum};
use cli_tool::{Config, FileProcessor, TextTransformer, TransformOperation, TextFilter, WordFrequencyProcessor};
use std::path::PathBuf;
use anyhow::{Result, Context};
#[derive(Parser)]
#[command(name = "textool", version = "1.0", about = "强大的文本处理工具", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// 文本转换操作
Transform {
/// 输入文件路径
#[arg(short, long)]
input: PathBuf,
/// 输出文件路径
#[arg(short, long)]
output: Option<PathBuf>,
/// 转换操作类型
#[arg(value_enum)]
operation: TransformOp,
/// 启用详细输出
#[arg(short, long)]
verbose: bool,
},
/// 文本过滤操作
Filter {
/// 输入文件路径
#[arg(short, long)]
input: PathBuf,
/// 输出文件路径
#[arg(short, long)]
output: Option<PathBuf>,
/// 过滤模式
#[arg(short, long)]
pattern: String,
/// 是否区分大小写
#[arg(short = 'i', long)]
case_sensitive: bool,
/// 启用详细输出
#[arg(short, long)]
verbose: bool,
},
/// 单词频率统计
Frequency {
/// 输入文件路径
#[arg(short, long)]
input: PathBuf,
/// 输出文件路径
#[arg(short, long)]
output: Option<PathBuf>,
/// 启用详细输出
#[arg(short, long)]
verbose: bool,
},
}
#[derive(Clone, ValueEnum)]
enum TransformOp {
Upper,
Lower,
Reverse,
Count,
}
impl From<TransformOp> for TransformOperation {
fn from(op: TransformOp) -> Self {
match op {
TransformOp::Upper => TransformOperation::UpperCase,
TransformOp::Lower => TransformOperation::LowerCase,
TransformOp::Reverse => TransformOperation::Reverse,
TransformOp::Count => TransformOperation::WordCount,
}
}
}
// 应用程序主结构体
struct TextoolApp;
impl TextoolApp {
fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Transform { input, output, operation, verbose } => {
self::handle_transform(input, output, operation, verbose)
}
Commands::Filter { input, output, pattern, case_sensitive, verbose } => {
self::handle_filter(input, output, pattern, case_sensitive, verbose)
}
Commands::Frequency { input, output, verbose } => {
self::handle_frequency(input, output, verbose)
}
}
}
}
fn handle_transform(
input: PathBuf,
output: Option<PathBuf>,
operation: TransformOp,
verbose: bool,
) -> Result<()> {
let config = Config::new(input)
.with_output_opt(output)
.with_verbose(verbose);
let processor = TextTransformer {
operation: operation.into(),
};
let file_processor = FileProcessor::new(config, processor);
file_processor.process()
.context("文本转换操作失败")
}
fn handle_filter(
input: PathBuf,
output: Option<PathBuf>,
pattern: String,
case_sensitive: bool,
verbose: bool,
) -> Result<()> {
let config = Config::new(input)
.with_output_opt(output)
.with_verbose(verbose);
let processor = TextFilter {
pattern,
case_sensitive,
};
let file_processor = FileProcessor::new(config, processor);
file_processor.process()
.context("文本过滤操作失败")
}
fn handle_frequency(
input: PathBuf,
output: Option<PathBuf>,
verbose: bool,
) -> Result<()> {
let config = Config::new(input)
.with_output_opt(output)
.with_verbose(verbose);
let processor = WordFrequencyProcessor;
let file_processor = FileProcessor::new(config, processor);
file_processor.process()
.context("单词频率统计失败")
}
// 为Config添加辅助方法
trait ConfigExt {
fn with_output_opt(self, output: Option<PathBuf>) -> Self;
}
impl ConfigExt for Config {
fn with_output_opt(mut self, output: Option<PathBuf>) -> Self {
self.output = output;
self
}
}
fn main() {
if let Err(e) = TextoolApp::run() {
eprintln!("错误: {:?}", e);
// 提供用户友好的错误信息
if let Some(source) = e.source() {
eprintln!("原因: {}", source);
}
std::process::exit(1);
}
}
配置管理和环境变量
src/config.rs
rust
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::CliToolError;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig {
pub default_input: Option<PathBuf>,
pub default_output_dir: Option<PathBuf>,
pub settings: HashMap<String, String>,
}
impl Default for AppConfig {
fn default() -> Self {
let mut settings = HashMap::new();
settings.insert("encoding".to_string(), "utf-8".to_string());
settings.insert("max_file_size".to_string(), "1048576".to_string()); // 1MB
Self {
default_input: None,
default_output_dir: Some(PathBuf::from(".")),
settings,
}
}
}
impl AppConfig {
pub fn load() -> Result<Self, CliToolError> {
let config_paths = vec![
Path::new("textool.toml"),
Path::new("~/.config/textool/config.toml"),
Path::new("/etc/textool/config.toml"),
];
for path in config_paths {
if path.exists() {
let content = fs::read_to_string(path)
.map_err(|e| CliToolError::Config(format!("无法读取配置文件 {}: {}", path.display(), e)))?;
return toml::from_str(&content)
.map_err(|e| CliToolError::Config(format!("配置文件解析错误: {}", e)));
}
}
// 没有找到配置文件,返回默认配置
Ok(Self::default())
}
pub fn save(&self, path: &Path) -> Result<(), CliToolError> {
let content = toml::to_string_pretty(self)
.map_err(|e| CliToolError::Config(format!("配置序列化错误: {}", e)))?;
fs::write(path, content)
.map_err(|e| CliToolError::Config(format!("无法保存配置文件: {}", e)))?;
Ok(())
}
pub fn get_setting(&self, key: &str) -> Option<&String> {
self.settings.get(key)
}
pub fn set_setting(&mut self, key: &str, value: &str) {
self.settings.insert(key.to_string(), value.to_string());
}
}
日志和监控
src/logging.rs
rust
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Logger {
inner: Arc<Mutex<LoggerInner>>,
}
struct LoggerInner {
file: Option<File>,
verbose: bool,
}
impl Logger {
pub fn new(verbose: bool) -> Self {
Self {
inner: Arc::new(Mutex::new(LoggerInner {
file: None,
verbose,
})),
}
}
pub fn with_file<P: AsRef<Path>>(self, path: P, verbose: bool) -> io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let mut inner = self.inner.lock().unwrap();
inner.file = Some(file);
inner.verbose = verbose;
Ok(self)
}
pub fn info(&self, message: &str) {
self.log("INFO", message);
}
pub fn warn(&self, message: &str) {
self.log("WARN", message);
}
pub fn error(&self, message: &str) {
self.log("ERROR", message);
}
pub fn debug(&self, message: &str) {
let inner = self.inner.lock().unwrap();
if inner.verbose {
self.log("DEBUG", message);
}
}
fn log(&self, level: &str, message: &str) {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let log_message = format!("[{}] {}: {}\n", timestamp, level, message);
let mut inner = self.inner.lock().unwrap();
// 输出到标准错误
eprint!("{}", log_message);
// 写入日志文件
if let Some(ref mut file) = inner.file {
let _ = file.write_all(log_message.as_bytes());
}
}
}
impl fmt::Debug for Logger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = self.inner.lock().unwrap();
write!(f, "Logger(verbose: {}, has_file: {})",
inner.verbose, inner.file.is_some())
}
}
性能优化和高级特性
src/optimized_processor.rs
rust
use crate::{CliToolError, TextProcessor};
use std::collections::HashMap;
use rayon::prelude::*;
// 使用Rayon进行并行处理
pub struct ParallelTextProcessor<P: TextProcessor + Send + Sync> {
chunk_size: usize,
processor: P,
}
impl<P: TextProcessor + Send + Sync> ParallelTextProcessor<P> {
pub fn new(processor: P, chunk_size: usize) -> Self {
Self { processor, chunk_size }
}
}
impl<P: TextProcessor + Send + Sync> TextProcessor for ParallelTextProcessor<P> {
fn process(&self, text: &str) -> Result<String, CliToolError> {
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= self.chunk_size {
// 对于小文件,使用串行处理
return self.processor.process(text);
}
// 并行处理行
let processed_lines: Result<Vec<String>, CliToolError> = lines
.par_chunks(self.chunk_size)
.map(|chunk| {
let chunk_text = chunk.join("\n");
self.processor.process(&chunk_text)
})
.collect();
let processed_chunks = processed_lines?;
Ok(processed_chunks.join("\n"))
}
}
// 流式处理器,用于处理大文件
pub struct StreamingProcessor {
buffer_size: usize,
}
impl StreamingProcessor {
pub fn new(buffer_size: usize) -> Self {
Self { buffer_size }
}
}
impl TextProcessor for StreamingProcessor {
fn process(&self, text: &str) -> Result<String, CliToolError> {
// 对于流式处理,我们可能想要不同的接口
// 这里简化实现,只是用缓冲方式处理
let mut result = String::with_capacity(text.len());
let mut words = text.split_whitespace();
let mut buffer = Vec::with_capacity(self.buffer_size);
while let Some(word) = words.next() {
buffer.push(word.to_uppercase());
if buffer.len() >= self.buffer_size {
result.push_str(&buffer.join(" "));
result.push(' ');
buffer.clear();
}
}
// 处理剩余内容
if !buffer.is_empty() {
result.push_str(&buffer.join(" "));
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TextTransformer, TransformOperation};
#[test]
fn test_parallel_processor() {
let base_processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let parallel_processor = ParallelTextProcessor::new(base_processor, 2);
let text = "line one\nline two\nline three\nline four";
let result = parallel_processor.process(text).unwrap();
assert!(result.contains("LINE ONE"));
assert!(result.contains("LINE TWO"));
assert!(result.contains("LINE THREE"));
assert!(result.contains("LINE FOUR"));
}
#[test]
fn test_streaming_processor() {
let processor = StreamingProcessor::new(3);
let text = "this is a test of streaming processing";
let result = processor.process(text).unwrap();
// 验证所有单词都被转换为大写
assert!(!result.contains("this"));
assert!(result.contains("THIS"));
}
}
完整的生产级工具使用示例
让我们创建一个完整的使用示例,展示工具的所有功能。
examples/advanced_usage.rs
rust
use cli_tool::{Config, FileProcessor, TextTransformer, TransformOperation, TextFilter, WordFrequencyProcessor};
use std::path::PathBuf;
fn main() -> anyhow::Result<()> {
println!("=== 文本工具高级使用示例 ===\n");
// 示例1: 基本文本转换
println!("1. 基本文本转换示例");
let config = Config::new(PathBuf::from("examples/sample.txt"))
.with_verbose(true);
let processor = TextTransformer {
operation: TransformOperation::UpperCase,
};
let file_processor = FileProcessor::new(config, processor);
// 在实际使用中,我们会处理真实文件
// 这里只是演示API使用
println!("配置: {:?}", file_processor);
println!("---\n");
// 示例2: 文本过滤
println!("2. 文本过滤示例");
let filter_config = Config::new(PathBuf::from("examples/sample.txt"))
.with_output(PathBuf::from("examples/filtered.txt"))
.with_verbose(true);
let filter_processor = TextFilter {
pattern: "important".to_string(),
case_sensitive: false,
};
let filter_file_processor = FileProcessor::new(filter_config, filter_processor);
println!("过滤器配置: {:?}", filter_file_processor);
println!("---\n");
// 示例3: 单词频率统计
println!("3. 单词频率统计示例");
let freq_config = Config::new(PathBuf::from("examples/sample.txt"))
.with_verbose(true);
let freq_processor = WordFrequencyProcessor;
let freq_file_processor = FileProcessor::new(freq_config, freq_processor);
println!("频率分析器: {:?}", freq_file_processor);
println!("\n=== 示例完成 ===");
Ok(())
}
总结
本章详细介绍了如何使用Rust构建功能完整的命令行工具:
- 命令行参数解析:使用标准库和clap库处理各种参数格式
- 文件操作和错误处理:健壮的文件读写和全面的错误处理策略
- 测试驱动开发:通过TDD模式开发可靠的核心库功能
- 生产级工具构建:集成配置管理、日志记录、性能优化等高级特性
通过本章的学习,你应该能够:
- 使用clap构建复杂的命令行界面
- 实现健壮的文件处理和错误处理
- 使用TDD方法开发可靠的库功能
- 构建包含配置管理、日志记录的生产级工具
- 优化工具性能,处理大文件和并行处理
这些技能不仅适用于构建文本处理工具,也可以应用于各种类型的命令行应用程序开发。Rust的性能优势和安全性使其成为构建命令行工具的绝佳选择。