Why Does Find -Exec Mv {} ./Target/ + Not Work

Why does find -exec mv {} ./target/ + not work?

The manual page (or the online GNU manual) pretty much explains everything.

find -exec command {} \;

For each result, command {} is executed. All occurences of {} are replaced by the filename. ; is prefixed with a slash to prevent the shell from interpreting it.

find -exec command {} +

Each result is appended to command and executed afterwards. Taking the command length limitations into account, I guess that this command may be executed more times, with the manual page supporting me:

the total number of invocations of the command will be much less than the number of matched files.

Note this quote from the manual page:

The command line is built in much the same way that xargs builds its command lines

That's why no characters are allowed between {} and + except for whitespace. + makes find detect that the arguments should be appended to the command just like xargs.

The solution

Luckily, the GNU implementation of mv can accept the target directory as an argument, with either -t or the longer parameter --target. It's usage will be:

mv -t target file1 file2 ...

Your find command becomes:

find . -type f -iname '*.cpp' -exec mv -t ./test/ {} \+

From the manual page:

-exec command ;

Execute command; true if 0 status is returned. All following arguments to find are taken to be arguments to the command until an argument consisting of `;' is encountered. The string `{}' is replaced by the current file name being processed everywhere it occurs in the arguments to the command, not just in arguments where it is alone, as in some versions of find. Both of these constructions might need to be escaped (with a `\') or quoted to protect them from expansion by the shell. See the EXAMPLES section for examples of the use of the -exec option. The specified command is run once for each matched file. The command is executed in the starting directory. There are unavoidable security problems surrounding use of the -exec action; you should use the -execdir option instead.

-exec command {} +

