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?

11
  • You might want to man 1 find and read the entries on -exec closely. Hint: the position of {} matters. Jan 1, 2023 at 13:25
  • See: understanding-the-exec-option-of-find
    – Jetchisel
    Jan 1, 2023 at 13:29
  • 1
    find -exec <command> \; always executes command once for each of its matches. Why not: mkdir foo; find . -name bar -exec mv -t foo {} \;? Jan 1, 2023 at 14:08
  • 1
    -exec mv -t csvW {} + is completely robust. You are needlessly complicating things and breaking support for arbitrary file names. See also mywiki.wooledge.org/BashFAQ/020
    – tripleee
    Jan 1, 2023 at 14:29
  • 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 1, 2023 at 14:37

5 Answers 5

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

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

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.

1
  • 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 1, 2023 at 19:39
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.

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.

5
  • The find -print0 and xargs -0 options are also GNU extensions, and not standard.
    – tripleee
    Jan 1, 2023 at 19:26
  • @tripleee, not standard, but widely supported, e.g. by FreeBSD, Solaris and Busybox.
    – ilkkachu
    Jan 1, 2023 at 19:34
  • @tripleee, they are there on my Mac.
    – ilkkachu
    Jan 1, 2023 at 19:37
  • Duh, you're right ... Wonder when that changed. Still rename is a portability problem because there are multiple utilities with that name, with different behavior, syntax, and options.
    – tripleee
    Jan 2, 2023 at 13:09
  • It's long explained in the linked post Jan 2, 2023 at 13:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .