Skip to content

Commit

Permalink
feature complete and tests passing
Browse files Browse the repository at this point in the history
not 100% coverage yet, though
  • Loading branch information
isaacs committed Feb 27, 2023
1 parent c3be35a commit 8c2e082
Show file tree
Hide file tree
Showing 19 changed files with 256 additions and 446 deletions.
18 changes: 18 additions & 0 deletions changelog.md
Expand Up @@ -27,6 +27,15 @@ changes.
in question.
- The `hasMagic` method will return false for patterns that only
contain brace expansion, but no other "magic" glob characters.
- Patterns ending in `/` will still be restricted to matching
directories, but will not have a `/` appended in the results.
In general, results will be in their default relative or
absolute forms, without any extraneous `/` and `.` characters,
unlike shell matches. (The `mark` option may still be used to
_always_ mark directory matches with a trailing `/` or `\`.)
- An options argument is required for the `Glob` class
constructor. `{}` may be provided to accept all default
options.

## Options Changes

Expand Down Expand Up @@ -54,6 +63,15 @@ changes.
unique.
- `nosort:true` is no longer supported. Result sets are never
sorted.
- When the `nocase` option is used, the assumption is that it
reflects the case sensitivity of the _filesystem itself_.
Using case-insensitive matching on a case-sensitive filesystem,
or vice versa, may thus result in more or fewer matches than
expected. In general, it should only be used when the
filesystem is known to differ from the platform default.
- `realpath:true` no longer implies `absolute:true`. The
relative path to the realpath will be emitted when `absolute`
is not set.

## Performance and Algorithm Changes

Expand Down
5 changes: 1 addition & 4 deletions src/glob.ts
Expand Up @@ -140,7 +140,7 @@ export class Glob<Opts extends GlobOptions> {
if (opts.noglobstar) {
throw new TypeError('base matching requires globstar')
}
pattern = pattern.map(p => (p.includes('/') ? p : `**/${p}`))
pattern = pattern.map(p => (p.includes('/') ? p : `./**/${p}`))
}

this.pattern = pattern
Expand Down Expand Up @@ -179,11 +179,9 @@ export class Glob<Opts extends GlobOptions> {
platform: this.platform,
}

// console.error('glob pattern arg', this.pattern)
const mms = this.pattern.map(p => new Minimatch(p, mmo))
const [matchSet, globSet, globParts] = mms.reduce(
(set: [MatchSet, GlobSet, GlobParts], m) => {
// console.error('globparts', m.globParts)
set[0].push(...m.set)
set[1].push(...m.globSet)
set[2].push(...m.globParts)
Expand All @@ -192,7 +190,6 @@ export class Glob<Opts extends GlobOptions> {
[[], [], []]
)
this.patterns = matchSet.map((set, i) => {
// console.error('globParts', globParts[i])
return new Pattern(set, globParts[i], 0, this.platform)
})
this.matchSet = matchSet
Expand Down
23 changes: 12 additions & 11 deletions src/ignore.ts
Expand Up @@ -36,7 +36,6 @@ export class Ignore {
const mmopts = {
platform: this.platform,
optimizationLevel: 2,
nocaseMagicOnly: true,
dot: true,
nocase,
}
Expand Down Expand Up @@ -74,25 +73,27 @@ export class Ignore {

ignored(p: Path): boolean {
const fullpath = p.fullpath()
const relative = p.relative()
for (const m of this.absolute) {
if (m.match(fullpath)) return true
}
const fullpaths = `${fullpath}/`
const relative = p.relative() || '.'
const relatives = `${relative}/`
for (const m of this.relative) {
if (m.match(relative)) return true
if (m.match(relative) || m.match(relatives)) return true
}
for (const m of this.absolute) {
if (m.match(fullpath) || m.match(fullpaths)) return true
}
return false
}

childrenIgnored(p: Path): boolean {
const fullpath = p.fullpath()
const relative = p.relative()
for (const m of this.absoluteChildren) {
if (m.match(fullpath)) return true
}
const fullpath = p.fullpath() + '/'
const relative = (p.relative() || '.') + '/'
for (const m of this.relativeChildren) {
if (m.match(relative)) return true
}
for (const m of this.absoluteChildren) {
if (m.match(fullpath)) true
}
return false
}
}
39 changes: 18 additions & 21 deletions src/pattern.ts
Expand Up @@ -35,14 +35,13 @@ export class Pattern {
#isDrive?: boolean
#isUNC?: boolean
#isAbsolute?: boolean
#globstarFollowed: number[]
#followGlobstar: boolean = true

constructor(
patternList: MMPattern[],
globList: string[],
index: number,
platform: NodeJS.Platform,
globstarFollowed: number[] = []
platform: NodeJS.Platform
) {
if (!isPatternList(patternList)) {
throw new TypeError('empty pattern list')
Expand All @@ -60,7 +59,6 @@ export class Pattern {
this.#patternList = patternList
this.#globList = globList
this.#index = index
this.#globstarFollowed = globstarFollowed
this.#platform = platform

// if the current item is not globstar, and the next item is .., skip ahead
Expand Down Expand Up @@ -162,30 +160,14 @@ export class Pattern {
this.#patternList,
this.#globList,
this.#index + 1,
this.#platform,
this.#globstarFollowed
this.#platform
)
this.#rest.#isAbsolute = this.#isAbsolute
this.#rest.#isUNC = this.#isUNC
this.#rest.#isDrive = this.#isDrive
return this.#rest
}

followGlobstar(): boolean {
if (!this.isGlobstar()) {
return false
}
// never follow a globstar if it's the first pattern in the list
if (this.#index === 0) {
return false
}
if (this.#globstarFollowed.includes(this.#index)) {
return false
}
this.#globstarFollowed.push(this.#index)
return true
}

// pattern like: //host/share/...
// split = [ '', '', 'host', 'share', ... ]
isUNC(pl = this.#patternList): pl is UNCPatternList {
Expand Down Expand Up @@ -246,4 +228,19 @@ export class Pattern {
}
return false
}

checkFollowGlobstar(): boolean {
return !(
this.#index === 0 ||
!this.isGlobstar() ||
!this.#followGlobstar
)
}

markFollowGlobstar(): boolean {
if (this.#index === 0 || !this.isGlobstar() || !this.#followGlobstar)
return false
this.#followGlobstar = false
return true
}
}
52 changes: 34 additions & 18 deletions src/processor.ts
Expand Up @@ -28,8 +28,8 @@ class MatchRecord {
store: Map<Path, number> = new Map()
add(target: Path, absolute: boolean, ifDir: boolean) {
const n = (absolute ? 2 : 0) | (ifDir ? 1 : 0)
const current = this.store.get(target) || 0
this.store.set(target, n & current)
const current = this.store.get(target)
this.store.set(target, current === undefined ? n : n & current)
}
// match, absolute, ifdir
entries(): [Path, boolean, boolean][] {
Expand Down Expand Up @@ -71,11 +71,13 @@ export class Processor {
subwalks = new SubWalks()
patterns?: Pattern[]
follow: boolean
dot: boolean
opts: GlobWalkerOpts

constructor(opts: GlobWalkerOpts, hasWalkedCache?: HasWalkedCache) {
this.opts = opts
this.follow = !!opts.follow
this.dot = !!opts.dot
this.hasWalkedCache = hasWalkedCache
? hasWalkedCache.copy()
: new HasWalkedCache()
Expand Down Expand Up @@ -117,7 +119,8 @@ export class Processor {
(rest = pattern.rest())
) {
const c = t.resolve(p)
if (c.isUnknown()) break
// we can be reasonably sure that .. is a readable dir
if (c.isUnknown() && p !== '..') break
t = c
pattern = rest
changed = true
Expand All @@ -129,8 +132,9 @@ export class Processor {
this.hasWalkedCache.storeWalked(t, pattern)
}

// now we have either a final string, or a pattern starting with magic,
// mounted on t.
// now we have either a final string for a known entry,
// more strings for an unknown entry,
// or a pattern starting with magic, mounted on t.
if (typeof p === 'string') {
// must be final entry
if (!rest) {
Expand All @@ -146,14 +150,12 @@ export class Processor {
// if it's a symlink, but we didn't get here by way of a
// globstar match (meaning it's the first time THIS globstar
// has traversed a symlink), then we follow it. Otherwise, stop.
if (this.follow || !t.isSymbolicLink()) {
this.subwalks.add(t, pattern)
} else if (
t.isSymbolicLink() &&
pattern.followGlobstar() &&
rest
if (
!t.isSymbolicLink() ||
this.follow ||
pattern.checkFollowGlobstar()
) {
this.subwalks.add(t, rest)
this.subwalks.add(t, pattern)
}
const rp = rest?.pattern()
const rrest = rest?.rest()
Expand Down Expand Up @@ -217,12 +219,26 @@ export class Processor {
rest: Pattern | null,
absolute: boolean
) {
if (e.name.startsWith('.')) return
if (!pattern.hasMore()) {
this.matches.add(e, absolute, false)
}
if (e.canReaddir()) {
this.subwalks.add(e, pattern)
if (this.dot || !e.name.startsWith('.')) {
if (!pattern.hasMore()) {
this.matches.add(e, absolute, false)
}
if (e.canReaddir()) {
// if we're in follow mode or it's not a symlink, just keep
// testing the same pattern. If there's more after the globstar,
// then this symlink consumes the globstar. If not, then we can
// follow at most ONE symlink along the way, so we mark it, which
// also checks to ensure that it wasn't already marked.
if (this.follow || !e.isSymbolicLink()) {
this.subwalks.add(e, pattern)
} else if (e.isSymbolicLink()) {
if (rest && pattern.checkFollowGlobstar()) {
this.subwalks.add(e, rest)
} else if (pattern.markFollowGlobstar()) {
this.subwalks.add(e, pattern)
}
}
}
}
// if the NEXT thing matches this entry, then also add
// the rest.
Expand Down
12 changes: 6 additions & 6 deletions src/walker.ts
Expand Up @@ -21,6 +21,7 @@ export interface GlobWalkerOpts {
platform?: NodeJS.Platform
nocase?: boolean
follow?: boolean
dot?: boolean
}

export type GWOFileTypesTrue = GlobWalkerOpts & {
Expand Down Expand Up @@ -80,12 +81,14 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
aborted: boolean = false
#onResume: (() => any)[] = []
#ignore?: Ignore
#sep: '\\' | '/'

constructor(patterns: Pattern[], path: Path, opts: O)
constructor(patterns: Pattern[], path: Path, opts: O) {
this.patterns = patterns
this.path = path
this.opts = opts
this.#sep = opts.platform === 'win32' ? '\\' : '/'
if (opts.ignore) {
this.#ignore = makeIgnore(opts.ignore, opts)
}
Expand All @@ -107,7 +110,6 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
}
resume() {
if (this.aborted) return
//console.error(new Error('resume').stack, this.#onResume.length)
this.paused = false
let fn: (() => any) | undefined = undefined
while (!this.paused && (fn = this.#onResume.shift())) {
Expand Down Expand Up @@ -252,7 +254,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
matchFinish(e: Path, absolute: boolean) {
if (this.#ignored(e)) return
this.seen.add(e)
const mark = this.opts.mark && e.isDirectory() ? '/' : ''
const mark = this.opts.mark && e.isDirectory() ? this.#sep : ''
// ok, we have what we need!
if (this.opts.withFileTypes) {
this.matchEmit(e)
Expand All @@ -261,7 +263,8 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
} else if (this.opts.absolute || absolute) {
this.matchEmit(e.fullpath() + mark)
} else {
this.matchEmit(e.relative() + mark)
const rel = e.relative()
this.matchEmit(!rel && mark ? './' : rel + mark)
}
}

Expand All @@ -273,7 +276,6 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {

matchSync(e: Path, absolute: boolean, ifDir: boolean): void {
if (this.#ignored(e)) return
//console.error(e.fullpath(), absolute, ifDir, new Error('matchtrace').stack)
const p = this.matchCheckSync(e, ifDir)
if (p) this.matchFinish(p, absolute)
}
Expand Down Expand Up @@ -370,7 +372,6 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
cb: () => any
) {
if (this.#childrenIgnored(target)) return cb()
//console.error('walkcb2sync', this.paused, target.fullpath(), patterns.map(p => p.globString()))
if (this.paused) {
this.onResume(() =>
this.walkCB2Sync(target, patterns, processor, cb)
Expand Down Expand Up @@ -489,7 +490,6 @@ export class GlobStream<

matchEmit(e: Result<O>): void
matchEmit(e: Path | string): void {
// console.error('MATCH EMIT', [e])
this.results.write(e)
if (!this.results.flowing) this.pause()
}
Expand Down
7 changes: 3 additions & 4 deletions test/00-setup.ts
Expand Up @@ -77,11 +77,10 @@ if (process.platform === 'win32' || !process.env.TEST_REGEN) {
'a/abc{fed,def}/g/h',
'a/abc{fed/g,def}/**/',
'a/abc{fed/g,def}/**///**/',
// TODO: match bash behavior
// When a ** is the FIRST item in a pattern, it has
// slightly dfferent symbolic link handling behavior.
// '**/a',
// '**/a/**',
// more restrictive symbolic link handling behavior.
'**/a',
'**/a/**',
'./**/a',
'./**/a/**/',
'./**/a/**',
Expand Down

0 comments on commit 8c2e082

Please sign in to comment.