Wie kann ich (Verzeichnis-) Pfade in C # vergleichen?

73

Wenn ich zwei DirectoryInfoObjekte habe, wie kann ich sie auf semantische Gleichheit vergleichen? Beispielsweise sollten die folgenden Pfade alle als gleich angesehen werden C:\temp:

  • C:\temp
  • C:\temp\
  • C:\temp\.
  • C:\temp\x\..\..\temp\.

Folgendes kann gleich sein oder nicht C:\temp:

  • \temp Wenn sich das aktuelle Arbeitsverzeichnis auf dem Laufwerk befindet C:\
  • temp wenn das aktuelle Arbeitsverzeichnis ist C:\
  • C:\temp.
  • C:\temp...\

Wenn es wichtig ist, das aktuelle Arbeitsverzeichnis zu berücksichtigen, kann ich das selbst herausfinden, das ist also nicht so wichtig. Nachgestellte Punkte werden in Fenstern entfernt, daher sollten diese Pfade wirklich gleich sein - aber sie werden unter Unix nicht entfernt, daher würde ich unter Mono andere Ergebnisse erwarten.

Die Groß- und Kleinschreibung ist optional. Die Pfade können vorhanden sein oder nicht, und der Benutzer kann Berechtigungen für den Pfad haben oder nicht - ich würde eine schnelle, robuste Methode bevorzugen, die keine E / A erfordert (also keine Berechtigungsprüfung), aber wenn etwas erstellt wurde -in Ich würde mich auch über alles freuen, was "gut genug" ist ...

Eamon Nerbonne
quelle
Warum System.IO.DirectoryInfoimplementiert die Klasse dies nicht bool Equals(DirectoryInfo other)? Mir scheint, dass dieses Zeug inzwischen so standardisiert sein sollte, dass wir nicht einmal in der Lage sein sollten , einfache Dinge wie den Vergleich zweier Pfade durcheinander zu bringen.
Elaskanator

Antworten:

39

Ausgehend von dieser Antwort kann diese Methode einige Randfälle behandeln:

public static string NormalizePath(string path)
{
    return Path.GetFullPath(new Uri(path).LocalPath)
               .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
               .ToUpperInvariant();
}

Weitere Details in der Originalantwort. Nennen Sie es wie:

bool pathsEqual = NormalizePath(path1) == NormalizePath(path2);

Sollte sowohl für Datei- als auch für Verzeichnispfade funktionieren.

nawfal
quelle
1
Etwas amüsant ist, dass ich das letztendlich getan habe, da dies keine E / A hat!
Eamon Nerbonne
5
new Uri(path).LocalPath- würde im Falle eines #Symbols im Pfad einen falschen Pfad
ili
Es gibt Fälle, insbesondere bei Netzwerkdateien, in denen dies die falsche Antwort liefert. In der Tat gibt es Fälle, in denen es beim Umgang mit Netzwerkdateien einfach nicht möglich ist, die Antwort ohne E / A korrekt zu bestimmen, dh ohne mit Dateihandles. In meiner Antwort unten finden Sie eine "korrektere" Lösung, bei der IO entgegen der spezifischen Anforderung in der Frage zugegebenermaßen verwendet wird.
David I. McIntosh
1
Beachten Sie, dass dies eine Ausnahme für Beispiele wie "\ temp" und "temp" in der Frage auslöst.
John Reynolds
3
Wird ToUpperInvariantin einem Dateisystempfad verwendet? Herzliche Glückwünsche. Sie haben jetzt eine Anwendung, die auf Betriebssystemen mit türkischen regionalen Einstellungen auf mysteriöse Weise in die Luft jagen kann.
Ishmaeel
93

GetFullPathscheint die Arbeit zu erledigen, mit Ausnahme von case different ( Path.GetFullPath("test") != Path.GetFullPath("TEST")) und Trailing Slash. Der folgende Code sollte also gut funktionieren:

String.Compare(
    Path.GetFullPath(path1).TrimEnd('\\'),
    Path.GetFullPath(path2).TrimEnd('\\'), 
    StringComparison.InvariantCultureIgnoreCase)

Oder wenn Sie beginnen möchten mit DirectoryInfo:

String.Compare(
    dirinfo1.FullName.TrimEnd('\\'),
    dirinfo2.FullName.TrimEnd('\\'), 
    StringComparison.InvariantCultureIgnoreCase)
