【发布时间】:2011-04-11 06:00:02
【问题描述】:
我有 2 个字符串 - dir1 和 dir2,我需要检查一个是否是另一个的子目录。我尝试使用 Contains 方法:
dir1.contains(dir2);
但这也返回 true,如果目录名称相似,例如 - c:\abc 和 c:\abc1 不是子目录,则 bet 返回 true。一定有更好的办法。
【问题讨论】:
-
如果目录有多个名称,例如符号链接?
我有 2 个字符串 - dir1 和 dir2,我需要检查一个是否是另一个的子目录。我尝试使用 Contains 方法:
dir1.contains(dir2);
但这也返回 true,如果目录名称相似,例如 - c:\abc 和 c:\abc1 不是子目录,则 bet 返回 true。一定有更好的办法。
【问题讨论】:
DirectoryInfo di1 = new DirectoryInfo(dir1);
DirectoryInfo di2 = new DirectoryInfo(dir2);
bool isParent = di2.Parent.FullName == di1.FullName;
或者在循环中允许嵌套子目录,即 C:\foo\bar\baz 是 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;
}
【讨论】:
\ 和 / 文件夹分隔符..\
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);
}
}
更新
..\,添加缺少的代码【讨论】:
[CanBeNull] 和[NotNull] 注释是JetBrains.Annotations nuget 包的一部分。在这里找到它们:JetBrains.Annotations.
试试:
dir1.contains(dir2+"\\");
【讨论】:
从 netstandard2.1 开始,终于有了一种几乎方便且独立于平台的方法来检查这一点:Path.GetRelativePath()。
var relPath = Path.GetRelativePath(basePath, subPath);
var isSubPath = !relPath.StartsWith('.') && !Path.IsPathRooted(relPath);
subPath 和 basePath 都必须是绝对路径。
便利扩展功能:
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
【讨论】:
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\
【讨论】:
在我的情况下,路径和可能的子路径不包含 '..' 并且永远不会以 '\' 结尾:
private static bool IsSubpathOf(string path, string subpath)
{
return (subpath.Equals(path, StringComparison.OrdinalIgnoreCase) ||
subpath.StartsWith(path + @"\", StringComparison.OrdinalIgnoreCase));
}
【讨论】:
我的路径可能包含不同的大小写,甚至有未修剪的段... 这似乎有效:
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;
}
【讨论】:
基于@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));
}
}
如果性能至关重要,则不理想。
【讨论】:
更新 - 我最初写的这个是错误的(见下文):
在我看来,您实际上坚持使用 .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() 函数来处理与传递给它的字符串有关的适当异常。
【讨论】:
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 - 这显然是误报。