This variant of the -exec action runs the specified command on the selected files, but the command line is built by appending each selected file name at the end; the total number of invocations of the command will be much less than the number of matched files. The command line is built in much the same way that xargs builds its command lines. Only one instance of `{}' is allowed within the command. The command is executed in the starting directory.

What is this strange syntax inside `find -exec`?

To understand you need to know 4 things:

  1. The find action -exec allows you to apply a command on the found files and directories.

  2. The -c bash option is documented as follows:

    BASH(1)
    ...
    OPTIONS
    ...
    -c If the -c option is present, then commands are read from
    the first non-option argument command_string.
    If there are arguments after the command_string, they
    are assigned to the positional parameters, starting with $0.

    ...

    If bash is started with the -c option, then $0 is set to the first
    argument after the string to be executed, if one is present.
    Otherwise, it is set to the filename used to invoke bash, as given
    by argument zero.
  3. In bash, $@ expands as all positional parameters ($1, $2...) starting at parameter $1.

  4. In a bash function, the positional parameters are the arguments passed to the function when it is called.

So, in your case, the command executed for each found file or directory is:

bash -c 'foo "$@"' bash <the-file>

The positional parameters are thus set to:

$0 = bash
$1 = <the-file>

and bash is asked to execute 'foo "$@"' in this context. "$@" is first expanded as "<the-file>". So, function foo is called with one single argument: "<the-file>". In the context of function foo the positional parameters are thus:

$1 = "<the-file>"

and echo "$@" expands as echo "<the-file>".

All this just prints the names of all found files or directories. It is almost as if you had any of:

find . -exec echo {} \;
find . -print
find .
find

(for find versions that accept the last one).

Almost as if, only, because if file or directory names contain spaces, depending on your use of find and of quotes, you will get different results. So, if you intend to have a more complex foo function, you should pay attention to the quotes. Examples:

$ touch "filename with spaces" plain
$ ls -1
filename with spaces
plain # 2 files
$ foo() { echo "$@"; } # print arguments
$ find . -type f
./filename with spaces
./plain
$ find . -type f -exec bash -c 'foo "$@"' bash {} \;
./filename with spaces
./plain
$ find . -type f -exec bash -c 'foo $@' bash {} \;
./filename with spaces
./plain

The 3 find commands apparently do the same but:

$ bar() { echo $#; }                                 # print number of arguments
$ wc -w < <(find . -type f)
4 # 4 words
$ find . -type f -exec bash -c 'bar "$@"' bash {} \;
1 # 1 argument
1 # per file
$ find . -type f -exec bash -c 'bar $@' bash {} \;
3 # 3 arguments
1 # 1 argument

With find . -type f -exec bash -c 'bar "$@"' bash {} \;, the first file name is passed to function bar as one single argument, while in all other cases it is considered as 3 separate arguments.

Why xargs find | xargs mv and xargs find -exec mv work but produce error printouts No such file or directory?

Short answer: find is being told to search all 4 subdirectories, even after some of them have been deleted.

Detailed eplanation: The root problem is that the wildcard in the xargs -I abc find ./testdir/* -maxdepth 1 ... part gets expanded by the shell before any of the commands get run. So that part of the script becomes:

... | xargs -I abc find ./testdir/dir1 ./testdir/dir2 ./testdir/dir3 ./testdir/dir4 -maxdepth 1 ...

What happens then is the stat | sed part sends the first inode number to xargs, that runs find, find searches all 4 directories for the matching item, and either deletes it directly or sends its path to the next xargs for deletion.

Next, the stat | sed part sends the second inode number to xargs, that runs find, find searches all 4 directories... oops, hey, one of them's missing! So it prints an error message about ./testdir/dir1 not existing, searches the other three, and (one way or another) deletes the next one.

Next comes the third inode number, and this time neither ./testdir/dir1 nor ./testdir/dir2 exists, so you get two error messages. Etc etc etc.

(There's also an additional problem with the second one, where immediately after running the mv command, find then tries to search its contents, and oops it's gone. That's why you get more error messages that way. I think you might've wanted -maxdepth 0 to keep it from trying to do that.)

Solution: I'm not sure what the larger context is, but my immediate reaction is that this is an overcomplex mess and as much as possible should be removed. But without knowing what can be changed without breaking the big picture, the minimal fix I see is to just have find search the entire testdir directory, rather than using a wildcard to list specific (wrong) subdirectories:

... | xargs -I abc find ./testdir -maxdepth 1 ...

(And note that in this form, -maxdepth 1 is actually correct.)

Move command error target is not directory UNIX

In find -exec the {} is replaced by the file/directory found.
The mv command used as in your example is trying to move the first argument to the second argument so your arguments are in the wrong order.

This would be correct:

[[ -d ~/destination ]] && find ~/testfiles -name '*.c' -exec mv -i '{}' ~/destination \;

A slightly more effetive way would be to use mvs -t (target) option which means that you can use the + in find (to supply as many arguments to mv in one go as possible):

[[ -d ~/destination ]] && find ~/testfiles -name '*.c' -exec mv -i -t ~/destination '{}' +

I added [[ -d ~/destination ]] && before the find command since one common mistake is to move a lot of files to the same destination and if it doesn't exist (or is a file and not a directory) it'll create a file named destination and overwrite that with all the files making you loose all your files (except the last one moved).

Explanation for the shift from + to \; in -exec taken directly from my man find page:

-exec command ;

All following arguments to find are taken to be arguments to the command until an argument consisting of ; is encountered. The string
{} is replaced by the current file name being processed everywhere it occurs in the arguments to the command, ... The specified command is run once for each matched file. ...

-exec command {} +

This variant of the -exec action runs the specified command on the selected files, but the command line is built by appending each selected file name at the end; The command line is built in much the same way that xargs builds its command lines. Only one instance of {} is allowed within the command, ...

Note that the syntax differs and that {} + must be at the end (where it's appending files) for the second version.

exec not working after using find ... -exec grep -L

The problem is that grep -L is exiting with a return code of 1 for some reason. I can't explain why it is doing that though. My understanding is that it shouldn't be doing that because lines are being output.

In any case that exit status causes the -exec to evaluate to false and causes find to stop processing the file (the same as any other failing test does).

This doesn't happen with + because when find gives grep multiple files at once at least one is matching those contents so grep exits with a zero return code (but that's obviously not reliable here). I also cannot explain this behavior this seems exactly backwards to me also.

I believe using -not -exec grep -L ... \; will do what you want but I'd have to think about that a bit more to be sure.

Hm... I suppose the return codes make sense in that when the filename is output no matching lines were found and when the filename is not output matching lines were found.

-exec option of the find command does not work as expected on Ubuntu

The default -and operation has higher precedence than -or. Use extra parentheses:

find ~ \( \( -type f -not -perm 0600 \) -or \( -type d -name 'D*' \) \) -exec ls -l '{}' ';'

You can probably omit the inner parentheses in this case.

linux find: move files by xargs

Using find and exec

$ find ~/Desktop -name "*.jpg" -exec mv '{}' /tmp/target/ \; -or -name "*.gif" -exec mv '{}' /tmp/target/ \; -or -name "*.png" -exec mv '{}' /tmp/target/ \;

Using xargs

$ find ~/Desktop -name "*.jpg" -or -name "*.gif" -or -name "*.png"  | xargs -I SRCFILE mv SRCFILE /tmp/target/

Why are the backslash and semicolon required with the find command's -exec option?

The backslash before the semicolon is used, because ; is one of list operators (or &&, ||) for separating shell commands. In example:

command1; command2

The find utility is using ; or + to terminate the shell commands invoked by -exec.

So to avoid special shell characters from interpretation, they need to be escaped with a backslash to remove any special meaning for the next character read and for line continuation.

Therefore the following example syntax is allowed for find command:

find . -exec echo {} \;
find . -exec echo {} ';'
find . -exec echo {} ";"
find . -exec echo {} \+
find . -exec echo {} +

See also:

  • Using semicolon (;) vs plus (+) with exec in find
  • Simple unix command, what is the {} and \; for


Related Topics



Leave a reply



Submit