Skip to main content

Glob Patterns Explained: Test * ? ** [] {} Against Real Paths

A practical guide to glob patterns — what * ? ** [] {} actually match, how they differ from regex, and how to test src/**/*.ts against real file paths.

Published By Li Lei
#glob #gitignore #regex #developer-tools #file-matching

Glob Patterns Explained: Test ? * [] {} Against Real Paths

Most build failures I have chased down lived in a single line of a config file: a glob that looked right and matched the wrong set of files. A .gitignore rule that did not catch the artifact. An ESLint include that quietly skipped a nested folder. A CI path filter that ran a job on every docs-only commit. None of these throw an error. They just silently match too much or too little, and you find out later.

This guide walks through the full glob vocabulary — *, ?, **, [], and {} — what each one matches, where it bites people, and how globs differ from regular expressions. The fastest way to settle any of these is to paste a pattern and a list of paths into the Glob Pattern Tester and watch each line flip to matched or not, but knowing the rules first means you write the right pattern on the first try.

What each glob symbol matches

A glob is a compact language for matching file paths. Five symbols do almost all the work:

  • * matches any run of characters inside one path segment. It stops at a slash. So *.ts matches index.ts but not src/index.ts.
  • ? matches exactly one character, and like * it never crosses a slash. file?.txt matches file1.txt and fileA.txt, but not file.txt (zero characters) or file12.txt (two).
  • ** matches across directory levels at any depth. This is the single most important distinction in the whole syntax, and the next section is devoted to it.
  • [] is a character class: [a-z] matches one lowercase letter, [jp] matches j or p, and [!0-9] matches one character that is not a digit.
  • {} is alternation: app.{js,ts} matches app.js or app.ts, exactly as if you had written two separate patterns.

That is the entire toolkit. Combine them and you can target almost any file set: src/{components,pages}/**/*.{jsx,tsx} reads as "any .jsx or .tsx file at any depth under src/components or src/pages."

The one rule that catches everyone: does not cross directories, * does

Here is the concrete point worth tattooing on the inside of your eyelids: a single star stays inside one path segment, while a double star walks across directory levels.

src/*.ts matches src/index.ts but skips src/utils/date.ts, because the * cannot jump over the slash between utils and date.ts. The moment your files can nest, * is the wrong tool. src/**/*.ts is the fix: the ** segment absorbs zero or more directories, and the trailing *.ts matches the filename wherever you land.

The other half of this rule is that ** needs its own path segment to do its job. Writing src/**.ts does not walk directories the way you expect — the directory-crossing form is src/**/*.ts, with the double star and the file pattern separated by a slash. If a pattern is misbehaving, this gap between **.ts and **/*.ts is the first thing to check.

A worked example: src/*/.ts

Let me trace src/**/*.ts against a realistic file list, the way I do when a build include looks wrong:

src/index.ts        → matched   (** absorbs zero directories)
src/utils/date.ts   → matched   (** absorbs one directory: utils)
src/a/b/c.ts        → matched   (** absorbs two directories: a, b)
src/styles.css      → not matched (wrong extension)
lib/helper.ts       → not matched (root is fixed to src/)
README.md           → not matched (not under src, not .ts)

Read the pattern left to right. src/ anchors the root, so anything outside src — like lib/helper.ts — is out immediately. The **/ means "zero or more directory levels." The *.ts means "any single filename ending in .ts in whatever folder you ended up in." That is why src/index.ts matches with zero extra folders and src/a/b/c.ts matches with two. The first time I switched a build config from src/*.ts to src/**/*.ts and watched every nested file flip from skipped to matched in the tester, the bug that had been failing my pipeline for an afternoon was just gone — the linter had never been seeing the nested files at all.

Glob is not regex (and the equivalent regex proves it)

People who know regular expressions often over-think globs, because the symbols overlap but the meanings do not. Three traps stand out:

  • A dot is literal in a glob. *.txt means "anything, then a literal .txt." In regex, . is any character — but not here.
  • There is no anchoring with ^ and $. A glob matches the whole path by structure, not by anchors you add.
  • A + is a literal plus, a ( is a literal paren. None of the regex quantifiers and grouping carry over.

The clearest way to internalize this is to look at what a glob compiles into. The tester shows the exact equivalent regular expression beside the results, so **/*.ts reveals a (?:.*/)? prefix — and that optional group is precisely why a top-level index.ts matches with no directories at all. If you live in regex and want to convert in the other direction or just sanity-check a pattern, the Regex Tester runs the raw expression against your strings directly.

Where you actually use globs

Globs are everywhere in a modern repo, which is exactly why getting them right pays off:

  • .gitignore and .dockerignoredist/**, *.log, **/node_modules. A rule anchored at the root like dist/** will miss build/dist/x.js; you often want **/dist/** instead. If you are starting from scratch, generate a baseline with the .gitignore Generator and then test your custom additions against real leaking paths.
  • Build tool includes and excludes — Vite, Webpack, ESLint, and Prettier all take glob lists. This is where the src/*.ts vs src/**/*.ts mistake quietly drops half your files.
  • CI path filters — workflows that decide which jobs run on a pull request. A filter that is too loose runs the full build on a docs-only commit and burns CI minutes; too tight and a real change skips its tests.
  • Shell expansion — your terminal expands *.png before the command ever sees it.

In every one of these, the cost of a wrong glob is a failed or wasted build, and the fix is the same: confirm the match set against real paths before you commit the config, not after the pipeline goes red.

The fastest loop

The whole point of testing a glob is to close the gap between what you think a pattern matches and what it actually matches. Paste the glob on one side, the candidate paths on the other, and read the verdict line by line in the Glob Pattern Tester. When a line surprises you, the equivalent-regex panel explains why. It runs entirely in your browser, so even a list of internal paths never leaves the tab — and the shareable link reopens your exact pattern and path list for whoever you are arguing with about whether dist/** covers that one stray file. It does not, by the way. Test it and see.


Made by Toolora · Updated 2026-06-13