3

Supposing we're using find in current directory with audio files (e. g. MP3) and (important!) also non-mp3 files (e. g. JPEG) present in current directory or somewhere below it.
Additionally to that, we have a directory in there with some other audio files (called exclude_me in the example), which should live up to its name by being excluded from the file search:

find . \( -type d -name 'exclude_me' -prune \) -o \( -type f -a -iname '*.mp3' -a -print \)

(Though not absolutely necessary in this case, I've deliberately set some redundant parentheses, as well as additional -a operators for the sake of clarity.)

Because of the explicit -print option (cf. also http://mywiki.wooledge.org/UsingFind), this will indeed omit anything from "exclude_me" directory, and only list the *.mp3 files recursively from current directory. Sometimes (not always) the alias command may already be sufficient for a find one-liner, but this command will require the argument at the end of the line.
Taking notice that the '*.mp3' argument to -iname is before the -print in this case, there appears to be no way to solve the problem with an alias.

But why not pretend as if in mathematics by simply swapping the two options?
Like this:

find . \( -type d -name 'exclude_me' -prune \) -o \( -type f -a -print -a -iname '*.mp3' \)

Remember I set a precondition to have at least one non-mp3 file (e. g. image) in the . directory or its subdirectories. It has been set to give a clearer proof that this does not work as expected under the hood. Without them, things will merely look as working fine. Having them present instead will cause the non-mp3 files to get listed as well despite the distinct -iname '*.mp3' specification in the one-liner. So why doesn't the -a operator work like in mathematics, where "A + B" is the same as "B + A"? Or, in find terms:

\( -type f -a -print -a -iname '*.mp3' \)

ought to be the same as

\( -type f -a -iname '*.mp3' -a -print \)

or not?

Is there no way to get the desired result and have the -iname argument at the end of the line, not anywhere in between? (except for ugly hackish solutions like grep -v exclude_me or the like)

2
  • To boil richard's answer down to a sentence: -a is never commutative, but with -print you notice because -print has side-effects.
    – hobbs
    Aug 31, 2014 at 3:51
  • Do you have an answer to this?, I see that you have not accepted any of the answers. Do you need more help? Dec 23, 2018 at 22:42

3 Answers 3

6

and and or are not commutative in many programming languages e.g. C, C++, C♯, shells, find, and many more.

They use lazy left to right evaluated i.e. it evaluates the term on the left first, and then only evaluates term on right if it needs to. So in false and b, b is not evaluated as the answer is always false. But in b and false, b is evaluated, as it has not yet seen that the second term is false.

This does not make any difference when the terms are pure functions i.e. a predicate (something that returns a boolean and does nothing else), but when it does something (has side effects) then it does matter.

-print does something. It prints, therefore it matters.

Note, Any system that allows side effects (such as -print), must define the order of evaluation, and left to right is probably the simplest.

3

They are not the same. -print primary is always evaluated as true and causes the current pathname to be written to standard output.

When you use:

\( -type f -a -print -a -iname '*.mp3' \)

All files found will be printed to stdout, it's the default behavior of -print, regardless of -iname '*.mp3' expression is true or false.

When you use:

\( -type f -a -iname '*.mp3' -a -print \)

-print part only reached if -iname '*.mp3' expression is true. So only files that end with .mp3 will be printed.

POSIX define find -a operator as:

expression [-a] expression

Conjunction of primaries; the AND operator is implied by the juxtaposition of two primaries or made explicit by the optional -a operator. The second expression shall not be evaluated if the first expression is false.

In this case, you don't have to use -print, because if the expression is true, -print is added by default:

If no expression is present, -print shall be used as the expression. Otherwise, if the given expression does not contain any of the primaries -exec, -ok, or -print, the given expression shall be effectively replaced by:

( given_expression ) -print

But this will cause exclude_me is printed in output, because when find matched it, -prune causes expression evaluated to true. And since when we had omitted -print in 2nd expresion, -print is added to the end of both expressions. It looks like:

find . \( -type d -name 'exclude_me' -prune \) -print -o \( -type f -a -iname '*.mp3' \) -print

This causes exclude_me is printed.

You can always omit exclude_me in output by explicitly adding a -print in 2nd expression. This causes -print only applied for 2nd expression:

find . \( -type d -name 'exclude_me' -prune \) -o \( -type f -a -iname '*.mp3' \) -print

Or adding an expression after -prune, and make sure that it makes the 1st expression false:

find . \( -type d -name 'exclude_me' -prune -a -name . \) -o \( -type f -a -iname '*.mp3' \)
2
  • Well, that's not quite the same. Try it out. "./exclude_me" does get listed once you omit the -print at the end of the line. But I want it both excluded and not even listed at all. Not until you add the explicit -print, however, "exclude_me" will actually be left out of the listing. Trust me, I can try here "live" and I know what I'm talking about :) Aug 30, 2014 at 18:55
  • Looks excellent now in my opinion. Thanks for your agreement on improvement. As you have seen, this stuff is way trickier than it looks at first.-- Lastly, this has now turned a great solution for alias as well. Remember, alias needs the argument at the EOL, and the -print was in the way all the time. Thanks to your marvellous "dummy option" -a -name ., this is now feasible. Fantastic! Thank you. Aug 30, 2014 at 20:13
2

Use a shell function instead of an alias:

function my_find
{
   exclude_dir="$1"
   shift
   find . \( -type d -name "$exclude_dir" -prune \) -o \( -type f -a -iname "$@" -a -print \)
}

You must log in to answer this question.

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