104

I want to know exactly what {} \; and {} \+ and | xargs ... do. Please clarify these with explanations.

Below 3 commands run and output same result but the first command takes a little time and the format is also little different.

find . -type f -exec file {} \;
find . -type f -exec file {} \+
find . -type f | xargs file

It's because 1st one runs the file command for every file coming from the find command. So, basically it runs as:

file file1.txt
file file2.txt

But latter 2 find with -exec commands run file command once for all files like below:

file file1.txt file2.txt

Then I run the following commands on which first one runs without problem but second one gives error message.

find . -type f -iname '*.cpp' -exec mv {} ./test/ \;
find . -type f -iname '*.cpp' -exec mv {} ./test/ \+ #gives error:find: missing argument to `-exec'

For command with {} \+, it gives me the error message

find: missing argument to `-exec'

why is that? can anyone please explain what am I doing wrong?

jww
  • 97,681
  • 90
  • 411
  • 885
Shahadat Hossain
  • 2,542
  • 3
  • 17
  • 13
  • real question is simple, why first one works and second one not? (1)find . -type f -iname '*.cpp' -exec mv {} ./test/ \; (2) find . -type f -iname '*.cpp' -exec mv {} ./test/ \+ – Shahadat Hossain Apr 09 '11 at 20:11

5 Answers5

194

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.

Lekensteyn
  • 64,486
  • 22
  • 159
  • 192
  • 1
    I know actually how it works, I have gone through this manual several times, but I have got error message for using {} +, although works for {} \; and I am using Cygwin in windows. – Shahadat Hossain Apr 09 '11 at 20:29
  • 1
    @Shahadat: have you read the part before "From the manual page"? You've put `./test/` between `{}` and `+`, but no non-whitespace characters are allowed between these. – Lekensteyn Apr 09 '11 at 20:31
  • r u saying that i should not put ./test/ in between {} and +. Then how mv command will work; mv needs source which is {} and needs destination which is ./test/ and termination with +. can you please write the command what you think right? – Shahadat Hossain Apr 09 '11 at 20:35
  • @Shahadat: I see what you're trying to achieve. Windows is slow on executing programs, so you want to combine it to one command. I'll add an alternative to the answer. – Lekensteyn Apr 09 '11 at 20:38
  • thank you, please try to solve it as soon as possible. i am stuck in it. – Shahadat Hossain Apr 09 '11 at 20:39
  • 2
    The `+` command is a bit strange AFAIU since it sticks the files at "the end" (and not in place of `{}`) so why using `{}` at all - this is confusing. Thanks for the `-t` option that I didn't know of, it seems that option was created as a workaround to that `-exec +` problem! – Déjà vu Nov 03 '17 at 16:04
  • Anyone know why this hasn't been added to find yet? I don't see any reason to limit the position of {} in the plus form but not in the semicolon form. – Quantum7 Jan 18 '22 at 17:04
9

I encountered the same issue on Mac OSX, using a ZSH shell: in this case there is no -t option for mv, so I had to find another solution. However the following command succeeded:

find .* * -maxdepth 0 -not -path '.git' -not -path '.backup' -exec mv '{}' .backup \;

The secret was to quote the braces. No need for the braces to be at the end of the exec command.

I tested under Ubuntu 14.04 (with BASH and ZSH shells), it works the same.

However, when using the + sign, it seems indeed that it has to be at the end of the exec command.

arvymetal
  • 2,787
  • 1
  • 30
  • 39
  • `{}` needs to be quoted in the `fish` and `rc` shells, but not in `zsh`, `bash` nor any other shells of the Bourne or csh families. – Stephane Chazelas Nov 03 '17 at 16:37
  • @StephaneChazelas Yep, retested under an Ubuntu with `bash`, indeed the quotes aren't necesary. Curiously, I had an issue if not quoting them under MacOS (using `zsh`). But I don't have a Mac at reach to try again... – arvymetal Nov 07 '17 at 17:14
5

The standard equivalent of find -iname ... -exec mv -t dest {} + for find implementations that don't support -iname or mv implementations that don't support -t is to use a shell to re-order the arguments:

find . -name '*.[cC][pP][pP]' -type f -exec sh -c '
  exec mv "$@" /dest/dir/' sh {} +

By using -name '*.[cC][pP][pP]', we also avoid the reliance on the current locale to decide what's the uppercase version of c or p.

Note that +, contrary to ; is not special in any shell so doesn't need to be quoted (though quoting won't harm, except of course with shells like rc that don't support \ as a quoting operator).

The trailing / in /dest/dir/ is so that mv fails with an error instead of renaming foo.cpp to /dest/dir in the case where only one cpp file was found and /dest/dir didn't exist or wasn't a directory (or symlink to directory).

Stephane Chazelas
  • 5,859
  • 2
  • 34
  • 31
  • +1... in-shell operation as a preliminary to executing a command is actually useful for a variety of use cases... nice. – Cbhihe Nov 03 '17 at 16:25
  • This is the best general solution. I was confused a bit why the second 'sh' was needed. From the bash man page, 'If there are arguments after the string, they are assigned to the positional parameters, starting with $0'. So the 'sh' becomes $0 in the script and $@ starts with the filename. – Quantum7 Jan 20 '22 at 21:21
0
find . -name "*.mp3" -exec mv --target-directory=/home/d0k/Музика/ {} \+
colidyre
  • 4,170
  • 12
  • 37
  • 53
DarkLabs
  • 1
  • 2
-1

no, the difference between + and \; should be reversed. + appends the files to the end of the exec command then runs the exec command and \; runs the command for each file.

The problem is find . -type f -iname '*.cpp' -exec mv {} ./test/ \+ should be find . -type f -iname '*.cpp' -exec mv {} ./test/ + no need to escape it or terminate the +

xargs I haven't used in a long time but I think works like +.

Mike Ramirez
  • 10,750
  • 3
  • 26
  • 20
  • I also tried with this but got the same error message. Moreover, everywhere I found to use only + but in my cygwin I have to use \+ or "+" to work. – Shahadat Hossain Apr 09 '11 at 20:18
  • oh this is a cygwin environment. Sorry then I don't know, I don't use the cygwin shell, I just use a *nix. – Mike Ramirez Apr 09 '11 at 20:21
  • 1
    @Shahadat Hossain try `-name "*.cpp"` I hardly use -iname unless I want to do some difficult regex search, like -iname '???work.*\.cpp' – Mike Ramirez Apr 09 '11 at 20:37
  • -iname is not creating problem here, as it means that name search will be case insensitive, however, i tried with only -name option but got the same error message. – Shahadat Hossain Apr 09 '11 at 20:42
  • 1
    @Mike: I think you misunderstand the difference between `-iname` and `-name`. `-iname` is the case-insensitive version of `-name` and has no differences in handling of the regular expressions. I suggest to try commands before posting, your command fails in my shell as well. – Lekensteyn Apr 09 '11 at 20:57
  • 1
    @Lekensteyn It was already established that was the case prior to your comment. I thought I had acknowledge Shahadat prior to your post, it was a simple "ok". No, I didn't run it manually, I did it from the top of my head and rarely use that form of regex searching with find. It was just a 'might help' type thing. – Mike Ramirez Apr 09 '11 at 21:02