VladV
quelle
2
Sie können Path.GetFullPath (pathx) .ToUpperInvariant (). TrimEnd ('\\') ausführen, um die Groß- und Kleinschreibung zu entfernen. Dies sollte unter UNIX jedoch mit Vorsicht angewendet werden, da UNIX zwei Namen unterschiedlicher Groß- und Kleinschreibung als unterschiedliche Ordner behandelt, während Windows sie als ein und denselben behandelt.
Andy Shellam
Wenn Sie dies so bearbeiten, dass es fallinvariant ist, und DirectoryInfos (über FullName) verwenden, haben Sie eine perfekte Antwort :-)
Eamon Nerbonne
@Eamon, ich habe DirectoryInfo Variante für dich hinzugefügt :-). Und es war bereits fallinvariant - genau das macht StringComparison.InvariantCultureIgnoreCase.
VladV
2
Oh, und falls ein anderer Benutzer über diese Antwort stolpert; Fullname macht Weg Entdeckung Sicherheitsberechtigungen erfordern und reagieren empfindlich auf das aktuelle arbeit-Verzeichnis (was effektiv bedeutet , dass Sie nur absolute Pfade vergleichen können - oder relative Pfade wie in der CWD ausgewertet).
Eamon Nerbonne
10
Vielleicht sollten Sie System.IO.Path.DirectorySeparatorChar anstelle von '\\' verwenden.
Pato
11

Es gibt einige kurze Hinweise zur Implementierung von Pfaden in .NET. Es gibt viele Beschwerden darüber. Patrick Smacchia , der Erfinder von NDepend, veröffentlichte eine Open-Source-Bibliothek, die die Handhabung allgemeiner und komplexer Pfadoperationen ermöglicht . Wenn Sie viele Vergleichsvorgänge für Pfade in Ihrer Anwendung ausführen, kann diese Bibliothek für Sie hilfreich sein.

Steven
quelle
Hmm, interessant - hast du es benutzt?
Eamon Nerbonne
3
Ich habe es verwendet, um festzustellen, ob ein Verzeichnis ein anderes enthält (z. B. C: \ A / B enthält C: \ a \ b \ c \ d \ e \ .. \ .. \ .. \ f \ g) und es funktioniert sehr gut Gut.
Kevin Coulombe
10

Mir ist klar, dass dies ein alter Beitrag ist, aber alle Antworten basieren letztendlich auf einem Textvergleich zweier Namen. Der Versuch, zwei "normalisierte" Namen zu erhalten, die die Vielzahl möglicher Verweismöglichkeiten auf dasselbe Dateiobjekt berücksichtigen, ist nahezu unmöglich. Es gibt Probleme wie: Junctions, symbolische Links, Netzwerkdateifreigaben (die auf unterschiedliche Weise auf dasselbe Dateiobjekt verweisen ) usw. usw. Tatsächlich führt jede einzelne Antwort oben, mit Ausnahme von Igor Korkhovs, absolut zu falschen Ergebnissen bestimmte Umstände (z. B. Kreuzungen, symbolische Links, Verzeichnislinks usw.)

