Problem :
I have around 8 sub directories that have image files in them. I need to move 300 of around 6000 photos from those sub directories to one common directory. I have a file with the names, not the paths, of the files I need moved. How can I do this?
I have seen things close but nothing where I can read a name in a file, search for that name in the sub directories to get the path and then move that file to the common directory then move on to the next. I can’t find a way to make find read in a file. This will be on a MacOS computer but anything with Python or bash should work just fine. I’m horrible with coding and especially with multiple variables. I thought I could cat the file then somehow send it to find which then can send that directory path to the copy command but the find command is the killer for me.
Thanks for any help that works.
Solution :
In this case find
is not the best tool. With some effort one could craft a command that reads from a file and builds a find
command that checks several directories for several names; or multiple find
commands: one per name or even one per a member of Cartesian product of names and directories.
Still tests like -name
, -path
or -regex
(if supported) take patterns. I understand you want to supply exact names. Probably the names are safe, but maybe not. E.g. a name Awesome.Picture.*OMG*.jpg
can match more than this exact string if interpreted as a filename pattern or a regular expression. In general you need to sanitize each name, so after being interpreted it matches the original string only.
Working with exact names is easy in a shell. In case of 300 names and about 8 directories to check, their Cartesian product contains about 2400 pairs. Even a slow shell can check them in reasonable time. This will probably be negligible in comparison to how much time the actual copying will take.
These are my assumptions:
- The file with names specifies one filename per line.
- Each line in the file is terminated by a newline character that does not belong to the respective filename. Note this assumption applies to the last line as well (compare this answer).
- Each line specified in the file should be taken literally. Spaces (if any) belong to the filename; the same for quotes, backslashes etc.
- You want to check few exact directories, not their subdirectories (i.e. no recursive search).
This should do:
#!/bin/sh -
target="$1//."
shift
while IFS= read -r name; do
for dir do
source="$dir/$name"
test -f "$source" && </dev/tty cp -i -- "$source" "$target" && break
done
done
Save it as contraption
and make it executable (chmod +x contraption
). Use it like this:
</file/with/names ./contraption /target/dir /source/dir1 /source/dir2 /another/source
Specify more source directories if you want.
Notes:
- It was my intention to write portable code. I think the code is portable, although in general one may need to adjust the shebang because POSIX does not force
sh
to be in/bin/sh
; nor does it forceenv
to be in/usr/bin/env
(so#!/usr/bin/env sh
in theory is not better; in practice see this answer). IFS= read -r name
is explained here: UnderstandingIFS= read -r line
.- Because of
test -f
the code will not (try to) copy matching files that are not regular files. </dev/tty cp -i
is to protect files already existing in the destination directory. Standardcp -i
would only break things (because it would read from stdin which is the file). Note/dev/tty
is specified by POSIX.--
is explained here: What does--
(double-dash) mean?.sh -
is explained here: Why the-
in the#! /bin/sh -
shebang?- The last slash in
$target
(added attarget="$1//."
) ensures it’s a directory. The dot is here only because I wanted my code to be overly safe (see this answer, it explains both). If you specify the first positional parameter with a trailing slash already, it should not matter. I inject two slashes (not one) to get///.
(not//.
) in case you specify/
. The point is//.
may be troublesome. Note you will get//.
if you specify an empty string as the target (the solution is not to specify an empty string, it’s not a valid pathname). break
causes the script to move on to the next name if copying succeeds, without checking the current name in not-yet-checked directories. If you expect the directories to contain files with identical names, in the command line specify preferred source directories before less preferred source directories.