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.

5
  • If your implementation of find supports -regex you can do find . -regex "\./.*\.[c|h]" Dec 5 '21 at 20:21
  • @ArkadiuszDrabczyk Note that [c|h] would match one character from the set c, |, and h.
    – they
    Dec 5 '21 at 20:34
  • @they: ah, right, it should be find . -regex "\./.*\.[c,h]" Dec 5 '21 at 20:44
  • 1
    Just another re-spin of unix.stackexchange.com/questions/15308/…
    – Quasímodo
    Dec 5 '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.
    – they
    Dec 5 '21 at 20:49
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'
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.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.