1

I am writing a simple method to mass move files and have attempted two approaches:

#(1)
find . -name '*.pdf' | xargs -I{} mkdir pdfs; mv {} pdfs

#(2) find . -name '*.pdf' -exec mv {} pdfs +

The first approach surprisingly worked 'sometimes', however, after deleting the folder with the pdfs a few times and returning the pdfs to the parent directory, it suddenly stopped working.

It produces the following error:

mv: rename {} to pdfs: No such file or directory
xargs: unterminated quote

Whereas the second approach gives the following error:

find: -exec: no terminating ";" or "+"

Update:

I got it working with:

find . -name '*.pdf' -exec mv "{}" pdfs \;

However, If I wanted to create the directory and move files in one line, this wont work, for example:

find . -name '*.csv' -exec mkdir -p csvs && mv "{}" csvs \;

find: -exec: no terminating ";" or "+"

How to implement directory creation and move?

don_crissti
  • 82,805
Emil11
  • 11
  • You might want to man 1 find and read the entries on -exec closely. Hint: the position of {} matters. – Stefan van den Akker Jan 01 '23 at 13:25
  • @Jetchisel I have looked at the document and attempted: find . -name '*.csv' -exec bash -c 'mkdir csvs' bash 'mv {} csvs' \; however this seems to recursively create the final and so move is never completed. – Emil11 Jan 01 '23 at 13:41
  • I wonder if there is a way to get bash -c 'mkdir csvs' to only execute once? – Emil11 Jan 01 '23 at 13:44
  • 1
    find -exec <command> \; always executes command once for each of its matches. Why not: mkdir foo; find . -name bar -exec mv -t foo {} \;? – Stefan van den Akker Jan 01 '23 at 14:08
  • @StefanvandenAkker What about file names with white spaces, these won't get moved. I have attempted: mkdir csvW; find . -name '*.csv' -print0 -exec bash -c $'\0' mv -t csvW {} \; based on your interpretation, but the separator does not seem to work. – Emil11 Jan 01 '23 at 14:26
  • 1
    -exec mv -t csvW {} + is completely robust. You are needlessly complicating things and breaking support for arbitrary file names. See also https://mywiki.wooledge.org/BashFAQ/020 – tripleee Jan 01 '23 at 14:29
  • (However, the -t option to mv is a GNU extension; it is available on most Linuxes, but on other systems commonly requires you to separately install the GNU userspace utilities.) – tripleee Jan 01 '23 at 14:31
  • @tripleee that would explain why I get errors when using -t, and likely what caused my previous comment. – Emil11 Jan 01 '23 at 14:32
  • @tripleee Although, when I attempt the following find . -maxdepth 1 -name '*.csv' -type f -exec mv csvW {} \+, I get that the .csv file is not a directory, am I mis-specifying something? – Emil11 Jan 01 '23 at 14:36
  • 1
    Without -t you can't do that; you are saying to move csvW and all the other files onto the last found file. Try instead find ... -exec sh -c 'mv "$@" "$0"' csvW {} + – tripleee Jan 01 '23 at 14:37

5 Answers5

2

Something like this should work:

$ touch foo.pdf white\ space.pdf
$ ls
 foo.pdf  'white space.pdf'
$ mkdir pdfs && \
    find . -path ./pdfs -prune \
    -o -name '*.pdf' -exec mv {} pdfs/ \;
$ tree
.
└── pdfs
    ├── foo.pdf
    └── white space.pdf
  1. This creates directory pdfs and only continues if successfully created.
  2. This finds all files that end in .pdf while making sure it does not match the files it already moved to the pdfs directory (the -path pdfs -prune part skips files in this directory).
  3. This does execute mv once for every match. Use mv -t pdfs/ {} \+ (GNU extension) for performance.
2

Assuming your case is really this easy (files ending in .pdf), a simple

mv -- **/*.pdf pdfs/

would do.

For zsh, that recursive glob ** is enabled by default, for bash you will have to enable it first, using shopt -s globstar.

I suspect your find invocation might be slightly more complicated in reality. But especially if you're using zsh, the extended globbing capabilities of your shell are quite capable and often save you the headache of remembering the right magic find arguments that work every time ;)

2

The unquoted ; separates the mv command from the find command so that cannot ever have worked.

The simplest solution by far is to create the destination directory before running find. If you don't want to keep an empty directory, you can rmdir it afterwards; this will fail if the directory contains any files.

If you have GNU mv, it has a -t option which lets you conveniently name the destination directory before the files you want to move so that you can say

mkdir pdfs
find . -name '*.pdf' -exec mv -t pdfs {} +
rmdir pdfs || true

If you don't have mv -t, try something like

find . -name '*.pdf' -exec sh -c 'mv "$@" "$0"' pdfs {} +

where we shamelessly use the "zeroth" parameter to sh -c for passing in the destination directory name separately from the argument list proper "$@". (Conventionally this "zeroth" argument would contain the shell's name, but since that's not useful for anything here and the string has to contain something anyway, we can pull off a bit of a hack so we don't have to process the argument list.)

find ... -exec mv {} pdfs \;

works but is somewhat inefficient, as it runs a separate mv command for each found file. The + terminator to -exec says to run a single subprocess for as many files as possible (similarly to xargs) but requires the {} token to be last.

tripleee
  • 7,699
  • find . -name '*.pdf' -exec sh -c 'dir=$1; shift; mv "$@" "$dir"' sh pdfs {} + isn't that much more to write... Or hmm, dir=pdfs find . -name '*.pdf' -exec sh -c 'mv "$@" "$dir"' sh {} +? – ilkkachu Jan 01 '23 at 19:39
0

One way, using Perl's rename:

touch foo.pdf white\ space.pdf $'new\nline.pdf'
find . -name '*pdf' -print0 | rename -n -0 's|[^/]+\.pdf$|./pdfs/$&|ms'

Drop -n when attempts are satisfactory.

0

Given you're moving files into a subdirectory pdfs I wonder if you're really only considering files in the current directory.

If so then a simple wildcard would suffice - but you would need to create the directory separately:

mkdir -p pdfs && mv -f *.pdf pdfs

I have written a script for a previous question that merges these two operations in a slightly more robust manner.

Chris Davies
  • 116,213
  • 16
  • 160
  • 287