5

I have this situation where there's a lot of files with similar names (but they all follow a pattern) in different subfolders

file1
file1 (Copy)
/folder1/file2.txt
/folder1/file2 (Copy).txt
/folder1/file3.png
/folder1/file3 (Copy).png

Each file is in the same folder of its copy and has the same extension, the difference is that it has (Copy) at the end of the name

I want to get all these files and delete the oldest one, then eventually rename the file from, for example, file1 (Copy) to file1 (that is, remove the (Copy) suffix) if it needs to be renamed.

I was thinking of using find and mv but I'm not sure how to tell it to move the most recent one.

  • 1
    what is the probability that (Copy) file would be the recent one? – RomanPerekhrest Nov 22 '17 at 10:27
  • 1
    the opposite of the probability that the non-copy has been edited. – Jasen Nov 22 '17 at 10:41
  • As @Jasen pointed out, files have been edited after being copied (in some cases the copy, in others the non-copy). So the probability is > 0% but < 100% if you need i might check and give you a more precise answer – valepu Nov 22 '17 at 10:55
4

Extended find + bash solution (also needs the GNU implementation of stat):

find . -type f -name "* (Copy).*" -exec bash -c 'p="${0%/*}"; bn="${0##*/}"; 
        main_bn="${bn/ (Copy)/}"; 
        if [ -f "$p/$main_bn" ]; then 
           t_copy_file=$(stat -c %Y "$0"); t_main_file=$(stat -c %Y "$p/$main_bn"); 
           if [[ $t_copy_file -gt $t_main_file ]]; then 
               mv "$0" "$p/$main_bn"; 
           else
               rm "$0"; 
           fi; 
        fi' {} \;

  • p="${0%/*}" - filepath/path with basename trimmed
  • bn="${0##*/}" - file's basename
  • main_bn="${bn/ (Copy)/}" - remove (Copy) substring from the basename to obtain the main/common basename
  • if [ -f "$p/$main_bn" ] - if the main/original file exists (and is found to be a regular file after symlink resolution)
    • t_copy_file=$(stat -c %Y "$0") - get last modification time of found "copy" file
    • t_main_file=$(stat -c %Y "$p/$main_bn") - get last modification time of original file
    • if [[ $t_copy_file -gt $t_main_file ]] - if the "copy" file is the recent one - move it to the original one (make it original) with mv "$0" "$p/$main_bn"
    • otherwise - the original file is the recent one, removing "copy" file with rm "$0"

Or a bit shorter with -nt file test operator ([ new­er­file –nt olderfile ] - check if newerfile was changed more recently than olderfile, or if newerfile exists and olderfile doesn't):

find . -type f -name "* (Copy).*" -exec bash -c 'p="${0%/*}"; bn="${0##*/}"; 
        main_bn="${bn/ (Copy)/}"; 
        if [ -f "$p/$main_bn" ]; then 
           if [ "$0" -nt "$p/$main_bn" ]; then 
               mv "$0" "$p/$main_bn"; 
           else
               rm "$0"; 
           fi; 
        fi' {} \;
  • See the [ file1 -nt file2 ] test operator to compare file modification time (available in most shells including bash). It also compares the mtime of the target of symlinks. See -L with GNU stat for that. – Stéphane Chazelas Nov 22 '17 at 11:09
  • You may want to change it to -exec bash -c 'for file do...' bash {} + to avoid running one bash instance per file, especially considering bash is one of the slowest shells to start. – Stéphane Chazelas Nov 22 '17 at 11:39
  • Thanks, I used the second solution you suggested and then checked files with meld, i can confirm it worked – valepu Nov 22 '17 at 12:33
  • Not for file in "$0"; do, just for file do (loop over the positional parameters ($1, $2... and leave $0 for what it's meant to be: the name of the inline script), there's no issue with newline). – Stéphane Chazelas Nov 22 '17 at 13:13
4

It may be easier with zsh:

setopt extendedglob # best in ~/.zshrc
for file (./**/?*" (Copy)"*(ND.)) {
  base=$file:h/${${file:t}/" (Copy)"}}
  [[ ! -f $base || -L $base ]] ||
    if [ $file -nt $base ]; then
      mv $file $base
    else
      rm -f $file
    fi
}

You might want to check that there's no file (Copy) (Copy).txt files first.

  • **/: any level of subdirectory
  • N: nullglob expand to nothing if there's no match instead of erroring out
  • D: include hidden files (Dot files) and descending into hidden directories.
  • .: only include regular files (no directory, fifo, device, symlink...)
  • $file:h: head of the file (directory part), like in csh
  • $file:t: tail (filename part)
  • ${var/pattern/replacement}, here with no replacement
  • [[ ! -f $base || -L $base ]] ||... skip non-regular files or symlinks (even if they point to regular files) as a safeguard.
  • [ $file -nt $base ]: returns true if $file was last modified after $base (or $base is not accessible, we should not happen following the check above).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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