Bash – How to shift filename numbers without collisions

bashrenamesort

Consider this list of files:

$ touch index-{1,2,3,4,5,6,7,8,9}.txt

If I want to shift them down so they start at zero, it's relatively easy:

$ rename --verbose 's/^index-([1-9])\.txt$/$1/; $_="index-" . ($_ - 1) . ".txt"' index-*.txt

This works because man bash specifies that globs are alphabetically sorted, so it will rename index-1.txt to index-0.txt before renaming index-2.txt to index-1.txt.

This breaks down if you want to shift up, or if the numbers have different lengths:

$ touch index-{10,11}.txt
$ rename --verbose 's/^index-([0-9]+)\.txt$/$1/; $_="index-" . ($_ + 1) . ".txt"' index-*.txt
index-10.txt not renamed: index-11.txt already exists
index-11.txt renamed as index-12.txt
index-1.txt not renamed: index-2.txt already exists

Possible long-term fixes:

  • A rename option to try to reorder operations until there are no collisions.
  • A rename option to move the files to a temporary directory first, and then move them back with the new name.
  • A rename option to rename files in two steps, using unique names in the first step which won't collide with the original or new names.
  • A way to do natural sorting + reversal of Bash globs.

Is there a simple way to get around this?

Best Answer

@l0b0's solution rewritten for better robustness:

printf '%s\0' index-*.txt |
  sort --zero-terminated --field-separator - --key 2rn |
  xargs -0r rename --verbose '
    s/^index-([0-9]+)\.txt$/$1/;
    $_="index-" . ($_ + 1) . ".txt"'

Feel free to include in your solution and I'll delete my answer afterward.

Note that that and @l0bo's solutions are GNU specific, not Unix (GNU's Not Unix).

Related Question