🦀 从零开发一个Rust命令行工具完整教程

  • King
  • 发布于 20小时前
  • 阅读 111

前言本教程将带你从零开始构建一个功能完整的Rust命令行工具包,涵盖文件操作、系统监控、网络工具、文本处理和加密功能。我们将学习如何构建一个具有交互式界面、自动补全、命令历史等现代CLI特性的工具。项目概览我们要构建的工具名为RustToolkit(RTK),它具有以下特性:🚀

前言

本教程将带你从零开始构建一个功能完整的Rust命令行工具包,涵盖文件操作、系统监控、网络工具、文本处理和加密功能。

我们将学习如何构建一个具有交互式界面、自动补全、命令历史等现代CLI特性的工具。

最终效果

项目概览

我们要构建的工具名为 Rust Toolkit (RTK),它具有以下特性:

  • 🚀 高性能:基于Rust的零成本抽象和内存安全
  • 🛡️ 安全可靠:严格的类型系统和全面的错误处理
  • 🎨 交互式Shell:丰富的TUI界面,支持命令自动补全、历史记录
  • 🌈 美观输出:彩色终端输出,进度条和加载动画
  • ⚡ 异步处理:基于Tokio的高效并发操作
  • 🔧 模块化设计:清晰的代码结构,易于扩展和维护

第一步:项目初始化

1.1 创建新项目

cargo new toolkit-rs --bin
cd toolkit-rs

1.2 配置 Cargo.toml

首先配置项目的基本信息和依赖:

[package]
name = "toolkit-rs"
version = "0.1.0"
edition = "2024"
authors = ["Your Name <your.email@example.com>"]
description = "A powerful and efficient CLI toolkit built with Rust"
license = "MIT"

[[bin]]
name = "rtk"
path = "src/main.rs"

[dependencies]
# 命令行参数解析
clap = { version = "4", features = ["derive"] }
# 异步运行时
tokio = { version = "1", features = ["full"] }
# 序列化/反序列化
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
# HTTP客户端
reqwest = { version = "0.12", features = ["json"] }
# 加密哈希
sha2 = "0.10"
hex = "0.4"
# 文件系统遍历
walkdir = "2.5"
# 正则表达式
regex = "1.11"
# 时间处理
chrono = { version = "0.4", features = ["serde"] }
# 彩色输出
colored = "3.0"
# 进度条
indicatif = "0.18"
# 错误处理
anyhow = "1.0"
thiserror = "2.0"
# 系统信息
sysinfo = "0.37"
# 终端控制
crossterm = "0.29"
# 随机数生成
rand = "0.9"
# TUI框架
ratatui = { version = "0.29", features = ["crossterm"] }
# 交互式输入
inquire = "0.7.5"
# 枚举工具
strum = { version = "0.26", features = ["derive"] }

第二步:项目结构设计

2.1 创建目录结构

mkdir -p src/{commands,interactive,utils}

最终的项目结构:

src/
├── main.rs              # 主入口点
├── commands/            # 命令模块
│   ├── mod.rs           # 命令注册和自动补全
│   ├── file.rs          # 文件操作
│   ├── system.rs        # 系统信息
│   ├── network.rs       # 网络工具
│   ├── text.rs          # 文本处理
│   └── crypto.rs        # 加密工具
├── interactive/         # 交互式Shell
│   ├── mod.rs
│   └── session.rs       # 会话管理
└── utils/               # 工具函数
    ├── mod.rs
    ├── tui.rs           # 终端UI组件
    ├── progress.rs      # 进度显示
    └── output.rs        # 输出格式化

第三步:核心架构实现

3.1 主入口点 (src/main.rs)

use anyhow::Result;
use colored::*;

mod commands;
mod interactive;
mod utils;

use interactive::InteractiveSession;
use utils::output;

#[tokio::main]
async fn main() -> Result<()> {
    print_welcome();

    let mut session = InteractiveSession::new();
    session.run().await?;

    Ok(())
}