In der Frage wurde ausdrücklich gefordert, dass für die Lösung keine E / A erforderlich sind. Wenn Sie sich jedoch mit vernetzten Pfaden befassen möchten, müssen Sie unbedingt E / A ausführen: Es gibt Fälle, in denen es einfach nicht möglich ist, anhand einer lokalen Pfadzeichenfolge zu bestimmen Manipulation, ob zwei Dateiverweise auf dieselbe physische Datei verweisen. (Dies kann wie folgt leicht verstanden werden. Angenommen, ein Dateiserver verfügt über eine Windows-Verzeichnisverbindung an einer Stelle innerhalb eines gemeinsam genutzten Teilbaums. In diesem Fall kann auf eine Datei entweder direkt oder über die Verbindung verwiesen werden. Die Verbindung befindet sich jedoch auf dem Dateiserver. Daher ist es für einen Client einfach unmöglich, allein durch lokale Informationen festzustellen, dass sich die beiden referenzierenden Dateinamen auf dieselbe physische Datei beziehen: Die Informationen stehen dem Client einfach nicht lokal zur Verfügung.

Die folgende Lösung führt einige E / A-Vorgänge aus, obwohl sie sehr minimal sind, bestimmt jedoch korrekt, ob zwei Dateisystemreferenzen semantisch identisch sind, dh auf dasselbe Dateiobjekt verweisen. (Wenn sich keine der Dateispezifikationen auf ein gültiges Dateiobjekt bezieht, sind alle Wetten deaktiviert):

    public static bool AreFileSystemObjectsEqual(string dirName1, string dirName2)
    {
        //Optimization: if strings are equal, don't bother with the IO
        bool bRet = string.Equals(dirName1, dirName2, StringComparison.OrdinalIgnoreCase);
        if (!bRet)
        {
            //NOTE: we cannot lift the call to GetFileHandle out of this routine, because we _must_
            // have both file handles open simultaneously in order for the objectFileInfo comparison
            // to be guaranteed as valid.
            using (SafeFileHandle directoryHandle1 = GetFileHandle(dirName1), directoryHandle2 = GetFileHandle(dirName2))
            {
                BY_HANDLE_FILE_INFORMATION? objectFileInfo1 = GetFileInfo(directoryHandle1);
                BY_HANDLE_FILE_INFORMATION? objectFileInfo2 = GetFileInfo(directoryHandle2);
                bRet = objectFileInfo1 != null
                       && objectFileInfo2 != null
                       && (objectFileInfo1.Value.FileIndexHigh == objectFileInfo2.Value.FileIndexHigh)
                       && (objectFileInfo1.Value.FileIndexLow == objectFileInfo2.Value.FileIndexLow)
                       && (objectFileInfo1.Value.VolumeSerialNumber == objectFileInfo2.Value.VolumeSerialNumber);
            }
        }
        return bRet;
    }

Die Idee dazu kam von einer Antwort von Warren Stevens in einer ähnlichen Frage, die ich auf SuperUser gepostet habe: https://superuser.com/a/881966/241981

David I. McIntosh
quelle
5

Es scheint, dass P / Invoking GetFinalPathNameByHandle () die zuverlässigste Lösung wäre.

UPD: Ups, ich habe Ihren Wunsch, keine E / A zu verwenden, nicht berücksichtigt

Igor Korkhov
quelle
Nun, ich würde keine E / A bevorzugen , aber eine einfache E / A-Lösung ist besser, als etwas von Grund auf neu zu schreiben ...
Eamon Nerbonne
6
@Eamon Nerbonne: Meine Lösung hat zwei weitere Nachteile: 1) Sie funktioniert nur unter Vista und neueren Betriebssystemen. 2) Sie funktioniert nicht, wenn mindestens einer der Pfade nicht vorhanden ist. Es hat aber auch einen Vorteil: Es funktioniert mit symbolischen Links, dh es beantwortet Ihre Frage "Wie kann ich sie auf semantische Gleichheit vergleichen?". während GetFullPath()nicht. Es liegt also an Ihnen, zu entscheiden, ob Sie echte semantische Gleichheit benötigen oder nicht.
Igor Korkhov
2
 System.IO.Path.GetFullPath(pathA).Equals(System.IO.Path.GetFullPath(PathB));
