views:

367

answers:

5

HI, I'm completely new to Bash and StackOverflow.

I need to move a set of files (all contained in the same folder) to a target folder where files with the same name could already exist.

In case a specific file exists, I need to rename the file before moving it, by appending for example an incremental integer to the file name.

The extensions should be preserved (in other words, that appended incremental integer should go before the extension). The file names could contain dots in the middle.

Originally, I was thinking about comparing the two folders to have a list of the existing files (I did this with "comm"), but then I got a bit stuck. I think I'm just trying to do things in the most complicated possible way.

Any hint to do this in the "bash way"? It's OK if it is done in a script other than bash script.

+5  A: 

If you don't mind renaming the files that already exist, GNU mv has the --backup option:

mv --backup=numbered * /some/other/dir
Ignacio Vazquez-Abrams
Actually, I need to preserve the names of the files of the target dir. Plus, I should preserve the extensions. But thanks for this hint, it could be useful in other situations...
Katie
+2  A: 

As per OP, this can be Perl, not just bash. Here we go

NEW SOLUTION: (paying attention to extension)

~/junk/a1$ ls
f1.txt   f2.txt   f3.txt   z1       z2


~/junk/a1$ ls ../a2
f1.txt     f2.1.txt   f2.2.txt   f2.3.txt   f2.txt     z1

# I split the one-liner into multiple lines for readability
$ perl5.8 -e 
     '{use strict; use warnings; use File::Copy; use File::Basename; 
       my @files = glob("*"); # assume current directory
       foreach my $file (@files) {
           my $file_base2 = basename($file); 
           my ($file_base, $ext) = ($file_base2 =~ /(.+?)([.][^.]+$)?$/);
           my $new_file_base = "../a2/$file_base";
           my $new_file = $new_file_base . $ext; 
           my $counter = 1;
           while (-e $new_file) { 
               $new_file = "$new_file_base." . $counter++ . $ext;
           }
           copy($file, $new_file)
               || die "could not copy $file to $new_file: $!\n";
        } }'

~/junk/a1> ls ../a2
f1.1.txt f1.txt  f2.1.txt  f2.2.txt  f2.3.txt  f2.4.txt  f2.txt  f3.txt
z1         z1.1       z2

OLD SOLUTION: (not paying attention to extension)

~/junk/a1$ ls
f1   f2   f3

~/junk/a1$ ls ../a2
f1     f2     f2.1   f2.2   f2.3

# I split the one-liner into multiple lines for readability
$ perl5.8 -e 
     '{use strict; use warnings; use File::Copy; use File::Basename; 
       my @files = glob("*"); # assume current directory
       foreach my $file (@files) {
           my $file_base = basename($file); 
           my $new_file_base = "../a2/$file_base"; 
           my $new_file = $new_file_base; 
           my $counter = 1;
           while (-e $new_file) { $new_file = "$new_file_base." . $counter++; }
           copy($file,$new_file)
               || die "could not copy $file to $new_file: $!\n";
        } }'

~/junk/a1> ls ../a2
f1     f1.1   f2     f2.1   f2.2   f2.3   f2.4   f3
DVK
This is great (I don't care about perl or bash), but I should apply the new name by preserving the extension (obtaining something like foo_1.txt for foo.txt, instead of foo.text.1). I'm trying to understand how to do this...
Katie
@Katie - See the updates
DVK
@Katie - and good attitude about "don't care as long as it gets the job done" :)
DVK
@Katie - replace "copy()" call at the end with "move()" to move instead of copy
DVK
Don't know what to say. It seems to be just perfect. Thank you so much.
Katie
@Katie - You're welcome! and welcome to SO!
DVK
+4  A: 

Here is a Bash script:

source="/some/dir"
dest="/another/dir"
find "$source" -maxdepth 1 -type f -printf "%f\n" | while read -r file
do
    suffix=
    if [[ -a "$dest/$file" ]]
    then
        suffix=".new"
    fi
    # to make active, comment out the next line and uncomment the line below it
    echo 'mv' "\"$source/$file\"" "\"$dest/$file$suffix\""
    # mv "source/$file" "$dest/$file$suffix"
 done

The suffix is added blindly. If you have files named like "foo.new" in both directories then the result will be one file named "foo.new" and the second named "foo.new.new" which might look silly, but is correct in that it doesn't overwrite the file. However, if the destination already contains "foo.new.new" (and "foo.new" is in both source and destination), then "foo.new.new" will be overwritten).

You can change the if above to a loop in order to deal with that situation. This version also preserves extensions:

source="/some/dir"
dest="/another/dir"
find "$source" -maxdepth 1 -type f -printf "%f\n" | while read -r file
do
    suffix=
    count=
    ext=
    base="${file%.*}"
    if [[ $file =~ \. ]]
    then
        ext=".${file##*.}"
    fi
    while [[ -a "$dest/$base$suffix$count$ext" ]]
    do
        (( count+=1 ))
        suffix="."
    done
    # to make active, comment out the next line and uncomment the line below it
    echo 'mv' "\"$source/$file\"" "\"$dest/$file$suffix$count$ext\""
    # mv "$source/$file" "$dest/$file$suffix$count$ext"
done
Dennis Williamson
@Dennis - nice touch re: commenting out 'mv' :)
DVK
A: 

I feel bad for posting this without testing it. However it is late and I have work in the morning. My attempt would look something like this:

## copy files from src to dst    
## inserting ~XX into any name between base and extension
## where a name collision would occur
src="$1"
dst="$2"

case "$dst" in
    /*) :;;               # absolute dest is fine
    *)  dst=$(pwd)/$dst;; # relative needs to be fixed up
    esac

cd "$src"
find . -type f | while read x; do
    x=${x#./}           # trim off the ./
    t=$x;               # initial target
    d=$(dirname $x);    # relative directory
    b=$(basename $x);   # initial basename
    ext=${b%%.*};       # extension 
    b=${b##*.};         # basename with ext. stripped off
    let zz=0;           # initial numeric
    while [ -e  "$dst/$t" ]; do
        # target exists, so try constructing a new target name
        t="$d/$bb~$zz.$ext"
        let zz+=1;
    done
    echo mv "./$x" "$dst/$t"
done

Overall the strategy is to get each name from the source path, break it into parts, and, for any collision, iterate over names of the form "base~XX.extension" until we find one that doesn't collide.

Obviously I have prepended the mv command with an echo because I'm a coward. Remove that at your own (files') peril.

Jim Dennis
A: 

If you dont need incremental suffix, rsync can do the job:

rsync --archive --backup --suffix=.sic src/ dst

Update:

find/sed/sort is used to manage versioned backup files:

#!/bin/bash                                                                                                        

src="${1}"
dst="${2}"

if test ! -d "${src}" -o ! -d "${dst}" ;then
    echo Usage: $0 SRC_DIR DST_DIR >&2
    exit 1
fi

rsync --archive --backup "${src}/" "${dst}/"
new_name() {
    local dst=$1
    local prefix=$2
    local suffix=$3
    local max=$(find ${dst} -type f -regex  ".*${prefix}.[0-9]*.${suffix}\$" \
        | sed 's/.*\.\([0-9]*\)\..*/\1/'|sort -n|tail -n 1)
    let max++
    echo ${prefix}.${max}.${suffix}
}

# swap BACKUP-extension/real-extension                                                                             
for backup_file in $(find $dst -name "*~"); do
    file=${backup_file%~}
    prefix=${file%.*}
    suffix=${file##*.}
    suffix=${suffix%\~}
    mv ${backup_file} $(new_name $dst $prefix $suffix)
done
Jürgen Hötzel