返回 SC 笔记
SC Day 28

Rust clap 命令行解析 + 项目结构化(mod/lib/bin) + CLI v2

### 一、clap — 命令行参数解析

2026-04-28
第二阶段:框架实战 (Day 25-30)
RustclapCLI模块系统项目结构

日期: 2026-04-28 方向: Rust 阶段: 第二阶段:框架实战 (Day 25-30) 标签: #Rust #clap #CLI #模块系统 #项目结构


今日目标

  1. 掌握 clap 的 derive API(Parser, Subcommand, Args)
  2. 深入理解 Rust 模块系统(mod, pub, use, lib.rs vs main.rs)
  3. 实现多子命令 CLI 工具(balance/tx/token)
  4. 学会将项目结构化为可维护的模块

核心概念

一、clap — 命令行参数解析

clap 是 Rust 生态中最主流的命令行解析库。v4 版本推荐使用 derive API,通过派生宏自动生成解析代码。

1.1 基础用法

use clap::Parser;

/// 区块链查询 CLI 工具
#[derive(Parser, Debug)]
#[command(name = "chain-cli")]
#[command(version = "0.2.0")]
#[command(about = "Query blockchain data from the command line")]
#[command(author = "Momo")]
struct Cli {
    /// 以太坊地址
    #[arg(short, long)]
    address: String,

    /// 链名称 (ethereum, arbitrum, polygon)
    #[arg(short, long, default_value = "ethereum")]
    chain: String,

    /// 输出格式
    #[arg(short, long, default_value = "text", value_parser = ["text", "json", "csv"])]
    format: String,

    /// 详细输出
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
}

fn main() {
    let cli = Cli::parse();
    println!("Address: {}", cli.address);
    println!("Chain: {}", cli.chain);
    println!("Format: {}", cli.format);
    println!("Verbose level: {}", cli.verbose);
}

使用方式:

# 基本用法
chain-cli --address 0x1234... --chain arbitrum

# 简写
chain-cli -a 0x1234... -c polygon -f json

# 多个 -v 增加详细度
chain-cli -a 0x1234... -vvv  # verbose = 3

# 帮助
chain-cli --help
chain-cli --version

1.2 子命令 (Subcommands)

这是 CLI 工具的核心模式 — 不同功能用不同子命令:

use clap::{Parser, Subcommand, Args};

#[derive(Parser, Debug)]
#[command(name = "chain-cli", version, about)]
struct Cli {
    /// 全局选项: RPC URL
    #[arg(long, env = "ETH_RPC_URL", global = true)]
    rpc_url: Option<String>,

    /// 全局选项: API Key
    #[arg(long, env = "ETHERSCAN_API_KEY", global = true)]
    api_key: Option<String>,

    /// 全局选项: 详细输出
    #[arg(short, long, global = true)]
    verbose: bool,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// 查询账户余额
    Balance(BalanceArgs),

    /// 查询交易历史
    Tx(TxArgs),

    /// 查询 Token 信息
    Token(TokenArgs),

    /// 查询当前 Gas 价格
    Gas,
}

#[derive(Args, Debug)]
struct BalanceArgs {
    /// 以太坊地址
    #[arg(short, long)]
    address: String,

    /// 链 (ethereum, arbitrum, polygon, base, optimism)
    #[arg(short, long, default_value = "ethereum")]
    chain: String,

    /// 同时查询所有支持的链
    #[arg(long)]
    all_chains: bool,
}

#[derive(Args, Debug)]
struct TxArgs {
    /// 以太坊地址
    #[arg(short, long)]
    address: String,

    /// 显示最近 N 笔交易
    #[arg(short, long, default_value = "10")]
    count: u32,

    /// 只显示发出的交易
    #[arg(long)]
    outgoing_only: bool,

    /// 只显示收到的交易
    #[arg(long)]
    incoming_only: bool,
}

#[derive(Args, Debug)]
struct TokenArgs {
    /// Token 合约地址
    #[arg(short = 'c', long)]
    contract: String,

    /// 查询某地址的 Token 余额 (可选)
    #[arg(short, long)]
    holder: Option<String>,
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Balance(args) => {
            println!("Querying balance for {} on {}", args.address, args.chain);
            if args.all_chains {
                println!("Checking all chains...");
            }
        }
        Commands::Tx(args) => {
            println!("Querying {} transactions for {}", args.count, args.address);
        }
        Commands::Token(args) => {
            println!("Token contract: {}", args.contract);
            if let Some(holder) = &args.holder {
                println!("Checking balance for holder: {}", holder);
            }
        }
        Commands::Gas => {
            println!("Querying current gas prices...");
        }
    }
}