fn print_welcome() {
    // 清屏
    print!("\x1B[2J\x1B[1;1H");

    output::print_header("🦀 Welcome to Rust Toolkit");
    println!();
    output::print_info("A powerful collection of CLI utilities.");
    println!();
    println!(
        "{} {} {} {}",
        "Type".bright_black(),
        "help".bright_yellow().bold(),
        "for a list of commands, or".bright_black(),
        "exit".bright_red().bold()
    );
    println!();
}

3.2 输出工具模块 (src/utils/output.rs)

use colored::{Color, Colorize};

pub fn print_header(title: &str) {
    println!("{}", title.cyan().bold());
    println!("{}", "=".repeat(title.len()).blue());
}

pub fn print_success(msg: &str) {
    println!("{} {}", "✅".green(), msg.green());
}

pub fn print_error(msg: &str) {
    println!("{} {}", "❌".red(), msg.red());
}

pub fn print_warning(msg: &str) {
    println!("{} {}", "⚠️".yellow(), msg.yellow());
}

pub fn print_info(msg: &str) {
    println!("{} {}", "ℹ️".blue(), msg.blue());
}

pub fn print_normal(msg: &str) {
    println!("{}", msg);
}

pub fn print_colored(msg: &str, color: Color) {
    match color {
        Color::Red => println!("{}", msg.red()),
        Color::Green => println!("{}", msg.green()),
        Color::Blue => println!("{}", msg.blue()),
        Color::Yellow => println!("{}", msg.yellow()),
        Color::Cyan => println!("{}", msg.cyan()),
        Color::Magenta => println!("{}", msg.magenta()),
        Color::White => println!("{}", msg.white()),
        Color::Black => println!("{}", msg.black()),
        _ => print_normal(msg),
    }
}

3.3 工具模块注册 (src/utils/mod.rs)

pub mod output;
pub mod progress;
pub mod tui;

第四步:命令系统设计

4.1 命令模块管理 (src/commands/mod.rs)

这是整个命令系统的核心,负责管理所有命令并提供自动补全功能:

pub mod file;
pub mod system;
pub mod network;
pub mod text;
pub mod crypto;

pub use file::FileCommands;
use strum::IntoEnumIterator;
pub use system::SystemCommands;
pub use network::NetworkCommands;
pub use text::TextCommands;
pub use crypto::CryptoCommands;

/// 将驼峰命名转换为短横线命名
/// 
/// 示例:
/// - LogAnalyze -> log-analyze
/// - Info -> info
/// - HttpRequest -> http-request
fn to_kebab_case(s: &str) -> String {
    let mut result = String::new();
    let mut chars = s.chars().peekable();

    while let Some(c) = chars.next() {
        if c.is_uppercase() && !result.is_empty() {
            result.push('-');
        }
        result.push(c.to_lowercase().next().unwrap());
    }

    result
}

/// 获取所有可用命令用于自动补全建议
/// 
/// 这个函数使用CommandNames trait实现来自动生成完整的可用命令列表。
/// 当添加新的命令变体时,只需更新相应的CommandNames实现。
pub fn get_all_commands() -> Vec<String> {
    let mut commands = Vec::new();

    // 自动从每个命令类型中提取命令
    for cmd in FileCommands::iter() {
        commands.push(format!("file {}", to_kebab_case(&cmd.to_string())));
    }

    for cmd in SystemCommands::iter() {
        commands.push(format!("system {}", to_kebab_case(&cmd.to_string())));
    }

    for cmd in NetworkCommands::iter() {
        commands.push(format!("network {}", to_kebab_case(&cmd.to_string())));
    }

    for cmd in TextCommands::iter() {
        commands.push(format!("text {}", to_kebab_case(&cmd.to_string())));
    }

    for cmd in CryptoCommands::iter() {
        commands.push(format!("crypto {}", to_kebab_case(&cmd.to_string())));
    }

    // 全局命令 (在session.rs中处理)
    commands.extend([
        "help",
        "clear", 
        "exit",
    ].iter().map(|s| s.to_string()));

    commands
}

4.2 文件操作命令 (src/commands/file.rs)

让我们实现一个完整的文件操作模块:

use crate::utils::{output, progress};
use anyhow::Result;
use clap::Subcommand;
use colored::Color;
use regex::Regex;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use strum::{Display, EnumIter};
use walkdir::WalkDir;

#[derive(Subcommand, Clone, EnumIter, Display)]
pub enum FileCommands {
    /// 按名称模式搜索文件
    Search {
        /// 搜索目录
        #[arg(short, long, default_value = ".")]
        dir: String,
        /// 搜索模式 (支持正则表达式)
        pattern: String,
        /// 不区分大小写搜索
        #[arg(short, long)]
        ignore_case: bool,
    },
    /// 获取文件统计信息
    Stats {
        /// 分析目录
        #[arg(short, long, default_value = ".")]
        dir: String,
    },
    /// 批量重命名文件
    Rename {
        /// 包含文件的目录
        #[arg(short, long, default_value = ".")]
        dir: String,
        /// 匹配模式 (正则表达式)
        pattern: String,
        /// 替换字符串
        replacement: String,
        /// 预览模式 (不实际重命名)
        #[arg(short, long)]
        preview: bool,
    },
    /// 查找重复文件
    Duplicates {
        /// 扫描目录
        #[arg(short, long, default_value = ".")]
        dir: String,
    },
}

pub async fn handle_file_command(command: FileCommands) -> Result<()> {
    match command {
        FileCommands::Search {
            dir,
            pattern,
            ignore_case,
        } => {
            search_files(&dir, &pattern, ignore_case).await?;
        }
        FileCommands::Stats { dir } => {
            show_file_stats(&dir).await?;
        }
        FileCommands::Rename {
            dir,
            pattern,
            replacement,
            preview,
        } => {
            batch_rename(&dir, &pattern, &replacement, preview).await?;
        }
        FileCommands::Duplicates { dir } => {
            find_duplicates(&dir).await?;
        }
    }
    Ok(())
}

async fn search_files(dir: &str, pattern: &str, ignore_case: bool) -> Result<()> {
    output::print_header("🔍 Searching files...");

    let regex = if ignore_case {
        Regex::new(&format!("(?i){}", pattern))?
    } else {
        Regex::new(pattern)?
    };

    let mut found_count = 0;

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        if let Some(filename) = entry.file_name().to_str() {
            if regex.is_match(filename) {
                let path = entry.path().display();
                output::print_colored(&format!("  ✓ {}", path), Color::Green);
                found_count += 1;
            }
        }
    }

    output::print_success(&format!("Found {} files", found_count));
    Ok(())
}

async fn show_file_stats(dir: &str) -> Result<()> {
    output::print_header("📊 Analyzing directory...");

    let mut total_files = 0;
    let mut total_dirs = 0;
    let mut total_size = 0u64;
    let mut extensions: HashMap<String, (u32, u64)> = HashMap::new();

    let entries: Vec<_> = WalkDir::new(dir)
        .into_iter()
        .filter_map(|e| e.ok())
        .collect();
    let pb = progress::create_progress_bar(entries.len() as u64);

    for entry in entries {
        pb.inc(1);

        if entry.file_type().is_dir() {
            total_dirs += 1;
        } else {
            total_files += 1;
            if let Ok(metadata) = entry.metadata() {
                let size = metadata.len();
                total_size += size;

                if let Some(ext) = entry.path().extension().and_then(|s| s.to_str()) {
                    let ext = ext.to_lowercase();
                    let entry = extensions.entry(ext).or_insert((0, 0));
                    entry.0 += 1;
                    entry.1 += size;
                }
            }
        }
    }

    pb.finish_with_message("Analysis complete!");

    output::print_normal("");
    output::print_colored("📈 Statistics:", Color::Green);
    output::print_normal(&format!("  Total files: {}", total_files));
    output::print_normal(&format!("  Total directories: {}", total_dirs));
    output::print_normal(&format!("  Total size: {}", format_size(total_size)));

    if !extensions.is_empty() {
        output::print_normal("");
        output::print_colored("📋 File type distribution:", Color::Green);
        let mut ext_vec: Vec<_> = extensions.into_iter().collect();
        ext_vec.sort_by(|a, b| b.1.1.cmp(&a.1.1));

        for (ext, (count, size)) in ext_vec.iter().take(10) {
            output::print_normal(&format!(
                "  .{}: {} files, {}",
                ext,
                count,
                format_size(*size)
            ));
        }
    }

    Ok(())
}

// ... 其他函数实现

fn format_size(size: u64) -> String {
    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
    let mut size = size as f64;
    let mut unit_index = 0;

    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
        size /= 1024.0;
        unit_index += 1;
    }

    format!("{:.2} {}", size, UNITS[unit_index])
}

第五步:交互式界面实现

5.1 TUI组件 (src/utils/tui.rs)

这是项目的亮点之一,实现了一个功能完整的交互式终端界面:


use anyhow::Result;
use crossterm::{
    event::{self, Event, KeyCode, KeyEventKind},
    terminal::{disable_raw_mode, enable_raw_mode},
};
use std::io::{self, Write};

pub struct TuiApp {
    input: String,
    all_suggestions: Vec<String>,
    filtered_suggestions: Vec<String>,
    show_suggestions: bool,
    selected_index: usize,
    history: Vec<String>,
    history_index: Option<usize>,
    current_input: String, // 保存当前输入内容
    scroll_offset: usize,  // 建议列表的滚动偏移
}

impl TuiApp {
    pub fn with_history(commands: Vec<String>, history: Vec<String>) -> Self {
        Self {
            input: String::new(),
            all_suggestions: commands,
            filtered_suggestions: Vec::new(),
            show_suggestions: false,
            selected_index: 0,
            history,
            history_index: None,
            current_input: String::new(),
            scroll_offset: 0,
        }
    }

    pub fn run(&mut self) -> Result<Option<String>> {
        enable_raw_mode()?;

        print!("❯ ");
        io::stdout().flush()?;

        let result = self.run_input_loop();

        disable_raw_mode()?;
        println!();

        result
    }

