How to shift filename numbers without collisions?

There's a little known standard command on Unix that can help us here find a general solution in finding in which order a series of renames have to be done: tsort

Say we have a list of renames to be done in a file called renames.txt (assuming for sake of demonstration that their name doesn't contain blanks):

d a
e f
b e
a c

Because d is to be renamed to a, that means a must be renamed before d. So we've got a partial sort order there which would be the reverse of the order the files should be renamed.

tsort is the tool to infer a full sort order from a list of partial sort orders. It would return with an error if there was a loop which would help us detect cases where there's no solution. If we apply tsort on that input, it gives us:

b
d
e
a
f
c

Which says b should be renamed after d after e. We can use GNU tac (some systems also have tail -r) to reverse that order:

c
f
a
e
d
b

And join it with our list of renames:

tsort renames.txt | tac | awk '
  NR==FNR {
    ren[$1] = $2
    next
  }
  $1 in ren {
    print "mv -i --", $1, ren[$1]
  }' renames.txt -

which gives us:

mv -i -- a c
mv -i -- e f
mv -i -- d a
mv -i -- b e

which we can then pipe to sh to execute.

Note however that the above code is not robust in that we didn't check the exit status of tsort above to detect loops, and filenames mustn't contain any special shell characters.

The robustification is left as an exercise to the reader ;-)


@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).

Tags:

Bash

Rename

Sort