【问题标题】:Given full path, check if path is subdirectory of some other path, or otherwise给定完整路径,检查路径是否是其他路径的子目录,否则
【发布时间】:2011-04-11 06:00:02
【问题描述】:

我有 2 个字符串 - dir1 和 dir2,我需要检查一个是否是另一个的子目录。我尝试使用 Contains 方法:

dir1.contains(dir2);

但这也返回 true,如果目录名称相似,例如 - c:\abcc:\abc1 不是子目录,则 bet 返回 true。一定有更好的办法。

【问题讨论】:

  • 如果目录有多个名称,例如符号链接?

标签: c# .net directory


【解决方案1】:
DirectoryInfo di1 = new DirectoryInfo(dir1);
DirectoryInfo di2 = new DirectoryInfo(dir2);
bool isParent = di2.Parent.FullName == di1.FullName;

或者在循环中允许嵌套子目录,即 C:\foo\bar\bazC:\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;
}

【讨论】:

【解决方案2】:
  • 不区分大小写
  • 允许混合使用 \/ 文件夹分隔符
  • 容忍路径中的..\
  • 避免匹配部分文件夹名称(c:\foobar 不是 c:\foo 的子路径)

注意:这仅匹配路径字符串,不适用于文件系统中的符号链接和其他类型的链接。

代码:

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

测试用例(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);
    }
}

更新

  • 2015-08-18:修复部分文件夹名称的错误匹配。添加测试用例。
  • 2015-09-02:在路径中支持..\,添加缺少的代码
  • 2017-09-06:添加符号链接注释。

【讨论】:

  • 路径 C:\foo\bar\..\bar2 vs C:\foo\bar2 怎么样?或 C:\foo\bar\ 与 C:\foo\bar\..\..\?
  • 好点。我相信我们应该添加 Path.GetFullPath() 来解决这些示例。
  • 添加了另外三个测试用例并修复了实现以支持您的示例。还添加了实现所依赖的两个缺少的扩展方法。我确信这一切都可以简化,但它似乎有效。
  • @anjdreas 这是你的第一个测试用例。我没有看到它是如何通过的。对我来说,我必须在调用 GetFullPath 之前将斜杠附加到目录,否则会得到意想不到的结果。
  • 值得注意的是[CanBeNull][NotNull] 注释是JetBrains.Annotations nuget 包的一部分。在这里找到它们:JetBrains.Annotations.
【解决方案3】:

试试:

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

【讨论】:

  • 在 windows 上不区分大小写,这会失败
【解决方案4】:

从 netstandard2.1 开始,终于有了一种几乎方便且独立于平台的方法来检查这一点:Path.GetRelativePath()

var relPath = Path.GetRelativePath(basePath, subPath);
var isSubPath = !relPath.StartsWith('.') && !Path.IsPathRooted(relPath);

subPathbasePath 都必须是绝对路径。

便利扩展功能:

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

.NET 摆弄一些测试用例:https://dotnetfiddle.net/9FIU0g

【讨论】:

    【解决方案5】:
    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 非常适用于路径,例如:C:\test\..\forbidden\

    【讨论】:

    • 此代码忽略平台的大小写(输入)敏感性。否则它看起来很简单而且很有效!
    • 想象 2 个目录:C:\SomeDirectory 和 C:\SomeDirectoryBackup 这将是真的,即使第二个目录不是第一个目录的子目录
    【解决方案6】:

    在我的情况下,路径和可能的子路径不包含 '..' 并且永远不会以 '\' 结尾:

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

    【讨论】:

      【解决方案7】:

      我的路径可能包含不同的大小写,甚至有未修剪的段... 这似乎有效:

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

      【讨论】:

        【解决方案8】:

        基于@BrokenGlass 的回答但经过调整:

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

        如果性能至关重要,则不理想。

        【讨论】:

          【解决方案9】:

          更新 - 我最初写的这个是错误的(见下文):

          在我看来,您实际上坚持使用 .StartsWith() 函数进行基本字符串比较(当然使用 .ToLower()),并计算路径分隔符,但您添加了一个额外的考虑因素路径分隔符的数量 - 您需要事先在字符串上使用 Path.GetFullPath() 之类的东西,以确保您处理的是一致的路径字符串格式。所以你最终会得到一些基本而简单的东西,就像这样:

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

          更新...

          正如我在使用我的代码时发现的那样,这是错误的原因是因为它没有考虑一个目录名称以与另一个目录的整个名称相同的字符开头的情况。我有一个案例,其中一个目录路径为“D:\prog\dat\Mirror_SourceFiles”,另一个目录路径为“D:\prog\dat\Mirror”。由于我的第一条路径确实“以”字母“D:\prog\dat\Mirror”“开头”,因此我的代码给了我一个错误的匹配。我完全摆脱了 .StartsWith 并将代码更改为此(方法:将路径拆分为各个部分,并将这些部分与较少的部分进行比较):

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

          当然,请注意,在“真实程序”中,您将在 try-catch 中使用 Path.GetFullPath() 函数来处理与传递给它的字符串有关的适当异常。

          【讨论】:

            【解决方案10】:
            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);
            }
            

            【讨论】:

            • 不幸的是,rootPath = @"c:\foo"subPath = @"c:\foobar" 返回 true - 这显然是误报。
            猜你喜欢
            • 2011-07-30
            • 2014-02-20
            • 1970-01-01
            • 2011-12-26
            • 1970-01-01
            • 2016-08-18
            • 2015-06-20
            • 1970-01-01
            相关资源
            最近更新 更多