    fn run_input_loop(&mut self) -> Result<Option<String>> {
        loop {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    match key.code {
                        KeyCode::Enter => {
                            if self.show_suggestions && !self.filtered_suggestions.is_empty() {
                                if let Some(selected) =
                                    self.filtered_suggestions.get(self.selected_index).cloned()
                                {
                                    self.clear_suggestions_display()?;
                                    return Ok(Some(selected));
                                }
                            }
                            if self.show_suggestions {
                                self.clear_suggestions_display()?;
                            }
                            return Ok(Some(self.input.clone()));
                        }
                        KeyCode::Esc => {
                            if self.show_suggestions {
                                self.hide_suggestions()?;
                            } else {
                                return Ok(None);
                            }
                        }
                        KeyCode::Char('/') => {
                            if !self.show_suggestions {
                                self.show_suggestions()?;
                            } else {
                                self.input.push('/');
                                self.update_suggestions_with_filter()?;
                            }
                        }
                        KeyCode::Tab => {
                            if !self.show_suggestions {
                                self.show_suggestions()?;
                            } else if !self.filtered_suggestions.is_empty() {
                                if let Some(selected) =
                                    self.filtered_suggestions.get(self.selected_index)
                                {
                                    self.input = selected.clone();
                                    self.hide_suggestions()?;
                                    self.redraw_input_line()?;
                                }
                            }
                        }
                        KeyCode::Char(c) => {
                            // 如果正在浏览历史记录,重置历史索引
                            if self.history_index.is_some() {
                                self.history_index = None;
                            }
                            self.input.push(c);
                            if self.show_suggestions {
                                self.update_suggestions_with_filter()?;
                            } else {
                                print!("{}", c);
                                io::stdout().flush()?;
                            }
                        }
                        KeyCode::Backspace => {
                            if !self.input.is_empty() {
                                // 如果正在浏览历史记录,重置历史索引
                                if self.history_index.is_some() {
                                    self.history_index = None;
                                }
                                self.input.pop();
                                if self.show_suggestions {
                                    if self.input.is_empty() {
                                        self.hide_suggestions()?;
                                    } else {
                                        self.update_suggestions_with_filter()?;
                                    }
                                } else {
                                    print!("\x08 \x08"); // 退格
                                    io::stdout().flush()?;
                                }
                            }
                        }
                        KeyCode::Up => {
                            if self.show_suggestions {
                                self.previous_suggestion()?;
                            } else {
                                self.previous_history()?;
                            }
                        }
                        KeyCode::Down => {
                            if self.show_suggestions {
                                self.next_suggestion()?;
                            } else {
                                self.next_history()?;
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    // ... 其他方法实现
}

5.2 会话管理 (src/interactive/session.rs)

use anyhow::Result;
use colored::*;
use crate::commands;
use crate::utils::{output, tui::TuiApp};
use clap::{Parser, Subcommand};

pub struct InteractiveSession {
    history: Vec<String>,
}

#[derive(Parser)]
#[command(name = "rtk", disable_help_flag = true)]
#[command(about = "Rust Toolkit - A powerful CLI utility collection")]
#[command(version = "0.1.0")]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand, Clone)]
enum Commands {
    /// 文件操作
    File {
        #[command(subcommand)]
        action: commands::FileCommands,
    },
    /// 系统信息
    System {
        #[command(subcommand)]
        action: commands::SystemCommands,
    },
    /// 网络工具
    Network {
        #[command(subcommand)]
        action: commands::NetworkCommands,
    },
    /// 文本处理工具
    Text {
        #[command(subcommand)]
        action: commands::TextCommands,
    },
    /// 加密工具
    Crypto {
        #[command(subcommand)]
        action: commands::CryptoCommands,
    },
}

impl InteractiveSession {
    pub fn new() -> Self {
        Self {
            history: Vec::new(),
        }
    }

    pub async fn run(&mut self) -> Result<()> {
        loop {
            // 从commands模块获取动态命令列表
            let command_list = commands::get_all_commands();

            let mut app = TuiApp::with_history(
                command_list,
                self.history.clone()
            );
            let user_input_result = app.run();

            match user_input_result {
                Ok(Some(input)) => {
                    self.history.push(input.clone());
                    let trimmed_input = input.trim();

                    if trimmed_input.is_empty() {
                        continue;
                    }

                    match trimmed_input {
                        "exit" | "quit" => {
                            output::print_success("👋 Goodbye!");
                            break;
                        }
                        "clear" => {
                            // 清屏并重新开始循环
                            print!("\x1B[2J\x1B[1;1H");
                            continue;
                        }
                        "help" => {
                            self.show_help();
                            println!();
                            continue;
                        }
                        _ => {}
                    }

                    let args = std::iter::once("rtk".to_string())
                        .chain(trimmed_input.split_whitespace().map(|s| s.to_string()));

                    match Cli::try_parse_from(args) {
                        Ok(cli) => {
                            if let Some(command) = cli.command {
                                if let Err(e) = self.handle_command(command.clone()).await {
                                    output::print_error(&e.to_string());
                                }
                                println!(); // 添加换行符用于间距
                            } else {
                                self.show_help();
                            }
                        }
                        Err(e) => {
                            output::print_error(&e.to_string());
                        }
                    }
                }
                Ok(None) => {
                    // 用户按了Esc,退出
                    output::print_success("👋 Goodbye!");
                    break;
                }
                Err(e) => {
                    // TUI循环本身失败,打印错误并退出
                    output::print_error(&format!("TUI Error: {}", e));
                    break;
                }
            }
        }
        Ok(())
    }

    async fn handle_command(&self, command: Commands) -> Result<()> {
        match command {
            Commands::File { action } => {
                commands::file::handle_file_command(action).await?;
            }
            Commands::System { action } => {
                commands::system::handle_system_command(action).await?;
            }
            Commands::Network { action } => {
                commands::network::handle_network_command(action).await?;
            }
            Commands::Text { action } => {
                commands::text::handle_text_command(action).await?;
            }
            Commands::Crypto { action } => {
                commands::crypto::handle_crypto_command(action).await?;
            }
        }
        Ok(())
    }

    fn show_help(&self) {
        output::print_header("🦀 Rust Toolkit Help");
        println!();
        output::print_info("A powerful collection of CLI utilities.");
        println!();
        println!("{}", "Available Commands:".bright_white().bold());
        println!("  {}         File operations (stats, search, rename, duplicates)", "file".bright_green());
        println!("  {}       System information (info, processes, monitor)", "system".bright_green());
        println!("  {}     Network utilities (ping, scan, http)", "network".bright_green());
        println!("  {}        Text processing (count, log, replace)", "text".bright_green());
        println!("  {}      Cryptographic tools (hash, password, base64, caesar)", "crypto".bright_green());
        println!();
        println!("{}", "Global Commands:".bright_white().bold());
        println!("  {}         Show this help message", "help".bright_yellow());
        println!("  {}       Clear the screen", "clear".bright_yellow());
        println!("  {}         Exit the application", "exit".bright_red());
        println!();
        println!("{}", "Usage:".bright_black());
        println!("{}", "  <command> <subcommand> [options]".bright_black());
        println!("{}", "  Example: file stats --dir .".bright_black());
        println!("{}", "  For help on a specific command, type: <command> --help".bright_black());
    }
}

第六步:进度条和工具函数

6.1 进度条工具 (src/utils/progress.rs)

use indicatif::{ProgressBar, ProgressStyle};

pub fn create_progress_bar(len: u64) -> ProgressBar {
    let pb = ProgressBar::new(len);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
            .unwrap()
            .progress_chars("#>-"),
    );
    pb
}

pub fn create_spinner(msg: &str) -> ProgressBar {
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.green} {msg}")
            .unwrap(),
    );
    pb.set_message(msg.to_string());
    pb
}

6.2 加密工具示例 (src/commands/crypto.rs)

use crate::utils::output;
use anyhow::Result;
use clap::Subcommand;
use colored::*;
use rand::Rng;
use sha2::{Digest, Sha256};
use std::fs;
use strum::{Display, EnumIter};

#[derive(Subcommand, Clone, EnumIter, Display)]
pub enum CryptoCommands {
    /// 计算文件哈希
    Hash {
        /// 要哈希的文件
        #[arg(short, long)]
        file: String,
        /// 哈希算法 (md5, sha1, sha256, sha512)
        #[arg(short, long, default_value = "sha256")]
        algorithm: String,
    },
    /// 生成安全密码
    Password {
        /// 密码长度
        #[arg(short, long, default_value = "16")]
        length: usize,
        /// 包含大写字母
        #[arg(long, default_value = "true")]
        uppercase: bool,
        /// 包含小写字母
        #[arg(long, default_value = "true")]
        lowercase: bool,
        /// 包含数字
        #[arg(long, default_value = "true")]
        numbers: bool,
        /// 包含符号
        #[arg(long)]
        symbols: bool,
    },
    /// Base64编码/解码
    Base64 {
        /// 输入文本或文件
        input: String,
        /// 解码而不是编码
        #[arg(short, long)]
        decode: bool,
        /// 将输入视为文件路径
        #[arg(short, long)]
        file: bool,
    },
    /// 简单文本加密 (凯撒密码)
    Caesar {
        /// 要加密/解密的文本
        text: String,
        /// 偏移量
        #[arg(short, long, default_value = "3")]
        shift: i32,
        /// 解密而不是加密
        #[arg(short, long)]
        decrypt: bool,
    },
}

pub async fn handle_crypto_command(command: CryptoCommands) -> Result<()> {
    match command {
        CryptoCommands::Hash { file, algorithm } => {
            calculate_hash(&file, &algorithm).await?;
        }
        CryptoCommands::Password {
            length,
            uppercase,
            lowercase,
            numbers,
            symbols,
        } => {
            generate_password(length, uppercase, lowercase, numbers, symbols).await?;
        }
        CryptoCommands::Base64 {
            input,
            decode,
            file,
        } => {
            handle_base64(&input, decode, file).await?;
        }
        CryptoCommands::Caesar {
            text,
            shift,
            decrypt,
        } => {
            caesar_cipher(&text, shift, decrypt).await?;
        }
    }
    Ok(())
}

async fn generate_password(
    length: usize,
    uppercase: bool,
    lowercase: bool,
    numbers: bool,
    symbols: bool,
) -> Result<()> {
    output::print_header("🔑 Generate Secure Password");

    let mut charset = String::new();

    if uppercase {
        charset.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
    }
    if lowercase {
        charset.push_str("abcdefghijklmnopqrstuvwxyz");
    }
    if numbers {
        charset.push_str("0123456789");
    }
    if symbols {
        charset.push_str("!@#$%^&*()_+-=[]{}|;:,.<>?");
    }

    if charset.is_empty() {
        output::print_error("At least one character type must be selected");
        return Err(anyhow::anyhow!(
            "At least one character type must be selected"
        ));
    }

    let charset_chars: Vec<char> = charset.chars().collect();
    let mut rng = rand::rng();

    let password: String = (0..length)
        .map(|_| charset_chars[rng.random_range(0..charset_chars.len())])
        .collect();

    output::print_normal("");
    output::print_colored("🔐 Generated Password:", Color::Green);
    output::print_normal(&format!("Length: {} characters", length));
    output::print_colored(&format!("Password: {}", password), Color::Yellow);

    let strength = evaluate_password_strength(&password);
    output::print_normal(&format!("Strength: {}", strength));

    if length < 12 {
        output::print_warning(
            "Consider using a longer password (12+ characters) for better security",
        );
    }

    Ok(())
}

fn evaluate_password_strength(password: &str) -> String {
    let mut score = 0;

    if password.len() >= 8 {
        score += 1;
    }
    if password.len() >= 12 {
        score += 1;
    }
    if password.chars().any(|c| c.is_ascii_lowercase()) {
        score += 1;
    }
    if password.chars().any(|c| c.is_ascii_uppercase()) {
        score += 1;
    }
    if password.chars().any(|c| c.is_ascii_digit()) {
        score += 1;
    }
    if password.chars().any(|c| !c.is_ascii_alphanumeric()) {
        score += 1;
    }

    match score {
        0..=2 => "Weak".red().to_string(),
        3..=4 => "Medium".yellow().to_string(),
        5..=6 => "Strong".green().to_string(),
        _ => "Very Strong".green().bold().to_string(),
    }
}

// ... 其他函数实现

第七步:构建和测试

7.1 构建项目

# 开发构建
cargo build

# 发布构建
cargo build --release

7.2 运行项目

# 直接运行
cargo run

# 或者运行构建后的二进制文件
./target/release/rtk

7.3 测试功能

启动程序后,你可以尝试以下命令:

# 文件操作
file stats --dir .
file search --pattern "*.rs"
file duplicates --dir .

# 加密工具
crypto password --length 20 --symbols
crypto hash --file Cargo.toml
crypto base64 "Hello World"

# 系统信息
system info
system processes

# 使用自动补全
# 按 '/' 或 Tab 键显示可用命令
# 使用上下箭头键浏览建议
# 使用上下箭头键浏览命令历史

第八步:高级特性

8.1 错误处理最佳实践

使用 anyhowthiserror 进行优雅的错误处理:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ToolkitError {
    #[error("File not found: {path}")]
    FileNotFound { path: String },

    #[error("Invalid pattern: {pattern}")]
    InvalidPattern { pattern: String },

    #[error("Network error: {0}")]
    Network(#[from] reqwest::Error),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

8.2 配置文件支持

添加配置文件支持:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
    pub default_dir: String,
    pub max_results: usize,
    pub theme: String,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            default_dir: ".".to_string(),
            max_results: 100,
            theme: "default".to_string(),
        }
    }
}

8.3 插件系统设计

为了支持扩展,可以设计一个简单的插件系统:

pub trait Plugin {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn execute(&self, args: &[String]) -> Result<()>;
}

pub struct PluginManager {
    plugins: Vec<Box<dyn Plugin>>,
}

impl PluginManager {
    pub fn new() -> Self {
        Self {
            plugins: Vec::new(),
        }
    }

    pub fn register_plugin(&mut self, plugin: Box<dyn Plugin>) {
        self.plugins.push(plugin);
    }

    pub fn execute_plugin(&self, name: &str, args: &[String]) -> Result<()> {
        for plugin in &self.plugins {
            if plugin.name() == name {
                return plugin.execute(args);
            }
        }
        Err(anyhow::anyhow!("Plugin not found: {}", name))
    }
}

第九步:性能优化

9.1 异步处理

充分利用Tokio的异步特性:

use tokio::fs;
use tokio::task;

async fn process_files_concurrently(files: Vec<String>) -> Result<Vec<String>> {
    let tasks: Vec<_> = files
        .into_iter()
        .map(|file| {
            task::spawn(async move {
                // 异步处理每个文件
                process_single_file(file).await
            })
        })
        .collect();

    let mut results = Vec::new();
    for task in tasks {
        results.push(task.await??);
    }

    Ok(results)
}

9.2 内存优化

使用流式处理大文件:

use tokio::io::{AsyncBufReadExt, BufReader};

async fn process_large_file(path: &str) -> Result<()> {
    let file = fs::File::open(path).await?;
    let reader = BufReader::new(file);
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await? {
        // 逐行处理,避免将整个文件加载到内存
        process_line(&line).await?;
    }

    Ok(())
}

第十步:部署和分发

10.1 交叉编译

为不同平台构建:

# 安装目标平台
rustup target add x86_64-pc-windows-gnu
rustup target add x86_64-apple-darwin
rustup target add x86_64-unknown-linux-gnu

# 交叉编译
cargo build --release --target x86_64-pc-windows-gnu
cargo build --release --target x86_64-apple-darwin
cargo build --release --target x86_64-unknown-linux-gnu

10.2 创建安装脚本

#!/bin/bash
# install.sh

set -e

REPO="your-username/toolkit-rs"
BINARY_NAME="rtk"

# 检测操作系统和架构
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)

case $ARCH in
    x86_64) ARCH="x86_64" ;;
    arm64|aarch64) ARCH="aarch64" ;;
    *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac

# 下载并安装
DOWNLOAD_URL="https://github.com/$REPO/releases/latest/download/$BINARY_NAME-$OS-$ARCH"
INSTALL_DIR="$HOME/.local/bin"

mkdir -p "$INSTALL_DIR"
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/$BINARY_NAME"
chmod +x "$INSTALL_DIR/$BINARY_NAME"

echo "✅ $BINARY_NAME installed successfully to $INSTALL_DIR"
echo "Make sure $INSTALL_DIR is in your PATH"

总结

通过这个教程,我们从零开始构建了一个功能完整的Rust命令行工具,涵盖了:

核心技术栈

  • Clap 4.x: 现代化的命令行参数解析
  • Tokio: 异步运行时,支持高并发操作
  • Ratatui + Crossterm: 终端UI框架,实现交互式界面
  • Serde: 序列化/反序列化,配置文件支持
  • Anyhow/Thiserror: 优雅的错误处理

关键特性

  • 🎨 交互式Shell: 支持命令自动补全、历史记录、智能建议
  • 🌈 美观输出: 彩色终端输出,进度条和加载动画
  • 高性能: 异步处理,零成本抽象
  • 🔧 模块化设计: 清晰的代码结构,易于扩展
  • 🛡️ 安全可靠: Rust的内存安全保证

学到的经验

  1. 项目结构设计: 合理的模块划分和依赖管理
  2. 用户体验: 现代CLI工具应该具备的交互特性
  3. 错误处理: 使用Rust的类型系统进行优雅的错误处理
  4. 性能优化: 异步编程和内存管理最佳实践
  5. 可扩展性: 插件系统和配置文件支持

这个项目展示了如何使用Rust构建一个生产级别的命令行工具,结合了现代CLI工具的最佳实践和Rust语言的优势。你可以基于这个框架继续添加更多功能,或者将其作为其他CLI项目的起点。

下一步建议

  • 添加更多命令模块 (数据库工具、API测试等)
  • 实现配置文件和主题系统
  • 添加单元测试和集成测试
  • 创建详细的文档和使用指南
  • 发布到 crates.io 和 GitHub Releases

希望这个教程对你学习Rust和CLI开发有所帮助!🦀

点赞 0
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发