1

This slightly more advanced for me Im not sure its possible I would like to find directories with mkv files that have 1 link find . -type f -links 1 -print' Currently im using this to find the mkv file but i wish to add an exec command to move directories that have said files somewhere else. Is that at all possible?

Edit: Downloads -> Directory1/*.mkv(link 1)

When command executed Directory1 would move, I think that would be max depth 1

1 Answers1

0

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 {} \; -print

Notes:

  • -name '*.[mM][kK][vV]' is a POSIX equivalent* of -iname '*.mkv'. Use the latter if your find supports -iname.

    * Well, almost. The example of Turkish locale where uppercase i is İ and the lowercase I is ı shows us it's not that simple. In a locale that does something similar to m, M, k, K, v or V, 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 like dirname, but without using the external tool. Unlike dirname this will not take care of edge cases, but since our find starts 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 mv will 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 for mv is a trick that makes the command fail when else does not exist. The problem we avoid is if we move a directory named foo to /somewhere/else and else does not yet exists then foo will become else inside /somewhere, instead of becoming /somewhere/else/foo. Then moving the next directory bar fill find else existing and bar will become /somewhere/else/bar. This in turn is expected, the original problem is no more; but the content of (former) foo is now directly under /somewhere/else, not in its own subdirectory.

    Moving something to /somewhere/else/. will simply fail if else does not exist.

  • -print will be performed if and only if mv succeeds.

  • find-sh is explained here: What is the second sh in `sh -c 'some shell code' sh`?

  • Our find -exec will move directories from "under" find's own directory traversing algorithm. Expect warnings when find tries 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 find considers 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 … +, find will test files and descend directories it wouldn't even see if it mved directories right away. Then you would need to truncate many pathnames and pass them to a single mv (as separate mvs 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 -u then to xargs that runs a shell (and ultimately mv) is an idea to avoid passing duplicates to mv, it will not prevent find from doing all the extra work in the first place. And it would be against your wish to "add an exec command".

    Using -prune on the parent directory of a matching mkv file is impossible because when our find finds the file, it has processed the parent directory and it's too late to -prune it.

    In my opinion it's good to just let find move 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 . and mv will fail (at least in my Debian mv . … fails).

  • If the name of the directory we are trying to mv is already taken inside else then mv will fail. This can happen if else is initially not empty; but also on the fly, since our find can 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 your find supports -mindepth and -maxdepth like GNU find, use them; but keep in mind we operate on directories when find itself 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 -prune directories 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.