I had a forty-line bash script that pruned old backups. It worked. It had worked for two years. So naturally I rewrote it in Rust, told myself it was for the experience, and now I'm going to be honest about whether that was a good idea.
The script took a directory and a retention count, listed the archives, kept the newest N, deleted the rest. The kind of thing bash is genuinely fine at, right up until you need to parse a date out of a filename or handle a path with a space in it, at which point bash becomes a series of small betrayals.
The Rust version is maybe ninety lines. structopt does the argument parsing, and the bit that won me over is that the help text and the parsing come from the same struct definition:
use structopt::StructOpt;
#[derive(StructOpt)]
#[structopt(name = "prune", about = "Keep the newest N backups, delete the rest")]
struct Opt {
/// Directory containing the backups
#[structopt(parse(from_os_str))]
dir: std::path::PathBuf,
/// How many to keep
#[structopt(short = "n", long = "keep", default_value = "7")]
keep: usize,
/// Show what would be deleted without deleting
#[structopt(long = "dry-run")]
dry_run: bool,
}
That --dry-run flag is the tell. In bash I never added one, because plumbing a "don't actually do it" mode through is just enough friction that I always meant to and never did. In Rust it was an if opt.dry_run { println!(...) } and I had it for free, so I built it, and it's caught me twice already.
the honest accounting
The compile times are the obvious cost. cargo build --release on this little thing is a few seconds, but the first build pulls in clap and friends and takes a while, and that's a tax bash simply doesn't charge. The binary is a couple of megabytes against a script you can read in one screen. And I cannot pipe a Rust binary into a quick edit on a remote box the way I can with a script; there's a build step between me and the running thing.
What I got back: it doesn't fall over on weird paths, it has a dry-run mode I actually use, the errors are real errors instead of bash's cheerful silence, and when I come back to it in a year the types will tell me what everything is. The filename-date parsing that would have been a fragile cut pipeline is a proper parse with a real error if it fails.
Was it worth it? For this script, on its own merits, no. Bash was fine and I've now got a build artefact to manage. But as a way to learn where Rust's small-CLI ergonomics actually sit, with structopt removing nearly all the boilerplate I'd dreaded, it was completely worth it, and the next CLI I write that's genuinely a bit fiddly, I'll reach for Rust without hesitating. The script didn't need rewriting. I needed the practice. Those are different questions, and it's worth being honest about which one you're answering.