Harry
Harry

Reputation: 3927

/usr/bin/find: cannot build its arguments dynamically

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.

Upvotes: 3

Views: 445

Answers (3)

Gordon Davisson
Gordon Davisson

Reputation: 125838

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')

Upvotes: 7

Chen Levy
Chen Levy

Reputation: 16368

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

Upvotes: 1

DarkDust
DarkDust

Reputation: 92345

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').

Upvotes: 2

Related Questions