5

I'm trying to use find -name in a sh script with a previously computed complex argument for the condition. Simplified, it goes like

cond="-name '*.txt*"
find . $cond -ls

But now I have the problem that either the wildcard in $cond is expanded by the shell before calling find or not expanded by find.

To test that, in an empty test directory I did:

touch a.txt b.txt c.dat

and then iterated

cond="-name '*.txt'"; echo $cond; find . $cond

with different values for $cond, using all types of quoting/escaping I could think of.

Either I get all four entries (including the . directory) or no files – or find complains about b.txt being an unknown primary or operator. Of course, the correct output should be just the two text files.

Any hints? I'm sure I'm just missing something basic.

Stefan
  • 153

1 Answers1

7

In Bourne-like shells (except zsh), leaving a variable expansion unquoted in list context is the split+glob operator. In:

cond="-name '*.txt'"; echo $cond

The content of $cond is first split according to the value of the $IFS special variable. By default, that's on ASCII space, tab and newline characters.

So that's split into -name and '*.txt'. Then the glob part is applied. For each of those words that contain wildcard characters (here * in the second one), the word is expanded to the list of files that match that pattern or left untouched if no file is matched. So, '*.txt' would expand to the list of files in the current directory whose name starts with ' and end in .txt' or to '*.txt' if no file matches.

That list of words is then passed as separate arguments to echo. If no file matched, that means echo will receive 3 arguments: "echo", "-name" and "'*.txt'".

Of course, the same applies for the find command.

You want the find command to receive the arguments -name and *.txt, so you need to tune your split+glob operator.

You need the value of $IFS to match the separator you use in $cond. So for instance:

cond='-name:*.txt'
IFS=':'

And you don't want the glob part of the split+glob operator, so you need to disable it with:

set -f

So it becomes:

cond='-name:*.txt'
IFS=:; set -f
find . $cond -ls

Or you could do:

cond='-name *.txt'
set -f
find . $cond -ls

and assume $IFS has not been modified from its default value.

Alternatively, if your shell supports arrays, you may want to use that instead of relying on a scalar variables and expecting the shell to split it upon expansion.

This would work with ksh93, mksh, bash or zsh:

cond=(-name '*.txt') # an array with two elements: "-name" and "*.txt"
find . "${cond[@]}" -ls # elements of the array expanded as separate arguments
                        # to find.
  • Thank you very much. I now just switch off blobbing locally, and it works like a charm. Regrettably, I cannot vote up your answer yet. – Stefan Nov 03 '14 at 11:46
  • 2
    @Stefan, beware that set -f disables globbing altogether (not only the implicit globbing done upon variable expansion or command substitution). That means rm -- *.txt for instance won't work anymore. You may also want to consider better shells that don't perform globbing upon variable expansion like zsh, rc or fish (of those, zsh is the most Bourne-like). – Stéphane Chazelas Nov 03 '14 at 11:52
  • (and of course it should read "globbing" — damn autocorrect ;-)) – Stefan Nov 03 '14 at 11:53
  • I have a () construct around the find, so I assume set -f stays local — right? For portability reasons, I prefer to keep at "classic" Bourne Shell. – Stefan Nov 03 '14 at 11:58
  • @Stefan, yes (...) and $(...) create a local context for options and variables, usually by forking a subshell process. – Stéphane Chazelas Nov 03 '14 at 12:09