Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

a little cli in rust, and whether the borrow checker earned its keep

Rewriting a small log-tidying CLI in Rust to find out whether the language pays off at toy scale, not just at Dropbox scale.

A terminal showing Rust compiler output

I had a scrappy little tool that walked a directory of log files, parsed timestamps out of the filenames, and deleted anything older than a retention window. It was a forty-line shell script that had quietly grown to two hundred lines, a case statement I no longer trusted, and one genuinely terrifying find -delete. So I rewrote it in Rust. The question I actually wanted answered: at this size, is Rust worth the ceremony, or am I just enjoying the compiler being strict at me?

Short version: it was worth it, but not for the reasons people usually sell.

The thing Rust got right for me here was not speed. The script was fast enough; nobody was waiting on it. It was that the type system forced me to be honest about the cases I'd been ignoring. A filename that didn't parse. A retention window of zero. A path that was a symlink pointing somewhere it shouldn't. In the shell version these were all silently "well, hope for the best". In Rust they were a Result I had to actually handle, or at least decide to unwrap and own the consequence.

fn parse_stamp(name: &str) -> Option<NaiveDate> {
    let stem = name.strip_suffix(".log")?;
    let date = stem.rsplit_once('-').map(|(_, d)| d)?;
    NaiveDate::parse_from_str(date, "%Y%m%d").ok()
}

That ? chain is the whole pitch, really. Every step that can fail says so, and the function returns None instead of pretending. With clap for argument parsing and chrono for the dates, the actual logic came out smaller than the shell version, and I could read it back six months later without flinching.

A close-up of source code on screen

The costs were real though, and I want to be fair about them. Compile times for a clean build with a few dependencies are not zero; the first cargo build after pulling in clap and chrono made me go and put the kettle on. The binary is a few megabytes where the script was a few hundred bytes. And there is genuinely a tax to be paid before the program runs at all: the borrow checker wanted a word about a String I was passing around by value when a &str would do, and it was right, but it was also a ten minute detour for a tool I'd have finished in the shell already.

So the honest answer to "was it worth it" depends entirely on lifespan. For a thing I'll run once and throw away, no, the shell wins, every time, by miles. For a thing that deletes files on a schedule, unattended, that I want to still trust in a year, the Rust version is the one I sleep better next to. The compiler did the code review I'd have skipped.

I'm not going to rewrite all my shell scripts in Rust. Most of them deserve to stay forty-line shell scripts. But the ones with teeth, the ones that delete or move or charge money, those are exactly where I want a language that refuses to let me be vague. This was a small CLI and a small win, but it was a win, and the kettle was already on anyway.