Asad
quelle
3
System.IO.Path.GetFullPath (@ "C: \ LOL"). Equals (System.IO.Path.GetFullPath (@ "C: \ LOL \")) gibt false zurück
2xMax
1

Die Eigenschaften "Name" sind gleich. Nehmen:

DirectoryInfo dir1 = new DirectoryInfo("C:\\Scratch");
DirectoryInfo dir2 = new DirectoryInfo("C:\\Scratch\\");
DirectoryInfo dir3 = new DirectoryInfo("C:\\Scratch\\4760");
DirectoryInfo dir4 = new DirectoryInfo("C:\\Scratch\\4760\\..\\");

dir1.Name == dir2.Name and dir2.Name == dir4.Name ("Scratch" in diesem Fall. Dir3 == "4760".) Nur die FullName-Eigenschaften unterscheiden sich.

Möglicherweise können Sie eine rekursive Methode ausführen, um die Namenseigenschaften jedes übergeordneten Elements anhand Ihrer beiden DirectoryInfo-Klassen zu überprüfen und sicherzustellen, dass der vollständige Pfad identisch ist.

EDIT : Funktioniert das für Ihre Situation? Erstellen Sie eine Konsolenanwendung und fügen Sie diese über die gesamte Program.cs-Datei ein. Wenn Sie der Funktion AreEquals () zwei DirectoryInfo-Objekte bereitstellen, wird True zurückgegeben, wenn sie dasselbe Verzeichnis sind. AreEquals()Wenn Sie möchten, können Sie diese Methode möglicherweise als Erweiterungsmethode für DirectoryInfo optimieren , sodass Sie dies einfach tun könnenmyDirectoryInfo.IsEquals(myOtherDirectoryInfo);

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

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(AreEqual(
                new DirectoryInfo("C:\\Scratch"),
                new DirectoryInfo("C:\\Scratch\\")));

            Console.WriteLine(AreEqual(
                new DirectoryInfo("C:\\Windows\\Microsoft.NET\\Framework"),
                new DirectoryInfo("C:\\Windows\\Microsoft.NET\\Framework\\v3.5\\1033\\..\\..")));

            Console.WriteLine(AreEqual(
                new DirectoryInfo("C:\\Scratch\\"),
                new DirectoryInfo("C:\\Scratch\\4760\\..\\..")));

            Console.WriteLine("Press ENTER to continue");
            Console.ReadLine();
        }

        private static bool AreEqual(DirectoryInfo dir1, DirectoryInfo dir2)
        {
            DirectoryInfo parent1 = dir1;
            DirectoryInfo parent2 = dir2;

            /* Build a list of parents */
            List<string> folder1Parents = new List<string>();
            List<string> folder2Parents = new List<string>();

            while (parent1 != null)
            {
                folder1Parents.Add(parent1.Name);
                parent1 = parent1.Parent;
            }

            while (parent2 != null)
            {
                folder2Parents.Add(parent2.Name);
                parent2 = parent2.Parent;
            }

            /* Now compare the lists */

            if (folder1Parents.Count != folder2Parents.Count)
            {
                // Cannot be the same - different number of parents
                return false;
            }

            bool equal = true;

            for (int i = 0; i < folder1Parents.Count && i < folder2Parents.Count; i++)
            {
                equal &= folder1Parents[i] == folder2Parents[i];
            }

            return equal;
        }
    }
}
Andy Shellam
quelle
1
Die Name-Eigenschaft gibt nur den Namen des tiefsten Unterverzeichnisses zurück, sodass "c: \ foo \ bar" "bar" zurückgibt. Der Vergleich von "d: \ foo \ bar" mit "c: \ bar" ergibt true, was nicht gut ist.
Steven
Deshalb machst du einen rekursiven Vergleich aller Eltern! Bitte entfernen Sie die Ablehnung, dies ist eine absolut akzeptable Lösung. Ich habe meine Antwort mit einem vollständigen Codebeispiel geändert.
Andy Shellam
Hmm, das funktioniert auch für D:\tempvs. C:\temp. Schöne Idee; Sie haben sich jedoch nicht mit Groß- und Kleinschreibung befasst, und es könnte etwas kürzer sein: while (dir1! = null && dir2! = null) if (! string.Equals (dir1.Name, dir2.Name, StringComparison.InvariantCultureIgnoreCase)) falsch zurückgeben; sonst {dir1 = dir1.Parent; dir2 = dir2.Parent;} return dir1 == dir2;
Eamon Nerbonne
1
Ja, Groß- und Kleinschreibung ist schwierig, da das OP wollte, dass Code sowohl unter Mono als auch unter Windows funktioniert. Unter Linux 2 werden Namen mit unterschiedlichen Groß- und Kleinschreibung als unterschiedlich angesehen. Unter Windows werden sie jedoch als dieselbe Datei angesehen. Plattformentscheidung.
Andy Shellam
1

Sie können Minimatch verwenden, einen Port des Minimatch von Node.js.

var mm = new Minimatcher(searchPattern, new Options { AllowWindowsPaths = true });

if (mm.IsMatch(somePath))
{
    // The path matches!  Do some cool stuff!
}

var matchingPaths = mm.Filter(allPaths);


Sehen Sie, warum die AllowWindowsPaths = trueOption erforderlich ist:

Bei Pfaden im Windows-Stil wurde die Minimatch-Syntax für Pfade im Linux-Stil entwickelt (nur mit Schrägstrichen). Insbesondere wird der Backslash als Escape-Zeichen verwendet, sodass Pfade im Windows-Stil nicht einfach akzeptiert werden können. Meine C # -Version behält dieses Verhalten bei.

Um dies zu unterdrücken und sowohl umgekehrte Schrägstriche als auch vorwärts Schrägstriche als Pfadtrennzeichen (in Mustern oder Eingaben) zuzulassen, setzen Sie die AllowWindowsPathsOption:

var mm = new Minimatcher(searchPattern, new Options { AllowWindowsPaths = true });

Wenn Sie diese Option übergeben, werden Escape-Zeichen vollständig deaktiviert.

Nuget: http://www.nuget.org/packages/Minimatch/

GitHub: https://github.com/SLaks/Minimatch

vulkanischer Rabe
quelle
0
bool equals = myDirectoryInfo1.FullName == myDirectoryInfo2.FullName;

?

herzmeister
quelle
2
Dies ist die einfache Version der Lösung von Binary Worrier. Bitte beachten Sie, dass es ein Problem mit dem abschließenden Schrägstrich gibt: "c: \ temp" ist ungleich "c: \ temp \".
Steven
1
Ok, wenn ich also die nachgestellten Schrägstriche und möglicherweise das Gehäuse normalisiere - und die Tatsache akzeptiere, dass es einige FileIOPermission-Sachen macht - sieht das nach einem guten Anfang aus, danke!
Eamon Nerbonne
0
using System;
using System.Collections.Generic;
using System.Text;

namespace EventAnalysis.IComparerImplementation
{

    public sealed class FSChangeElemComparerByPath : IComparer<FSChangeElem>
    {
        public int Compare(FSChangeElem firstPath, FSChangeElem secondPath)
        {
            return firstPath.strObjectPath == null ?
                (secondPath.strObjectPath == null ? 0 : -1) :
                (secondPath.strObjectPath == null ? 1 : ComparerWrap(firstPath.strObjectPath, secondPath.strObjectPath));
        }

        private int ComparerWrap(string stringA, string stringB)
        {
            int length = 0;
            int start = 0;
            List<string> valueA = new List<string>();
            List<string> valueB = new List<string>();

            ListInit(ref valueA, stringA);
            ListInit(ref valueB, stringB);

            if (valueA.Count != valueB.Count)
            {
                length = (valueA.Count > valueB.Count)
                           ? valueA.Count : valueB.Count;

                if (valueA.Count != length)
                {
                    for (int i = 0; i < length - valueA.Count; i++)
                    {
                        valueA.Add(string.Empty);
                    }
                }
                else
                {
                    for (int i = 0; i < length - valueB.Count; i++)
                    {
                        valueB.Add(string.Empty);
                    }
                }
            }

            else
                length = valueA.Count;

            return RecursiveComparing(valueA, valueB, length, start);
        }

        private void ListInit(ref List<string> stringCollection, string stringToList)
        {
            foreach (string s in stringToList.Remove(0, 2).Split('\\'))
            {
                stringCollection.Add(s);
            }
        }

        private int RecursiveComparing(List<string> valueA, List<string> valueB, int length, int start)
        {
            int result = 0;

            if (start != length)
            {
                if (valueA[start] == valueB[start])
                {
                    result = RecursiveComparing(valueA, valueB, length, ++start);
                }
                else
                {
                    result = String.Compare(valueA[start], valueB[start]);
                }
            }
            else
                return 0;

            return result;
        }
    }
}
Denis
quelle
Liste <T> _list = neue Liste <T> (bla-bla-bla); _list.Sort (neuer FSChangeElemComparerByPath ());
Denis
0
bool Equals(string path1, string path2)
{
    return new Uri(path1) == new Uri(path2);
}

Der Uri-Konstruktor normalisiert den Pfad.

Boris
quelle
Dies ist im Wesentlichen gleichbedeutend mit dem, was @nawfal vorschlägt (was derzeit die akzeptierte Antwort ist)
Eamon Nerbonne
1
Was ist mit dem Problem des abschließenden Schrägstrichs?
Sam
0

Ich habe Rekursion verwendet, um dieses Problem für mich selbst zu lösen.

 public bool PathEquals(string Path1, string Path2)
 {
     FileInfo f1 = new FileInfo(Path1.Trim('\\','/','.'));
     FileInfo f2 = new FileInfo(Path2.Trim('\\', '/','.'));
     if(f1.Name.ToLower() == f2.Name.ToLower())
     {
         return DirectoryEquals(f1.Directory, f2.Directory);
     }
     else
     {
         return false;
     }
}

public bool DirectoryEquals(DirectoryInfo d1, DirectoryInfo d2)
{
    if(d1.Name.ToLower() == d2.Name.ToLower())
    {
        if((d1.Parent != null) && (d2.Parent != null))
        {
            return DirectoryEquals(d1.Parent, d2.Parent);
        }
        else
        {
            return true;//C:\Temp1\Temp2 equals \Temp1\Temp2
            //return (d1.Parent == null) && (d2.Parent == null);//C:\Temp1\Temp2 does not equal \Temp1\Temp2
        }
    }
    else
    {
        return false;
    }
}

Hinweis: new FileInfo(path)Gibt eine gültige FileInfo zurück, auch wenn der Pfad keine Datei ist (das Namensfeld entspricht dem Verzeichnisnamen).

Hen3
quelle