确定路径是否是Node.js中另一个路径的子目录

时间:2016-05-30 09:15:39

标签: javascript regex node.js

我正在处理一个MQTT handler,我想为每个有一个事件监听器的父目录发出一个事件。例如:

如果有以下MQTT路径可用,其中有下标 - 这些路径有事件侦听器 -

  • test
  • replyer/request
  • test/replyer/request

有人发表关于主题test/replyer/request/@issuer的帖子,应该有2个活动:testtest/replyer/request

鉴于任何路径都是可能的并且没有可用的有效事件列表,我们必须仅检查路径是否是另一个路径的父路径。我们可以用正则表达式做到这一点吗?如果是这样,它会是什么样子?有更简单/更有效的解决方案吗?

10 个答案:

答案 0 :(得分:26)

让节点本身完成工作。

const path = require('path');
const relative = path.relative(parent, dir);
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);

它也为你做了标准化。

const path = require('path');

const tests = [
  ['/foo', '/foo'],
  ['/foo', '/bar'],
  ['/foo', '/foobar'],
  ['/foo', '/foo/bar'],
  ['/foo', '/foo/../bar'],
  ['/foo', '/foo/./bar'],
  ['/bar/../foo', '/foo/bar'],
  ['/foo', './bar'],
  ['C:\\Foo', 'C:\\Foo\\Bar'],
  ['C:\\Foo', 'C:\\Bar'],
  ['C:\\Foo', 'D:\\Foo\\Bar'],
];

tests.forEach(([parent, dir]) => {
    const relative = path.relative(parent, dir);
    const isSubdir = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
    console.log(`[${parent}, ${dir}] => ${isSubdir} (${relative})`);
});

也适用于跨驱动器的Windows。

[/foo, /foo] => false ()
[/foo, /bar] => false (..\bar)
[/foo, /foobar] => false (..\foobar)
[/foo, /foo/bar] => true (bar)
[/foo, /foo/../bar] => false (..\bar)
[/foo, /foo/./bar] => true (bar)
[/bar/../foo, /foo/bar] => true (bar)
[/foo, ./bar] => false (..\Users\kozhevnikov\Desktop\bar)
[C:\Foo, C:\Foo\Bar] => true (Bar)
[C:\Foo, C:\Bar] => false (..\Bar)
[C:\Foo, D:\Foo\Bar] => false (D:\Foo\Bar)

答案 1 :(得分:6)

2017年末回答

在ES6中。

const isChildOf = (child, parent) => {
  if (child === parent) return false
  const parentTokens = parent.split('/').filter(i => i.length)
  return parentTokens.every((t, i) => child.split('/')[i] === t)
}

如果您正在使用node.js并希望跨平台工作,请添加path模块并将split('/')替换为split(path.sep)

工作原理:

所以,你想知道目录(如home/etc/subdirectory)是否是另一个目录的子目录(如home/etc)。

它使用假设的childparent路径并使用split将它们转换为数组:

['home', 'etc', 'subdirectory'], ['home', 'etc']

然后迭代遍历parent数组中的所有标记,并使用ES6的child逐个检查它们在.every()数组中的相对位置。

如果父母中的所有内容都符合孩子的所有内容,我们知道我们已经排除了他们完全相同的目录(使用child !== parent),我们将得到答案。

答案 2 :(得分:2)

这是一个非常老的问题,但是我使用path.relative内置的node提出了一个非常简单的解决方案。如果孩子在父母的内部,则从孩子到的相对路径将始终以..开头。很简单。

import { relative } from 'path';

function isSubDirectory(parent, child) {
  return relative(child, parent).startsWith('..');
}

答案 3 :(得分:1)

使用正则表达式来实现它是一种方法(对于每个具有事件监听器的路径,检查发布的主题是否以该路径开头),但是因为它更有可能你会有许多不同的路径,比你拥有荒谬的长URL,打破发布的主题可能会更有效率。

这样的东西也可能更容易阅读:

编辑@huaoguo绝对正确,indexOf === 0就是我们真正需要的!

let paths = [
  'test',
  'replyer/request',
  'test/replyer/request'
]

let topic = 'test/replyer/request/@issuer'

let respondingPaths = (paths, topic) => paths.filter(path => topic.indexOf(path) === 0)

console.log(respondingPaths(paths, topic)) // ['test', 'test/replyer/request']

答案 4 :(得分:0)

@ dom-vinyard的想法很好,但代码无法正常工作,例如使用此输入:

isChildOf('/x/y', '/x') //false

我在这里写了自己的版本:

function isParentOf(child, parent) {
  const childTokens = child.split('/').filter(i => i.length);
  const parentTokens = parent.split('/').filter(i => i.length);

  if (parentTokens.length > childTokens.length || childTokens.length === parentTokens.length) {
    return false;
  }

  return childTokens
    .slice(0, parentTokens.length)
    .every((childToken, index) => parentTokens[index] === childToken);
}

答案 5 :(得分:0)

这里有一些事情需要防止失败:

  • 我们应该尝试解析文件系统路径吗? (我想是的)
  • 检查一个目录是否包含另一个目录应该使用符号链接

