Glob Pattern Tester: Validate Wildcards and .gitignore Rules
Free glob and .gitignore pattern tester — see exactly which rule matched each path, including which negation won. No sign-up, runs in your browser.
Try it now: Glob Pattern Tester — Test glob and .gitignore patterns against a list of paths and see exactly which rule matched each one — including which negation won.
What a Glob Pattern Actually Matches
A glob pattern is a small notation for matching file paths — simpler than regex, and built around exactly one job: describing a set of files without listing them. Four pieces of syntax cover almost every glob pattern you'll ever write. * matches any run of characters except a path separator — *.log matches app.log but not logs/app.log, because the slash stops it. ** is the one people confuse with *: it matches across directory boundaries recursively, so src/**/*.test.ts reaches a test file at any depth under src, not just one level down. ? matches exactly one character — file?.txt matches file1.txt but not file10.txt. And a character class, [abc], matches any single character listed inside the brackets, with [a-z] and [^abc] (negated) as the two variants worth knowing.
That's the whole core vocabulary. A glob matcher is deliberately less expressive than regex — no backreferences, no lookahead, no alternation beyond brace expansion in some implementations — and that restriction is the point: it keeps a path pattern readable at a glance, which matters because most globs are read far more often than they're written.
Why .gitignore Rules Aren't Just Plain Globs
A .gitignorefile uses glob syntax, but Git layers three extra rules on top of it that a plain shell glob doesn't have — and all three are common sources of a rule that silently does the wrong thing.
- No slash means match at any depth. A pattern with no
/in it —*.log,node_modules— isn't anchored to the repository root. It matches a file or directory with that name anywhere in the tree, at any depth, exactly as if**/had been prepended. A pattern that does contain a/— even a trailing one — is anchored relative to the location of the.gitignorefile instead. - A trailing slash means “directories only.”
build/ignores a directory namedbuildand everything inside it, but will never match a plain file namedbuild. Drop the trailing slash and the pattern matches either a file or a directory with that name — worth being deliberate about, since the two forms aren't interchangeable. - Order matters, and later rules override earlier ones. Rules are evaluated top to bottom, and a later pattern in the same file — including one that comes from a different
.gitignorein a nested directory — can override an earlier one. A!prefix negates a pattern, re-including something an earlier rule excluded. But negation has one sharp edge, covered next, that trips up more real repositories than any other.gitignorerule.
The Negation Rule That Actually Trips People Up
The single most commonly misunderstood rule in .gitignore syntax: a !negation pattern cannot re-include a file if that file's parent directorywas already excluded by an earlier rule. This isn't a quirky edge case — it's a direct consequence of how Git walks the tree. Once a directory matches an ignore rule, Git never descends into it to look for anything at all, negation patterns included. There's nothing to re-include, because the file inside was never examined in the first place.
*.log node_modules/ dist/ !dist/README.md
app.log → ignored (matches *.log)
node_modules/react/index.js → ignored (matches node_modules/, any depth, no leading slash needed)
dist/bundle.js → ignored (matches dist/)
dist/README.md → ignored (!dist/README.md never fires — dist/ was already excluded,
so Git never looks inside it to find this rule)
src/README.md → not ignored (no rule matches this path)dist/README.md is the line worth staring at. It looks re-included on paper — the negation rule is right there — but it stays ignored, because the rule that would un-ignore it is inside a directory Git already skipped. The fix, when this is actually the intent, is to un-ignore the directory itself first (!dist/) and then exclude everything inside it except the one file you want kept — reversing which rule does the excluding, rather than trying to negate your way back in from outside.
Why 'Which Rule Matched' Beats a Yes/No Answer
A real .gitignoreaccumulates dozens of rules over a project's lifetime — one per framework, one per editor, a few pasted in from a template, a few added in a hurry to unblock a commit. When a file turns up unexpectedly tracked, or unexpectedly ignored, “does this path match some pattern in the file” is the wrong question to be asking. A plain yes/no doesn't tell you whichof the dozens of lines is responsible, and it can't tell you whether a later negation overrode an earlier exclusion — which is exactly the information you need to fix the rule instead of guessing at it.
That's the specific gap GenKitLab's Glob Pattern Tester is built to close: test glob and .gitignore patterns against a list of paths and see exactly which rule matched each one — including which negation won. Paste your actual .gitignore and a list of paths from your repository, and each one comes back labeled with the specific line number that decided its fate, not just a matched-or-not verdict. It runs entirely client-side — nothing you paste is uploaded anywhere, the same way every tool on GenKitLab handles input.
Glob and regex solve overlapping but distinct problems: glob is a shorthand purpose-built for file paths, while regex is a general pattern language for arbitrary text with far more expressive power and far more ways to get subtly wrong. If the pattern you're debugging is matching text rather than a file path, the Regex Tester guide and Regex Tester are the right tool for that job instead.
Frequently asked questions
›What's the difference between * and ** in a glob pattern?
* matches any run of characters except a path separator, so it stays within a single directory level. ** matches across directory boundaries recursively, reaching files at any depth. src/*.ts matches a file directly in src; src/**/*.ts matches one at any depth under src.
›Does a .gitignore pattern with no slash only match the root?
No — that's a common assumption and it's backwards. A pattern with no slash in it, like *.log or node_modules, matches at any depth in the tree, as if **/ had been added in front of it. Only a pattern that contains a slash is anchored to a specific location.
›What does a trailing slash mean in .gitignore?
It restricts the pattern to directories only. build/ ignores a directory named build and everything inside it, but never matches a plain file named build. Without the trailing slash, the pattern matches either a file or a directory with that name.
›Why doesn't my !negation rule in .gitignore re-include a file?
Because Git never descends into a directory that an earlier rule already excluded, negation patterns inside that directory are never evaluated in the first place. If dist/ is ignored, !dist/README.md won't un-ignore that file — there's no rule to apply, since Git skipped the directory before ever looking for one. Un-ignoring the directory itself first is the usual fix.
›How do I test a .gitignore file against real paths?
Paste the .gitignore rules and a list of paths into a glob matcher that shows, per path, which specific rule matched — not just whether it matched. GenKitLab's Glob Pattern Tester does this client-side, labeling each path with the winning rule, including which negation overrode an earlier exclusion.
›Is glob the same as regex?
No. Glob is a narrower, purpose-built notation for matching file paths, with a handful of tokens (*, **, ?, character classes). Regex is a general-purpose pattern language for matching arbitrary text, far more expressive and far more prone to subtle mistakes. Use glob for paths and .gitignore rules; use regex when the pattern needs to describe text content instead.
Last updated