添加Rust示例项目ex02,实现安全字符处理器命令行工具,展示所有权和生命周期特性
This commit is contained in:
parent
d02e229239
commit
52a1aa7bdf
|
|
@ -0,0 +1,380 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "ex02"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
"clap",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "ex02"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "安全字符处理器命令行工具,展示Rust的所有权和生命周期特性"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.4", features = ["derive"] } # 命令行参数解析
|
||||
anyhow = "1.0" # 错误处理
|
||||
thiserror = "1.0" # 自定义错误类型
|
||||
once_cell = "1.18" # 用于单例模式实现
|
||||
serde = { version = "1.0", features = ["derive"] } # 序列化/反序列化
|
||||
serde_json = "1.0" # JSON处理
|
||||
atty = "0.2" # 检测终端类型
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::{OnceLock, Mutex};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
settings: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
// 获取配置实例
|
||||
pub fn get_instance() -> &'static Mutex<Config> {
|
||||
static INSTANCE: OnceLock<Mutex<Config>> = OnceLock::new();
|
||||
INSTANCE.get_or_init(|| Mutex::new(Config::default()))
|
||||
}
|
||||
|
||||
// 从文件加载配置
|
||||
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
let config_str = fs::read_to_string(path)?;
|
||||
let new_config: Config = serde_json::from_str(&config_str)?;
|
||||
|
||||
let instance = Self::get_instance().lock().unwrap();
|
||||
let mut instance = instance;
|
||||
*instance = new_config;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 获取配置项
|
||||
pub fn get_setting(&self, key: &str) -> Option<&String> {
|
||||
self.settings.get(key)
|
||||
}
|
||||
|
||||
// 设置配置项
|
||||
pub fn set_setting(&mut self, key: String, value: String) {
|
||||
self.settings.insert(key, value);
|
||||
}
|
||||
|
||||
// 保存配置到文件
|
||||
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||
let config_str = serde_json::to_string_pretty(self)?;
|
||||
fs::write(path, config_str)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let mut settings = HashMap::new();
|
||||
settings.insert("case_sensitive".to_string(), "true".to_string());
|
||||
settings.insert("max_word_length".to_string(), "100".to_string());
|
||||
settings.insert("min_word_length".to_string(), "1".to_string());
|
||||
|
||||
Config { settings }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_singleton_pattern() {
|
||||
let instance1 = Config::get_instance();
|
||||
let instance2 = Config::get_instance();
|
||||
|
||||
// 验证两个实例是同一个
|
||||
assert_eq!(
|
||||
format!("{:p}", instance1),
|
||||
format!("{:p}", instance2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_operations() {
|
||||
let instance = Config::get_instance();
|
||||
let mut config = instance.lock().unwrap();
|
||||
|
||||
// 测试设置和获取配置
|
||||
config.set_setting("test_key".to_string(), "test_value".to_string());
|
||||
assert_eq!(
|
||||
config.get_setting("test_key"),
|
||||
Some(&"test_value".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_operations() -> Result<()> {
|
||||
// 创建临时文件
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
let test_config = r#"{"settings":{"test_key":"test_value"}}"#;
|
||||
write!(temp_file, "{}", test_config)?;
|
||||
|
||||
// 测试加载配置
|
||||
Config::load_from_file(temp_file.path())?;
|
||||
let config = Config::get_instance().lock().unwrap();
|
||||
assert_eq!(
|
||||
config.get_setting("test_key"),
|
||||
Some(&"test_value".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
mod config;
|
||||
mod processor;
|
||||
mod utils;
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use anyhow::{Result, Context};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::processor::{Operation, TextProcessor};
|
||||
use crate::utils::{read_input, InputSource, safe_print};
|
||||
|
||||
/// 安全字符处理器 - 展示Rust所有权和生命周期特性的命令行工具
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// 要处理的文本(如果不提供,将从标准输入或文件读取)
|
||||
#[arg(index = 1)]
|
||||
text: Option<String>,
|
||||
|
||||
/// 处理操作类型
|
||||
#[arg(short, long, value_enum, default_value = "analyze")]
|
||||
operation: OperationType,
|
||||
|
||||
/// 输入文件路径
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
|
||||
/// 配置文件路径
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// 是否显示详细输出
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
enum OperationType {
|
||||
/// 转换为大写
|
||||
Upper,
|
||||
/// 转换为小写
|
||||
Lower,
|
||||
/// 移除重复字符
|
||||
Dedup,
|
||||
/// 统计单词
|
||||
Count,
|
||||
/// 分析文本
|
||||
Analyze,
|
||||
}
|
||||
|
||||
impl From<OperationType> for Operation {
|
||||
fn from(op_type: OperationType) -> Self {
|
||||
match op_type {
|
||||
OperationType::Upper => Operation::ToUpper,
|
||||
OperationType::Lower => Operation::ToLower,
|
||||
OperationType::Dedup => Operation::RemoveDuplicates,
|
||||
OperationType::Count => Operation::CountWords,
|
||||
OperationType::Analyze => Operation::Analyze,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// 解析命令行参数
|
||||
let args = Args::parse();
|
||||
|
||||
// 如果提供了配置文件,加载配置
|
||||
if let Some(config_path) = &args.config {
|
||||
Config::load_from_file(config_path)
|
||||
.with_context(|| format!("加载配置文件失败: {:?}", config_path))?;
|
||||
|
||||
if args.verbose {
|
||||
println!("已加载配置文件: {:?}", config_path);
|
||||
}
|
||||
}
|
||||
|
||||
// 确定输入源
|
||||
let input_source = match (&args.text, &args.file) {
|
||||
(Some(text), _) => InputSource::Text(text),
|
||||
(None, Some(file)) => InputSource::File(file.to_string_lossy().to_string()),
|
||||
(None, None) => {
|
||||
if atty::is(atty::Stream::Stdin) {
|
||||
eprintln!("错误: 未提供输入。请提供文本参数、输入文件或通过管道传入内容。");
|
||||
eprintln!("使用 --help 查看帮助信息。");
|
||||
process::exit(1);
|
||||
}
|
||||
InputSource::StdIn
|
||||
}
|
||||
};
|
||||
|
||||
// 读取输入
|
||||
let input = read_input(input_source)
|
||||
.context("读取输入失败")?;
|
||||
|
||||
if args.verbose {
|
||||
println!("输入长度: {} 字符", input.len());
|
||||
}
|
||||
|
||||
// 处理文本
|
||||
let processor = TextProcessor;
|
||||
let operation: Operation = args.operation.into();
|
||||
|
||||
if args.verbose {
|
||||
println!("执行操作: {:?}", operation);
|
||||
}
|
||||
|
||||
// 使用生命周期标注来确保引用安全
|
||||
let result = processor.process_text(&input, operation)
|
||||
.context("处理文本失败")?;
|
||||
|
||||
// 输出结果
|
||||
safe_print(&result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 演示生命周期和所有权的示例函数
|
||||
// 这些函数不会被直接调用,但展示了Rust的所有权系统
|
||||
#[allow(dead_code)]
|
||||
fn ownership_examples() {
|
||||
// 示例1: 移动语义
|
||||
let s1 = String::from("hello");
|
||||
let s2 = s1;
|
||||
// println!("{}", s1); // 编译错误: s1的所有权已移动到s2
|
||||
|
||||
// 示例2: 借用
|
||||
let s3 = String::from("world");
|
||||
let len = calculate_length(&s3);
|
||||
println!("'{}' 的长度是 {}", s3, len); // s3仍然有效
|
||||
|
||||
// 示例3: 可变借用
|
||||
let mut s4 = String::from("hello");
|
||||
change(&mut s4);
|
||||
println!("修改后: {}", s4);
|
||||
|
||||
// 示例4: 生命周期
|
||||
let string1 = String::from("长字符串");
|
||||
{
|
||||
let string2 = String::from("短");
|
||||
let result = longest(string1.as_str(), string2.as_str());
|
||||
println!("最长的字符串是: {}", result);
|
||||
}
|
||||
}
|
||||
|
||||
// 借用示例
|
||||
fn calculate_length(s: &String) -> usize {
|
||||
s.len()
|
||||
}
|
||||
|
||||
// 可变借用示例
|
||||
fn change(s: &mut String) {
|
||||
s.push_str(", world");
|
||||
}
|
||||
|
||||
// 生命周期标注示例
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
use crate::config::Config;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProcessorError {
|
||||
#[error("无效的输入文本")]
|
||||
InvalidInput,
|
||||
|
||||
#[error("处理操作失败: {0}")]
|
||||
ProcessingFailed(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Operation {
|
||||
ToUpper,
|
||||
ToLower,
|
||||
RemoveDuplicates,
|
||||
CountWords,
|
||||
Analyze,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Operation {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"upper" => Ok(Operation::ToUpper),
|
||||
"lower" => Ok(Operation::ToLower),
|
||||
"dedup" => Ok(Operation::RemoveDuplicates),
|
||||
"count" => Ok(Operation::CountWords),
|
||||
"analyze" => Ok(Operation::Analyze),
|
||||
_ => Err(format!("未知的操作类型: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextProcessor;
|
||||
|
||||
impl TextProcessor {
|
||||
// 处理文本的主函数,根据操作类型调用不同的处理函数
|
||||
pub fn process_text<'a>(&self, input: &'a str, operation: Operation) -> Result<String> {
|
||||
if input.is_empty() {
|
||||
return Err(ProcessorError::InvalidInput.into());
|
||||
}
|
||||
|
||||
match operation {
|
||||
Operation::ToUpper => Ok(self.to_upper(input)),
|
||||
Operation::ToLower => Ok(self.to_lower(input)),
|
||||
Operation::RemoveDuplicates => Ok(self.remove_duplicates(input)),
|
||||
Operation::CountWords => Ok(self.count_words(input)),
|
||||
Operation::Analyze => Ok(self.analyze_text(input)),
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为大写
|
||||
pub fn to_upper(&self, text: &str) -> String {
|
||||
text.to_uppercase()
|
||||
}
|
||||
|
||||
// 转换为小写
|
||||
pub fn to_lower(&self, text: &str) -> String {
|
||||
text.to_lowercase()
|
||||
}
|
||||
|
||||
// 移除重复字符(保持顺序)
|
||||
pub fn remove_duplicates(&self, text: &str) -> String {
|
||||
let mut seen = HashSet::new();
|
||||
let mut result = String::with_capacity(text.len());
|
||||
|
||||
for c in text.chars() {
|
||||
if seen.insert(c) {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// 统计单词数量
|
||||
pub fn count_words(&self, text: &str) -> String {
|
||||
let config = Config::get_instance().lock().unwrap();
|
||||
let case_sensitive = config.get_setting("case_sensitive")
|
||||
.map(|s| s == "true")
|
||||
.unwrap_or(true);
|
||||
|
||||
let min_length = config.get_setting("min_word_length")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
let max_length = config.get_setting("max_word_length")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(100);
|
||||
|
||||
let mut word_counts = HashMap::new();
|
||||
|
||||
// 分割文本为单词并计数
|
||||
for word in text.split_whitespace() {
|
||||
let word = word.trim_matches(|c: char| !c.is_alphanumeric());
|
||||
|
||||
if word.len() < min_length || word.len() > max_length {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = if case_sensitive {
|
||||
word.to_string()
|
||||
} else {
|
||||
word.to_lowercase()
|
||||
};
|
||||
|
||||
*word_counts.entry(key).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// 格式化输出结果
|
||||
let mut result = String::new();
|
||||
result.push_str("单词统计结果:\n");
|
||||
|
||||
let mut words: Vec<(&String, &i32)> = word_counts.iter().collect();
|
||||
words.sort_by(|a, b| b.1.cmp(a.1));
|
||||
|
||||
for (word, count) in words {
|
||||
result.push_str(&format!("{}: {}\n", word, count));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// 分析文本(字符频率、平均单词长度等)
|
||||
pub fn analyze_text(&self, text: &str) -> String {
|
||||
let char_count = text.chars().count();
|
||||
let byte_count = text.len();
|
||||
let line_count = text.lines().count();
|
||||
let word_count = text.split_whitespace().count();
|
||||
|
||||
// 字符频率分析
|
||||
let mut char_freq = HashMap::new();
|
||||
for c in text.chars() {
|
||||
*char_freq.entry(c).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// 计算平均单词长度
|
||||
let avg_word_length = if word_count > 0 {
|
||||
let total_word_length: usize = text.split_whitespace()
|
||||
.map(|w| w.chars().count())
|
||||
.sum();
|
||||
total_word_length as f64 / word_count as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// 格式化输出结果
|
||||
let mut result = String::new();
|
||||
result.push_str("文本分析结果:\n");
|
||||
result.push_str(&format!("字符数: {}\n", char_count));
|
||||
result.push_str(&format!("字节数: {}\n", byte_count));
|
||||
result.push_str(&format!("行数: {}\n", line_count));
|
||||
result.push_str(&format!("单词数: {}\n", word_count));
|
||||
result.push_str(&format!("平均单词长度: {:.2}\n", avg_word_length));
|
||||
|
||||
result.push_str("\n最常见的字符:\n");
|
||||
let mut chars: Vec<(&char, &i32)> = char_freq.iter().collect();
|
||||
chars.sort_by(|a, b| b.1.cmp(a.1));
|
||||
|
||||
for (i, (c, count)) in chars.iter().take(10).enumerate() {
|
||||
let display_char = if c.is_whitespace() {
|
||||
match **c {
|
||||
' ' => "空格".to_string(),
|
||||
'\n' => "换行".to_string(),
|
||||
'\t' => "制表符".to_string(),
|
||||
_ => format!("Unicode({})", **c as u32),
|
||||
}
|
||||
} else {
|
||||
c.to_string()
|
||||
};
|
||||
|
||||
result.push_str(&format!("{}. '{}': {} ({:.2}%)\n",
|
||||
i + 1,
|
||||
display_char,
|
||||
count,
|
||||
(**count as f64 / char_count as f64) * 100.0
|
||||
));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_upper() {
|
||||
let processor = TextProcessor;
|
||||
assert_eq!(processor.to_upper("hello world"), "HELLO WORLD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_lower() {
|
||||
let processor = TextProcessor;
|
||||
assert_eq!(processor.to_lower("HELLO WORLD"), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_duplicates() {
|
||||
let processor = TextProcessor;
|
||||
assert_eq!(processor.remove_duplicates("hello"), "helo");
|
||||
assert_eq!(processor.remove_duplicates("aabbcc"), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_words() {
|
||||
let processor = TextProcessor;
|
||||
let result = processor.count_words("hello world hello");
|
||||
assert!(result.contains("hello: 2"));
|
||||
assert!(result.contains("world: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_text() {
|
||||
let processor = TextProcessor;
|
||||
let result = processor.analyze_text("hello world\nhello");
|
||||
assert!(result.contains("字符数: 17"));
|
||||
assert!(result.contains("行数: 2"));
|
||||
assert!(result.contains("单词数: 3"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
use anyhow::{Result, Context};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum InputError {
|
||||
#[error("文件不存在: {0}")]
|
||||
FileNotFound(String),
|
||||
|
||||
#[error("读取文件失败: {0}")]
|
||||
ReadError(#[from] io::Error),
|
||||
|
||||
#[error("无法处理输入: {0}")]
|
||||
ProcessingError(String),
|
||||
}
|
||||
|
||||
/// 表示文本输入的来源
|
||||
pub enum InputSource<'a> {
|
||||
/// 从文件读取
|
||||
File(String),
|
||||
/// 从标准输入读取
|
||||
StdIn,
|
||||
/// 直接使用字符串
|
||||
Text(&'a str),
|
||||
}
|
||||
|
||||
/// 从不同来源读取文本
|
||||
pub fn read_input(source: InputSource) -> Result<String> {
|
||||
match source {
|
||||
InputSource::File(path) => {
|
||||
read_from_file(&path)
|
||||
.with_context(|| format!("读取文件 '{}' 失败", path))
|
||||
},
|
||||
InputSource::StdIn => {
|
||||
read_from_stdin()
|
||||
.context("从标准输入读取失败")
|
||||
},
|
||||
InputSource::Text(text) => {
|
||||
Ok(text.to_string())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 从文件读取文本
|
||||
pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let path_str = path.as_ref().to_string_lossy().to_string();
|
||||
|
||||
if !path.as_ref().exists() {
|
||||
return Err(InputError::FileNotFound(path_str).into());
|
||||
}
|
||||
|
||||
fs::read_to_string(path)
|
||||
.map_err(|e| InputError::ReadError(e).into())
|
||||
}
|
||||
|
||||
/// 从标准输入读取文本
|
||||
pub fn read_from_stdin() -> Result<String> {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// 安全地打印结果
|
||||
pub fn safe_print(text: &str) {
|
||||
println!("{}", text);
|
||||
}
|
||||
|
||||
/// 检查文本是否为有效的UTF-8
|
||||
pub fn is_valid_utf8(bytes: &[u8]) -> bool {
|
||||
String::from_utf8(bytes.to_vec()).is_ok()
|
||||
}
|
||||
|
||||
/// 安全地处理文件路径
|
||||
pub fn sanitize_path(path: &str) -> String {
|
||||
// 移除可能导致路径遍历的模式
|
||||
let path = path.replace("..", "");
|
||||
|
||||
// 规范化路径分隔符
|
||||
#[cfg(target_os = "windows")]
|
||||
let path = path.replace("/", "\\");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let path = path.replace("\\", "/");
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// 创建一个带有生命周期的文本引用
|
||||
pub struct TextRef<'a> {
|
||||
content: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> TextRef<'a> {
|
||||
/// 创建一个新的文本引用
|
||||
pub fn new(content: &'a str) -> Self {
|
||||
TextRef { content }
|
||||
}
|
||||
|
||||
/// 获取内容
|
||||
pub fn content(&self) -> &'a str {
|
||||
self.content
|
||||
}
|
||||
|
||||
/// 获取内容的一部分
|
||||
pub fn substring(&self, start: usize, end: usize) -> &'a str {
|
||||
let start = start.min(self.content.len());
|
||||
let end = end.min(self.content.len());
|
||||
|
||||
if start >= end {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 确保我们在字符边界上切割
|
||||
let mut char_indices = self.content.char_indices();
|
||||
|
||||
let start_idx = char_indices
|
||||
.find(|(i, _)| *i >= start)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(self.content.len());
|
||||
|
||||
let end_idx = char_indices
|
||||
.find(|(i, _)| *i >= end)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(self.content.len());
|
||||
|
||||
&self.content[start_idx..end_idx]
|
||||
}
|
||||
}
|
||||
|
||||
/// 实现Drop trait,用于演示资源清理
|
||||
impl<'a> Drop for TextRef<'a> {
|
||||
fn drop(&mut self) {
|
||||
// 在实际应用中,这里可能会执行一些清理操作
|
||||
// 这里只是为了演示Drop trait的使用
|
||||
println!("TextRef被释放,长度为: {}", self.content.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_read_from_file() -> Result<()> {
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
write!(temp_file, "测试内容")?;
|
||||
|
||||
let content = read_from_file(temp_file.path())?;
|
||||
assert_eq!(content, "测试内容");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_utf8() {
|
||||
assert!(is_valid_utf8("你好".as_bytes()));
|
||||
assert!(!is_valid_utf8(&[0xFF, 0xFE, 0xFD]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_path() {
|
||||
assert_eq!(sanitize_path("../dangerous/path"), "/dangerous/path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_ref() {
|
||||
let text = "Hello, world!";
|
||||
let text_ref = TextRef::new(text);
|
||||
|
||||
assert_eq!(text_ref.content(), "Hello, world!");
|
||||
assert_eq!(text_ref.substring(0, 5), "Hello");
|
||||
assert_eq!(text_ref.substring(7, 12), "world");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Hello Rust!
|
||||
This is a test file.
|
||||
Multiple lines for testing.
|
||||
Loading…
Reference in New Issue