文章目錄
- 第21章 構建命令行工具
- 21.1 接受命令行參數
- 使用標準庫處理參數
- 使用 clap 庫進行高級參數解析
- 參數驗證和轉換
- 21.2 讀取文件和錯誤處理
- 基本文件操作
- 高級文件處理
- 健壯的錯誤處理
- 21.3 使用TDD模式開發庫功能
- 設置測試環境
- 編寫測試
- 實現功能
- 集成測試
- 21.4 編寫完整的生產級工具
- 完整的CLI應用
- 配置管理和環境變量
- 日誌和監控
- 性能優化和高級特性
- 完整的生產級工具使用示例
- 總結
第21章 構建命令行工具
命令行工具是系統編程和日常開發中不可或缺的一部分。Rust憑藉其出色的性能、內存安全性和強大的生態系統,成為了構建命令行工具的絕佳選擇。本章將全面介紹如何使用Rust構建功能完整、用户友好的命令行應用程序。
21.1 接受命令行參數
命令行參數是用户與工具交互的主要方式。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 中添加依賴:
[dependencies]
clap = { version = "4.0", features = ["derive"] }
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 工具的重要環節。
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 的所有權系統和錯誤處理機制讓這些操作既安全又高效。
基本文件操作
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(())
}
高級文件處理
對於大型文件或需要更復雜處理的情況,我們需要更高效的文件處理技術。
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 工具中,良好的錯誤處理對於用户體驗至關重要。
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:
[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 - 核心庫功能
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
#[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
#[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
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
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
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
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
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
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
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的性能優勢和安全性使其成為構建命令行工具的絕佳選擇。