0

I did a web seach for "linux run find on find" but it produced no relevant results. I want to build list of files by find and then run find again on that list.

After fixing some "silly" mistakes I guess coming from little experience with command line work, final command is: find "path1" -size 0 -printf "%f\0" | xargs -0 --max-args=1 --verbose find "path2" -exec ls -l {} \\\; -name. As --verbose, I see commands run as find path -exec ls -l {} \; -name foundfilename and output "missing argument to exec".

If I run resulting command build by xargs directly (adding double quotes as for some reason xargs --verbose output misses, but apparently uses in practice as find does not have issues with path that includes spaces: find "path" -exec ls -l {} \; -name foundfilename and output looks like all files in path.

  1. Why different result from xargs and direct find run?
  2. Looks like -exec cannot precede -name, is it correct?
  3. Finally to the point: How to properly run find on results on another find?

System: Linux Mint 19.2

Added after comment to clarify 2nd question:

If I run find "path" -name foundfilename -exec ls -l {} \; I got output of several files found and listed by ls as expected, that is to the 2nd question.

I did test setup of two files named 1 and 2 in /media/ramdrive.

marina@tpx:~$ find /media/ramdrive -exec ls {} \; -name 1
1  2
/media/ramdrive/2
/media/ramdrive/1
marina@tpx:~$ find /media/ramdrive -name 1 -exec ls {} \;
/media/ramdrive/1

Man on find:

-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 sec‐ tion for examples of the use of the -exec option. The specified command is run once for each matched file. The command is exe‐ cuted in the starting directory. There are unavoidable secu‐ rity problems surrounding use of the -exec action; you should use the -execdir option instead.

I don't see why -exec cannot precede options for matching.

Actually I don't understand meaning of "everywhere it occurs in the arguments to the command, not just in arguments where it is alone".

ADDED TWO:
I though I found a solution by: find "path1" -size 0 -printf "%f\0" | xargs -0 --max-args=1 --verbose find "path2" -name | xargs ls -l but default print from find does not quote results, so fails for path with spaces. If there utility to add quotes?

  • @Theophrastus, path is real. The point is to find some file names by criteria (here zero size as an example) and than to find all such file names in some maybe other path. I now amending question to clarify. – Marisha Jan 7 at 4:15
  • @ Theophrastus , reproducible example for 2nd question added. – Marisha Jan 7 at 4:27
  • @Theophrastus, it is stated from the start: I want to build list of files by find and then run find again on that list. Would it be more helpful to state as "I want to find files by some complex criteria and then find files based list prepared on 1st step?" – Marisha Jan 7 at 4:56
  • @Theophrastus, for now it is zero size. – Marisha Jan 7 at 5:08
2

Why different result from xargs and direct find run?

When you type find path -exec ls -l {} \; -name foundfilename in a shell, the shell reads \; as "pass ; to the command". If it was just ; typed, it would be a command separator. The backslash gets "eaten" by the shell and changes its behavior regarding ;.

When xargs says it runs find path -exec ls -l {} \; -name foundfilename, it actually passes \; to the command (i.e. to find), there is no shell to strip the backslash. So \; is wrong here: -exec expects ; or +, it gets neither, therefore "missing argument to exec".


Looks like -exec cannot precede -name, is it correct?

Incorrect. It can. Maybe it shouldn't, it depends on what you want to do. The different results from your two commands can be easily explained. The two commands are:

find /media/ramdrive -exec ls {} \; -name 1
find /media/ramdrive -name 1 -exec ls {} \;

Crucial facts:

  • -exec is also a test. It succeeds iff the inner command returns 0.
  • Tests are joined by operators: -o (logical OR) or -a (logical AND). Where an operator is missing (like in your case), -a is assumed.
  • With -test1 -o -test2 or -test1 -a -test2, -test1 is tested first. If the result is determined solely by -test1 then -test2 won't be performed. In particular this means -test1 -a -test2 will run test2 if and only if -test1 succeeds (if it fails then -test2 is not needed because we already know the result: failure).

If your overall test is -exec ls {} \; -name 1 (i.e. -exec ls {} \; -a -name 1) then -exec is performed for every candidate. This already prints something to stdout. For each candidate ls succeeds, so the other test is performed; but it doesn't matter because the other test prints nothing and there are no further tests/actions (the default -print is suppressed by previous -exec).

If your overall test is -name 1 -exec ls {} \; (i.e. -name 1 -a -exec ls {} \;) then -name is performed for every candidate. The test prints nothing, but the result of it decides if -exec is performed. So ls runs iff -name succeeds, it prints something iff -name succeeds.


How to properly run find on results on another find?

It's possible to run find from within -exec of another find. E.g. this finds all symlinks in directories named lib:

find / -type d -name lib -exec find {} -type l \;

You can get results from /lib/, /var/lib/, /usr/lib/ etc.

There are few problems:

  • This runs one inner find per one result from the outer find. The situation is different if you want to use all the results from one find at once, i.e. with a single other find.
  • If the inner find also needs to run -exec then there is no straightforward way to pass {}, ; (typed as \;) or + to it, because these will be interpreted by the outer find. One solution is to use xargs (and you did) which in general requires non-POSIX options to make it not fail for names containing newlines. Another solution is to spawn a shell between the two finds (like in this other answer which appeared when I was composing mine, I won't repeat its solution).

I don't understand meaning of "everywhere it occurs in the arguments to the command, not just in arguments where it is alone".

POSIX requires find to expand alone {}. For arguments like {}.txt or foo{}bar "it is implementation-defined whether find replaces those two characters or uses the string without change". Your version of find will replace {} in arguments like {}.txt or foo{}bar and the manual explicitly states this.

| improve this answer | |
2

Just do:

find path1 -size 0 -exec sh -c '
  for file do
    find path2 -name "${file##%/}" -exec ls -ld \{\} +
  done' sh {} +

(assuming none of the names of the empty files contain wildcard characters or backslashes as -name treats its argument as a wildcard pattern).

Since your find is the one from GNU, you could also replace -exec ls -ld \{\} + with -ls (neither -printf nor -ls are standard but while -printf is GNU-specific, -ls is much more portable though the output format varies slightly between implementations).

More generally, you don't want to pipe the output of find into xargs unless you use the GNU-style xargs -r0 and get find to output NUL-delimited records (with -print0 or your -printf '%f\0' for instance). In most of the cases it's better to use -exec ... {} + than piping to xargs. More generally, Kamil's answer very nicely explains the problems in your approach, I won't repeat it here.

Here, you could use the zsh shell instead:

for file (path1/**/*(NDL0)) ls -ld path2/**/$file:t(DN)

(which doesn't have the wildcard character issue)

| improve this answer | |
1

Following hint by @muru to use -I in xargs, the working command (assuming GNU implementations of find and xargs) is:

find "path1" -size 0 -printf "%f\0" |
  xargs -r0 -I mystr find "path2" -name mystr -exec ls -ld {} +

(that assumes the names of the empty files don't contain wildcard characters as -name interprets its argument as a wildcard pattern)

| improve this answer | |
  • @Stéphane, thanx for the edit/ why + at the end of exec here is better than \;? – Marisha Jan 7 at 10:37
  • 2
    -exec cmd {} + passes as many filenames as possible to each invocation of cmd while -exec cmd {} ';' runs one cmd invocation with one filename argument for each file. Here ls can take more than one file name as argument (note that it will also sort the list it receives) and running fewer invocations will make it a lot more efficient. – Stéphane Chazelas Jan 7 at 10:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.