3

It's an zsh extended glob pattern. The point is to match all files with a pattern under a directory hierarchy with the exception of specific sub-hierarchies. The pattern in the title works, but I wanted something that doesn't require repeating long/path/.

I tried the following:

long/path/(^(bad-1|bad-2)/|)**/^*.(complex|pattern)(.)
long/path/(**~(bad-1|bad-2)/*)/^*.(complex|pattern)(.)

with KSH_GLOB added:

long/path/?(^(bad-1|bad-2)/)**/^*.(complex|pattern)(.)

They all result in a bad pattern error. It seems the problem is having a / in parentheses. The documentation says:

As mentioned above, a `/' used as a directory separator may not appear inside parentheses

That confused me because (dir/)# is a good pattern. It's even mentioned in the docs:

A pathname component of the form `(foo/)#' matches a path consisting of zero or more directories matching the pattern foo.

I guess it's the exception.

To be clear, I'm looking for equivalents. That excludes:

long/path/^(bad-1|bad-2)/**/^*.(complex|pattern)(.)

since it excludes things like long/path/good.pattern, and

long/path/(^(bad-1|bad-2)/)#/^*.(complex|pattern)(.)
long/path/**/^*.(complex|pattern)~*/(bad-1|bad-2)/*(.)

since those exclude things like what's under long/path/good/bad-1.

EDIT: Just to mention, a solution doesn't necessarily need to use only globbing. If it can be done by adding use of brace or history expansion or some other zsh facility, that's great.

EDIT 2: Following my own ideas, I tried history expansion:

long/path/**/^*.(complex|pattern)~!{#$:s/\**//}/(bad-1|bad-2)/*(.)

However, this failed because 1) the last word of the current line is the one prior to the one that the expansion is in, and 2) because history substitution seems to be done after glob expansion (I must be misunderstanding something here because that seems to go against the docs; maybe all expansions are done on a word by word basis).

I also tried brace expansion:

( IFS=\~; printf "%s\n" long/path/{**/^*.(complex|pattern),(bad-1|bad-2)/*(.)} )

It didn't work. The 2 glob patterns were still taken as separate words instead of being joined into a single pattern. I guess IFS isn't taken into consideration here. Makes sense.

I also tried substitution in glob qualifiers:

+/**/^*.(complex|pattern)~+/(bad-1|bad-2)/*(.:s,+,long/path,)

But, of course, that wouldn't work because substitution would be done after matching not before.

2 Answers 2

3

Got it! Double slashes to mark the prefix:

long/path//**/^*.(complex|pattern)~*//(bad-1|bad-2)/*(.)

Sometimes you don't realize something is a feature until you find a use for it. I can't believe the fact that contiguous slashes are equivalent to one can actually be this useful. :)

0

The docs for (...) list / as an exception:

Note that grouping cannot extend over multiple directories: it is an error to have a `/' within a group (this only applies for patterns used in filename generation). There is one exception: a group of the form (pat/)# appearing as a complete path segment can match a sequence of directories.

The repetition can otherwise be avoided by using an expression to filter the results:

% print -l a/**/*(.)
a/b/c/foo/file
a/b/d/foo/file
a/bad/e/foo/file
a/nope/d/foo/file
% print -l a/**/*(.e:'[[ $REPLY != *(bad|nope)* ]]':)
a/b/c/foo/file
a/b/d/foo/file
1
  • I see now that the documentation is not inconsistent. Your solution, however, is like the examples I gave that would exclude things like long/path/good/bad-1, or in your case, something like a/b/bad/foo/file. It still does require repeating to be equivalent: a/**/*(.e:'[[ $REPLY != a/(bad|nope)/* ]]':).
    – JoL
    Aug 18, 2018 at 1:19

You must log in to answer this question.

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