Reputation: 33
I have a list of filenames of .jpg files. I would like to move all images out of a given filetree except those in the list. I've been trying to use a find loop to do this, but it doesn't quite work:
x=$(find . -type f -not -name "$line")
while IFS= read -r line ; do
mv "$x" Destination/
done < Filenames.txt
Excerpt from Filenames.txt:
IMG_5253 (2).jpg
IMG_5255 (2).jpg
IMG_5256 (2).jpg
IMG_3988.JPG
IMG_3989.JPG
IMG_3990.JPG
I think I can see why this is problematic (I don't really want a loop, exactly), but I can't see how to fix it. What's a good way to do this?
Edit: Rsync worked for me, with help from @ghoti and this thread: https://unix.stackexchange.com/questions/76237/rsync-certain-files-excluding-the-rest-ignoring-svn-directory-recursively
rsync -rv --exclude-from '/Path/To/Filename.txt' --exclude='*Pattern/For/Other/Files/That/Should/Stay/Put*' --include='*.jpg' --include='*.JPG' --include='*/' --exclude="*" --remove-source-files Path/To/Source Path/To/Destination
I also had great luck with the Perl solution posted below, and it ran very fast by comparison to some of the other options. I marked rsync as my solution since that's what I used, but others coming after might want to look at @Tom_Fenench's solution too.
Upvotes: 3
Views: 156
Reputation: 20032
I think your filenames can have spaces but no newlines.
With newlines the layout of Filenames.txt can be problematic.
When all pictures are in one dir (assume images), you can do without find:
comm -23 <(ls -1 images) <(sort Filenames.txt) | while read file; do
mv images/"$file" Destination/
done
This solution is worse when you want to descent directories:
#!/bin/bash
# Only sort Filenames.txt once
tmpSorted=/tmp/sortedFiles
sort Filenames.txt > ${tmpSorted}
# Go through directories
find . -type d | while read dir; do
comm -23 <(ls -1 "${dir}" 2>/dev/null | sort) <(cat ${tmpSorted}) | while read file; do
# remove the echo when it looks ok
echo mv "${dir}"/"$file" Destination/
done
done
rm ${tmpSorted}
Upvotes: 0
Reputation: 792
I believe using find
in this case is too unwieldy. I would recommend the following:
#!/bin/sh
dest=/fully/qualified/destination
# run this script in the directory with all your .jpg files
for filename in *.jpg; do
if grep -q "$filename" Filenames.txt
then
#echo "Found file $filename in Filenames.txt, will do nothing"
else
#echo "Did not find file $filename in Filenames.txt, will move unwanted file"
mv "$filename" $dest
fi
done
I think your solution with find
is only useful if you have thousands and thousands of file names in the same directory to the point where the command line cannot expand all of the entries, which is a different problem altogether.
If you have this many files in on directory, then I would agree with Tom, you should use a more powerful language, or use find find
.
Upvotes: 0
Reputation: 46876
This isn't fast, but it should work.
find /startpath -name \*.jpg -not -exec grep -q '^{}$' filenames.txt \; -exec mv -v {} /target/path/
This will start in /startpath
, then drill through all subdirectories looking for files that end in .jpg
. For each one of these, it will run a grep
of your file, and if that grep is -not
successful, will print the filename as it moves it in a second -exec
.
Note that because of the way find
matches things, you may need to prepend filenames with paths, even if those paths are ./
.
Note also that this is NOT a speed- or IO-optimized solution, as it launches a separate grep
command for each file it finds.
Alternately, if you want to use rsync
, you could do so with something like this:
rsync -a -v --exclude-from=/path/to/list.txt --remove-source-files /sourcedir/ /targetdir/
Upvotes: 2
Reputation: 74695
Here's one way that you could solve your problem using Perl:
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
use File::Find;
use File::Copy;
my %files;
open my $fh, "<", "Filenames.txt";
while (<$fh>) {
chomp;
$files{$_}++;
}
sub wanted {
if (-f && /\.jpg$/ && !exists $files{$_}) {
move($_, "/path/to/destination");
}
}
find(\&wanted, ".");
It reads the file names from Filenames.txt
and uses them to build a hash %files
. It then searches recursively in the current directory, executing the subroutine wanted
for everything it finds. It moves every file that ends in .jpg
and isn't found in the hash into the destination directory path/to/destination
(it is assumed that this directory already exists).
It's important to make sure that the destination is outside of the tree, otherwise it will interfere with the search.
Upvotes: 1
Reputation: 12625
Updated answer based on @Sorpigal comment below:
while IFS= read -d '' -r file ; do
mv "$file" Destination/
done < <(find . -type f -print0 | grep -ZzFfv Filenames.txt)
Upvotes: 0