Skip to content

Commit

Permalink
add custom Ignore support
Browse files Browse the repository at this point in the history
Fix: #261
Fix: #363
Fix: #335
  • Loading branch information
isaacs committed Mar 2, 2023
1 parent a2fb688 commit cdfde4b
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 24 deletions.
53 changes: 49 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,43 @@ const timeSortedFiles = results
const groupReadableFiles = results
.filter(path => path.mode & 0o040)
.map(path => path.fullpath())

// custom ignores can be done like this, for example by saying
// you'll ignore all markdown files, and all folders named 'docs'
const customIgnoreResults = await glob('**', {
ignore: {
ignored: (p) => /\.md$/.test(p.name),
childrenIgnored: (p) => p.isNamed('docs'),
},
})

// another fun use case, only return files with the same name as
// their parent folder, plus either `.ts` or `.js`
const folderNamedModules = await glob('**/*.{ts,js}', {
ignore: {
ignored: (p) => {
const pp = p.parent
return !(p.isNamed(pp.name + '.ts') || p.isNamed(pp.name + '.js'))
}
}
})

// find all files edited in the last hour
const newFiles = await glob('**', {
// need stat so we have mtime
stat: true,
// only want the files, not the dirs
nodir: true,
ignore: {
ignored: (p) => {
return (new Date() - p.mtime) <= (60 * 60 * 1000)
},
// could add similar childrenIgnored here as well, but
// directory mtime is inconsistent across platforms, so
// probably better not to, unless you know the system
// tracks this reliably.
}
})
```

**Note** Glob patterns should always use `/` as a path separator,
Expand Down Expand Up @@ -342,14 +379,22 @@ share the previously loaded cache.
as modified time, permissions, and so on. Note that this will
incur a performance cost due to the added system calls.

- `ignore` string or string[]. A glob pattern or array of glob
patterns to exclude from matches. To ignore all children within
a directory, as well as the entry itself, append `/**'` to the
ignore pattern.
- `ignore` string or string[], or an object with `ignore` and
`ignoreChildren` methods.

If a string or string[] is provided, then this is treated as a
glob pattern or array of glob patterns to exclude from matches.
To ignore all children within a directory, as well as the entry
itself, append `'/**'` to the ignore pattern.

**Note** `ignore` patterns are _always_ in `dot:true` mode,
regardless of any other settings.

If an object is provided that has `ignored(path)` and/or
`childrenIgnored(path)` methods, then these methods will be
called to determine whether any Path is a match or if its
children should be traversed, respectively.

- `follow` Follow symlinked directories when expanding `**`
patterns. This can result in a lot of duplicate references in
the presence of cyclic links, and make performance quite bad.
Expand Down
25 changes: 19 additions & 6 deletions src/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PathScurryWin32,
} from 'path-scurry'
import { fileURLToPath } from 'url'
import { Ignore } from './ignore.js'
import { IgnoreLike } from './ignore.js'
import { Pattern } from './pattern.js'
import { GlobStream, GlobWalker } from './walker.js'

Expand Down Expand Up @@ -99,11 +99,23 @@ export interface GlobOptions {
follow?: boolean

/**
* A glob pattern or array of glob patterns to exclude from matches. To
* ignore all children within a directory, as well as the entry itself,
* append `/**'` to the ignore pattern.
* string or string[], or an object with `ignore` and `ignoreChildren`
* methods.
*
* If a string or string[] is provided, then this is treated as a glob
* pattern or array of glob patterns to exclude from matches. To ignore all
* children within a directory, as well as the entry itself, append `'/**'`
* to the ignore pattern.
*
* **Note** `ignore` patterns are _always_ in `dot:true` mode, regardless of
* any other settings.
*
* If an object is provided that has `ignored(path)` and/or
* `childrenIgnored(path)` methods, then these methods will be called to
* determine whether any Path is a match or if its children should be
* traversed, respectively.
*/
ignore?: string | string[] | Ignore
ignore?: string | string[] | IgnoreLike

