Rename the Way You Mean To

Posted on Mon 22 December 2025 in tech

I rename a lot of files.

I respect the Unix way — mv file1 file2 — but in practice, file1 and file2 are usually the same name failing by one to three characters. Re-typing 97% of a filename feels ceremonial rather than intentional, especially when what I want is something like:

mv server_log.log server_log.bak

That friction is persistent throughout your work day.

I created a small rust utility to make renaming mean what it looks like it means.


The Idea: Renaming as Expansion

Instead of explicitly naming the source file, the tool infers it from the destination name using one constrained rule:

The destination must be a strict expansion of the source.

In other words, the new filename must contain the old filename intact, with extra characters added somewhere in the middle or at the edges — but never altered.

This gives us two important properties:

  • Intentionality: you only specify what changed
  • Determinism: there is either exactly one valid source, or the command fails

So instead of writing:

mv server_log.log server_log.bak

You write:

rn server_log.bak

And the tool figures out the rest — or refuses to guess.


The Core Algorithm: A Two-Pointer “Vice”

At the heart of the tool is a simple string-matching rule implemented as a two-pointer squeeze from both ends. Think of it as a vice that compresses inward until it proves the old name is fully contained in the new one.

Here is the core function:

pub fn matches_expansion(old: &str, new: &str) -> bool {
    let o: Vec<char> = old.chars().collect();
    let n: Vec<char> = new.chars().collect();

    // consume common prefix
    let mut i1 = 0;
    let mut i2 = 0;
    while i1 < o.len() && i2 < n.len() && o[i1] == n[i2] {
        i1 += 1;
        i2 += 1;
    }

    // consume common suffix without crossing prefix
    let mut j1 = o.len();
    let mut j2 = n.len();
    while j1 > i1 && j2 > i2 && o[j1 - 1] == n[j2 - 1] {
        j1 -= 1;
        j2 -= 1;
    }

    // valid iff the old name is fully consumed
    i1 == j1 && i1 > 0
}

What matters is the invariant at the end:

  • If the “vice” can fully consume the old name → it’s a valid expansion
  • If it can’t → it’s not a match
  • If multiple files match → the tool errors and exits

No ambiguity.


Why This Matters

For a rename tool, correctness isn’t optional — it’s the entire product.

This approach guarantees:

  • No silent ambiguity
  • No accidental mass renames
  • No guessing when intent is unclear

If the rename is obvious, it works.
If it isn’t, it fails.

That’s the behavior I want from my work environment.

In a small way, this fixes a long-standing Unix papercut: renaming files should express change, not repetition.


Project Links

Rename the way you mean to.

rn server_log.bak

rustlang #cli #algorithms #dx