我想出了一个解决方案,尝试尽可能多地解析文件系统路径,同时允许可能存在或可能不存在的路径

  • 拆分操作系统路径分隔符上的路径
  • 尽可能多地解析文件系统中的那些路径组件
  • 附加无法解决的剩余组件
  • 如果父母和孩子之间的相对路径不是以.. + path.sep开头且不是..,那么父路径包含子路径

这一切都有效,假设只使用目录和文件创建任何不存在的路径组件(没有符号链接)。例如,假设您的脚本只需要写入列入白名单的路径,并且您接受不受信任(用户提供的)文件名。您可以使用PHP's mkdir$recursive = true之类的内容创建子目录,以便在一个步骤中创建目录结构,类似于this example

这是代码(在Stack Overflow支持Node.js之前不可运行),重要的函数是resolveFileSystemPath()pathContains()



const kWin32 = false;

const fs = require('fs');
const path = kWin32 ? require('path').win32 : require('path');

////////// functions //////////

// resolves (possibly nonexistent) path in filesystem, assuming that any missing components would be files or directories (not symlinks)
function resolveFileSystemPath(thePath) {
	let remainders = [];

	for (
		let parts = path.normalize(thePath).split(path.sep); // handle any combination of "/" or "\" path separators
		parts.length > 0;
		remainders.unshift(parts.pop())
	) {
		try {
			thePath =
				fs.realpathSync(parts.join('/')) + // fs expects "/" for cross-platform compatibility
				(remainders.length ? path.sep + remainders.join(path.sep) : ''); // if all attempts fail, then path remains unchanged

			break;
		} catch (e) {}
	}

	return path.normalize(thePath);
}

// returns true if parentPath contains childPath, assuming that any missing components would be files or directories (not symlinks)
function pathContains(parentPath, childPath, resolveFileSystemPaths = true) {
	if (resolveFileSystemPaths) {
		parentPath = resolveFileSystemPath(parentPath);
		childPath = resolveFileSystemPath(childPath);
	}

	const relativePath = path.relative(parentPath, childPath);

	return !relativePath.startsWith('..' + path.sep) && relativePath != '..';
}

////////// file/directory/symlink creation //////////

console.log('directory contents:');

console.log();

try {
	fs.mkdirSync('parent');
} catch (e) {} // suppress error if already exists

fs.writeFileSync('parent/child.txt', 'Hello, world!');

try {
	fs.mkdirSync('outside');
} catch (e) {} // suppress error if already exists

try {
	fs.symlinkSync(path.relative('parent', 'outside'), 'parent/child-symlink');
} catch (e) {} // suppress error if already exists

fs.readdirSync('.').forEach(file => {
	const stat = fs.lstatSync(file);

	console.log(
		stat.isFile()
			? 'file'
			: stat.isDirectory() ? 'dir ' : stat.isSymbolicLink() ? 'link' : '    ',
		file
	);
});
fs.readdirSync('parent').forEach(file => {
	file = 'parent/' + file;

	const stat = fs.lstatSync(file);

	console.log(
		stat.isFile()
			? 'file'
			: stat.isDirectory() ? 'dir ' : stat.isSymbolicLink() ? 'link' : '    ',
		file
	);
});

////////// tests //////////

console.log();

console.log(
	"path.resolve('parent/child.txt'):       ",
	path.resolve('parent/child.txt')
);
console.log(
	"fs.realpathSync('parent/child.txt'):    ",
	fs.realpathSync('parent/child.txt')
);
console.log(
	"path.resolve('parent/child-symlink'):   ",
	path.resolve('parent/child-symlink')
);
console.log(
	"fs.realpathSync('parent/child-symlink'):",
	fs.realpathSync('parent/child-symlink')
);

console.log();

console.log(
	'parent contains .:                                ',
	pathContains('parent', '.', true)
);
console.log(
	'parent contains ..:                               ',
	pathContains('parent', '..', true)
);
console.log(
	'parent contains parent:                           ',
	pathContains('parent', 'parent', true)
);
console.log(
	'parent contains parent/.:                         ',
	pathContains('parent', 'parent/.', true)
);
console.log(
	'parent contains parent/..:                        ',
	pathContains('parent', 'parent/..', true)
);
console.log(
	'parent contains parent/child.txt (unresolved):    ',
	pathContains('parent', 'parent/child.txt', false)
);
console.log(
	'parent contains parent/child.txt (resolved):      ',
	pathContains('parent', 'parent/child.txt', true)
);
console.log(
	'parent contains parent/child-symlink (unresolved):',
	pathContains('parent', 'parent/child-symlink', false)
);
console.log(
	'parent contains parent/child-symlink (resolved):  ',
	pathContains('parent', 'parent/child-symlink', true)
);




输出:

directory contents:

file .bash_logout
file .bashrc
file .profile
file config.json
dir  node_modules
dir  outside
dir  parent
link parent/child-symlink
file parent/child.txt

path.resolve('parent/child.txt'):        /home/runner/parent/child.txt
fs.realpathSync('parent/child.txt'):     /home/runner/parent/child.txt
path.resolve('parent/child-symlink'):    /home/runner/parent/child-symlink
fs.realpathSync('parent/child-symlink'): /home/runner/outside

