开发者

Given full path, check if path is subdirectory of some other path, or otherwise

开发者 https://www.devze.com 2023-02-23 05:39 出处:网络
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:

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.


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;
}


  • 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.


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


Try:

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


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\


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));
}


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;
}


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.


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.


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);
}
0

精彩评论

暂无评论...
验证码 换一张
取 消