AgentSkillsCN

Code Like Hughdbrown Rust

像 Hughdbrown 的 Rust 代码风格

SKILL.md

SKILL.md: Code Like hughdbrown (Rust)

This document captures the coding patterns, practices, and stylistic preferences observed across hughdbrown's Rust projects.

Project Structure

Cargo.toml

  • Use Rust 2024 edition for all new projects
  • Include author email: authors = ["hughdbrown@gmail.com"] or ["Hugh Brown"]
  • Add metadata: description, license = "MIT", repository, keywords, categories
  • Group dependencies with comments explaining purpose:
toml
[dependencies]
# CLI parsing
clap = { version = "4", features = ["derive"] }

# Error handling
anyhow = "1.0"
thiserror = "1.0"

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
  • Always include release profile optimizations:
toml
[profile.release]
lto = true
codegen-units = 1
opt-level = "z"        # Optimize for size
panic = "abort"        # Don't include unwinding code
strip = true           # Strip symbols from binary
  • Use tempfile as a dev-dependency for filesystem tests:
toml
[dev-dependencies]
tempfile = "3"

File Organization

  • main.rs: Binary entry point, minimal logic
  • lib.rs: Library root, re-exports modules
  • Modules by concern: cli.rs, error.rs, file.rs, output.rs, types.rs
  • Module declarations in lib.rs:
rust
//! Project description in module doc comment.

pub mod cli;
pub mod error;
pub mod file;
pub mod output;
pub mod types;

CLI Argument Parsing

Use clap with derive macros exclusively. Never use builder pattern.

Standard CLI Structure

rust
use clap::{Parser, Subcommand, ValueEnum};

/// A tool description in doc comment.
#[derive(Parser, Debug)]
#[command(name = "tool-name")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(author = "Hugh Brown <hughdbrown@gmail.com>")]
#[command(about = "Short description")]
struct Args {
    /// Argument description.
    #[arg(short, long, default_value = "default")]
    option: String,

    /// Flag description.
    #[arg(short, long)]
    verbose: bool,

    /// Numeric with default.
    #[arg(short, long, default_value_t = 100)]
    count: usize,

    #[command(subcommand)]
    command: Option<Commands>,
}

Custom Value Parsers for Validation

rust
/// Parse and validate min_match value (must be at least 1).
fn min_match_parser(s: &str) -> Result<usize, String> {
    let value: usize = s
        .parse()
        .map_err(|_| format!("'{s}' is not a valid number"))?;
    if value == 0 {
        Err("minimum match must be at least 1".to_string())
    } else {
        Ok(value)
    }
}

#[arg(short = 'm', long = "match", default_value = "5", value_parser = min_match_parser)]
pub min_match: usize,

Output Format Enum Pattern

rust
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum OutputFormat {
    #[default]
    Text,
    Json,
}

Error Handling

Application-Level: Use anyhow

rust
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let args = Args::parse();
    run(args)
}

fn run(args: Args) -> Result<()> {
    let path = resolve_path(args.path)
        .context("Failed to resolve path")?;

    let content = std::fs::read_to_string(&path)
        .with_context(|| format!("Failed to read file: {}", path.display()))?;

    if content.is_empty() {
        anyhow::bail!("File is empty: {}", path.display());
    }

    Ok(())
}

Library-Level: Use thiserror

rust
use std::path::PathBuf;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Failed to read file '{path}': {source}")]
    FileRead {
        path: PathBuf,
        source: std::io::Error,
    },

    #[error("No files found matching the specified patterns")]
    NoFilesFound,

    #[error("Invalid glob pattern '{pattern}': {message}")]
    InvalidGlob { pattern: String, message: String },
}

pub type Result<T> = std::result::Result<T, AppError>;

Simple Error Enums (when thiserror is overkill)

rust
#[derive(Debug)]
pub enum BrowserHistError {
    Sql(rusqlite::Error),
    Io(std::io::Error),
}

impl From<std::io::Error> for BrowserHistError {
    fn from(err: std::io::Error) -> Self {
        BrowserHistError::Io(err)
    }
}

impl From<rusqlite::Error> for BrowserHistError {
    fn from(err: rusqlite::Error) -> Self {
        BrowserHistError::Sql(err)
    }
}

Box<dyn Error> Pattern

rust
pub type MyResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;

Main Function Patterns

Pattern 1: main() with run() returning Result

rust
fn main() -> Result<()> {
    let args = Args::parse();
    run(args)
}

fn run(args: Args) -> Result<()> {
    // Application logic here
    Ok(())
}

Pattern 2: main() with explicit error handling

rust
fn main() {
    let args = Args::parse();

    if let Err(e) = run(&args) {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    }
}

Pattern 3: ExitCode for semantic exit status

rust
use std::process::ExitCode;

