You can't with grep alone. From info -- grep --exclude on a GNU system (emphasis mine):
--exclude=GLOB
Skip any command-line file with a name suffix that matches the pattern GLOB, using wildcard matching; a name suffix is either the whole name, or a trailing part that starts with a non-slash character immediately after a slash (‘/’) in the name. When searching recursively, skip any subfile whose base name matches GLOB; the base name is the part after the last slash. A pattern can use ‘*’, ‘?’, and ‘[’...‘]’ as wildcards, and ‘\’ to quote a wildcard or backslash character literally.
Same applies to --exclude-dir, which can only exclude directories based on their base name (here lib), not their full path relative to the starting point (build/lib).
Use find as you would with grep implementations that don't support those -r/--exclude non-standard GNU extensions.
To skip the whole of build/lib altogether (not even descend into it):
find . -path ./build/lib -prune -o -type f -size +4c \ -exec grep Hello /dev/null {} +
Or if it's only the *.py files in there you want to exclude:
find . ! -path './build/lib/*.py' -type f -size +4c \ -exec grep Hello /dev/null {} +
Or:
find . -path './build/lib/*.py' -prune -o -type f -size +4c \ -exec grep Hello /dev/null {} +
To also exclude ./build/lib/dir.py/any/file/underneath/that/misleadingly-named/directory.
/dev/null is to make sure the file path is printed even if there's only one of them passed; with GNU grep or compatible, you can use the -H option instead.
-size +4c as an optimisation to skip those files with 4 or fewer bytes which can't possibly contain Hello. Will likely not gain much if any, so can be omitted, but shows that using the right tool for the task (find to find files, grep to grep them) allows you to be more thorough in your filter criteria.
-path pattern does a fnmatch() pattern matching against each file path while **/ is a zsh globbing operator (now supported by some other shells though often not by default). Both fnmatch("./build/lib/*.py", "./build/lib/file.py", flags) and fnmatch("./build/lib/*.py", "./build/lib/dir/file.py", flags) will return true as * matches any sequence of characters, / included. fnmatch("./build/lib/**/*.py", "./build/lib/file.py", flags) where ** is interpreted the same as * would return false. If you wanted to only exclude .py files in build/lib and not those in subdirs, you'd need ! '(' -path './build/lib/*.py' ! -path './build/lib/*/*.py' ')' or use the -regex non-standard extension of some find implementations: ! -regex '\./build/lib/[^/]*\.py'.
In zsh, you could do:
set -o extendedglob # best in ~/.zshrc grep -- Hello /dev/null **/*~build/lib/*.py(D.L+4)
But note that:
- you may run into the
execve() limit on the size of arguments (though the zargs function could be used to work around that). - the list of files is sorted. If you don't care about the order, you can add the
oN qualifier to skip that sorting. - it makes it easier to skip hidden files: just remove the
D qualifier. - it avoids a
./ prefix being added to file paths (which is also why we need that --; you can change the glob to ./**/*~./build... if you do want that ./ prefix). - with some
find/fnmatch() implementations, ! -path './build/lib/*.py' may fail to skip some .py files if part of their path cannot be decoded as valid text in the user's locale. zsh globs don't have that issue.
find.