I have been using Rust for small internal tools for about a year. Not systems programming, not game engines — just the kind of CLI utilities you write for yourself: log parsers, file converters, data transformers.
Here is what I have learned about where Rust helps and where it genuinely hurts.
The basic setup
A minimal Rust CLI tool with argument parsing looks like this. I use clap for arguments because the derive macro makes it readable.
[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1"
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "logfilter", about = "Filter log files by level and pattern")]
struct Args {
/// Path to the log file
#[arg(short, long)]
file: String,
/// Minimum log level to include (debug, info, warn, error)
#[arg(short, long, default_value = "info")]
level: String,
/// Optional pattern to match against log messages
#[arg(short, long)]
pattern: Option<String>,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
run(&args)
}
The derive macro generates all the argument parsing, --help output, and error messages. This is genuinely ergonomic. You add a field and it just works.
Error handling with anyhow
For internal tools, I use anyhow everywhere. It lets you use ? freely without defining your own error types.
use std::fs::File;
use std::io::{BufRead, BufReader};
use anyhow::{Context, Result};
fn run(args: &Args) -> Result<()> {
let file = File::open(&args.file)
.with_context(|| format!("Failed to open log file: {}", args.file))?;
let reader = BufReader::new(file);
let min_level = parse_level(&args.level)?;
for (line_num, line) in reader.lines().enumerate() {
let line = line.with_context(|| format!("Failed to read line {}", line_num + 1))?;
let entry = parse_log_entry(&line);
if entry.level >= min_level {
if let Some(pattern) = &args.pattern {
if line.contains(pattern.as_str()) {
println!("{}", line);
}
} else {
println!("{}", line);
}
}
}
Ok(())
}
The .with_context() call attaches a human-readable message to the error. When something goes wrong, you get useful output instead of a cryptic error code.
Structuring the data model
Even for small tools, I define a struct for log entries. It makes the code self-documenting and eliminates string-parsing bugs.
#[derive(Debug, PartialEq, PartialOrd)]
enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Debug)]
struct LogEntry<'a> {
level: LogLevel,
message: &'a str,
timestamp: &'a str,
}
fn parse_log_entry(line: &str) -> LogEntry<'_> {
// Simple parsing for logs like: "2026-03-20T10:00:00Z [INFO] message"
let parts: Vec<&str> = line.splitn(3, ' ').collect();
let timestamp = parts.first().copied().unwrap_or("");
let level_str = parts.get(1).copied().unwrap_or("[INFO]");
let message = parts.get(2).copied().unwrap_or(line);
let level = match level_str {
"[DEBUG]" => LogLevel::Debug,
"[WARN]" => LogLevel::Warn,
"[ERROR]" => LogLevel::Error,
_ => LogLevel::Info,
};
LogEntry { level, message, timestamp }
}
fn parse_level(s: &str) -> anyhow::Result<LogLevel> {
match s.to_lowercase().as_str() {
"debug" => Ok(LogLevel::Debug),
"info" => Ok(LogLevel::Info),
"warn" => Ok(LogLevel::Warn),
"error" => Ok(LogLevel::Error),
other => anyhow::bail!("Unknown log level: {}", other),
}
}
The PartialOrd derive on LogLevel makes level comparison (entry.level >= min_level) just work, because the enum variants are ordered by declaration.
Where Rust genuinely hurts for this use case
The compile times are the main friction. A project with clap and anyhow takes 20–30 seconds for the first cargo build --release. Incremental builds are fast, but the initial build and CI times are real costs.
For a script you write once and run forever, this is fine. For something you are iterating on quickly, it is annoying.
The other friction: lifetimes. The LogEntry<'a> lifetime annotation above is necessary because I hold references into the original line string. For most tools, avoiding lifetimes by just using String (allocating) is the right call. The performance difference is irrelevant at this scale.
When I reach for Rust vs Python
I reach for Rust when:
- The tool processes large files where memory usage matters
- I want a single binary I can
scpto a server without worrying about Python versions - I want the compiler to prevent a category of runtime bugs before I ship it
I reach for Python when:
- I need the output in 20 minutes
- The logic involves a lot of string manipulation or regex
- I am going to run it twice and throw it away
Both are good answers. The honest rule: use Rust for the tools that will outlive the week you wrote them.