diff --git a/doc/api/path.markdown b/doc/api/path.markdown index cc3dc6da8f..5f6555464f 100644 --- a/doc/api/path.markdown +++ b/doc/api/path.markdown @@ -3,9 +3,23 @@ This module contains utilities for dealing with file paths. Use `require('path')` to use it. It provides the following methods: +### path.normalize(p) + +Normalize a string path, taking care of `'..'` and `'.'` parts. + +When multiple slashes are found, they're replaces by a single one; +when the path contains a trailing slash, it is preserved. +On windows backslashes are used. + +Example: + + path.normalize('/foo/bar//baz/asdf/quux/..') + // returns + '/foo/bar/baz/asdf' + ### path.join([path1], [path2], [...]) -Join all arguments together and resolve the resulting path. +Join all arguments together and normalize the resulting path. Example: @@ -13,26 +27,36 @@ Example: ... '/foo', 'bar', 'baz/asdf', 'quux', '..') '/foo/bar/baz/asdf' -### path.normalizeArray(arr) +### path.resolve([from ...], to) -Normalize an array of path parts, taking care of `'..'` and `'.'` parts. +Resolves `to` to an absolute path name and normalizes it. -Example: +One ore more `from` arguments may be provided to specify the the starting +point from where the path will be resolved. `resolve` will prepend `from` +arguments from right to left until an absolute path is found. If no `from` +arguments are specified, or after prepending them still no absolute path is +found, the current working directory will be prepended eventually. - path.normalizeArray(['', - 'foo', 'bar', 'baz', 'asdf', 'quux', '..']) - // returns - [ '', 'foo', 'bar', 'baz', 'asdf' ] +Trailing slashes are removed unless the path gets resolved to the root +directory. -### path.normalize(p) +Examples: -Normalize a string path, taking care of `'..'` and `'.'` parts. + path.resolve('index.html') + // returns + '/home/tank/index.html' -Example: + path.resolve('/foo/bar', './baz') + // returns + '/foo/baz/baz' - path.normalize('/foo/bar/baz/asdf/quux/..') + path.resolve('/foo/bar', '/tmp/file/') // returns - '/foo/bar/baz/asdf' + '/tmp/file' + + path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') + // returns + '/home/tank/wwwroot/static_files/gif/image.gif' ### path.dirname(p) diff --git a/lib/path.js b/lib/path.js index c5313e9bad..0d81d814ff 100644 --- a/lib/path.js +++ b/lib/path.js @@ -1,127 +1,247 @@ -function validPathPart(p) { - return typeof p === 'string' && p; + +var isWindows = process.platform === 'win32'; + + +// resolves . and .. elements in a path array with directory names +// there must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length; i >= 0; i--) { + var last = parts[i]; + if (last == '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for ( ; up--; up) { + parts.unshift('..'); + } + } + + return parts; } -exports.join = function() { - var args = Array.prototype.slice.call(arguments); - return exports.normalizeArray(args).join('/'); -}; +if (isWindows) { + // Regex to split a filename into [*, dir, basename, ext] + // windows version + var splitPathRe = /^(.+(?:[\\\/](?!$)|:)|[\\\/])?((?:.+?)?(\.[^.]*)?)$/; -exports.split = function(path) { - // split based on / and \, but only if that / is not at the start or end. - return exports.normalizeArray(path.split(/^|[\\\/](?!$)/)); -}; + // Regex to split a windows path into three parts: [*, device, slash, tail] + // windows-only + var splitDeviceRe = /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?([\\\/])?(.*?)$/; + // path.resolve([from ...], to) + // windows version + exports.resolve = function() { + // Prepend cwd to provided paths + var paths = [process.cwd()].concat(Array.prototype.slice.call(arguments, 0)); -function cleanArray(parts) { - var i = 0; - var l = parts.length - 1; - var stripped = false; + var resolvedDevice = "", + resolvedTail = "", + resolvedAbsolute = false; - // strip leading empty args - while (i < l && !validPathPart(parts[i])) { - stripped = true; - i++; - } + for (var i = paths.length; i >= 0; i--) { + var path = paths[i]; - // strip tailing empty args - while (l >= i && !validPathPart(parts[l])) { - stripped = true; - l--; - } + // Skip empty and invalid entries + if (typeof path !== 'string' || !path) { + continue; + } - if (stripped) { - // if l chopped all the way back to i, then this is empty - parts = Array.prototype.slice.call(parts, i, l + 1); + var result = splitDeviceRe.exec(path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':', + isAbsolute = !!result[2] || isUnc, // UNC paths are always absolute + tail = result[3]; + + if (device && resolvedDevice && device.toLowerCase() !== resolvedDevice.toLowerCase()) { + // This path points to another device so it is not applicable + continue; + } + + if (!resolvedDevice) { + resolvedDevice = device; + } + if (!resolvedAbsolute) { + resolvedTail = tail + '\\' + resolvedTail; + resolvedAbsolute = isAbsolute; + } + + if (resolvedDevice && resolvedAbsolute) { + break; + } + } + + if (!resolvedAbsolute && resolvedDevice) { + // If we still don't have an absolute path, + // prepend the current path for the device found. + + // TODO + // Windows stores the current directories for 'other' drives + // as hidden environment variables like =C:=c:\windows (literally) + // var deviceCwd = os.getCwdForDrive(resolvedDevice); + var deviceCwd = ""; + + // If there is no cwd set for the drive, it is at root + resolvedTail = deviceCwd + '\\' + resolvedTail; + resolvedAbsolute = true; + } + + // Replace slashes (in UNC share name) by backslashes + resolvedDevice = resolvedDevice.replace(/\//g, '\\'); + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the tail path + resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/).filter(function(p) { + return !!p; + }), !resolvedAbsolute).join('\\'); + + return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) || '.'; } - return parts.filter(function(p) { return validPathPart(p) }) - .join('/') - .split(/^|[\\\/](?!$)/); -} + // windows version + exports.normalize = function(path) { + var result = splitDeviceRe.exec(path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':', + isAbsolute = !!result[2] || isUnc, // UNC paths are always absolute + tail = result[3], + trailingSlash = /[\\\/]$/.test(tail); + // Normalize the tail path + tail = normalizeArray(tail.split(/[\\\/]+/).filter(function(p) { + return !!p; + }), !isAbsolute).join('\\'); -exports.normalizeArray = function(original) { - var parts = cleanArray(original); - if (!parts.length || (parts.length === 1 && !parts[0])) return ['.']; - - // now we're fully ready to rock. - // leading/trailing invalids have been stripped off. - // if it comes in starting with a slash, or ending with a slash, - var leadingSlash = (parts[0].charAt(0) === '/'); - - if (leadingSlash) parts[0] = parts[0].substr(1); - var last = parts.slice(-1)[0]; - var tailingSlash = (last.substr(-1) === '/'); - if (tailingSlash) parts[parts.length - 1] = last.slice(0, -1); - var directories = []; - var prev; - for (var i = 0, l = parts.length - 1; i <= l; i++) { - var directory = parts[i]; - - // if it's blank, and we're not keeping blanks, then skip it. - if (directory === '') continue; - - // if it's a dot, then skip it - if (directory === '.' && (directories.length || - (i === 0 && !(tailingSlash && i === l)) || - (i === 0 && leadingSlash))) continue; - - // if we're dealing with an absolute path, then discard ..s that go - // above that the base. - if (leadingSlash && directories.length === 0 && directory === '..') { - continue; + if (!tail && !isAbsolute) { + tail = '.' } - // trying to go up a dir - if (directory === '..' && directories.length && prev !== '..' && - prev !== undefined) { - directories.pop(); - prev = directories.slice(-1)[0]; - } else { - directories.push(directory); - prev = directory; + if (tail && trailingSlash) { + tail += '\\' } + + return device + (isAbsolute ? '\\' : '') + tail; } - if (!directories.length) { - directories = [leadingSlash || tailingSlash ? '' : '.']; + + // windows version + exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0).filter(function(p) { + return p && typeof p === 'string'; + }), + joined = paths.join('\\'); + + // Make sure that the joined path doesn't start with two slashes + // - it will be mistaken for an unc path by normalize() - + // unless the paths[0] also starts with two slashes + if (/^[\\\/]{2}/.test(joined) && !/^[\\\/]{2}/.test(paths[0])) { + joined = joined.slice(1); + } + + return exports.normalize(joined); } - var last = directories.slice(-1)[0]; - if (tailingSlash && last.substr(-1) !== '/') { - directories[directories.length - 1] += '/'; + + +} else /* posix */ { + + // Regex to split a filename into [*, dir, basename, ext] + // posix version + var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; + + // path.resolve([from ...], to) + // posix version + exports.resolve = function() { + // Prepend cwd to provided paths + var paths = [process.cwd()].concat(Array.prototype.slice.call(arguments, 0)); + + var resolvedPath = "", + resolvedAbsolute = false; + + for (var i = paths.length; i >= 0 && !resolvedAbsolute; i--) { + var path = paths[i]; + // Skip empty and invalid entries + if (typeof path !== 'string' || !path) { + continue; + } + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; } - if (leadingSlash && directories[0].charAt(0) !== '/') { - if (directories[0] === '.') directories[0] = ''; - directories[0] = '/' + directories[0]; + + // path.normalize(path) + // posix version + exports.normalize = function(path) { + var isAbsolute = path.charAt(0) === '/', + trailingSlash = path.slice(-1) === '/'; + + // Normalize the path + path = normalizeArray(path.split('/').filter(function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.' + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; } - return directories; -}; -exports.normalize = function(path) { - return exports.join(path); -}; + // posix version + exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(paths.filter(function(p, index) { + return p && typeof p === 'string' + }).join('/')); + } +} exports.dirname = function(path) { - if (path.length > 1 && '\\/'.indexOf(path[path.length-1]) != -1) { - path = path.replace(/\/+$/, ''); - } - var lastSlash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); - switch (lastSlash) { - case -1: - return '.'; - case 0: - return '/'; - default: - return path.substring(0, lastSlash); + var dir = splitPathRe.exec(path)[1] || ''; + if (!dir) { + // No dirname + return '.' + } else if (dir.length === 1 || + (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { + // It is just a slash or a drive letter with a slash + return dir; + } else { + // It is a full dirname, strip trailing slash + return dir.substring(0, dir.length - 1); } }; exports.basename = function(path, ext) { - var f = path.substr(path.lastIndexOf('/') + 1); + var f = splitPathRe.exec(path)[2] || ''; + // TODO: make this comparison case-insensitive on windows? if (ext && f.substr(-1 * ext.length) === ext) { f = f.substr(0, f.length - ext.length); } @@ -130,12 +250,7 @@ exports.basename = function(path, ext) { exports.extname = function(path) { - var dot = path.lastIndexOf('.'), - slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); - // The last dot must be in the last path component, and it (the last dot) must - // not start the last path component (i.e. be a dot that signifies a hidden - // file in UNIX). - return dot <= slash + 1 ? '' : path.substring(dot); + return splitPathRe.exec(path)[3] || ''; }; diff --git a/test/simple/test-path.js b/test/simple/test-path.js index 9fc50ad590..c263bbcbfd 100644 --- a/test/simple/test-path.js +++ b/test/simple/test-path.js @@ -3,12 +3,14 @@ var assert = require('assert'); var path = require('path'); +var isWindows = process.platform === 'win32'; + var f = __filename; assert.equal(path.basename(f), 'test-path.js'); assert.equal(path.basename(f, '.js'), 'test-path'); assert.equal(path.extname(f), '.js'); -assert.equal(path.dirname(f).substr(-11), 'test/simple'); +assert.equal(path.dirname(f).substr(-11), isWindows ? 'test\\simple' : 'test/simple'); assert.equal(path.dirname('/a/b/'), '/a'); assert.equal(path.dirname('/a/b'), '/a'); assert.equal(path.dirname('/a'), '/'); @@ -87,7 +89,7 @@ var joinTests = ]; joinTests.forEach(function(test) { var actual = path.join.apply(path, test[0]); - var expected = test[1]; + var expected = isWindows ? test[1].replace(/\//g, '\\') : test[1]; var message = 'path.join(' + test[0].map(JSON.stringify).join(',') + ')' + '\n expect=' + JSON.stringify(expected) + '\n actual=' + JSON.stringify(actual); @@ -96,14 +98,52 @@ joinTests.forEach(function(test) { }); assert.equal(failures.length, 0, failures.join('')); +// path normalize tests +if (isWindows) { + assert.equal(path.normalize('./fixtures///b/../b/c.js'), + 'fixtures\\b\\c.js'); + assert.equal(path.normalize('/foo/../../../bar'), '\\bar'); + assert.equal(path.normalize('a//b//../b'), 'a\\b'); + assert.equal(path.normalize('a//b//./c'), 'a\\b\\c'); + assert.equal(path.normalize('a//b//.'), 'a\\b'); +} else { + assert.equal(path.normalize('./fixtures///b/../b/c.js'), + 'fixtures/b/c.js'); + assert.equal(path.normalize('/foo/../../../bar'), '/bar'); + assert.equal(path.normalize('a//b//../b'), 'a/b'); + assert.equal(path.normalize('a//b//./c'), 'a/b/c'); + assert.equal(path.normalize('a//b//.'), 'a/b'); +} -assert.equal(path.normalize('./fixtures///b/../b/c.js'), - 'fixtures/b/c.js'); -assert.equal(path.normalize('/foo/../../../bar'), '/bar'); - -assert.deepEqual(path.normalizeArray(['fixtures', 'b', '', '..', 'b', 'c.js']), - ['fixtures', 'b', 'c.js']); - -assert.equal(path.normalize('a//b//../b'), 'a/b'); -assert.equal(path.normalize('a//b//./c'), 'a/b/c'); -assert.equal(path.normalize('a//b//.'), 'a/b'); +// path.resolve tests +if (isWindows) { + // windows + var resolveTests = + // arguments result + [[['c:/blah\\blah', 'd:/games', 'c:../a' ], 'c:\\blah\\a' ], + [['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe' ], 'd:\\e.exe' ], + [['c:/ignore', 'c:/some/file' ], 'c:\\some\\file' ], + [['d:/ignore', 'd:some/dir//' ], 'd:\\ignore\\some\\dir' ], + [['.' ], process.cwd() ], + [['//server/share', '..', 'relative\\' ], '\\\\server\\share\\relative' ]]; +} else { + // Posix + var resolveTests = + // arguments result + [[['/var/lib', '../', 'file/' ], '/var/file' ], + [['/var/lib', '/../', 'file/' ], '/file' ], + [['a/b/c/', '../../..' ], process.cwd() ], + [['.' ], process.cwd() ], + [['/some/dir', '.', '/absolute/' ], '/absolute' ]]; +} +var failures = [] +resolveTests.forEach(function(test) { + var actual = path.resolve.apply(path, test[0]); + var expected = test[1]; + var message = 'path.resolve(' + test[0].map(JSON.stringify).join(',') + ')' + + '\n expect=' + JSON.stringify(expected) + + '\n actual=' + JSON.stringify(actual); + if (actual !== expected) failures.push('\n' + message); + // assert.equal(actual, expected, message); +}); +assert.equal(failures.length, 0, failures.join(''));