使用方式:

chain-cli balance --address 0x1234... --chain arbitrum
chain-cli balance -a 0x1234... --all-chains
chain-cli tx -a 0x1234... --count 20 --outgoing-only
chain-cli token -c 0xdAC17F... --holder 0x1234...
chain-cli gas
chain-cli --rpc-url https://... balance -a 0x1234...

1.3 高级 clap 特性

use clap::{Parser, ValueEnum};

// 用枚举限制选项值
#[derive(Debug, Clone, ValueEnum)]
enum Chain {
    Ethereum,
    Arbitrum,
    Optimism,
    Base,
    Polygon,
}

impl std::fmt::Display for Chain {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Chain::Ethereum => write!(f, "ethereum"),
            Chain::Arbitrum => write!(f, "arbitrum"),
            Chain::Optimism => write!(f, "optimism"),
            Chain::Base => write!(f, "base"),
            Chain::Polygon => write!(f, "polygon"),
        }
    }
}

#[derive(Debug, Clone, ValueEnum)]
enum OutputFormat {
    Text,
    Json,
    Csv,
    Table,
}

#[derive(Parser, Debug)]
struct QueryArgs {
    /// 查询链
    #[arg(short, long, value_enum, default_value_t = Chain::Ethereum)]
    chain: Chain,

    /// 输出格式
    #[arg(short, long, value_enum, default_value_t = OutputFormat::Table)]
    format: OutputFormat,

    /// 自定义 RPC URL (覆盖默认值)
    #[arg(long, env = "ETH_RPC_URL")]
    rpc_url: Option<String>,

    /// 请求超时 (秒)
    #[arg(long, default_value = "10", value_parser = clap::value_parser!(u64).range(1..=60))]
    timeout: u64,
}

二、Rust 模块系统

2.1 模块基础

Rust 的模块系统用于组织代码,控制可见性(封装),避免命名冲突。

模块系统核心规则:
1. 每个 .rs 文件就是一个模块
2. mod 声明引入子模块
3. pub 控制可见性
4. use 引入路径简写
5. crate:: 指代当前 crate 根
6. self:: 指代当前模块
7. super:: 指代父模块

2.2 文件即模块

src/
├── main.rs          # 二进制入口 (bin crate root)
├── lib.rs           # 库入口 (lib crate root)
├── config.rs        # config 模块
├── client.rs        # client 模块
├── commands/        # commands 模块 (目录形式)
│   ├── mod.rs       # commands 模块入口
│   ├── balance.rs   # commands::balance 子模块
│   ├── tx.rs        # commands::tx 子模块
│   └── token.rs     # commands::token 子模块
└── types/           # types 模块
    ├── mod.rs
    └── chain.rs

2.3 lib.rs vs main.rs

文件角色用途
lib.rs库 crate 根定义公共 API,可被其他 crate 依赖
main.rs二进制 crate 根程序入口点,通常调用 lib.rs 的函数

最佳实践: 将逻辑放在 lib.rsmain.rs 只做参数解析和调用。这样可以:

  • 方便编写集成测试(只能测试 lib crate 的公共 API)
  • 方便被其他项目复用
  • 保持关注点分离

2.4 可见性规则

// 默认: 私有 (只在当前模块可见)
fn private_fn() {}

// pub: 公共 (任何地方可见)
pub fn public_fn() {}

// pub(crate): 包内可见 (当前 crate 内可见)
pub(crate) fn crate_visible_fn() {}

// pub(super): 父模块可见
pub(super) fn parent_visible_fn() {}

// pub(in path): 指定路径可见
pub(in crate::commands) fn commands_visible_fn() {}

// 结构体的字段也可以独立控制可见性
pub struct Config {
    pub chain: String,          // 公开
    pub(crate) api_key: String, // 包内可见
    secret: String,             // 私有
}

代码实战

完整 CLI v2 项目

项目结构

chain-cli/
├── Cargo.toml
├── .env.example
├── src/
│   ├── main.rs          # 入口 + clap 解析
│   ├── lib.rs           # 库导出
│   ├── config.rs        # 配置管理
│   ├── client.rs        # HTTP/RPC 客户端
│   ├── types.rs         # 公共类型定义
│   ├── utils.rs         # 工具函数
│   └── commands/        # 子命令实现
│       ├── mod.rs       # 模块入口
│       ├── balance.rs   # balance 子命令
│       ├── tx.rs        # tx 子命令
│       └── token.rs     # token 子命令

Cargo.toml

[package]
name = "chain-cli"
version = "0.2.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dotenv = "0.15"

src/types.rs

use clap::ValueEnum;
use serde::Deserialize;
use std::fmt;

/// 支持的链
#[derive(Debug, Clone, ValueEnum)]
pub enum Chain {
    Ethereum,
    Arbitrum,
    Optimism,
    Base,
    Polygon,
}

impl Chain {
    pub fn rpc_url(&self) -> &'static str {
        match self {
            Chain::Ethereum => "https://eth.llamarpc.com",
            Chain::Arbitrum => "https://arb1.arbitrum.io/rpc",
            Chain::Optimism => "https://mainnet.optimism.io",
            Chain::Base => "https://mainnet.base.org",
            Chain::Polygon => "https://polygon-rpc.com",
        }
    }

    pub fn chain_id(&self) -> u64 {
        match self {
            Chain::Ethereum => 1,
            Chain::Arbitrum => 42161,
            Chain::Optimism => 10,
            Chain::Base => 8453,
            Chain::Polygon => 137,
        }
    }

    pub fn explorer_url(&self) -> &'static str {
        match self {
            Chain::Ethereum => "https://etherscan.io",
            Chain::Arbitrum => "https://arbiscan.io",
            Chain::Optimism => "https://optimistic.etherscan.io",
            Chain::Base => "https://basescan.org",
            Chain::Polygon => "https://polygonscan.com",
        }
    }

    pub fn all() -> Vec<Chain> {
        vec![
            Chain::Ethereum,
            Chain::Arbitrum,
            Chain::Optimism,
            Chain::Base,
            Chain::Polygon,
        ]
    }
}

impl fmt::Display for Chain {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Chain::Ethereum => write!(f, "Ethereum"),
            Chain::Arbitrum => write!(f, "Arbitrum"),
            Chain::Optimism => write!(f, "Optimism"),
            Chain::Base => write!(f, "Base"),
            Chain::Polygon => write!(f, "Polygon"),
        }
    }
}

/// RPC 响应
#[derive(Deserialize)]
pub struct RpcResponse {
    pub jsonrpc: String,
    pub id: u64,
    pub result: Option<serde_json::Value>,
    pub error: Option<RpcError>,
}

#[derive(Deserialize, Debug)]
pub struct RpcError {
    pub code: i64,
    pub message: String,
}

/// 余额查询结果
#[derive(Debug)]
pub struct BalanceResult {
    pub chain: String,
    pub address: String,
    pub balance_wei: u128,
    pub balance_eth: f64,
    pub latency_ms: u128,
}

/// Etherscan API 响应
#[derive(Deserialize)]
pub struct EtherscanResponse<T> {
    pub status: String,
    pub message: String,
    pub result: T,
}

/// 交易信息
#[derive(Debug, Deserialize)]
pub struct TransactionInfo {
    #[serde(rename = "blockNumber")]
    pub block_number: String,
    #[serde(rename = "timeStamp")]
    pub timestamp: String,
    pub hash: String,
    pub from: String,
    pub to: String,
    pub value: String,
    #[serde(rename = "gasUsed")]
    pub gas_used: String,
    #[serde(rename = "isError")]
    pub is_error: String,
    #[serde(rename = "functionName")]
    pub function_name: String,
}

src/config.rs

use crate::types::Chain;
use std::env;

/// 应用配置
pub struct AppConfig {
    pub etherscan_api_key: String,
    pub default_rpc_url: String,
    pub timeout_secs: u64,
}

impl AppConfig {
    pub fn from_env() -> Self {
        Self {
            etherscan_api_key: env::var("ETHERSCAN_API_KEY")
                .unwrap_or_else(|_| "YourApiKeyToken".to_string()),
            default_rpc_url: env::var("ETH_RPC_URL")
                .unwrap_or_else(|_| Chain::Ethereum.rpc_url().to_string()),
            timeout_secs: env::var("TIMEOUT_SECS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(10),
        }
    }
}

src/client.rs

use crate::types::*;
use reqwest::Client;
use serde::Serialize;
use std::time::{Duration, Instant};

#[derive(Serialize)]
struct RpcRequest {
    jsonrpc: String,
    method: String,
    params: Vec<serde_json::Value>,
    id: u64,
}

/// 区块链 RPC 客户端
pub struct ChainClient {
    http: Client,
    pub etherscan_key: String,
}

impl ChainClient {
    pub fn new(timeout_secs: u64, etherscan_key: String) -> Self {
        let http = Client::builder()
            .timeout(Duration::from_secs(timeout_secs))
            .build()
            .expect("Failed to create HTTP client");

        Self { http, etherscan_key }
    }