parent contains .:                                 false
parent contains ..:                                false
parent contains parent:                            true
parent contains parent/.:                          true
parent contains parent/..:                         false
parent contains parent/child.txt (unresolved):     true
parent contains parent/child.txt (resolved):       true
parent contains parent/child-symlink (unresolved): true
parent contains parent/child-symlink (resolved):   false

实例:https://repl.it/repls/LawngreenWorriedGreyware

输出的最后一行是重要的一行,显示已解析的文件系统路径如何导致正确的结果(与上面未解析的结果不同)。

限制文件系统对某些目录的读/写对于安全性非常重要,我希望Node.js将此功能合并到其内置中。我还没有在原生Windows机箱上对此进行测试,所以如果kWin32标志正常工作,请告诉我。我会尝试在时间允许的情况下策划这个答案。

答案 6 :(得分:0)

基于&改进了Dom Vinyard的代码:

const path = require('path');

function isAncestorDir(papa, child) {
    const papaDirs = papa.split(path.sep).filter(dir => dir!=='');
    const childDirs = child.split(path.sep).filter(dir => dir!=='');

    return papaDirs.every((dir, i) => childDirs[i] === dir);
}

结果:

assert(isAncestorDir('/path/to/parent', '/path/to/parent/and/child')===true);
assert(isAncestorDir('/path/to/parent', '/path/to')===false);
assert(isAncestorDir('/path/to/parent', '/path/to/parent')===true);

答案 7 :(得分:0)

这是另一个使用indexOf的解决方案(或通过比较字符串起作用的解决方案)。
在下面的函数中,我没有使用indexOf支持多个路径分隔符。您可以进行检查,但是如果您确定只有一个分隔符,则可以毫无疑问地使用indexOf
诀窍检查路径是否以分隔符结尾,如果不是,则只需向其添加分隔符。这样,在子路径中具有不是完整路径的子字符串将没有问题。 [/this/isme_man/this/isme](如果我们仅使用indexOf(如果为false,则为第二个,第一个为第二个的子代),但是如果您确实使用了这种技巧[[{ {1}}和/this/isme/],并且您使用相同的/this/isme_man/进行比较就没有问题,并且可以正常运行)]。
还要注意,还有一个选项,它是第三个可选参数,允许使用orEqual(子级或等于)进行检查。

检查下面的代码。

indexOf

下面是一个测试示例:

const PATH_SEPA = ['\\', '/'];

function isPathChildOf(path, parentPath, orEqual) {
    path = path.trim();
    parentPath = parentPath.trim();

    // trick: making sure the paths end with a separator
    let lastChar_path = path[path.length - 1];
    let lastChar_parentPath = path[parentPath.length - 1];
    if (lastChar_parentPath !== '\\' && lastChar_parentPath !== '/') parentPath += '/';
    if (lastChar_path !== '\\' && lastChar_path !== '/') path += '/';

    if (!orEqual && parentPath.length >= path.length) return false; // parent path should be smaller in characters then the child path (and they should be all the same from the start , if they differ in one char then they are not related)

    for (let i = 0; i < parentPath.length; i++) {
        // if both are not separators, then we compare (if one is separator, the other is not, the are different, then it return false, if they are both no separators, then it come down to comparaison, if they are same nothing happen, if they are different it return false)
        if (!(isPathSeparator(parentPath[i]) && isPathSeparator(path[i])) && parentPath[i] !== path[i]) {
            return false;
        }
    }
    return true;
}

function isPathSeparator(chr) {
    for (let i = 0; i < PATH_SEPA.length; i++) {
        if (chr === PATH_SEPA[i]) return true;
    }
    return false;
}

在上一个示例中,您可以看到我们如何使用该函数检查孩子是否相等(可能确实很少)。

也知道,您可以检查我的两个相关的github存储库,其中包括split方法的另一种实现(不使用regex引擎的具有多个分隔符的split方法),也包括该方法,以及一些很好的解释(检查代码中的注释):

答案 8 :(得分:0)

我还想指出 npm 包 path-is-inside,它完全符合 TO 的要求:

npm

用法(引自自述文件):

<块引用>

很简单。首先是被测试的路径;然后是潜在的父母。像这样:

var pathIsInside = require("path-is-inside");

pathIsInside("/x/y/z", "/x/y") // true
pathIsInside("/x/y", "/x/y/z") // false

路径被认为是在它们自身内部:

pathIsInside("/x/y", "/x/y"); // true

对我来说,它可以胜任,而且在一个额外的包中维护这种非平凡的逻辑肯定比 StackOverflow 答案更好。 :-)

答案 9 :(得分:-3)

使用indexOf就足够了。

function isParentOf(parent, dir) {
  return dir.indexOf(parent) === 0;
}

isParentOf('test/replyer/request/@issuer', 'test') // true
isParentOf('test/replyer/request/@issuer', 'replyer/request') // false
isParentOf('test/replyer/request/@issuer', 'test/replyer/request') // true