Skip to content

Fix inverse extended glob followed by a nested extended glob - #245

Merged
facelessuser merged 2 commits into
facelessuser:mainfrom
spicysharkco:fix/inverse-group-nested-group
Oct 18, 2026
Merged

facelessuser merged 2 commits into
facelessuser:mainfrom
spicysharkco:fix/inverse-group-nested-group

Conversation

@spicysharkco

@spicysharkco spicysharkco commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

fnmatch.fnmatch('xb', '!(a)@(b|*(c))', flags=fnmatch.EXTMATCH) raises re.error: missing ), unterminated subpattern at position 5. glob.globmatch('xb', '!(a)@(b|*(c))', flags=glob.EXTGLOB) raises the same error. The pattern is valid and xb is x followed by b, so both should return True, as Bash does. The latest release, 11.1, has the same error. It is the nested form of #218, which was fixed in #219 for !(2)_@(foo|bar).

An inverse group stays open in the regular expression until the text after it is known. When a nested extended group finishes, clean_up_inverse sets self.inv_ext = 0 while the outer inverse group is still open. The final cleanup at the end of the pattern then returns early and never closes it.

This removes that reset and has clean_up_inverse lower self.inv_ext by one for each inverse group it closes.

I added three rows to TestFnMatch.cases and two to TestGlobMatch.cases. Without the fix, pytest tests/test_fnmatch.py gives 3 failed, 200 passed, 1 skipped and pytest tests/test_globmatch.py gives 2 failed, 655 passed, 63 skipped. With it, they give 203 passed, 1 skipped and 657 passed, 63 skipped. The full suite passes with the fix. There is also a changelog line under Unreleased.

A pattern such as `!(a)@(b|*(c))` raised `re.error: missing ),
unterminated subpattern` when the EXTMATCH or EXTGLOB flag was set.

An inverse group is left open in the regular expression until the text
that follows it is known. Finishing the nested group zeroed the count of
open inverse groups, so the final cleanup for the outer group returned
early and never closed it. Stop resetting the count there.
@gir-bot gir-bot added S: needs-review Needs to be reviewed and/or approved. C: docs Related to documentation. C: fnmatch Fnmatch library. C: glob Glob library. C: pattern-parser Related to pattern parsing. C: source Related to source code. C: tests Related to testing. labels Oct 18, 2026
@facelessuser

Copy link
Copy Markdown
Owner

@spicysharkco, I believe the more appropriate approach is to decrement self.inv_ext inside the loop.

        index = len(current) - 1
        while index >= 0:
            if isinstance(current[index], InvPlaceholder):
                content = current[index + 1:]
                if not nested:
                    content.append(_EOP if not self.pathname else self.path_eop)
                current[index] = (
                    (''.join(content).replace('(?#)', '?:') if self.capture else ''.join(content)) +
                    (_EXCLA_GROUP_CLOSE.format(str(current[index])))
                )
                self.inv_ext -= 1
            index -= 1

When `clean_up_inverse` closes an inverse group it now lowers `inv_ext`
by one. A nested cleanup then leaves the count for a still open outer
group alone, and the final cleanup at the end of the pattern still runs.
@spicysharkco

Copy link
Copy Markdown
Contributor Author

@facelessuser, done in 88b57cf: clean_up_inverse now decrements self.inv_ext for each inverse group it closes, and the reset stays removed. Translated regexes are identical to the previous commit across 15,078 generated pattern and flag combinations.

@facelessuser

Copy link
Copy Markdown
Owner

@gir-bot lgtm

@gir-bot gir-bot added S: approved The pull request is ready to be merged. and removed S: needs-review Needs to be reviewed and/or approved. labels Oct 18, 2026
@facelessuser
facelessuser merged commit 4cee92d into facelessuser:main Oct 18, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: docs Related to documentation. C: fnmatch Fnmatch library. C: glob Glob library. C: pattern-parser Related to pattern parsing. C: source Related to source code. C: tests Related to testing. S: approved The pull request is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants