36

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.

ArunPratap
  • 4,816
  • 7
  • 25
  • 43
andree
  • 3,084
  • 9
  • 34
  • 42

10 Answers10

38
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;
}
BrokenGlass
  • 158,293
  • 28
  • 286
  • 335
  • 3
    This works only if the directories lack the final slash. See [Why isn't this DirectoryInfo comparison working?](http://stackoverflow.com/questions/3155034) – Darcara Mar 10 '13 at 17:23
31
  • 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.
Community
  • 1
  • 1
angularsen
  • 8,160
  • 1
  • 69
  • 83
  • What about path C:\foo\bar\..\bar2 vs C:\foo\bar2? Or C:\foo\bar\ vs C:\foo\bar\..\..\? – milanio Sep 02 '15 at 16:06
  • Good point. I believe we should add Path.GetFullPath() to resolve those examples. – angularsen Sep 02 '15 at 19:52
  • 1
    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. – angularsen Sep 02 '15 at 20:14
  • For me, Path.GetFullPath("c:") returns "c:\windows\system32" – emery.noel Jul 11 '17 at 15:35
  • @emery.noel I believe that is expected behavior, meaning you only specify a drive and not a path. `Path.GetFullPath(@"c:\")` returns `c:\ ` to me. How does this relate to the implementation of `IsSubpathOf()`? – angularsen Jul 11 '17 at 22:35
  • 1
    @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. – emery.noel Jul 14 '17 at 12:53
  • @anjdreas ... and of course now I go back and look, I see that you are doing it too. Missed the 2nd parenthesis. – emery.noel Jul 14 '17 at 12:55
  • Unfortunately doesn't work when symbolic links are involved (and presumably directory junctions). Still, +1 for covering the vast majority of directory layouts. – Kevin Shea Sep 05 '17 at 15:23
  • @KevinShea Yes, this only works on path strings, not on the filesystem level. I added a note to the answer now. To my knowledge, Windows API does not have a way to test for similar paths across symbolic links, network shares etc. You could do a check of created and edited timestamps, then finally a full checksum of the files, but that may not be a feasible solution in many cases. – angularsen Sep 06 '17 at 04:38
  • 1
    It's worth noting that the `[CanBeNull]` and `[NotNull]` annotations are part of the `JetBrains.Annotations` nuget package. Find them here: [JetBrains.Annotations](https://www.nuget.org/packages/JetBrains.Annotations/). – STLDev Oct 12 '18 at 02:31
8

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

Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65
  • Pls explain *!rel.StartsWith('.')* – Massimo Sep 09 '22 at 14:55
  • And what about folders with name starting with . (DOT) ? – Massimo Sep 09 '22 at 14:55
  • @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: https://dotnetfiddle.net/sVoXCH (result is in parenthesis at the end of each line). – Good Night Nerd Pride Sep 10 '22 at 16:28
  • 1
    @Massimo I updated the answer with a fixed version that now can handle folder names starting with a dot `.`. – Good Night Nerd Pride Oct 10 '22 at 10:39
  • @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. – montonero Feb 21 '23 at 14:39
  • @montonero thanks for point that out! I assumed that `c:/foo` should not be a subpath of `c:/foo`. That's why `.` is checked. But actually I don't know if that is correct. Do you have any reference for what the correct result should be? – Good Night Nerd Pride Feb 22 '23 at 09:56
  • @montonero You are correct that `@"c:\foo".IsSubPathOf("c:")` returns in a wrong result. But `"c:/foo".IsSubPathOf("c:")` does not for some reason. I'm looking for a fix and gonna update the answer later. – Good Night Nerd Pride Feb 22 '23 at 09:57
  • @GoodNightNerdPride regarding same path as subpath I've added an additional parameter `bool samePathIsSubpath = false` and check it this way `(samePathIsSubpath || rel != ".")`. IMO this is the best approach. Regarding `c:` - this happens due to OS understanding about what path is and `c:` is actually "current path on c: drive" but this isn't obvious so I've added a workaround `subPath += Path.DirectorySeparatorChar;` – montonero Feb 22 '23 at 10:12
  • @montonero yeah you could either append the separtor char or just replace all backslashes with forward slashes which is what I did in the updated answer. It has the additional benefit that another case now works correctly on linux as well. – Good Night Nerd Pride Feb 22 '23 at 10:19
5

Try:

dir1.contains(dir2+"\\");
Andrew Cooper
  • 32,176
  • 5
  • 81
  • 116
2
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\

Pawel Maga
  • 5,428
  • 3
  • 38
  • 62
  • This code ignores case (in)sensitivity of the platform. Otherwise it seems simple and working! – Josef Bláha Dec 18 '19 at 16:49
  • 7
    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 – Konstantin Dec 28 '19 at 14:00
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));
}
Tal Aloni
  • 1,429
  • 14
  • 14
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;
}
AlexeiOst
  • 574
  • 4
  • 13
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.

Johan Larsson
  • 17,112
  • 9
  • 74
  • 88
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.

Steve Greene
  • 490
  • 5
  • 10
-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);
}
John Whiter
  • 1,492
  • 1
  • 11
  • 5