tinyglobby icon indicating copy to clipboard operation
tinyglobby copied to clipboard

`deep` option behaves differently from fast-glob

Open outslept opened this issue 4 months ago • 4 comments

tinyglobby produces different results than fast-glob for some patterns when deep is set. Reproducible on win10 and archlinux (via WSL).

  1. My branch - https://github.com/outslept/tinyglobby/tree/fix/deep-option
  2. Test file - https://github.com/outslept/tinyglobby/blob/fix/deep-option/test/deep.test.mjs

Globstar with deep = 1 returns extra files compared to fast-glob. Parent-directory patterns (../**/*.txt) miss matches compared to fast-glob.

Thoughts:

  • normalizePattern moves the root up, but the processed match stays relative to the original cwd (i.e., it still contains the ../ prefix). The walker crawls from root=parent (producing paths like child/deep.txt), while the matcher expects paths relative to cwd (../child/deep.txt). That mismatch (root vs cwd) leads to no matches.
  • The calculatedMaxDepth looks correct in logs, so it shouldn't be an arithmetic issue I guess

outslept avatar Aug 17 '25 04:08 outslept

does this only happen in main? does it also happen in 0.2.14?

SuperchupuDev avatar Aug 18 '25 13:08 SuperchupuDev

UPD: This might be a smaller repro (test from branch fails as well.. sadly)

import fs from 'node:fs';
import path from 'node:path';
import { tmpdir } from 'node:os';
import fg from 'fast-glob';
import { glob } from 'tinyglobby';

const testDir = path.join(tmpdir(), 'glob-test-' + Date.now());
fs.mkdirSync(testDir, { recursive: true });
fs.mkdirSync(path.join(testDir, 'dir1'), { recursive: true });
fs.mkdirSync(path.join(testDir, 'dir1', 'dir2'), { recursive: true });

fs.writeFileSync(path.join(testDir, 'root.txt'), 'root');
fs.writeFileSync(path.join(testDir, 'dir1', 'file1.txt'), 'file1');
fs.writeFileSync(path.join(testDir, 'dir1', 'dir2', 'file2.txt'), 'file2');
fs.writeFileSync(path.join(testDir, '.hidden.txt'), 'hidden');

console.log('**/*.txt deep: 0');
console.log('tiny:', await glob('**/*.txt', { deep: 0, cwd: testDir }));
console.log('fast:', await fg('**/*.txt', { deep: 0, cwd: testDir }));

console.log('**/*.txt deep: 1');
console.log('tiny:', await glob('**/*.txt', { deep: 1, cwd: testDir }));
console.log('fast:', await fg('**/*.txt', { deep: 1, cwd: testDir }));

console.log('**/*.txt deep: 2');
console.log('tiny:', await glob('**/*.txt', { deep: 2, cwd: testDir }));
console.log('fast:', await fg('**/*.txt', { deep: 2, cwd: testDir }));

console.log('*.txt deep: 0');
console.log('tiny:', await glob('*.txt', { deep: 0, cwd: testDir }));
console.log('fast:', await fg('*.txt', { deep: 0, cwd: testDir }));

console.log('**/.*.txt deep: 1');
console.log('tiny:', await glob('**/.*.txt', { deep: 1, cwd: testDir }));
console.log('fast:', await fg('**/.*.txt', { deep: 1, cwd: testDir }));

fs.rmSync(testDir, { recursive: true });
Image

outslept avatar Aug 18 '25 13:08 outslept

hmm, so according to the results, fast-glob treats deep: 0 the same as deep: 1?

SuperchupuDev avatar Aug 18 '25 15:08 SuperchupuDev

fast-glob

fast‑glob’s rule is straightforward and, once you get it, comforting in its consistency:

  • At each directory, compute the directory’s level (number of path segments relative to the task base).
  • If level >= deep, stop descending into that directory.

Short and mathematical. But let me put it another way, because metaphors help.

Imagine you’re on floor N of a building. The deep option is the ceiling level. If the ceiling is floor 2, you are not allowed to go to floor 2 or above. You can be at floor 1 and go no further down. That’s the rule.

Consequence (the thing people may trip on)

  • deep = 0 → don’t enter any subdirectory at all.
  • deep = 1 → still don’t enter first-level subdirectories (because their level is 1, and 1 >= 1).
  • deep = 2 → you may enter first-level directories, but not second-level ones.

So deep = 0 and deep = 1 may look the same. That’s by design.

tinyglobby

tinyglobby’s implementation tries two things:

  1. It tries to find a crawl root by analyzing patterns (normalizePattern). If you have patterns like ../foo/**, tinyglobby can move the crawl root up to the parent directory and adjust an internal depthOffset to reflect how many parent steps were consumed.
  2. It maps the user’s deep option to fdir’s maxDepth by computing: fdirOptions.maxDepth = Math.round(options.deep - props.depthOffset);

tinyglobby’s mapping makes deep act like a “budget of steps you can take from the walker’s root.” That means deep = 1 lets you take one step down — first-level directories are crawled. That’s one floor deeper than fast‑glob’s ceiling semantics.

Analogy: tinyglobby had a handful of stair steps in your pocket. You were told “take up to X steps” and you do. fast‑glob instead says “hold up! stop when you reach floor X.” Subtle difference. Real consequences.

Soo.. Be aware of a possible migration gotcha!


1 base / task.base — the directory that a single task walks from. Depth is measured relative to this base. In fast-glob, patterns are grouped by their static base directory for efficiency.

outslept avatar Aug 18 '25 22:08 outslept