fn main() -> ExitCode {
    let args = Args::parse_args();

    match run(&args) {
        Ok(has_issues) => {
            if has_issues {
                ExitCode::from(1)
            } else {
                ExitCode::SUCCESS
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            ExitCode::from(2)
        }
    }
}

Async Main

rust
#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();
    run(args).await
}

Type Annotations

Prefer explicit type annotations for clarity, even when inference would work.

Explicit Types on Let Bindings

rust
let file_stems: Vec<_> = files.iter()
    .map(|path| path.file_stem().unwrap_or_default().to_string())
    .collect();

let matches: ArgMatches = get_matches();
let query: String = build_query(&matches);
let conn: Connection = Connection::open(path)?;
let rows: Vec<Row> = get_rows(&conn, &query)?;

Explicit Types on Closures

rust
.filter(|s: &&String| !s.is_empty())
.map(|path: &PathBuf| -> MyResult<String> {
    process(path)
})
.filter_map(|(idx, pb): (usize, &PathBuf)| {
    if condition { Some(pb.clone()) } else { None }
})

Turbofish for Collect

rust
.collect::<Vec<_>>()
.collect::<Result<Vec<_>, _>>()

Import Style

Group and Order Imports

rust
// 1. Standard library
use std::{
    collections::{BinaryHeap, HashMap},
    error::Error,
    path::{Path, PathBuf},
};

// 2. External crates
use anyhow::{Context, Result};
use clap::Parser;
use rayon::prelude::*;
use serde::Deserialize;

// 3. Internal modules
use crate::cli::Args;
use crate::error::AppError;
use crate::types::FileDescription;

Block-Style for Multiple Items from Same Module

rust
use clap::{
    Arg,
    ArgMatches,
    Command,
};

use rusqlite::{
    Connection,
    OpenFlags,
};

Parallelism

Rayon for Data Parallelism

rust
use rayon::prelude::*;

// Parallel iteration
let results: Vec<_> = files
    .par_iter()
    .filter_map(|path| process(path).ok())
    .collect();

// With adaptive chunk size
let chunk_size = std::cmp::max(
    1,
    files.len() / rayon::current_num_threads().max(1)
);

let result: Result<Vec<String>, _> = files.par_iter()
    .with_min_len(chunk_size)
    .map(|path| process(path))
    .collect();

Tokio for Async

rust
use std::sync::Arc;
use tokio::task::JoinSet;

let stats = Arc::new(Statistics::new());
let mut join_set = JoinSet::new();

for i in 0..thread_count {
    let stats_clone = Arc::clone(&stats);
    let url_clone = url.clone();

    join_set.spawn(async move {
        run_task(url_clone, stats_clone).await;
    });
}

while let Some(_) = join_set.join_next().await {}

Atomic Counters

rust
use std::sync::atomic::{AtomicU32, Ordering};

struct Statistics {
    success_count: AtomicU32,
    failure_count: AtomicU32,
}

impl Statistics {
    fn new() -> Self {
        Self {
            success_count: AtomicU32::new(0),
            failure_count: AtomicU32::new(0),
        }
    }

    fn record_success(&self) {
        self.success_count.fetch_add(1, Ordering::Relaxed);
    }

    fn get_counts(&self) -> (u32, u32) {
        (
            self.success_count.load(Ordering::Relaxed),
            self.failure_count.load(Ordering::Relaxed),
        )
    }
}

File I/O Patterns

Buffered Reading

rust
use std::fs::File;
use std::io::{BufRead, BufReader, Read};

let file = File::open(path)?;
let reader = BufReader::new(file);

for line in reader.lines() {
    let line = line?;
    // process line
}

File Hashing with BLAKE3

rust
use blake3;

pub fn file_hash(path: &Path) -> Result<String, io::Error> {
    const SMALL_FILE_THRESHOLD: u64 = 1_000_000; // 1MB
    const BUFFER_SIZE: usize = 64 * 1024; // 64KB

    let file = File::open(path)?;
    let file_size = file.metadata()?.len();

    if file_size < SMALL_FILE_THRESHOLD {
        // Buffered reading for small files
        let mut hasher = blake3::Hasher::new();
        let mut reader = BufReader::with_capacity(BUFFER_SIZE, file);
        let mut buffer = [0; BUFFER_SIZE];

        loop {
            let bytes_read = reader.read(&mut buffer)?;
            if bytes_read == 0 { break; }
            hasher.update(&buffer[..bytes_read]);
        }

        Ok(hasher.finalize().to_hex().to_string())
    } else {
        // Memory-mapped for large files
        let mmap = unsafe { memmap2::Mmap::map(&file)? };
        let hash = blake3::Hasher::new()
            .update_rayon(&mmap)
            .finalize();
        Ok(hash.to_hex().to_string())
    }
}

Temporary Files

rust
use tempfile::{NamedTempFile, TempDir};

// Single temp file
let temp_file = NamedTempFile::new()?;
fs::copy(source_path, temp_file.path())?;

// Temp directory for tests
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test.txt");

Testing

Test Module at Bottom of File

rust
#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::TempDir;

    #[test]
    fn test_function_name() {
        let temp_dir = TempDir::new().unwrap();
        // test logic
        assert!(result.is_ok());
    }
}

Helper Functions for Test Setup

rust
#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    fn create_temp_file(dir: &Path, name: &str, content: &[u8]) -> PathBuf {
        let path = dir.join(name);
        File::create(&path)
            .expect("Failed to create file")
            .write_all(content)
            .expect("Failed to write");
        path
    }

    #[test]
    fn test_with_files() {
        let temp_dir = TempDir::new().expect("Failed to create temp dir");
        let file1 = create_temp_file(temp_dir.path(), "file1.txt", b"content");
        // test logic
    }
}

Test Naming Conventions

rust
#[test]
fn test_validate_args_valid() { }

#[test]
fn test_validate_args_missing_dot() { }

#[test]
fn test_validate_args_nonexistent_dir() { }

#[test]
fn test_hash_file_identical_content() { }

#[test]
fn test_group_by_hash_no_duplicates() { }

Assertions with Context

rust
assert!(result.is_ok());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("must start with a dot"));
assert_eq!(groups.len(), 1);
assert!(rm_count >= 2, "Expected at least 2 rm commands, got {}", rm_count);

Common Patterns

Path Validation

rust
fn validate_path(path: &Path) -> Result<()> {
    if !path.exists() {
        anyhow::bail!("Path does not exist: {}", path.display());
    }
    if !path.is_dir() {
        anyhow::bail!("Path is not a directory: {}", path.display());
    }
    Ok(())
}

Home Directory Expansion

rust
use dirs;

fn expand_tilde(path: &str) -> PathBuf {
    if let Some(stripped) = path.strip_prefix("~/") {
        if let Some(home) = dirs::home_dir() {
            return home.join(stripped);
        }
    }
    PathBuf::from(path)
}

File Discovery with Glob

rust
use glob::glob;

pub fn files_matching_pattern(dir: &str, pattern: &str) -> Result<Vec<PathBuf>> {
    let glob_pattern = format!("{}/{}", dir.trim_end_matches('/'), pattern);
    let paths = glob(&glob_pattern)
        .map_err(|e| format!("Invalid glob pattern '{}': {}", glob_pattern, e))?
        .flatten()
        .collect();
    Ok(paths)
}

Binary File Detection

rust
const BINARY_CHECK_SIZE: usize = 8192;

pub fn is_binary_file(path: &Path) -> Result<bool> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);
    let mut buffer = [0u8; BINARY_CHECK_SIZE];

    let bytes_read = reader.read(&mut buffer)?;
    Ok(buffer[..bytes_read].contains(&0))
}

Preferred Crates

PurposeCrate
CLI parsingclap with derive
Error handling (app)anyhow
Error handling (lib)thiserror
Serializationserde, serde_json, serde_yaml
Parallelismrayon
Async runtimetokio
HTTP clientreqwest
Hashingblake3, sha1
File patternsglob
Regexregex
Date/timechrono
Temp filestempfile
Home directorydirs
Terminal UIratatui, crossterm
SQLiterusqlite
Colored outputcolored
Progress barsprogress_bar
Memory mappingmemmap2

Code Style

Method Chaining

rust
let result: Vec<_> = items
    .iter()
    .filter(|x| condition(x))
    .map(|x| transform(x))
    .collect();

Match with Early Return

rust
match result {
    Ok(value) => value,
    Err(e) => {
        eprintln!("Error: {}", e);
        return ExitCode::from(1);
    }
}

if let for Optional Processing

rust
if let Some(dir) = directory {
    let discovered = scan_directory(dir)?;
    files.extend(discovered);
}

Option Chaining

rust
path.file_stem()
    .and_then(|s| s.to_str())
    .unwrap_or("")
    .to_string()

Documentation

Module-Level Doc Comments

rust
//! samesame - A tool to identify repeated fragments of code across multiple files.

Function-Level Doc Comments (for public APIs)

rust
/// Read a file and create a FileDescription.
///
/// Lines are stored with original indentation intact.
/// Hashes are computed from trimmed lines for comparison.
/// Non-UTF-8 bytes are handled with lossy conversion.
pub fn read_file(path: &Path) -> Result<FileDescription> {

Internal Documentation with #[doc(hidden)]

rust
/// Internal helper function.
#[doc(hidden)]
pub fn hash_line(line: &str) -> u64 {

Things to Avoid

  1. Don't change these established patterns without good reason:

    • build_global() single-call pattern
    • O(n^2) lookups when n is typically < 10
    • Unsafe memory access for stable file reads during hashing
  2. Keep type annotations in closures - they add clarity

  3. Don't over-engineer for hypothetical scale - optimize for actual use cases

  4. Avoid unnecessary abstractions - three similar lines is better than a premature abstraction