/**
* Treat brace expansion like `{a,b}` as a "magic" pattern. Has no
Expand Down Expand Up @@ -306,7 +318,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
dot: boolean
dotRelative: boolean
follow: boolean
ignore?: Ignore
ignore?: string | string[] | IgnoreLike
magicalBraces: boolean
mark?: boolean
matchBase: boolean
Expand Down Expand Up @@ -373,6 +385,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
this.maxDepth =
typeof opts.maxDepth === 'number' ? opts.maxDepth : Infinity
this.stat = !!opts.stat
this.ignore = opts.ignore

if (this.withFileTypes && this.absolute !== undefined) {
throw new Error('cannot set absolute and withFileTypes:true')
Expand Down
9 changes: 8 additions & 1 deletion src/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { Path } from 'path-scurry'
import { Pattern } from './pattern.js'
import { GlobWalkerOpts } from './walker.js'

export interface IgnoreLike {
ignored?: (p: Path) => boolean
childrenIgnored?: (p: Path) => boolean
}

const defaultPlatform: NodeJS.Platform =
typeof process === 'object' &&
process &&
Expand All @@ -18,7 +23,7 @@ const defaultPlatform: NodeJS.Platform =
/**
* Class used to process ignored patterns
*/
export class Ignore {
export class Ignore implements IgnoreLike {
relative: Minimatch[]
relativeChildren: Minimatch[]
absolute: Minimatch[]
Expand Down Expand Up @@ -46,6 +51,8 @@ export class Ignore {
noglobstar,
optimizationLevel: 2,
platform,
nocomment: true,
nonegate: true,
}

// this is a little weird, but it gives us a clean set of optimized
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,10 @@ export type {
GlobOptionsWithFileTypesUnset,
} from './glob.js'
export { hasMagic } from './has-magic.js'
export type { IgnoreLike } from './ignore.js'
export type { MatchStream } from './walker.js'

/* c8 ignore stop */

export default Object.assign(glob, {
glob,
globSync,
Expand Down
22 changes: 10 additions & 12 deletions src/walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import Minipass from 'minipass'
import { Path } from 'path-scurry'
import { Ignore } from './ignore.js'
import { Ignore, IgnoreLike } from './ignore.js'

// XXX can we somehow make it so that it NEVER processes a given path more than
// once, enough that the match set tracking is no longer needed? that'd speed
Expand All @@ -23,7 +23,7 @@ export interface GlobWalkerOpts {
dot?: boolean
dotRelative?: boolean
follow?: boolean
ignore?: string | string[] | Ignore
ignore?: string | string[] | IgnoreLike
mark?: boolean
matchBase?: boolean
// Note: maxDepth here means "maximum actual Path.depth()",
Expand Down Expand Up @@ -79,16 +79,14 @@ export type MatchStream<O extends GlobWalkerOpts> =
: Minipass<Path | string, Path | string>

const makeIgnore = (
ignore: string | string[] | Ignore,
ignore: string | string[] | IgnoreLike,
opts: GlobWalkerOpts
): Ignore =>
): IgnoreLike =>
typeof ignore === 'string'
? new Ignore([ignore], opts)
: Array.isArray(ignore)
? new Ignore(ignore, opts)
: /* c8 ignore start */
ignore
/* c8 ignore stop */
: ignore

/**
* basic walking utilities that all the glob walker types use
Expand All @@ -101,7 +99,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
paused: boolean = false
aborted: boolean = false
#onResume: (() => any)[] = []
#ignore?: Ignore
#ignore?: IgnoreLike
#sep: '\\' | '/'
signal?: AbortSignal
maxDepth: number
Expand Down Expand Up @@ -129,10 +127,10 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
}

#ignored(path: Path): boolean {
return this.seen.has(path) || !!this.#ignore?.ignored(path)
return this.seen.has(path) || !!this.#ignore?.ignored?.(path)
}
#childrenIgnored(path: Path): boolean {
return !!this.#ignore?.childrenIgnored(path)
return !!this.#ignore?.childrenIgnored?.(path)
}

// backpressure mechanism
Expand Down Expand Up @@ -177,9 +175,9 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
matchCheckTest(e: Path | undefined, ifDir: boolean): Path | undefined {
return e &&
(this.maxDepth === Infinity || e.depth() <= this.maxDepth) &&
!this.#ignored(e) &&
(!ifDir || e.canReaddir()) &&
(!this.opts.nodir || !e.isDirectory())
(!this.opts.nodir || !e.isDirectory()) &&
!this.#ignored(e)
? e
: undefined
}
Expand Down
46 changes: 46 additions & 0 deletions test/custom-ignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { basename, resolve } from 'path'
import { Path } from 'path-scurry'
import t from 'tap'
import { glob, globSync, IgnoreLike } from '../'
const cwd = resolve(__dirname, 'fixtures')

const j = (a: string[]) =>
a
.map(s => s.replace(/\\/g, '/'))
.sort((a, b) => a.localeCompare(b, 'en'))

t.test('ignore files with long names', async t => {
const ignore: IgnoreLike = {
ignored: (p: Path) => p.name.length > 1,
}
const syncRes = globSync('**', { cwd, ignore })
const asyncRes = await glob('**', { cwd, ignore })
const expect = j(
globSync('**', { cwd }).filter(p => basename(p).length === 1)
)
t.same(j(syncRes), expect)
t.same(j(asyncRes), expect)
for (const r of syncRes) {
if (basename(r).length > 1) t.fail(r)
}
})

t.test('ignore symlink and abcdef directories', async t => {
const ignore: IgnoreLike = {
childrenIgnored: (p: Path) => {
return p.isNamed('symlink') || p.isNamed('abcdef')
},
}
const syncRes = globSync('**', { cwd, ignore, nodir: true })
const asyncRes = await glob('**', { cwd, ignore, nodir: true })
const expect = j(
globSync('**', { nodir: true, cwd }).filter(p => {
return !/\bsymlink\b|\babcdef\b/.test(p)
})
)
t.same(j(syncRes), expect)
t.same(j(asyncRes), expect)
for (const r of syncRes) {
if (r === 'symlink' || r === 'basename') t.fail(r)
}
})

0 comments on commit cdfde4b

Please sign in to comment.