Skip to content

Commit

Permalink
Realpath support
Browse files Browse the repository at this point in the history
Close #148
Close #142
  • Loading branch information
isaacs committed Mar 6, 2015
1 parent cabeb27 commit ba29ec3
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 5 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ be immediately available on the `g.found` member.
path multiple times.
* `symlinks` A record of which paths are symbolic links, which is
relevant in resolving `**` patterns.
* `realpathCache` An optional object which is passed to `fs.realpath`
to minimize unnecessary syscalls. It is stored on the instantiated
Glob object, and may be re-used.

### Events

Expand Down Expand Up @@ -204,9 +207,9 @@ All options are added to the Glob object, as well.
If you are running many `glob` operations, you can pass a Glob object
as the `options` argument to a subsequent operation to shortcut some
`stat` and `readdir` calls. At the very least, you may pass in shared
`symlinks`, `statCache`, and `cache` options, so that parallel glob
operations will be sped up by sharing information about the
filesystem.
`symlinks`, `statCache`, `realpathCache`, and `cache` options, so that
parallel glob operations will be sped up by sharing information about
the filesystem.

* `cwd` The current working directory in which to search. Defaults
to `process.cwd()`.
Expand Down Expand Up @@ -263,11 +266,16 @@ filesystem.
* `nonegate` Suppress `negate` behavior. (See below.)
* `nocomment` Suppress `comment` behavior. (See below.)
* `nonull` Return the pattern when no matches are found.
* `nodir` Do not match directories, only files.
* `nodir` Do not match directories, only files. (Note: to match
*only* directories, simply put a `/` at the end of the pattern.)
* `ignore` Add a pattern or an array of patterns to exclude matches.
* `follow` Follow symlinked directories when expanding `**` patterns.
Note that this can result in a lot of duplicate references in the
presence of cyclic links.
* `realpath` Set to true to call `fs.realpath` on all of the results.
In the case of a symlink that cannot be resolved, the full absolute
path to the matched entry is returned (though it will usually be a
broken symlink)

## Comparisons to other fnmatch/glob implementations

Expand Down
4 changes: 4 additions & 0 deletions common.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ function setopts (self, pattern, options) {

self.pattern = pattern
self.strict = options.strict !== false
self.realpath = !!options.realpath
self.realpathCache = options.realpathCache || Object.create(null)
self.follow = !!options.follow
self.dot = !!options.dot
self.mark = !!options.mark
Expand Down Expand Up @@ -205,6 +207,8 @@ function makeAbs (self, f) {
abs = f
} else if (self.changedCwd) {
abs = path.resolve(self.cwd, f)
} else if (self.realpath) {
abs = path.resolve(f)
}
return abs
}
Expand Down
54 changes: 53 additions & 1 deletion glob.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function Glob (pattern, options, cb) {
return new Glob(pattern, options, cb)

setopts(this, pattern, options)
this._didRealPath = false

// process each pattern in the minimatch set
var n = this.minimatch.set.length
Expand Down Expand Up @@ -163,11 +164,62 @@ Glob.prototype._finish = function () {
if (this.aborted)
return

//console.error('FINISH', this.matches)
if (this.realpath && !this._didRealpath)
return this._realpath()

common.finish(this)
this.emit('end', this.found)
}

Glob.prototype._realpath = function () {
if (this._didRealpath)
return

this._didRealpath = true

var n = this.matches.length
if (n === 0)
return this.finish()

var self = this
this.matches.forEach(function (matchset, index) {
self._realpathSet(index, next)
})

function next () {
if (--n === 0)
self._finish()
}
}

Glob.prototype._realpathSet = function (index, cb) {
var found = Object.keys(this.matches[index])
var self = this
var n = found.length
if (n === 0)
return cb()

var set = this.matches[index] = Object.create(null)
found.forEach(function (p, i) {
// If there's a problem with the stat, then it means that
// one or more of the links in the realpath couldn't be
// resolved. just return the abs value in that case.
fs.realpath(p, self.realpathCache, function (er, real) {
if (!er)
set[real] = true
else if (er.syscall === 'stat')
set[self._makeAbs(p)] = true
else
self.emit('error', er) // srsly wtf right here

if (--n === 0) {
self.matches[index] = set
cb()
}
})
})
}

Glob.prototype._mark = function (p) {
return common.mark(this, p)
}
Expand Down
17 changes: 17 additions & 0 deletions sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ function GlobSync (pattern, options) {

GlobSync.prototype._finish = function () {
assert(this instanceof GlobSync)
if (this.realpath) {
var self = this
this.matches.forEach(function (matchset, index) {
var set = self.matches[index] = Object.create(null)
for (var p in matchset) {
try {
var real = fs.realpathSync(p, this.realpathCache)
set[real] = true
} catch (er) {
if (er.syscall === 'stat')
set[self._makeAbs(p)] = true
else
throw er
}
}
})
}
common.finish(this)
}

Expand Down
1 change: 1 addition & 0 deletions test/bash-results.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"./test/nodir.js",
"./test/pause-resume.js",
"./test/readme-issue.js",
"./test/realpath.js",
"./test/root-nomount.js",
"./test/root.js",
"./test/stat.js",
Expand Down
70 changes: 70 additions & 0 deletions test/realpath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
var glob = require('../')
var test = require('tap').test
// pattern to find a bunch of duplicates
var pattern = 'a/symlink/{*,**/*/*/*,*/*/**,*/*/*/*/*/*}'
process.chdir(__dirname)

// options, results
// realpath:true set on each option
var cases = [
[ {},
[ 'a/symlink', 'a/symlink/a', 'a/symlink/a/b' ] ],

[ { mark: true },
[ 'a/symlink/', 'a/symlink/a/', 'a/symlink/a/b/' ] ],

[ { stat: true },
[ 'a/symlink', 'a/symlink/a', 'a/symlink/a/b' ] ],

[ { follow: true },
[ 'a/symlink', 'a/symlink/a', 'a/symlink/a/b' ] ],

[ { nounique: true },
[ 'a/symlink',
'a/symlink',
'a/symlink',
'a/symlink/a',
'a/symlink/a',
'a/symlink/a/b',
'a/symlink/a/b' ] ],

[ { nounique: true, mark: true },
[ 'a/symlink/',
'a/symlink/',
'a/symlink/',
'a/symlink/a/',
'a/symlink/a/',
'a/symlink/a/b/',
'a/symlink/a/b/' ] ],

[ { nounique: true, mark: true, follow: true },
[ 'a/symlink/',
'a/symlink/',
'a/symlink/',
'a/symlink/a/',
'a/symlink/a/',
'a/symlink/a/',
'a/symlink/a/b/',
'a/symlink/a/b/' ] ],
]

cases.forEach(function (c) {
var opt = c[0]
var expect = c[1].map(function (d) {
return __dirname.replace(/\\/g, '/') + '/' + d
})

opt.realpath = true

test(JSON.stringify(opt), function (t) {
opt.realpath = true
var sync = glob.sync(pattern, opt)
t.same(sync, expect, 'sync')
glob(pattern, opt, function (er, async) {
if (er)
throw er
t.same(async, expect, 'async')
t.end()
})
})
})

0 comments on commit ba29ec3

Please sign in to comment.