40

I have 2 strings - dir1 and dir2, and I need to check if one is sub-directory for other. I tried to go with Contains method:

dir1.contains(dir2);

but that also returns true, if directories have similar names, for example - c:\abc and c:\abc1 are not sub-directories, bet returns true. There must be a better way.

1
  • 1
    what about directories that have multiple names, e.g. Symlinks? Commented Apr 11, 2011 at 6:53

12 Answers 12

41
DirectoryInfo di1 = new DirectoryInfo(dir1);
DirectoryInfo di2 = new DirectoryInfo(dir2);
bool isParent = di2.Parent.FullName == di1.FullName;

Or in a loop to allow for nested sub-directories, i.e. C:\foo\bar\baz is a sub directory of C:\foo :

DirectoryInfo di1 = new DirectoryInfo(dir1);
DirectoryInfo di2 = new DirectoryInfo(dir2);
bool isParent = false;
while (di2.Parent != null)
{
    if (di2.Parent.FullName == di1.FullName)
    {
        isParent = true;
        break;
    }
    else di2 = di2.Parent;
}
Sign up to request clarification or add additional context in comments.

1 Comment

This works only if the directories lack the final slash. See Why isn't this DirectoryInfo comparison working?
34
  • Case insensitive
  • Tolerates mix of \ and / folder delimiters
  • Tolerates ..\ in path
  • Avoids matching on partial folder names (c:\foobar not a subpath of c:\foo)

Note: This only matches on the path string and does not work for symbolic links and other kinds of links in the filesystem.

Code:

public static class StringExtensions
{
    /// <summary>
    /// Returns true if <paramref name="path"/> starts with the path <paramref name="baseDirPath"/>.
    /// The comparison is case-insensitive, handles / and \ slashes as folder separators and
    /// only matches if the base dir folder name is matched exactly ("c:\foobar\file.txt" is not a sub path of "c:\foo").
    /// </summary>
    public static bool IsSubPathOf(this string path, string baseDirPath)
    {
        string normalizedPath = Path.GetFullPath(path.Replace('/', '\\')
            .WithEnding("\\"));

        string normalizedBaseDirPath = Path.GetFullPath(baseDirPath.Replace('/', '\\')
            .WithEnding("\\"));

        return normalizedPath.StartsWith(normalizedBaseDirPath, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Returns <paramref name="str"/> with the minimal concatenation of <paramref name="ending"/> (starting from end) that
    /// results in satisfying .EndsWith(ending).
    /// </summary>
    /// <example>"hel".WithEnding("llo") returns "hello", which is the result of "hel" + "lo".</example>
    public static string WithEnding([CanBeNull] this string str, string ending)
    {
        if (str == null)
            return ending;

        string result = str;

        // Right() is 1-indexed, so include these cases
        // * Append no characters
        // * Append up to N characters, where N is ending length
        for (int i = 0; i <= ending.Length; i++)
        {
            string tmp = result + ending.Right(i);
            if (tmp.EndsWith(ending))
                return tmp;
        }

        return result;
    }

    /// <summary>Gets the rightmost <paramref name="length" /> characters from a string.</summary>
    /// <param name="value">The string to retrieve the substring from.</param>
    /// <param name="length">The number of characters to retrieve.</param>
    /// <returns>The substring.</returns>
    public static string Right([NotNull] this string value, int length)
    {
        if (value == null)
        {
            throw new ArgumentNullException("value");
        }
        if (length < 0)
        {
            throw new ArgumentOutOfRangeException("length", length, "Length is less than zero");
        }

        return (length < value.Length) ? value.Substring(value.Length - length) : value;
    }
}

Test cases (NUnit):

[TestFixture]
public class StringExtensionsTest
{
    [TestCase(@"c:\foo", @"c:", Result = true)]
    [TestCase(@"c:\foo", @"c:\", Result = true)]
    [TestCase(@"c:\foo", @"c:\foo", Result = true)]
    [TestCase(@"c:\foo", @"c:\foo\", Result = true)]
    [TestCase(@"c:\foo\", @"c:\foo", Result = true)]
    [TestCase(@"c:\foo\bar\", @"c:\foo\", Result = true)]
    [TestCase(@"c:\foo\bar", @"c:\foo\", Result = true)]
    [TestCase(@"c:\foo\a.txt", @"c:\foo", Result = true)]
    [TestCase(@"c:\FOO\a.txt", @"c:\foo", Result = true)]
    [TestCase(@"c:/foo/a.txt", @"c:\foo", Result = true)]
    [TestCase(@"c:\foobar", @"c:\foo", Result = false)]
    [TestCase(@"c:\foobar\a.txt", @"c:\foo", Result = false)]
    [TestCase(@"c:\foobar\a.txt", @"c:\foo\", Result = false)]
    [TestCase(@"c:\foo\a.txt", @"c:\foobar", Result = false)]
    [TestCase(@"c:\foo\a.txt", @"c:\foobar\", Result = false)]
    [TestCase(@"c:\foo\..\bar\baz", @"c:\foo", Result = false)]
    [TestCase(@"c:\foo\..\bar\baz", @"c:\bar", Result = true)]
    [TestCase(@"c:\foo\..\bar\baz", @"c:\barr", Result = false)]
    public bool IsSubPathOfTest(string path, string baseDirPath)
    {
        return path.IsSubPathOf(baseDirPath);
    }
}

Updates

  • 2015-08-18: Fix bug matching on partial folder names. Add test cases.
  • 2015-09-02: Support ..\ in paths, add missing code
  • 2017-09-06: Add note on symbolic links.

10 Comments

What about path C:\foo\bar\..\bar2 vs C:\foo\bar2? Or C:\foo\bar\ vs C:\foo\bar\..\..\?
Good point. I believe we should add Path.GetFullPath() to resolve those examples.
Added three more test cases and fixed the implementation to support your examples. Also added two missing extension methods the implementation relied on. I'm sure this can all be simplified, but it seems to work.
@anjdreas it is your first test case. I did not see how it could pass. For me, I had to append slashes to directories BEFORE I call GetFullPath or I get unexpected results.
It's worth noting that the [CanBeNull] and [NotNull] annotations are part of the JetBrains.Annotations nuget package. Find them here: JetBrains.Annotations.
|
9

Since netstandard2.1 there is finally an almost convenient and platform-independent way to check this: Path.GetRelativePath().

var relPath = Path.GetRelativePath(
    basePath.Replace('\\', '/'),
    subPath.Replace('\\', '/'));
var isSubPath =
    rel != "." && rel != ".."
    && !rel.StartsWith("../")
    && !Path.IsPathRooted(rel);

Both subPath and basePath must be absolut paths.

Convenience extension function:

public static bool IsSubPathOf(this string subPath, string basePath) {
    var rel = Path.GetRelativePath(
        basePath.Replace('\\', '/'),
        subPath.Replace('\\', '/'));
    return rel != "."
        && rel != ".."
        && !rel.StartsWith("../")
        && !Path.IsPathRooted(rel);
}

.NET Fiddle with some test cases: https://dotnetfiddle.net/di4ze6

9 Comments

Pls explain !rel.StartsWith('.')
And what about folders with name starting with . (DOT) ?
@Massimo Good catch! My proposed solution will not work with such folders. As to your question: check the result of Path.GetRelativePath() to understand !rel.StartsWith(). See this example here: dotnetfiddle.net/sVoXCH (result is in parenthesis at the end of each line).
@Massimo I updated the answer with a fixed version that now can handle folder names starting with a dot ..
@GoodNightNerdPride not sure why rel != "." is checked, it fails on @"c:\foo".IsSubPathOf(@"c:\foo"). Also @"c:\foo".IsSubPathOf(@"c:") won't work as expected.
|
5

Try:

dir1.contains(dir2+"\\");

1 Comment

On windows it is case-insensitive and this will fail
1

In my case the path and possible subpath do not contains '..' and never end in '\':

private static bool IsSubpathOf(string path, string subpath)
{
    return (subpath.Equals(path, StringComparison.OrdinalIgnoreCase) ||
            subpath.StartsWith(path + @"\", StringComparison.OrdinalIgnoreCase));
}

Comments

1
string path1 = "C:\test";
string path2 = "C:\test\abc";

var root = Path.GetFullPath(path1);
var secondDir = Path.GetFullPath(path2 + Path.AltDirectorySeparatorChar);

if (!secondDir.StartsWith(root))
{
}

Path.GetFullPath works great with paths, like: C:\test\..\forbidden\

2 Comments

This code ignores case (in)sensitivity of the platform. Otherwise it seems simple and working!
Imagine 2 directories: C:\SomeDirectory and C:\SomeDirectoryBackup this will give true, even though the second directory is not a child of the first one
0

My paths could possibly contain different casing and even have untrimmed segments... This seems to work:

public static bool IsParent(string fullPath, string base)
{
	var fullPathSegments = SegmentizePath(fullPath);
	var baseSegments = SegmentizePath(base);
	var index = 0;
	while (fullPathSegments.Count>index && baseSegments.Count>index && 
		fullPathSegments[index].Trim().ToLower() == baseSegments[index].Trim().ToLower())
		index++;
	return index==baseSegments.Count-1;
}

public static IList<string> SegmentizePath(string path)
{
	var segments = new List<string>();
	var remaining = new DirectoryInfo(path);
	while (null != remaining)
	{
		segments.Add(remaining.Name);
		remaining = remaining.Parent;
	}
	segments.Reverse();
	return segments;
}

Comments

0

Based on @BrokenGlass's answer but tweaked:

using System.IO;

internal static class DirectoryInfoExt
{
    internal static bool IsSubDirectoryOfOrSame(this DirectoryInfo directoryInfo, DirectoryInfo potentialParent)
    {
        if (DirectoryInfoComparer.Default.Equals(directoryInfo, potentialParent))
        {
            return true;
        }

        return IsStrictSubDirectoryOf(directoryInfo, potentialParent);
    }

    internal static bool IsStrictSubDirectoryOf(this DirectoryInfo directoryInfo, DirectoryInfo potentialParent)
    {
        while (directoryInfo.Parent != null)
        {
            if (DirectoryInfoComparer.Default.Equals(directoryInfo.Parent, potentialParent))
            {
                return true;
            }

            directoryInfo = directoryInfo.Parent;
        }

        return false;
    }
}

using System;
using System.Collections.Generic;
using System.IO;

public class DirectoryInfoComparer : IEqualityComparer<DirectoryInfo>
{
    private static readonly char[] TrimEnd = { '\\' };
    public static readonly DirectoryInfoComparer Default = new DirectoryInfoComparer();
    private static readonly StringComparer OrdinalIgnoreCaseComparer = StringComparer.OrdinalIgnoreCase;

    private DirectoryInfoComparer()
    {
    }

    public bool Equals(DirectoryInfo x, DirectoryInfo y)
    {
        if (ReferenceEquals(x, y))
        {
            return true;
        }

        if (x == null || y == null)
        {
            return false;
        }

        return OrdinalIgnoreCaseComparer.Equals(x.FullName.TrimEnd(TrimEnd), y.FullName.TrimEnd(TrimEnd));
    }

    public int GetHashCode(DirectoryInfo obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException(nameof(obj));
        }
        return OrdinalIgnoreCaseComparer.GetHashCode(obj.FullName.TrimEnd(TrimEnd));
    }
}

Not ideal if performance is essential.

Comments

0

Update - this I wrote originally is wrong (see below):

It seems to me that you actually stick with the basic string comparison (using .ToLower() of course) using the .StartsWith() function, along with counting the path separators, but you add in an additional consideration in regard to the number of path separators - and you need to employ something like Path.GetFullPath() on the strings beforehand to make sure you're dealing with consistent path string formats. So you'd end up with something basic and simple, like this:

string dir1a = Path.GetFullPath(dir1).ToLower();
string dir2a = Path.GetFullPath(dir2).ToLower();
if (dir1a.StartsWith(dir2a) || dir2a.StartsWith(dir1a)) {
    if (dir1a.Count(x => x = Path.PathSeparator) != dir2a.Count(x => x = Path.PathSeparator)) {
        // one path is inside the other path
    }
}

Update...

As I discovered in using my code, the reason this is wrong, is because it does not account for cases where one directory name begins with the same characters as the entire name of the other directory. I had a case where I had one directory path of "D:\prog\dat\Mirror_SourceFiles" and another directory path of "D:\prog\dat\Mirror". Since my first path does indeed "start with" the letters "D:\prog\dat\Mirror" my code gave me a false match. I got rid of .StartsWith entirely and changed the code to this (method: split the path to the individual parts, and compare the parts up to the smaller number of parts):

// make sure "dir1" and "dir2a" are distinct from each other
// (i.e., not the same, and neither is a subdirectory of the other)
string[] arr_dir1 = Path.GetFullPath(dir1).Split(Path.DirectorySeparatorChar);
string[] arr_dir2 = Path.GetFullPath(dir2).Split(Path.DirectorySeparatorChar);
bool bSame = true;
int imax = Math.Min(arr_dir1.Length, arr_dir2.Length);
for (int i = 0; i < imax; ++i) {
  if (String.Compare(arr_dir1[i], arr_dir2[i], true) != 0) {
    bSame = false;
    break;
  }
}

if (bSame) {
  // do what you want to do if one path is the same or
  // a subdirectory of the other path
}
else {
  // do what you want to do if the paths are distinct
}

Of course, note that in a "real program" you are going to be using the Path.GetFullPath() function in a try-catch to handle the appropriate exceptions in regard to the string you're passing into it.

Comments

0

You could use Matcher from Microsoft.Extensions.FileSystemGlobbing. This has been available since .NET Standard 2.0.

Example:

//path parameter is "FileSystemGlobbing\\abc"

public Task<bool> IsPathInDirectory(string path)
{
    var paths = new string[] { "C:\\Temp\\FileSystemGlobbing\\abc\\" };
     
    //You can also use this to check if a relative file path exits
    //var paths = Directory.GetFiles("YourDirectoryToSearch", "*", SearchOption.AllDirectories);

    path = $"**/{path}/**";

    Matcher callMatcher = new();

    callMatcher.AddIncludePatterns(new string[] { path });

    var matches = callMatcher.Match("C:\\", paths);
    
    //Returns true in this case
    return Task.FromResult(matches.HasMatches);
}

Comments

0

Every answer I tried in this entire thread is wrong, so here is my solution:

public static bool IsSubdirectory(string path, string parentFolder)
{
    var di1 = new DirectoryInfo(parentFolder).FullName;
    var di2 = new DirectoryInfo(path).FullName;

    // On Windows, paths are case-insensitive
    if (Environment.OSVersion.Platform == PlatformID.Win32NT)
    {
        di1 = di1.ToLowerInvariant();
        di2 = di2.ToLowerInvariant();
    }

    // Ensure both end with a directory separator
    if (!di1.EndsWith(Path.DirectorySeparatorChar.ToString()))
    {
        di1 += Path.DirectorySeparatorChar;
    }
    if (!di2.EndsWith(Path.DirectorySeparatorChar.ToString()))
    {
        di2 += Path.DirectorySeparatorChar;
    }

    // Check if the child path starts with the parent path
    return di2.StartsWith(di1, StringComparison.Ordinal);
}

This is the only thing I've found that gets these test cases correct:

IsSubdirectory("C:/abc1", "C:/abc"); // -> false
IsSubdirectory("C:\\Parent\\Child", "C:/PARENT"); // -> true on windows
IsSubdirectory("/Parent/Child", "/PARENT"); // -> false on linux

Keep in mind it also returns true for a directory being a subdirectory of itself, so just remove the separator at the end of d2 instead of adding it if you don't want this.

Comments

-1
public static bool IsSubpathOf(string rootPath, string subpath)
{
    if (string.IsNullOrEmpty(rootPath))
        throw new ArgumentNullException("rootPath");
    if (string.IsNullOrEmpty(subpath))
        throw new ArgumentNulLException("subpath");
    Contract.EndContractBlock();

    return subath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase);
}

1 Comment

This unfortunately returns true for rootPath = @"c:\foo" and subPath = @"c:\foobar" - which is obviously a false positive.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.