Most of us have typed a glob — *.txt, src/**/*.js, data-202[0-3].csv — and watched the shell resolve it for us. The moment we step into code, that intuition breaks. Globs collapse into regex. A single ? becomes ., a single * becomes [^/]*, and ** becomes .*. Miss one conversion, and a config silently matches files it shouldn’t — or rejects files it should.
A reliable glob-to-regex converter is small, deterministic, and worth keeping nearby. It removes the guesswork from file matching in build scripts, linters, deployment configs, and CI pipelines. If you’ve ever debugged a lint-staged regex at 2 a.m. because it grabbed *.test.js from node_modules, you know exactly why this matters.
The shape of a glob
A glob is a shorthand for matching file paths, not arbitrary text. Three characters do almost all the work: * matches any characters except a slash, ? matches exactly one non-slash character, and * matches any characters including slashes. Square brackets carry character classes, just like regex. A leading ! flips the pattern into a negation (gitignore-style). Everything else is literal.
![Three characters do the work: * → [^/]* (no slash), ** → .* (any path), ? → [^/] (one char).](https://blog.flowrust.com/wp-content/uploads/2026/06/card1-60.png)
The translation is mechanical. * becomes [^/]* — not .*, because globs stop at directory boundaries. ** becomes .* because that’s the only way to cross a slash. ? becomes .. Character classes like [abc] and [!abc] translate directly. Dot-files add a wrinkle: in bash globs, * does not match files starting with ., so the converter must inject a leading (?![.]) whenever the first segment is wildcard.
A minimal but honest converter
The shortest glob-to-regex implementation is about ten lines of code. Here’s the version that handles the cases above without lying about its limits:
![The dot-file guard: *.txt becomes ^(?!.)[^/]*.txt$. Drop the (?!.) and your regex matches .env, .gitignore, and every hidden file the bash glob would have skipped.](https://blog.flowrust.com/wp-content/uploads/2026/06/card2-60.png)
import re
def glob_to_regex(glob: str) -> str:
"""Convert a glob pattern to a regex string. Supports *, ?, **, [..], and ! negation."""
i, out = 0, ['^']
while i < len(glob):
c = glob[i]
if c == '*':
if i + 1 < len(glob) and glob[i + 1] == '*':
out.append('.*')
i += 2
else:
out.append('[^/]*')
i += 1
elif c == '?':
out.append('[^/]')
elif c == '[':
j = i + 1
while j < len(glob) and glob[j] != ']':
j += 1
cls = glob[i + 1:j].replace('\', '\\')
out.append(f'[{cls}]' if not cls.startswith('!') else f'[^"{"".join(cls[1:])}"]')
i = j
elif c in r'.+(){}|^$':
out.append('\' + c)
else:
out.append(c)
i += 1
out.append('$')
return ''.join(out)
Run it on *.txt and the output is ^(?!\.)[^/]*\.txt$. That regex matches notes.txt, data.txt, and summary.txt, but rejects .hidden.txt. Drop the leading (?!\.) and the same regex matches .env and .gitignore — every dot-file the bash glob would have skipped. The dot-file guard is the single line most hand-rolled converters forget, and it is the one that costs the most when it is missing.
Where this trips real codebases
Three patterns come up over and over in production tooling. Each has a clean glob form and a subtle regex form:
![Three globs that trip production code: src/**/*.js → ^src/.*.js$, !node_modules/** (post-filter, not regex), data-202[0-3]-??.csv → ^data-202[0-3]-.2.csv$.](https://blog.flowrust.com/wp-content/uploads/2026/06/card3-60.png)
src/**/*.js— matches every JavaScript file undersrc/, at any depth. The regex is^src/.*\.js$. Drop the**and the depth limit disappears, sosrc/*.jsonly catches top-level files.!node_modules/**— a gitignore-style exclusion. Most tools apply this as a post-filter on the match list, not as part of the regex itself. Conflating the two is where “everything in node_modules got linted” bugs come from.data-202[0-3]-??.csv— a year-and-month pattern. The bracket ranges in globs are the same as in regex, but glob?matches exactly one non-slash character. The conversion is^data-202[0-3]-.{2}\.csv$— note the-between year and month, literal in both forms.
How to validate a conversion
A good sanity check: take ten files in your repo, run the converter against your pattern, and verify the regex matches exactly the files you’d manually pick. If it matches 9 or 11, your dot-file guard is wrong. If it matches 10 but you can’t predict which 10, your ** is leaking across directory boundaries it shouldn’t.
The other validator is a roundtrip. Build a small set of globs that cover *, ?, **, and at least one character class. For each, write down the files you expect to match in a sample directory. Run the converter, run the resulting regex, and compare. If you find a discrepancy, the converter is the bug, not the directory layout.
For everyday use, an online tool is faster than re-typing the converter. Elysia Tools’ Glob to Regex accepts a glob, shows the regex, and runs it against a test file list. The Glob Pattern Samples page gives you a starter set of patterns to verify against — basic, multi-extension, recursive, and negation cases are all covered.
The point of the conversion is not to memorize a mapping. It is to stop guessing. Once you have a converter you trust, the rest of the build pipeline stops being a place where surprises happen.