    /// 通过 RPC 查询余额
    pub async fn get_balance(
        &self,
        rpc_url: &str,
        address: &str,
    ) -> Result<BalanceResult, Box<dyn std::error::Error + Send + Sync>> {
        let start = Instant::now();

        let request = RpcRequest {
            jsonrpc: "2.0".to_string(),
            method: "eth_getBalance".to_string(),
            params: vec![
                serde_json::Value::String(address.to_string()),
                serde_json::Value::String("latest".to_string()),
            ],
            id: 1,
        };

        let resp: RpcResponse = self.http.post(rpc_url).json(&request).send().await?.json().await?;

        let latency = start.elapsed().as_millis();

        if let Some(err) = resp.error {
            return Err(format!("RPC error: {}", err.message).into());
        }

        let hex = resp.result.ok_or("No result")?.as_str().ok_or("Not string")?.to_string();
        let balance_wei = u128::from_str_radix(hex.trim_start_matches("0x"), 16)?;
        let balance_eth = balance_wei as f64 / 1e18;

        Ok(BalanceResult {
            chain: String::new(),
            address: address.to_string(),
            balance_wei,
            balance_eth,
            latency_ms: latency,
        })
    }

    /// 通过 RPC 查询区块号
    pub async fn get_block_number(
        &self,
        rpc_url: &str,
    ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
        let request = RpcRequest {
            jsonrpc: "2.0".to_string(),
            method: "eth_blockNumber".to_string(),
            params: vec![],
            id: 1,
        };

        let resp: RpcResponse = self.http.post(rpc_url).json(&request).send().await?.json().await?;

        if let Some(err) = resp.error {
            return Err(format!("RPC error: {}", err.message).into());
        }

        let hex = resp.result.ok_or("No result")?.as_str().ok_or("Not string")?.to_string();
        Ok(u64::from_str_radix(hex.trim_start_matches("0x"), 16)?)
    }

