Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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?

share|improve this question
    
unix.stackexchange.com/questions/27382/… if you're not averse to running rename in a loop. (You could reverse the list obtained in a loop if you want a single rename call I guess.) –  Mat Sep 22 '12 at 10:12
    
@Mat No problem, but I was hoping to do it in less than four lines of code (not counting ; trickery) –  l0b0 Sep 22 '12 at 10:26

4 Answers 4

up vote 2 down vote accepted

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

share|improve this answer
    
If you add a -- parameter at the end and tweak the regex it actually works for $'--$`!*@\a\b\E\f\r\t\v\\\'"\360\240\202\211-'{1,2,3,4,5,6,7,8,9,10,11}$'\n' –  l0b0 Sep 24 '12 at 14:55

This works for the specific case above:

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

But:

  1. it doesn't handle whitespace in filenames, and
  2. it would be much more complicated for arbitrary filenames.
share|improve this answer

Not a direct answer to your question as you're mentioning bash, but with zsh, you can specify the sort order and numerical sort with globbing qualifiers.

$ echo index-*.txt
index-12.txt index-1.txt index-2.txt index-4.txt
$ echo index-*.txt(n)
index-1.txt index-2.txt index-4.txt index-12.txt
$ echo index-*.txt(nOn)
index-12.txt index-4.txt index-2.txt index-1.txt

Or you can define your own sort order with a function and use index-*(o+that-function).

Also note that zsh has its own rename builtin function called zmv:

$ zmv -fvQ 'index-(<->).txt(n)' 'index-$(($1-1)).txt'
mv -- index-1.txt index-0.txt
mv -- index-2.txt index-1.txt
mv -- index-4.txt index-3.txt
mv -- index-12.txt index-11.txt

$ zmv -fvQ 'index-(<->).txt(nOn)' 'index-$(($1+1)).txt'
mv -- index-11.txt index-12.txt
mv -- index-3.txt index-4.txt
mv -- index-1.txt index-2.txt
mv -- index-0.txt index-1.txt
share|improve this answer
    
I'm looking for a Bash solution, as indicated by the tags. –  l0b0 Sep 24 '12 at 14:45

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 it is not robust in that we don'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 ;-)

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.