5

I've previously written a script that searches a directory tree for .h and .c files and then runs clang-format on them:

find $directory -name '*.[hc]' -exec clang-format -i {} \;

This works just as expected. Now I'd like to add .cpp files to the search. However, neither

-name '*.{[hc],cpp}'

nor

-name '{*.[hc],*.cpp}'

work. That is, they find no files.

I know that I could get my logic to work if I used find's -o option. However, there must be a way to do this with a single -name directive.

Daniel Walker
  • 801
  • 1
  • 9
  • 35
  • If your implementation of find supports -regex you can do find . -regex "\./.*\.[c|h]" – Arkadiusz Drabczyk Dec 05 '21 at 20:21
  • @ArkadiuszDrabczyk Note that [c|h] would match one character from the set c, |, and h. – Kusalananda Dec 05 '21 at 20:34
  • @they: ah, right, it should be find . -regex "\./.*\.[c,h]" – Arkadiusz Drabczyk Dec 05 '21 at 20:44
  • 1
    Just another re-spin of https://unix.stackexchange.com/questions/15308/how-to-use-find-command-to-search-for-multiple-extensions – Quasímodo Dec 05 '21 at 20:46
  • 1
    @ArkadiuszDrabczyk And [c,h] matches a comma. You mean (c|h), but that misses out on cpp, so you end up with (c|h|cpp). Unfortunately though, rugelar expressions are non-standard when used with find and we don't even know what Unix the user is using let alone what find implementation thay have. – Kusalananda Dec 05 '21 at 20:49

2 Answers2

11

The patterns used with the -name predicate in find are standard filename globbing patterns. What you are trying to use is a brace expansion, which find does not support.

Note that there is no single standard globbing pattern that matches filenames that end with .c, .h or .cpp.

You might have wanted to use something like '*.'{c,h,cpp}, which expands to *.c, *.h, and *.cpp, but that does not include the -name predicate nor the -o.

The next thing to try is '-o -name "*.'{c,h,cpp}'"', but this expands to the three strings -o -name "*.c" -o -name "*.h", and -o -name "*.cpp". This also can't be used as you would have to split them on spaces to get find to recognize the separate substrings (and remove the -o from the first one). It would possibly work with eval though, but it seems like more hassle than it's worth.

Instead of that, you may use two -name tests with an OR in-between:

find  "$directory" -type f \( -name '*.[ch]' -o -name '*.cpp' \) \
    -exec clang-format -i {} +

This uses two -name tests as described earlier (-o is the OR operator), and also calls clang-format as few times as possible by passing batches of found pathnames to the tool rather than invoking it once for each file.

With a tiny extra bit of programming, you may store all the filename suffixes that you want to pick up in a list, and create the needed find expression from that.

Since you did not mention what shell you're using, I'm doing this for the POSIX sh shell:

set -- c h cpp

for suffix do set -- "$@" -o -name "*.$suffix" shift done

shift # shifts off the initial "-o"

find "$directory" -type f ( "$@" ) -exec clang-format -i {} +

or

set --
for suffix in c h cpp; do
    set -- "$@" -o -name "*.$suffix"
done

shift

find "$directory" -type f ( "$@" ) -exec clang-format -i {} +

The list that "$@" expands to in this example would be the equivalent of

-name '*.c' -o -name '*.h' -o -name '*.cpp'
Kusalananda
  • 333,661
7

-name supports patterns that are not unlike shell globs, but it doesn't support braces (which the GNU find man page explicitly mentions), nor ksh-style extended globs.

But many finds support the -regex, and you may be able to use that. Depending on what regex dialects your find supports that is, as standard basic regexes don't support alternation.

With GNU find, this should work:

find . -regextype posix-extended -regex '.*\.(c|h|cpp)'

(Also note that the match there is a match against the whole path, not just the filename part, but if you're matching just the final suffix of the filename, that's not going to be an issue.)

ilkkachu
  • 138,973