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