3

The following command works as expected interactively, in a terminal.

$ find . -name '*.foo' -o -name '*.bar' ./a.foo ./b.bar $ 

However, if I do this, I get no results!

$ ftypes="-name '*.foo' -o -name '*.bar'" $ echo $ftypes -name '*.foo' -o -name '*.bar' $ find . $ftypes $ 

My understanding was/is that $ftypes would get expanded by bash before find got a chance to run. In which case, the ftypes approach should also have worked.

What is going on here?

Many thanks in advance.

PS: I have a need to dynamically build a list of file types (the ftypes variable above) to be given to find later in a script.

7
  • The single quotes in "... -name '*.foo' remain in the interpolated value. If you want ftypes to be a variable (not necessarily a good idea anyway) try with just ... -name *.foo - the double quotes preve Commented Feb 16, 2012 at 9:49
  • @triplee no quotes around *.foo would result in expanded list of files which will be passed to find. I anyway tried what you said, and got this error: find: paths must precede expression Usage: find [-H] [-L] [-P] [path...] [expression] Commented Feb 16, 2012 at 9:56
  • That is not at all my experience. touch fnord.foo ick.bar none.svch; ftypes="-name *.foo -o -name *.bar"; find . $ftypes does exactly what I expect. Commented Feb 16, 2012 at 10:12
  • Try this (with 2 .foo files): touch fnord.foo another.foo ick.bar none.svch; ftypes="-name *.foo -o -name *.bar"; find . $ftypes. Commented Feb 16, 2012 at 10:24
  • Right you are. I guess eval is the best option then. Commented Feb 16, 2012 at 10:35

3 Answers 3

7

Both answers so far have recommended using eval, but that has a well-deserved reputation for causing bugs. Here's an example of the sort of bizarre behavior you can get with this:

$ touch a.foo b.bar "'wibble.foo'" $ ftypes="-name '*.foo' -o -name '*.bar'" $ eval find . $ftypes ./b.bar 

Why didn't it find the file ./a.foo? It's because of exactly how that eval command got parsed. bash's parsing goes something like this (with some irrelevant steps left out):

  1. bash looks for quotes first (none found -- yet).
  2. bash substitutes variables (but doesn't go back and look for quotes in the substituted values -- this is what lead to the problem in the first place).
  3. bash does wildcard matching (in this case it looks for files matching '*.foo' and '*.bar' -- note that it hasn't parsed the quotes, so it just treats them as part of the filename to match -- and finds 'wibble.foo' and substitutes it for '*.foo'). After this the command is roughly eval find . -name "'wibble.foo'" -o "'*.bar'". BTW, if it had found multiple matches things would've gotten even sillier by the end.
  4. bash sees that the command on the line is eval, and runs the whole parsing process over on the rest of the line.
  5. bash does quote matching again, this time finding two single-quoted strings (so it'll skip most parsing on those parts of the command).
  6. bash looks for variables to substitute and wildcards to matching, etc, but there aren't any in the unquoted sections of the command.
  7. Finally, bash runs find, passing it the arguments ".", "-name", "wibble.foo", "-o", "-name", and "*.bar".
  8. find finds one match for "*.bar", but no match for "wibble.foo". It never even knows you wanted it to look for "*.foo".

So what can you do about this? Well, in this particular case adding strategic double-quotes (eval "find . $ftypes") would prevent the spurious wildcard substitution, but in general it's best to avoid eval entirely. When you need to build commands, an array is a much better way to go (see BashFAQ #050 for more discussion):

$ ftypes=(-name '*.foo' -o -name '*.bar') $ find . "${ftypes[@]}" ./a.foo ./b.bar 

Note that you can also build the options bit by bit:

$ ftypes=(-name '*.foo') $ ftypes+=(-o -name '*.bar') $ ftypes+=(-o -name '*.baz') 
Sign up to request clarification or add additional context in comments.

1 Comment

Go r d on, appreciate your taking the time to explain so wonderfully, elaborating on eval, and last but not the least, your pointer to the Bash FAQ item. I had chosen the eval-based solution earlier, but now, I will re-edit my code and go with your more elegant array-based solution. PS: Please upvote the question, if you can.
2

Simply prefix the line with eval to force the shell to expand and parse the command:

eval find . $ftypes 

Without the eval, the '*.foo' is passed on literally instead of just *.foo (that is, the ' are suddenly considered to be part of the filename, so find is looking for files that start with a single quote and have an extension of foo').

5 Comments

Your solution works, and explanation somewhat makes sense. But I don't fully get it: Why, just why, should ' be 'suddenly' part of filename when bash is supposed to expand $ftypes and pass its 5, space-separated string components to find as arguments, namely: -name, '*.foo', -o, -name, and '*.bar' much as would happen in an interactive session?
When you eval the single quotes are evaluated. If you just interpolate without eval, the single quotes in the value of $ftypes are interpolated verbatim, not evaluated (and removed by the evaluation logic).
@jfgagne: as per the discussion above, that does not work if there are multiple matches in the current directory, as they will be expanded when $ftypes is interpolated. (I didn't realize this at first, and it kind of sucks IMHO, but that's the way it is.)
@Harry: If you type foo 'bar' then the shell evaluates the ' and calls the command foo, with the argument bar (without the single quotes). But if you do t="'bar'" ; foo $bar" the shell will actually pass 'bar' to command foo. You can easily check by replacing foo with echo: echo 'bar' ; t="'bar'" ; echo $t ; eval echo $t. The point of eval is to evaluate the previously expanded line, so this time the single quotes get eaten by the shell like we want it. Also try this with t='$PATH' for fun.
@DarkDust thanks for your responses. Have upvoted all of them.
1

The problem is that since $ftypes a single quoted value, find does see it as a single argument.

One way around it is:

$ eval find . $ftypes 

1 Comment

No, it's double quoted. The problem is that the single quotes remain in the expansion.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.