Skip to content

Handle backslash escapes in gitignore bracket expressions - #157

Merged
cpburnz merged 1 commit into
cpburnz:masterfrom
gyanu2507:fix/bracket-escapes
Oct 10, 2026
Merged

cpburnz merged 1 commit into
cpburnz:masterfrom
gyanu2507:fix/bracket-escapes

Conversation

@gyanu2507

Copy link
Copy Markdown
Contributor

A backslash inside a gitignore bracket expression escapes the next character in git, the same as it does outside one. pathspec treats it as a literal backslash instead, so several valid patterns match the wrong files:

pattern git matches pathspec matched
[a\-c] a, -, c a, b, c, \, ], ^ (read as the range \-c)
[\]] ] \]
[\]a] ], a nothing
[!\]] any one character except ] ]]
[\n] n n, \

The git column comes from git ls-files -o -i --exclude-standard in a scratch repo with core.ignorecase=false (git 2.50.1). ignore on npm agrees with git on all of these.

Cause

_translate_segment_glob() had two problems with escapes inside [...]:

  • The scan for the closing ] didn't skip escaped characters, so [\]] ended at the escaped ].
  • The bracket body was built with pattern[i:j].replace('\\', '\\\\'), which turned every backslash into a literal backslash member of the set.

Fix

  • The closing bracket scan now steps over \ and the character after it.
  • The bracket body is translated with one regex that matches either a POSIX class token or an escaped character, left to right. An escaped character becomes re.escape(char). The existing _translate_posix_class() still handles classes. Because escapes are consumed in the same pass, [\[:alpha:]] is the literal set [, :, a, l, p, h followed by ], which is what git does too.

This lives in the shared base translator, so it applies to both GitIgnoreSpecPattern and GitIgnoreBasicPattern.

Tests

Added test_13_bracket_escape to tests/test_04_gitignore_spec.py and test_14_bracket_escape to tests/test_03_gitignore_basic.py. Every expected result was checked against git first.

python -m unittest \
  tests.test_04_gitignore_spec.GitIgnoreSpecPatternTest.test_13_bracket_escape \
  tests.test_03_gitignore_basic.GitIgnoreBasicPatternTest.test_14_bracket_escape

Without the fix, 32 subtests fail. With it, both tests pass.

The full suite passes with python -m unittest on Python 3.13 with hyperscan and google-re2 installed (243 tests, 1 skipped). I also ran [a\-c], [\]], [!\]], [\[:alpha:]], [\#x] and [\ a] through GitIgnoreSpec and PathSpec('gitignore') on the simple, re2 and hyperscan backends, and all three give git's answers. mypy and pyright output is unchanged from master.

Out of scope: reversed ranges such as [z-a] or [a-\]] are still discarded the same way as on master. git matches the start character in that case, which is a separate difference.

@cpburnz

cpburnz commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Thanks for finding and fixing this!

@cpburnz
cpburnz merged commit 2116932 into cpburnz:master Oct 10, 2026
7 checks passed
cpburnz added a commit that referenced this pull request Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants