2

I'm trying to use find to return all file names that have a specific directory in their path, but don't have another specific directory anywhere in the file path. Something like:

myRegex= <regex> 
targetDir= <source directory>
find $targetDir -regex $myRegex -print

I know I might also be able to do this by piping one find command into another, but I would like to know how to do this with a single regular expression.

For example, I want every file that has the directory "good" in it's path, but doesn't have the directory "bad" anywhere in its path no matter the combination. Some examples:

/good/file_I_want.txt #Captured
/good/bad/file_I_dont_want.txt #Not captured

/dir1/good/file_I_want.txt #Captured
/dir2/good/bad/file_I_dont_want.txt #Not captured

/dir1/good/dir2/file_I_want.txt #Captured
/dir1/good/dir2/bad/file_I_want.txt #Not captured

/bad/dir1/good/file_I_dont_want.txt #Not captured

Keep in mind some file names might contain "good" or "bad", but I only want to account for directory names.

/good/bad.txt #Captured
/bad/good.txt #Not captured

My research suggests I should use a Negative Lookahead and a Negative Lookbehind. However, nothing I have tried has worked so far. Some help would be appreciated. Thanks.

0

3 Answers 3

9

As Inian said, you don't need -regex (which is non standard, and the syntax varies greatly between the implementations that do support -regex¹).

You can use -path for that, but you can also tell find not to enter directories called bad, which would be more efficient than discovering every file in them for later filtering them out with -path:

LC_ALL=C find . -name bad -prune -o -path '*/good/*.txt' -type f -print

(LC_ALL=C so find's * wildcard doesn't choke on filenames with sequence of bytes not forming valid characters in the locale).

Or for more than one folder name:

LC_ALL=C find . '(' -name bad -o -name worse ')' -prune -o \
  '(' -path '*/good/*' -o -path '*/better/*' ')' -name '*.txt' -type f -print

With zsh, you can also do:

set -o extendedglob # best in ~/.zshrc
print -rC1 -- (^bad/)#*.txt~^*/good/*(ND.)
print -rC1 -- (^(bad|worse)/)#*.txt~^*/(good|better)/*(ND.)

Or for the lists in arrays:

good=(good better best)
bad=(bad worse worst)
print -rC1 -- (^(${(~j[|])bad})/)#*.txt~^*/(${(~j[|])good})/*(ND.)

To not descend into dirs called bad, or (less efficient like with -path '*/good/*' ! -path '*/bad/*'):

print -rC1 -- **/*.txt~*/bad/*~^*/good/*(ND.)

In zsh -o extendedglob, ~ is the except (and-not) globbing operator while ^ is the negation operator and # is 0-or-more-of-the-preceding-thing like regexp *. ${(~j[|])array} joins the elements of the array with |, with that | being treated as a glob operator instead of a literal | with ~.

In zsh, you'd be able to use PCRE matching after set -o rematchpcre:

set -o rematchpcre
regex='^(?!.*/bad/).*/good/.*\.txt\Z'
print -rC1 -- **/*(ND.e['[[ $REPLY =~ $regex ]]'])

But that evaluation of shell code for every file (including those in bad directories) is likely to make it a lot slower than other solutions.

Also beware that PCRE (contrary to zsh globs) would choke on sequences of bytes that don't form valid characters in the locale, and doesn't support multi-byte charsets other than UTF-8. Fixing the locale to C like for find above would address both for this particular pattern.

If you'd rather [[ =~ ]] only does extended regexp matching like in bash, you can also instead just load the pcre module (zmodload zsh/pcre) and use [[ -pcre-match ]] instead of [[ =~ ]] to do PCRE matching.

Or you could do the filtering with grep -zP (assuming GNU grep or compatible):

regex='^(?!.*/bad/).*/good/.*\.txt\Z'
find . -type f -print0 |
  LC_ALL=C grep -zPe "$regex" |
  tr '\0' '\n'

(though find still discovers all files in all bad directories).

Replace tr '\0' '\n' with xargs -r0 cmd if you need to do anything with those files (other than printing them one per line).


¹ In any case, I don't know any find implementation that supports perl-like or vim-like regular expressions which you'd need for look-around operators.

2
  • Thanks. How would you deal with more folder names than "good"? For example, can I match for both "good" and "goods" folders? What if I wanted to match "good" and "better"?
    – Teatree
    Commented Oct 28, 2021 at 2:19
  • @Teatree, see edit. Commented Oct 28, 2021 at 7:08
6

You don't need a regex for this, you can use the -path predicate to exclude directories with a specific name at any level

find . -type f -path '*/good/*' '!' -path '*/bad/*'
3
  • 1
    better enclose the bang between single quotes to avoid the shell's (bash?) expansion : '!' Commented Oct 28, 2021 at 9:59
  • 1
    Or escape it: \! (one less keystroke if you're typing) Commented Oct 29, 2021 at 8:03
  • useful suggestions! updated
    – Inian
    Commented Oct 29, 2021 at 8:03
2

While it's likely less-efficient (though I'm not certain!) and less "correct" than find's powerful filtering (for example naive grep here will not work for names containing newline chars, though these are extremely rare and normally represent an error), it's often much easier to stack a few instances of grep which successively filter the results using simpler matches and inverse matches -v

This takes more caution around substrings to ensure you're really finding a directory name, but generally will give a much easier syntax to understand and may do all that you need!

find ./ | grep "/good/" | grep -v "/bad/" | grep '\.txt$'
4
  • oh, TMYK! cheers, let met tidy that up
    – ti7
    Commented Oct 27, 2021 at 15:55
  • cheers! I've updated my Answer to incorporate both; specifically, I want to convey the idea that this is both not as good, but the perfection often doesn't matter if the environment can be asserted or assumed not to have some collection of (evil) edge cases
    – ti7
    Commented Oct 27, 2021 at 16:01
  • 2
    Thanks. It's perfectly acceptable to post answers with limitations as long as they are enunciated so the reader can take an educated decision about it. Welcome to Unix.SE btw! Commented Oct 27, 2021 at 16:08
  • Here, as I showed in my answer, one usual way to deal with file arbitrary file paths with text utilities if on GNU systems at least is to use NUL-delimited records instead of newline delimited records as NUL contrary to NL can't be found in a file path. Commented Oct 27, 2021 at 16:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .