I have an array of strings. I need to grab the URL's value on each string. I have created a function with Regex to accomplish this. It works fine, but there's an edge case I haven't been able to cover. When there is not any URL:'s value, I get the error: Cannot read property '1' of null.
This is my code:
const arrStr = [
`describe
url: https://url-goes-here-/1,
https://url-goes-here-/2,
https://url-goes-here-/3
});`
,
`
before(() => {
url: https://url-goes-here-/4
});
`,
`
before(() => {
url: https://url-goes-here-/5
});
`,
`describe
// nothing http link here
});
`
]
const getXrayUrl = str => str.match(/url:([^;]+)/)[1].trim().split('(')[0]; // cannot read property '1' of null
const allXrayUrls = arrStr.map(item => getXrayUrl(item));
If I removed the string with no URL value from the array, I get this output:
[ 'https://url-goes-here-/1,\n https://url-goes-here-/2,\n https://url-goes-here-/3\n })',
'https://url-goes-here-/4\n })',
'https://url-goes-here-/5\n })' ]
How do I cover this edge case & return another array with all the string in the level?
According to match function documentation, it returns an array of matches, or null if no matches are found.
If you need to handle the case of a missing URL attribute so check match array is null before accessing the capture group as following:
const match = str.match(/url:\s*([^;]+)\n/)
// in case no match retrun empty string
// split the match on , to handle multi URL case
const url = match? match[1].split(",").map(item => item.trim()) : [""];
after that filter match results removing empty values as following:
arrStr.map(getXrayUrl).flat().filter(item => item !== "");
so final solution as following:
const arrStr = [
`describe
url: https://url-goes-here-/1,
https://url-goes-here-/2,
https://url-goes-here-/3
});`
,
`
before(() => {
url: https://url-goes-here-/4
});
`,
`
before(() => {
url: https://url-goes-here-/5
});
`,
`describe
// nothing http link here
});
`
]
const getXrayUrl = str => {
const match = str.match(/url:\s*([^;]+)\n/)
// in case no match retrun empty string
// split the match on , to handle multi URL case
return match? match[1].split(",").map(item => item.trim()) : [""];
}
const allXrayUrls = arrStr.map(getXrayUrl).flat().filter(item => item !== "");
console.log(allXrayUrls)
console output:
["https://url-goes-here-/1", "https://url-goes-here-/2", "https://url-goes-here-/3", "https://url-goes-here-/4", "https://url-goes-here-/5"]
I am working on a MQTT handler for which I want to emit an event for each parent directory where there is a event listener. For example:
If there are the following MQTT paths available, where there are subscriptors –there are event listeners for these paths–
test
replyer/request
test/replyer/request
And someone publishes on topic test/replyer/request/#issuer, there should be 2 events emmited: test, test/replyer/request.
Given than any path is possible and there is no list of available valid events, we must check only if a path is a parent of another. Can we do this with regex? If so, how would it look like? Is there a simpler/more efficient solution?
Let Node itself do the work.
const path = require('path');
const relative = path.relative(parent, dir);
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
It does normalisation for you as well.
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})`);
});
Works on Windows across drives too.
[/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)
2021 Answer
Use #Ilya's solution.
2017 Answer
In ES6.
const isChildOf = (child, parent) => {
if (child === parent) return false
let parentTokens = parent.split('/').filter(i => i.length)
let childTokens = child.split('/').filter(i => i.length)
return parentTokens.every((t, i) => childTokens[i] === t)
}
If you're working in node.js and you want to make it cross-platform, include the path module and replace split('/') with split(path.sep).
How it works:
So, you want to find out if a directory (like home/etc/subdirectory) is a subdirectory of another directory (like home/etc).
It takes both the hypothesised child and parent paths and convert them into arrays using split:
['home', 'etc', 'subdirectory'], ['home', 'etc']
It then iterates through all of the tokens in the parent array and checks them one-by-one against their relative position in the child array using ES6's .every().
If everything in parent matches up to everything in child, knowing that we've ruled out they are exactly the same directory (using child !== parent), we will have our answer.
For those who care about performances, which seem to pass unnoticed to people who have already answered, checking whether the sub path starts with its parent path should be enough.
const path = require('path');
function isSubPathOf(subPath, parentPath) {
parentPath = normalize(parentPath);
if (subPath.length <= parentPath.length)
return false;
function normalize(p) {
p = path.normalize(p);
if (!p.endsWith(path.sep))
p += path.sep;
return p;
}
subPath = normalize(subPath);
return subPath.startsWith(parentPath);
}
console.log(isSubPathOf('/a/b/c/d/e', '/a/b/c'));
console.log(isSubPathOf('/a/b/c/de', '/a/b/c'));
console.log(isSubPathOf('/a/b/c', '/a/y/c'));
console.log(isSubPathOf('/a/y/c/k', '/a/y/c'));
This is a really old question, but I came up with a dead simple solution for this using Node's built–in path.relative: if the child is inside the parent, the relative path from the former to the latter will always start with '..'.
import { relative } from 'path';
function isSubDirectory(parent, child) {
return relative(child, parent).startsWith('..');
}
There are a couple of things going on here that are needed to prevent failure:
Should we attempt to resolve filesystem paths? (I think so)
Checking if one directory contains another should work with symlinks
I came up with a solution that attempts to resolve filesystem paths as much as possible while allowing paths which may or may not exist:
Split the path on the OS's path separator
Resolve as many of those path components in the filesystem as possible
Append remaining components that couldn't be resolved
If the relative path between parent and child doesn't start with .. + path.sep and isn't .. then the parent path contains the child path
This all works, assuming that any nonexistent path components would be created using only directories and files (no symlinks). For example say your script needs to write only to whitelisted paths and you're accepting untrusted (user-supplied) filenames. You could create subdirectories using something like PHP's mkdir with $recursive = true to create the directory structure in one step, similar to this example.
Here is the code (not runnable until Stack Overflow supports Node.js), the important functions are resolveFileSystemPath() and 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)
);
Output:
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
Live example: https://repl.it/repls/LawngreenWorriedGreyware
The last line of the output is the important one, showing how resolved filesystem paths lead to the correct result (unlike the unresolved result above it).
Limiting filesystem reads/writes to certain directories is so important for security that I hope Node.js incorporates this functionality into their builtins. I haven't tested this on a native Windows box so please let me know if the kWin32 flag is working. I'll try to curate this answer as time allows.
Based on & improved Dom Vinyard's code:
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);
}
Result in:
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);
Doing it with regex is one way to go about it (for every path that has an event listener, check whether the published topic starts with that path), but since it's more likely you'll have many different paths than you having absurdly long URLs, breaking down the published topic might be more efficient.
Something like this is probably easier to read, too:
Edit: #huaoguo is definitely right, indexOf === 0 is all we really need!
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']
I would also like to point to the npm package path-is-inside which does exactly what the TO has been asking for:
Usage (quoted from the readme):
Pretty simple. First the path being tested; then the potential parent. Like so:
var pathIsInside = require("path-is-inside");
pathIsInside("/x/y/z", "/x/y") // true
pathIsInside("/x/y", "/x/y/z") // false
Paths are considered to be inside themselves:
pathIsInside("/x/y", "/x/y"); // true
For me, it does the job, and it is for sure a better to maintain such non-trivial logic in an extra package instead of a StackOverflow answer. :-)
#dom-vinyard's idea is good but code is not working correctly, for instance with this input:
isChildOf('/x/y', '/x') //false
I wrote my own version here:
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);
}
Here another solution that use indexOf (or that work by comparing strings).
In the function bellow i didn't use indexOf in order to support multiple path separators. You can check, but if you are sure you have just one separator, you can use indexOf with no problem.
The trick is to check if the path end with a separator, if not you just add such a separator to it. In that, there will be no problem of having a substring that is not a complete path in the child path. [/this/isme_man and /this/isme] (the first is child of the second, if we simply use indexOf (which of course if false), but if you do using the trick like this [/this/isme/ and /this/isme_man/] and you compare using same indexOf there will be no problem, and it work nikel)].
Notice too that there is an option, to allow a check with orEqual (a child or equal), it's the third optional parameter.
Check the code bellow.
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;
}
Here a test example:
let path = '/ok/this/is/the/path';
let parentPath = '/ok/this/is';
let parentPath2 = '/ok/this/is/';
let parentPath3 = '/notok/this/is/different';
console.log("/ok/this/is/the/path' is child of /ok/this/is => " + isPathChildOf(path, parentPath));
console.log("/ok/this/is/the/path' is child of /ok/this/is/=> " + isPathChildOf(path, parentPath2));
console.log("/ok/this/is/' is child of /ok/this/is/ => " + isPathChildOf(parentPath2, parentPath2));
console.log("/ok/this/is/the/path' is child of /notok/this/is/different => " + isPathChildOf(path, parentPath3));
// test number 2:
console.log('test number 2 : ');
console.log("=============================");
let pthParent = '/look/at/this/path';
let pth = '/look/at/this/patholabi/hola'; // in normal use of indexof it will return true (know too we didn't use indexof just to support the different path separators, otherwise we would have used indexof in our function)
//expected result is false
console.log(`${pth} is a child of ${pthParent} ===> ${isPathChildOf(pth, pthParent)}`);
let pthParent2 = '/look/at/this/path';
let pth2 = '/look/at/this/path/hola';
//expected result is true
console.log(`${pth2} is a child of ${pthParent2} ===> ${isPathChildOf(pth2, pthParent2)}`);
let pthParent3 = '/look/at/this/path';
let pth3 = '/look/at/this/pathholabi';
//expected result is false
console.log(`${pth3} is a child of ${pthParent3} ===> ${isPathChildOf(pth3, pthParent3)}`);
// test 3: equality
console.log('\ntest 3 : equality');
console.log("==========================");
let pParent = "/this/is/same/Path";
let p = "/this\\is/same/Path/";
console.log(`${p} is child of ${pParent} ====> ${isPathChildOf(p, pParent, true)}`);
You can see in the last example how we used the function to check for both being a child or equal (which can be really handful).
Also know that, You can check my two related github repos, that include too another implementation for the split method (a spliting method with multiple separator without using the regex engine)), also this method too, and some good explanation (check the comments within the codes):
https://github.com/MohamedLamineAllal/isPathChildOfJS
https://github.com/MohamedLamineAllal/splitStrJS
In my understanding, the issue is a little more complex than what my colleagues understand.
Paths could be tested with fs.existsSync(), but in this case we would create a dependency on the fs library and limit it to testing only absolute directories, both should be tested for their respective existence unless that the user would be interested in not using Windows as the Operating System so as not to need to inform the root directory, which is formed by default by "letter:\" treating the partitions as if they were sisters to each other, no none being nested to a larger directory.
The operation is totally different on Unix systems. The root directory is by default / and all mounted disks are nested to it, making it unique for the entire OS. It can be seen, therefore, that this is not ideal.
It is not possible to solve only with regular expression, considering that, if "pathFoldersLength" is greater than "path2CheckIfIsSupposedParentFoldersLength", a false negative could be obtained if, for example, "path" was equal to "laden /subfolder-1" and "path2CheckIfIsParent" to "bin/laden/subfolder-1/subfolder-1.1" and if the search was done with $ at the end of "path"; leaving without the $ would give a false positive if, for example, "path" was equal to "bin/laden/subfolder-1/subfolder-1.1" and "path2CheckIfIsParent" to "laden/subfolder-1";
If "pathFoldersLength" is less than "path2CheckIfIsSupposedParentFoldersLength, you could get a false negative if, for example, "path" is equal to "laden/subpath-1" and "path2CheckIfIsParent" to "bin/laden/ subpath-1" if the search was done with ^ at the beginning of "path2CheckIfIsParent", or it could give a false positive if, for example, "path" was equal to "bin/laden" and "path2CheckIfIsParent" to " bin/laden/subpath-1".
In this way we eliminate dependencies, leaving the method as little language dependent as possible.
import Path from 'path';
const isASubpathFromAnyOfSubpathSet = (path, path2CheckIfIsParent, isAbsolute = true) => {
const pathSeparatorPattern = new RegExp(/\\+/, 'g');
const pathSeparatorAtStartOrAtEndPattern = new RegExp(/^\/+|\/+$/, 'g');
path = path.replace(pathSeparatorPattern, `/`);
path2CheckIfIsParent = path2CheckIfIsParent.replace(pathSeparatorPattern, `/`);
path = path.replace(pathSeparatorAtStartOrAtEndPattern , ``);
path2CheckIfIsParent = path2CheckIfIsParent.replace(pathSeparatorAtStartOrAtEndPattern , ``)
if (path === path2CheckIfIsParent) return false;
const pathFolders = path.split(`/`);
const path2CheckIfIsSupposedParentFolders = path2CheckIfIsParent.split(`/`);
const pathFoldersLength = pathFolders.length;
const path2CheckIfIsSupposedParentFoldersLength = path2CheckIfIsSupposedParentFolders.length;
const indexesOf = [];
let pathFolderIndex = 0;
let supposedParentFolderIndex = 0;
let stopCriterian;
for (let i = 0; i < path2CheckIfIsSupposedParentFoldersLength; i++) {
if (pathFolders[0] === path2CheckIfIsSupposedParentFolders[i]) indexesOf.push(i);
}
if (indexesOf.length) {
if (isAbsolute) {
if (pathFoldersLength > path2CheckIfIsSupposedParentFoldersLength) {
path2CheckIfIsParent = path2CheckIfIsParent.replace(/\./g, `\\.`);
return (new RegExp(`^${path2CheckIfIsParent}`)).test(path);
}
} else {
if (indexesOf[0] === 0) indexesOf.shift();
if (pathFoldersLength < path2CheckIfIsSupposedParentFoldersLength) {
stopCriterian = () => pathFolderIndex < pathFoldersLength - 1;
} else {
stopCriterian = () => supposedParentFolderIndex < path2CheckIfIsSupposedParentFoldersLength - 1;
}
for (let indexOf of indexesOf) {
pathFolderIndex = 0;
for (supposedParentFolderIndex = indexOf; stopCriterian();) {
if (path2CheckIfIsSupposedParentFolders[supposedParentFolderIndex] !== pathFolders[pathFolderIndex]) break;
supposedParentFolderIndex++;
pathFolderIndex++;
}
}
if (pathFoldersLength < path2CheckIfIsSupposedParentFoldersLength) {
return pathFolderIndex === pathFoldersLength - 1;
} else {
return supposedParentFolderIndex === path2CheckIfIsSupposedParentFoldersLength - 1;
}
}
}
return false;
}
/*
// >
console.log(isASubpathFromAnyOfSubpathSet (`bin/laden/subfolder-1`, `bin/laden`)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1/subfolder-1.1.1`, `bin/laden/subfolder-1`)) // => false
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1`, `bin/laden`)) // => false
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1/subfolder-1.1.1`, `bin/laden/subfolder-1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1`, `bin/laden`, false)) // => true
// <
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1`, `bin/laden/subfolder-1/subfolder-1.1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1`, `bin/laden/subfolder-1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`subfolder-1/subfolder-1.1`, `bin/laden/subfolder-1`, false)) // => true
// ===
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1/subfolder-1.1`, `bin/laden/subfolder-1`, false)) // => true
console.log(isASubpathFromAnyOfSubpathSet (`laden/subfolder-1`, `bin/laden`, false)) // => true
/**/
Use indexOf to check that the path to the child directory starts with the path to the parent directory is enough:
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