Bash – Auto Increment Filename

bashrenameshell-script

I have a need to collect some duplicate files and want to avoid name collisions. The problem is that this collection of files might be added to by another execution of my script before the files are cleaned out and just want to continue to increment the number.

I decided on a simple until loop to increment the number like so.

until [[ ! -f ${PWD}/DUPES/${num}-$1 ]]; do
num=$((num +1))
done
mv --no-clobber $1 ${PWD}/DUPES/${num}-$1

I can't imagine ever exceeding 1k files in an extreme example, so my questions are…

Is this a terribly inefficient way to achieve this?
Should I be parsing the existing files to get the highest leading number to start incrementing from there, or is this ok for the extreme example I put forth?
Or is there a better way all together?

Best Answer

You might do it like:

set -C; num=0                                  ### set -o noclobber; init $num
[ -e "$1" ] &&                                 ### this needs to be true
until 2>&3 >"./DUPES/$((num+=1))-$1" &&        ### try to open() num+=1
      mv --  "$1"  "./DUPES/$num-$1"           ### if opened, mv over it
do :; done 3>/dev/null                         ### do nothing

You would at once assure that multiple instances cannot secure the same name for any given file, and increment your variable.

The /dev/null < stderr just drops the shell's complaint about a file existing when it tries to do the output truncate/redirect and finds an existing target. While noclobber is enabled it won't overwrite another file - it will only open() a new one unless you use >|. And so you don't need its complaint because the whole point is to increment over existing files until a non-existing name is found.

Regarding the performance aspect - it would be better if you didn't start at zero. Or, if you tried to make up the difference. I guess the above might be improved somewhat like:

set -C; num=0                                  ### set -o noclobber; init $num
until 2>&3 >"./DUPES/$((num+=1))-$1"  &&       ### try to open() num+=1
      mv --  "$1"  "./DUPES/$num-$1"           ### if opened, mv over it
do    [ -e  "./DUPES/$((num*2))-$1" ] &&       ### halve fail distance
      num=$((num*2))                           ### up to a point of course
done  3>/dev/null                              ### done

...but up to 1000 you probably don't have to worry about it terribly. I've got to 65k over random names in a couple seconds.

By the way - you might think you could just:

>"./DUPES/$((num+=1))-$1" mv -- "$1" "./DUPES/$num-$1"

...but it doesn't work in a bash shell.

num=0; echo >"/tmp/$((num+=1))" >&2 "$num"; echo "$num" /tmp/[01]

0
1 /tmp/1

For whatever reason bash does the assignment in some other context for redirections - and so the expansions happen in a strange order. So you need a separate simple command to expand the correct $num value as I get here with &&. Otherwise, though:

num=0; echo "$((num+=1))" "$num"

1 1
Related Question