I wish to add an exec command to move directories that have said files somewhere else. Is that at all possible?
Yes. I assume by "exec" you mean -exec in find. This is my basic solution. Read the whole answer before you try it.
find . -type f -name '*.[mM][kK][vV]' -links 1 -exec sh -c ' exec mv -- "${1%/*}" /somewhere/else/.' find-sh {} \; -printNotes:
-name '*.[mM][kK][vV]'is a POSIX equivalent* of-iname '*.mkv'. Use the latter if yourfindsupports-iname.* Well, almost. The example of Turkish locale where uppercase
iisİand the lowercaseIisıshows us it's not that simple. In a locale that does something similar tom,M,k,K,vorV, or so, the two tests are not equivalent. I do not know if such locale exists, but in general it may.${1%/*}removes the last pathname component, it's likedirname, but without using the external tool. Unlikedirnamethis will not take care of edge cases, but since ourfindstarts from.and our earlier tests do not match., all pathnames shall give us the expected result when truncated in this simple way.And to be clear: if a matching mkv file is found somewhere deep then our
mvwill act on the direct parent directory of the file.The resulting pathnames will never be considered options, so we don't really need the double dash. I put it there as a good practice.
/somewhere/else/.(instead of/somewhere/else) as the target formvis a trick that makes the command fail whenelsedoes not exist. The problem we avoid is if we move a directory namedfooto/somewhere/elseandelsedoes not yet exists thenfoowill becomeelseinside/somewhere, instead of becoming/somewhere/else/foo. Then moving the next directorybarfill findelseexisting andbarwill become/somewhere/else/bar. This in turn is expected, the original problem is no more; but the content of (former)foois now directly under/somewhere/else, not in its own subdirectory.Moving something to
/somewhere/else/.will simply fail ifelsedoes not exist.-printwill be performed if and only ifmvsucceeds.find-shis explained here: What is the second sh in `sh -c 'some shell code' sh`?Our
find -execwill move directories from "under"find's own directory traversing algorithm. Expect warnings whenfindtries to proceed to the next file in a directory but the directory has just been moved; or maybe when it tries to descend to a subdirectory no longer there. There should be no serious consequences though.If there is a matching mkv in a directory and another mkv somewhere deep under the same directory then they will be moved along with the whole directory when
findconsiders the first file. The tool will never get to the second one.Do not try to optimize our
-exec … \;by converting to-exec …+. In general there may be many mkv files in a directory and in subdirectories. To gather as many pathnames as possible for-exec …+,findwill test files and descend directories it wouldn't even see if itmved directories right away. Then you would need to truncate many pathnames and pass them to a singlemv(as separatemvs would make the optimization close to pointless), and yet in general some of them will be there in vain (as duplicates or subdirectories of earlier pathnames)Piping the pathnames to
sort -uthen toxargsthat runs a shell (and ultimatelymv) is an idea to avoid passing duplicates tomv, it will not preventfindfrom doing all the extra work in the first place. And it would be against your wish to "add an exec command".Using
-pruneon the parent directory of a matching mkv file is impossible because when ourfindfinds the file, it has processed the parent directory and it's too late to-pruneit.In my opinion it's good to just let
findmove directories from "under" its own traversing algorithm as soon as possible, i.e. with-exec … \;. KISS.If there is a matching mkv directly under
.then our command will try to move.andmvwill fail (at least in my Debianmv . …fails).If the name of the directory we are trying to
mvis already taken insideelsethenmvwill fail. This can happen ifelseis initially not empty; but also on the fly, since ourfindcan move directories from different directories (even from different depths) and the directories to move may have identical names. Unless…… unless you want to consider directories directly under
.and no subdirectories. It seems indeed you do. If yourfindsupports-mindepthand-maxdepthlike GNUfind, use them; but keep in mind we operate on directories whenfinditself considers a matching mkv file, so it's formally one level deeper. To only move directories from depth 1, use-mindepth 2 -maxdepth 2.This should help with some quirks described above.
POSIXly you can
-prunedirectories at depth 2 with:find . -type d -path './*/*' -prune -o -type f …where
-type f …is what we have in our original snippet.
And this is an alternative solution where the outer find considers directories of depth 1 and the inner find tests regular files directly inside each:
find . -type d \( -name . -o -path './*/*' -prune -o -exec sh -c 'for d; do find "$d" -type d -path "./*/*" -prune \ -o -type f -name "*.[mM][kK][vV]" -links 1 -print \ | grep -q . && mv -- "$d" /somewhere/else/. && printf "%s\n" "$d"done' find-sh {} + \)I find it quite elegant, still it does not "add an exec" to your -type f, it adds an exec to -type d (and therefore it's my secondary solution here). A generalization to other depths is relatively easy by replacing -path … with -mindepth … -maxdepth …, I think, but if POSIXly then some combinations of depths allowed for the outer find and the inner -find may require substantially complicated code.
Note: grep -q . is expected to exit as soon as the inner find finds a matching mkv regular file, still the inner find itself may later work in vain if the directory contains many other files and the second match comes late (or never). The problem is described here, find is not as smart as GNU tail in this matter. In case of directories containing reasonably small number of files the problem should be negligible. If not negligible and if your find supports -quit then use it just after -print. A POSIX solution may be like:
find . -type d \( -name . -o -path './*/*' -prune -o -exec sh -c 'for d; do find "$d" -type d -path "./*/*" -prune \ -o -type f -name "*.[mM][kK][vV]" -links 1 -print -exec sh -c " kill -s PIPE \"\$PPID\"" Oedipus \; \ | grep -q . && mv -- "$d" /somewhere/else/. && printf "%s\n" "$d"done' find-sh {} + \)Here the inner find spawns a child shell named Oedipus just after it finds the first matching file. The child kills the parent.