    /// 查询交易列表 (Etherscan)
    pub async fn get_transactions(
        &self,
        address: &str,
        count: u32,
    ) -> Result<Vec<TransactionInfo>, Box<dyn std::error::Error + Send + Sync>> {
        let url = format!(
            "https://api.etherscan.io/api?module=account&action=txlist&address={}&startblock=0&endblock=99999999&page=1&offset={}&sort=desc&apikey={}",
            address, count, self.etherscan_key
        );

        let resp: EtherscanResponse<Vec<TransactionInfo>> =
            self.http.get(&url).send().await?.json().await?;

        if resp.status != "1" {
            return Err(format!("Etherscan error: {}", resp.message).into());
        }

        Ok(resp.result)
    }
}

src/utils.rs

/// 格式化 ETH 余额
pub fn format_eth(eth: f64) -> String {
    if eth >= 1000.0 {
        format!("{:.2} ETH", eth)
    } else if eth >= 1.0 {
        format!("{:.4} ETH", eth)
    } else if eth >= 0.001 {
        format!("{:.6} ETH", eth)
    } else {
        format!("{:.10} ETH", eth)
    }
}

/// 缩短地址显示
pub fn shorten_address(addr: &str) -> String {
    if addr.len() > 10 {
        format!("{}...{}", &addr[..6], &addr[addr.len() - 4..])
    } else {
        addr.to_string()
    }
}

/// 验证以太坊地址格式
pub fn validate_address(addr: &str) -> Result<(), String> {
    if !addr.starts_with("0x") {
        return Err("Address must start with 0x".to_string());
    }
    if addr.len() != 42 {
        return Err("Address must be 42 characters (0x + 40 hex)".to_string());
    }
    if !addr[2..].chars().all(|c| c.is_ascii_hexdigit()) {
        return Err("Address contains non-hex characters".to_string());
    }
    Ok(())
}

/// 格式化时间戳
pub fn format_timestamp(ts: &str) -> String {
    let secs: i64 = ts.parse().unwrap_or(0);
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    let diff = now - secs;

    if diff < 60 {
        format!("{}s ago", diff)
    } else if diff < 3600 {
        format!("{}m ago", diff / 60)
    } else if diff < 86400 {
        format!("{}h ago", diff / 3600)
    } else {
        format!("{}d ago", diff / 86400)
    }
}

src/commands/mod.rs

pub mod balance;
pub mod tx;
pub mod token;

src/commands/balance.rs

use crate::client::ChainClient;
use crate::types::Chain;
use crate::utils::{format_eth, shorten_address, validate_address};

pub async fn execute(client: &ChainClient, address: &str, chain: &Chain, all_chains: bool) {
    if let Err(e) = validate_address(address) {
        eprintln!("Invalid address: {}", e);
        return;
    }

    if all_chains {
        execute_all_chains(client, address).await;
    } else {
        execute_single_chain(client, address, chain).await;
    }
}

async fn execute_single_chain(client: &ChainClient, address: &str, chain: &Chain) {
    println!("Querying balance on {}...", chain);
    match client.get_balance(chain.rpc_url(), address).await {
        Ok(result) => {
            println!(
                "Address: {}\nChain:   {}\nBalance: {}\nLatency: {}ms",
                shorten_address(address),
                chain,
                format_eth(result.balance_eth),
                result.latency_ms
            );
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn execute_all_chains(client: &ChainClient, address: &str) {
    println!("Querying all chains for {}...\n", shorten_address(address));

    let chains = Chain::all();
    let mut handles = Vec::new();

    for chain in &chains {
        let rpc_url = chain.rpc_url().to_string();
        let addr = address.to_string();
        let chain_name = chain.to_string();
        let client_clone = ChainClient::new(10, client.etherscan_key.clone());

        handles.push(tokio::spawn(async move {
            let result = client_clone.get_balance(&rpc_url, &addr).await;
            (chain_name, result)
        }));
    }

    println!("{:<12} {:>15} {:>10}", "Chain", "Balance", "Latency");
    println!("{}", "-".repeat(40));

    let mut total = 0.0;
    for handle in handles {
        if let Ok((chain_name, result)) = handle.await {
            match result {
                Ok(r) => {
                    println!(
                        "{:<12} {:>15} {:>8}ms",
                        chain_name,
                        format_eth(r.balance_eth),
                        r.latency_ms
                    );
                    total += r.balance_eth;
                }
                Err(e) => {
                    println!("{:<12} {:>15} {:>10}", chain_name, "Error", e);
                }
            }
        }
    }

    println!("{}", "-".repeat(40));
    println!("{:<12} {:>15}", "Total", format_eth(total));
}

src/commands/tx.rs

use crate::client::ChainClient;
use crate::utils::{format_eth, format_timestamp, shorten_address, validate_address};

pub async fn execute(
    client: &ChainClient,
    address: &str,
    count: u32,
    outgoing_only: bool,
    incoming_only: bool,
) {
    if let Err(e) = validate_address(address) {
        eprintln!("Invalid address: {}", e);
        return;
    }

    println!(
        "Recent transactions for {} (last {}):\n",
        shorten_address(address),
        count
    );

    match client.get_transactions(address, count).await {
        Ok(txs) => {
            let filtered: Vec<_> = txs
                .iter()
                .filter(|tx| {
                    if outgoing_only {
                        tx.from.to_lowercase() == address.to_lowercase()
                    } else if incoming_only {
                        tx.to.to_lowercase() == address.to_lowercase()
                    } else {
                        true
                    }
                })
                .collect();

            if filtered.is_empty() {
                println!("  No transactions found.");
                return;
            }

            for (i, tx) in filtered.iter().enumerate() {
                let value_eth: f64 = tx.value.parse::<f64>().unwrap_or(0.0) / 1e18;
                let direction = if tx.from.to_lowercase() == address.to_lowercase() {
                    "OUT"
                } else {
                    "IN "
                };
                let status = if tx.is_error == "0" { "OK" } else { "FAIL" };

                let method = if tx.function_name.is_empty() {
                    "Transfer".to_string()
                } else {
                    tx.function_name.split('(').next().unwrap_or("Unknown").to_string()
                };

                println!(
                    "  {}. [{}][{}] {} | {} | {} | {}",
                    i + 1,
                    direction,
                    status,
                    format_eth(value_eth),
                    method,
                    shorten_address(&tx.hash),
                    format_timestamp(&tx.timestamp),
                );
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

src/commands/token.rs

use crate::utils::validate_address;

pub async fn execute(contract: &str, holder: &Option<String>) {
    if let Err(e) = validate_address(contract) {
        eprintln!("Invalid contract address: {}", e);
        return;
    }

    println!("Token info for contract: {}", contract);

    if let Some(holder_addr) = holder {
        if let Err(e) = validate_address(holder_addr) {
            eprintln!("Invalid holder address: {}", e);
            return;
        }
        println!("Checking balance for holder: {}", holder_addr);
    }

    // TODO: 实现 ERC20 合约查询
    println!("(Token query will be implemented in CLI v3)");
}

src/lib.rs

pub mod client;
pub mod commands;
pub mod config;
pub mod types;
pub mod utils;

src/main.rs

use clap::{Parser, Subcommand, Args};
use chain_cli::client::ChainClient;
use chain_cli::config::AppConfig;
use chain_cli::types::Chain;
use chain_cli::commands;

#[derive(Parser)]
#[command(name = "chain-cli", version = "0.2.0")]
#[command(about = "Blockchain query CLI tool")]
struct Cli {
    #[arg(short, long, global = true)]
    verbose: bool,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Query account balance
    Balance(BalanceArgs),
    /// Query transaction history
    Tx(TxArgs),
    /// Query token information
    Token(TokenArgs),
}

#[derive(Args)]
struct BalanceArgs {
    #[arg(short, long)]
    address: String,
    #[arg(short, long, value_enum, default_value_t = Chain::Ethereum)]
    chain: Chain,
    #[arg(long)]
    all_chains: bool,
}

#[derive(Args)]
struct TxArgs {
    #[arg(short, long)]
    address: String,
    #[arg(short, long, default_value = "10")]
    count: u32,
    #[arg(long)]
    outgoing_only: bool,
    #[arg(long)]
    incoming_only: bool,
}

#[derive(Args)]
struct TokenArgs {
    #[arg(short = 'c', long)]
    contract: String,
    #[arg(short, long)]
    holder: Option<String>,
}

#[tokio::main]
async fn main() {
    dotenv::dotenv().ok();
    let cli = Cli::parse();
    let config = AppConfig::from_env();
    let client = ChainClient::new(config.timeout_secs, config.etherscan_api_key);

    match cli.command {
        Commands::Balance(args) => {
            commands::balance::execute(&client, &args.address, &args.chain, args.all_chains).await;
        }
        Commands::Tx(args) => {
            commands::tx::execute(
                &client,
                &args.address,
                args.count,
                args.outgoing_only,
                args.incoming_only,
            ).await;
        }
        Commands::Token(args) => {
            commands::token::execute(&args.contract, &args.holder).await;
        }
    }
}

关键要点总结

模块系统心智模型

crate (lib.rs 或 main.rs)
├── mod config;         → config.rs
├── mod types;          → types.rs
├── mod client;         → client.rs
├── mod utils;          → utils.rs
└── mod commands;       → commands/mod.rs
    ├── mod balance;    → commands/balance.rs
    ├── mod tx;         → commands/tx.rs
    └── mod token;      → commands/token.rs

clap 核心概念

概念宏/属性说明
Parser#[derive(Parser)]CLI 根结构
Subcommand#[derive(Subcommand)]子命令枚举
Args#[derive(Args)]参数组
ValueEnum#[derive(ValueEnum)]枚举选项
#[arg(short, long)]短/长选项-a / --address
#[arg(env = "...")]环境变量可从环境变量读取
#[arg(global = true)]全局选项所有子命令共享

常见误区

  1. 模块声明遗漏: 每个文件都需要在父模块的 mod.rs 或父文件中用 mod xxx; 声明
  2. pub 遗漏: 跨模块调用的函数、结构体、字段都需要 pub
  3. clap 的 env 属性需要 feature: clap = { features = ["env"] }
  4. 子命令名称大小写: clap 自动将 PascalCase 转为 kebab-case(AllChains -> all-chains
  5. main.rs 引用 lib.rs: 使用 use crate_name::module,不是 use crate::module

面试关联

面试题本课关联
"如何组织大型 Rust 项目?"lib.rs + mod + 目录结构
"Rust 的可见性规则?"pub / pub(crate) / 默认私有
"如何设计 CLI 工具?"clap + 子命令 + 全局选项
"lib crate 和 bin crate 的区别?"lib.rs 是库,main.rs 是二进制

参考资源