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
tempfileas 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
| Purpose | Crate |
|---|---|
| CLI parsing | clap with derive |
| Error handling (app) | anyhow |
| Error handling (lib) | thiserror |
| Serialization | serde, serde_json, serde_yaml |
| Parallelism | rayon |
| Async runtime | tokio |
| HTTP client | reqwest |
| Hashing | blake3, sha1 |
| File patterns | glob |
| Regex | regex |
| Date/time | chrono |
| Temp files | tempfile |
| Home directory | dirs |
| Terminal UI | ratatui, crossterm |
| SQLite | rusqlite |
| Colored output | colored |
| Progress bars | progress_bar |
| Memory mapping | memmap2 |
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
- •
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
- •
- •
Keep type annotations in closures - they add clarity
- •
Don't over-engineer for hypothetical scale - optimize for actual use cases
- •
Avoid unnecessary abstractions - three similar lines is better than a premature abstraction