From 2d17193f20930bfe594d7008fe5c08f813f03c7b Mon Sep 17 00:00:00 2001 From: Rory Bradford Date: Tue, 8 Apr 2014 22:48:24 +0100 Subject: [PATCH] path: added parse() and format() functions The parse() function splits a path and returns an object with the different elements. The format() function is the reverse of this and adds an objects corresponding path elements to make up a string. Fixes #6976. Fixes: https://github.com/joyent/node/issues/6976 PR-URL: https://github.com/joyent/node/pull/8750 Reviewed-by: Julien Gilli --- doc/api/path.markdown | 42 +++++++++ lib/path.js | 117 +++++++++++++++++++++++--- test/simple/test-path-parse-format.js | 99 ++++++++++++++++++++++ 3 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 test/simple/test-path-parse-format.js diff --git a/doc/api/path.markdown b/doc/api/path.markdown index 054159533af7..3489f1811f5b 100644 --- a/doc/api/path.markdown +++ b/doc/api/path.markdown @@ -202,6 +202,48 @@ An example on Windows: // returns ['C:\Windows\system32', 'C:\Windows', 'C:\Program Files\nodejs\'] +## path.parse(pathString) + +Returns an object from a path string. + +An example on *nix: + + path.parse('/home/user/dir/file.txt') + // returns + { + root : "/", + dir : "/home/user/dir", + base : "file.txt", + ext : ".txt", + name : "file" + } + +An example on Windows: + + path.parse('C:\\path\\dir\\index.html') + // returns + { + root : "C:\", + dir : "C:\path\dir", + base : "index.html", + ext : ".html", + name : "index" + } + +## path.format(pathObject) + +Returns a path string from an object, the opposite of `path.parse` above. + + path.format({ + root : "/", + dir : "/home/user/dir", + base : "file.txt", + ext : ".txt", + name : "file" + }) + // returns + '/home/user/dir/file.txt' + ## path.posix Provide access to aforementioned `path` methods but always interact in a posix diff --git a/lib/path.js b/lib/path.js index 2cab82acd5e2..70a901644946 100644 --- a/lib/path.js +++ b/lib/path.js @@ -54,7 +54,6 @@ function normalizeArray(parts, allowAboveRoot) { return parts; } - // Regex to split a windows path into three parts: [*, device, slash, // tail] windows-only var splitDeviceRe = @@ -67,7 +66,7 @@ var splitTailRe = var win32 = {}; // Function to split a filename into [root, dir, basename, ext] -win32.splitPath = function(filename) { +function win32SplitPath(filename) { // Separate device+slash from tail var result = splitDeviceRe.exec(filename), device = (result[1] || '') + (result[2] || ''), @@ -78,7 +77,7 @@ win32.splitPath = function(filename) { basename = result2[2], ext = result2[3]; return [device, dir, basename, ext]; -}; +} var normalizeUNCRoot = function(device) { return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\'); @@ -331,7 +330,7 @@ win32._makeLong = function(path) { win32.dirname = function(path) { - var result = win32.splitPath(path), + var result = win32SplitPath(path), root = result[0], dir = result[1]; @@ -350,7 +349,7 @@ win32.dirname = function(path) { win32.basename = function(path, ext) { - var f = win32.splitPath(path)[2]; + var f = win32SplitPath(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); @@ -360,7 +359,57 @@ win32.basename = function(path, ext) { win32.extname = function(path) { - return win32.splitPath(path)[3]; + return win32SplitPath(path)[3]; +}; + + +win32.format = function(pathObject) { + if (!util.isObject(pathObject)) { + throw new TypeError( + "Parameter 'pathObject' must be an object, not " + typeof pathObject + ); + } + + var root = pathObject.root || ''; + + if (!util.isString(root)) { + throw new TypeError( + "'pathObject.root' must be a string or undefined, not " + + typeof pathObject.root + ); + } + + var dir = pathObject.dir; + var base = pathObject.base || ''; + if (dir.slice(dir.length - 1, dir.length) === win32.sep) { + return dir + base; + } + + if (dir) { + return dir + win32.sep + base; + } + + return base; +}; + + +win32.parse = function(pathString) { + if (!util.isString(pathString)) { + throw new TypeError( + "Parameter 'pathString' must be a string, not " + typeof pathString + ); + } + var allParts = win32SplitPath(pathString); + if (!allParts || allParts.length !== 4) { + throw new TypeError("Invalid path '" + pathString + "'"); + } + return { + root: allParts[0], + dir: allParts[0] + allParts[1].slice(0, allParts[1].length - 1), + base: allParts[2], + ext: allParts[3], + name: allParts[2].slice(0, allParts[2].length - allParts[3].length) + }; }; @@ -375,9 +424,9 @@ var splitPathRe = var posix = {}; -posix.splitPath = function(filename) { +function posixSplitPath(filename) { return splitPathRe.exec(filename).slice(1); -}; +} // path.resolve([from ...], to) @@ -512,7 +561,7 @@ posix._makeLong = function(path) { posix.dirname = function(path) { - var result = posix.splitPath(path), + var result = posixSplitPath(path), root = result[0], dir = result[1]; @@ -531,7 +580,7 @@ posix.dirname = function(path) { posix.basename = function(path, ext) { - var f = posix.splitPath(path)[2]; + var f = posixSplitPath(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); @@ -541,7 +590,53 @@ posix.basename = function(path, ext) { posix.extname = function(path) { - return posix.splitPath(path)[3]; + return posixSplitPath(path)[3]; +}; + + +posix.format = function(pathObject) { + if (!util.isObject(pathObject)) { + throw new TypeError( + "Parameter 'pathObject' must be an object, not " + typeof pathObject + ); + } + + var root = pathObject.root || ''; + + if (!util.isString(root)) { + throw new TypeError( + "'pathObject.root' must be a string or undefined, not " + + typeof pathObject.root + ); + } + + var dir = pathObject.dir ? pathObject.dir + posix.sep : ''; + var base = pathObject.base || ''; + return dir + base; +}; + + +posix.parse = function(pathString) { + if (!util.isString(pathString)) { + throw new TypeError( + "Parameter 'pathString' must be a string, not " + typeof pathString + ); + } + var allParts = posixSplitPath(pathString); + if (!allParts || allParts.length !== 4) { + throw new TypeError("Invalid path '" + pathString + "'"); + } + allParts[1] = allParts[1] || ''; + allParts[2] = allParts[2] || ''; + allParts[3] = allParts[3] || ''; + + return { + root: allParts[0], + dir: allParts[0] + allParts[1].slice(0, allParts[1].length - 1), + base: allParts[2], + ext: allParts[3], + name: allParts[2].slice(0, allParts[2].length - allParts[3].length) + }; }; diff --git a/test/simple/test-path-parse-format.js b/test/simple/test-path-parse-format.js new file mode 100644 index 000000000000..4f6e5af45c06 --- /dev/null +++ b/test/simple/test-path-parse-format.js @@ -0,0 +1,99 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var assert = require('assert'); +var path = require('path'); + +var winPaths = [ + 'C:\\path\\dir\\index.html', + 'C:\\another_path\\DIR\\1\\2\\33\\index', + 'another_path\\DIR with spaces\\1\\2\\33\\index', + '\\foo\\C:', + 'file', + '.\\file', + + // unc + '\\\\server\\share\\file_path', + '\\\\server two\\shared folder\\file path.zip', + '\\\\teela\\admin$\\system32', + '\\\\?\\UNC\\server\\share' + +]; + +var unixPaths = [ + '/home/user/dir/file.txt', + '/home/user/a dir/another File.zip', + '/home/user/a dir//another&File.', + '/home/user/a$$$dir//another File.zip', + 'user/dir/another File.zip', + 'file', + '.\\file', + './file', + 'C:\\foo' +]; + +var errors = [ + {method: 'parse', input: [null], message: /Parameter 'pathString' must be a string, not/}, + {method: 'parse', input: [{}], message: /Parameter 'pathString' must be a string, not object/}, + {method: 'parse', input: [true], message: /Parameter 'pathString' must be a string, not boolean/}, + {method: 'parse', input: [1], message: /Parameter 'pathString' must be a string, not number/}, + {method: 'parse', input: [], message: /Parameter 'pathString' must be a string, not undefined/}, + // {method: 'parse', input: [''], message: /Invalid path/}, // omitted because it's hard to trigger! + {method: 'format', input: [null], message: /Parameter 'pathObject' must be an object, not/}, + {method: 'format', input: [''], message: /Parameter 'pathObject' must be an object, not string/}, + {method: 'format', input: [true], message: /Parameter 'pathObject' must be an object, not boolean/}, + {method: 'format', input: [1], message: /Parameter 'pathObject' must be an object, not number/}, + {method: 'format', input: [{root: true}], message: /'pathObject.root' must be a string or undefined, not boolean/}, + {method: 'format', input: [{root: 12}], message: /'pathObject.root' must be a string or undefined, not number/}, +]; + +check(path.win32, winPaths); +check(path.posix, unixPaths); +checkErrors(path.win32); +checkErrors(path.posix); + +function checkErrors(path) { + errors.forEach(function(errorCase) { + try { + path[errorCase.method].apply(path, errorCase.input); + } catch(err) { + assert.ok(err instanceof TypeError); + assert.ok( + errorCase.message.test(err.message), + 'expected ' + errorCase.message + ' to match ' + err.message + ); + return; + } + + assert.fail('should have thrown'); + }); +} + + +function check(path, paths) { + paths.forEach(function(element, index, array) { + var output = path.parse(element); + assert.strictEqual(path.format(output), element); + assert.strictEqual(output.dir, output.dir ? path.dirname(element) : ''); + assert.strictEqual(output.base, path.basename(element)); + assert.strictEqual(output.ext, path.extname(element)); + }); +}