Pourquoi Path.Combine ne concatène-t-il pas correctement les noms de fichiers commençant par Path.DirectorySeparatorChar?


186

À partir de la fenêtre d'exécution dans Visual Studio:

> Path.Combine(@"C:\x", "y")
"C:\\x\\y"
> Path.Combine(@"C:\x", @"\y")
"\\y"

Il semble que les deux devraient être identiques.

L'ancien FileSystemObject.BuildPath () ne fonctionnait pas de cette façon ...



@Joe, stupide a raison! Aussi, je dois souligner que la fonction équivalente fonctionne très bien dans Node.JS ... Secouant la tête chez Microsoft ...
NH.

2
@zwcloud Pour .NET Core / Standard, Path.Combine()est principalement destiné à la rétrocompatibilité (avec le comportement existant). Vous feriez mieux d'utiliser Path.Join(): "Contrairement à la méthode Combine, la méthode Join ne tente pas de rooter le chemin renvoyé. (Autrement dit, si path2 est un chemin absolu, la méthode Join ne rejette pas path1 et renvoie path2 comme Combine method does.) "
Stajs

Réponses:


205

C'est une sorte de question philosophique (à laquelle seul Microsoft peut peut-être vraiment répondre), car il fait exactement ce que dit la documentation.

System.IO.Path.Combine

"Si chemin2 contient un chemin absolu, cette méthode renvoie chemin2."

Voici la méthode Combine réelle de la source .NET. Vous pouvez voir qu'il appelle CombineNoChecks , qui appelle ensuite IsPathRooted sur path2 et renvoie ce chemin si tel est le cas:

public static String Combine(String path1, String path2) {
    if (path1==null || path2==null)
        throw new ArgumentNullException((path1==null) ? "path1" : "path2");
    Contract.EndContractBlock();
    CheckInvalidPathChars(path1);
    CheckInvalidPathChars(path2);

    return CombineNoChecks(path1, path2);
}

internal static string CombineNoChecks(string path1, string path2)
{
    if (path2.Length == 0)
        return path1;

    if (path1.Length == 0)
        return path2;

    if (IsPathRooted(path2))
        return path2;

    char ch = path1[path1.Length - 1];
    if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar &&
            ch != VolumeSeparatorChar) 
        return path1 + DirectorySeparatorCharAsString + path2;
    return path1 + path2;
}

Je ne sais pas quelle est la justification. Je suppose que la solution est de supprimer (ou de couper) DirectorySeparatorChar depuis le début du deuxième chemin; écrivez peut-être votre propre méthode Combine qui fait cela, puis appelez Path.Combine ().


En regardant le code démonté (vérifiez mon message), vous avez raison d'une certaine manière.
Gulzar Nazim

7
Je suppose que cela fonctionne de cette façon pour permettre un accès facile à l'algorithme "current working dir".
BCS

Cela semble fonctionner comme faire une séquence cd (component)depuis la ligne de commande. Cela me semble raisonnable.
Adrian Ratnapala

11
J'utilise cette garniture pour obtenir la chaîne d'effet souhaitée strFilePath = Path.Combine (basePath, otherPath.TrimStart (new char [] {'\\', '/'}));
Matthew Lock

3
J'ai changé mon code de travail en Path.Combinejuste pour être sûr, mais ensuite il s'est cassé .. C'est tellement stupide :)
Sotn le

23

Il s'agit du code désassemblé de la méthode .NET Reflector for Path.Combine. Vérifiez la fonction IsPathRooted. Si le deuxième chemin est enraciné (commence par un DirectorySeparatorChar), retournez le deuxième chemin tel quel.

public static string Combine(string path1, string path2)
{
    if ((path1 == null) || (path2 == null))
    {
        throw new ArgumentNullException((path1 == null) ? "path1" : "path2");
    }
    CheckInvalidPathChars(path1);
    CheckInvalidPathChars(path2);
    if (path2.Length == 0)
    {
        return path1;
    }
    if (path1.Length == 0)
    {
        return path2;
    }
    if (IsPathRooted(path2))
    {
        return path2;
    }
    char ch = path1[path1.Length - 1];
    if (((ch != DirectorySeparatorChar) &&
         (ch != AltDirectorySeparatorChar)) &&
         (ch != VolumeSeparatorChar))
    {
        return (path1 + DirectorySeparatorChar + path2);
    }
    return (path1 + path2);
}


public static bool IsPathRooted(string path)
{
    if (path != null)
    {
        CheckInvalidPathChars(path);
        int length = path.Length;
        if (
              (
                  (length >= 1) &&
                  (
                      (path[0] == DirectorySeparatorChar) ||
                      (path[0] == AltDirectorySeparatorChar)
                  )
              )

              ||

              ((length >= 2) &&
              (path[1] == VolumeSeparatorChar))
           )
        {
            return true;
        }
    }
    return false;
}

23

Je voulais résoudre ce problème:

string sample1 = "configuration/config.xml";
string sample2 = "/configuration/config.xml";
string sample3 = "\\configuration/config.xml";

string dir1 = "c:\\temp";
string dir2 = "c:\\temp\\";
string dir3 = "c:\\temp/";

string path1 = PathCombine(dir1, sample1);
string path2 = PathCombine(dir1, sample2);
string path3 = PathCombine(dir1, sample3);

string path4 = PathCombine(dir2, sample1);
string path5 = PathCombine(dir2, sample2);
string path6 = PathCombine(dir2, sample3);

string path7 = PathCombine(dir3, sample1);
string path8 = PathCombine(dir3, sample2);
string path9 = PathCombine(dir3, sample3);

Bien sûr, tous les chemins 1 à 9 doivent contenir une chaîne équivalente à la fin. Voici la méthode PathCombine que j'ai proposée:

private string PathCombine(string path1, string path2)
{
    if (Path.IsPathRooted(path2))
    {
        path2 = path2.TrimStart(Path.DirectorySeparatorChar);
        path2 = path2.TrimStart(Path.AltDirectorySeparatorChar);
    }

    return Path.Combine(path1, path2);
}

Je pense aussi que c'est assez ennuyeux que cette manipulation des cordes doive être faite manuellement, et je serais intéressé par la raison derrière cela.


19

À mon avis, c'est un bug. Le problème est qu'il existe deux types différents de chemins "absolus". Le chemin "d: \ mydir \ myfile.txt" est absolu, le chemin "\ mydir \ myfile.txt" est également considéré comme "absolu" même s'il manque la lettre de lecteur. Le comportement correct, à mon avis, serait de préfixer la lettre de lecteur du premier chemin lorsque le deuxième chemin commence par le séparateur de répertoire (et n'est pas un chemin UNC). Je recommanderais d'écrire votre propre fonction de wrapper d'assistance qui a le comportement que vous désirez si vous en avez besoin.


7
Cela correspond aux spécifications, mais ce n'est pas non plus ce à quoi je m'attendais.
dthrasher le

@Jake Cela n'évite pas une correction de bogue; c'est plusieurs personnes qui réfléchissent longuement et sérieusement à la façon de faire quelque chose, puis s'en tiennent à tout ce sur quoi elles sont d'accord. Notez également la différence entre le framework .Net (une bibliothèque qui contient Path.Combine) et le langage C #.
Grault

9

Depuis MSDN :

Si l'un des chemins d'accès spécifiés est une chaîne de longueur nulle, cette méthode renvoie l'autre chemin. Si chemin2 contient un chemin absolu, cette méthode retourne chemin2.

Dans votre exemple, path2 est absolu.


7

Suite aux conseils de Christian Graus dans son blog "Things I Hate about Microsoft" intitulé " Path.Combine is essentiellement inutile. ", Voici ma solution:

public static class Pathy
{
    public static string Combine(string path1, string path2)
    {
        if (path1 == null) return path2
        else if (path2 == null) return path1
        else return path1.Trim().TrimEnd(System.IO.Path.DirectorySeparatorChar)
           + System.IO.Path.DirectorySeparatorChar
           + path2.Trim().TrimStart(System.IO.Path.DirectorySeparatorChar);
    }

    public static string Combine(string path1, string path2, string path3)
    {
        return Combine(Combine(path1, path2), path3);
    }
}

Certains conseillent que les espaces de noms doivent entrer en collision, ... Je suis allé avec Pathy, comme un léger, et pour éviter une collision d'espace de noms avec System.IO.Path.

Edit : Ajout de vérifications de paramètres nuls


4

Ce code devrait faire l'affaire:

        string strFinalPath = string.Empty;
        string normalizedFirstPath = Path1.TrimEnd(new char[] { '\\' });
        string normalizedSecondPath = Path2.TrimStart(new char[] { '\\' });
        strFinalPath =  Path.Combine(normalizedFirstPath, normalizedSecondPath);
        return strFinalPath;

4

Ne connaissant pas les détails réels, je suppose que cela tente de se joindre comme si vous pouviez rejoindre des URI relatifs. Par exemple:

urljoin('/some/abs/path', '../other') = '/some/abs/other'

Cela signifie que lorsque vous joignez un chemin avec une barre oblique précédente, vous joignez en fait une base à une autre, auquel cas la seconde a la priorité.


Je pense que les barres obliques devraient être expliquées. De plus, qu'est-ce que cela a à voir avec .NET?
Peter Mortensen

3

Raison:

Votre deuxième URL est considérée comme un chemin absolu, la Combineméthode ne retournera le dernier chemin que si le dernier chemin est un chemin absolu.

Solution: supprimez simplement la barre oblique /de départ de votre deuxième chemin ( /SecondPathvers SecondPath). Ensuite, cela fonctionne comme vous l'avez excepté.


3

Cela a du sens, d'une certaine manière, compte tenu de la façon dont les chemins (relatifs) sont généralement traités:

string GetFullPath(string path)
{
     string baseDir = @"C:\Users\Foo.Bar";
     return Path.Combine(baseDir, path);
}

// Get full path for RELATIVE file path
GetFullPath("file.txt"); // = C:\Users\Foo.Bar\file.txt

// Get full path for ROOTED file path
GetFullPath(@"C:\Temp\file.txt"); // = C:\Temp\file.txt

La vraie question est: pourquoi les chemins, qui commencent par "\", sont-ils considérés comme «enracinés»? C'était nouveau pour moi aussi, mais cela fonctionne de cette façon sous Windows :

new FileInfo("\windows"); // FullName = C:\Windows, Exists = True
new FileInfo("windows"); // FullName = C:\Users\Foo.Bar\Windows, Exists = False

1

Si vous souhaitez combiner les deux chemins sans perdre aucun chemin, vous pouvez utiliser ceci:

?Path.Combine(@"C:\test", @"\test".Substring(0, 1) == @"\" ? @"\test".Substring(1, @"\test".Length - 1) : @"\test");

Ou avec des variables:

string Path1 = @"C:\Test";
string Path2 = @"\test";
string FullPath = Path.Combine(Path1, Path2.IsRooted() ? Path2.Substring(1, Path2.Length - 1) : Path2);

Les deux cas renvoient "C: \ test \ test".

Tout d'abord, j'évalue si Path2 commence par / et si c'est vrai, renvoie Path2 sans le premier caractère. Sinon, renvoyez le chemin complet2.


1
Il est probablement plus sûr de remplacer le == @"\"chèque par un Path.IsRooted()appel car ce "\"n'est pas le seul caractère à prendre en compte.
rumblefx0

0

Ces deux méthodes devraient vous éviter de joindre accidentellement deux chaînes contenant toutes les deux le délimiteur.

    public static string Combine(string x, string y, char delimiter) {
        return $"{ x.TrimEnd(delimiter) }{ delimiter }{ y.TrimStart(delimiter) }";
    }

    public static string Combine(string[] xs, char delimiter) {
        if (xs.Length < 1) return string.Empty;
        if (xs.Length == 1) return xs[0];
        var x = Combine(xs[0], xs[1], delimiter);
        if (xs.Length == 2) return x;
        var ys = new List<string>();
        ys.Add(x);
        ys.AddRange(xs.Skip(2).ToList());
        return Combine(ys.ToArray(), delimiter);
    }

0

Cela signifie "le répertoire racine du lecteur actuel". Dans votre exemple, cela signifie le dossier "test" dans le répertoire racine du lecteur actuel. Donc, cela peut être égal à "c: \ test".


0

Supprimez la barre oblique de départ ('\') dans le deuxième paramètre (path2) de Path.Combine.


La question ne pose pas cela.
LarsTech

0

J'ai utilisé la fonction d'agrégation pour forcer la combinaison des chemins comme ci-dessous:

public class MyPath    
{
    public static string ForceCombine(params string[] paths)
    {
        return paths.Aggregate((x, y) => Path.Combine(x, y.TrimStart('\\')));
    }
}

0

Comme l'a mentionné Ryan, il fait exactement ce que dit la documentation.

A partir de l'heure DOS, le disque actuel et le chemin actuel sont distingués. \est le chemin racine, mais pour le DISQUE ACTUEL.

Pour chaque " disque ", il existe un " chemin actuel " séparé . Si vous changez le disque en utilisant cd D:vous ne changez pas le chemin actuel en D:\, mais en: "D: \ peu importe \ était \ le \ dernier \ chemin \ consulté \ sur \ ce \ disque" ...

Ainsi, dans Windows, un littéral @"\x"signifie: "CURRENTDISK: \ x". A donc Path.Combine(@"C:\x", @"\y")comme deuxième paramètre un chemin racine, pas un chemin relatif, mais pas dans un disque connu ... Et comme on ne sait pas quel pourrait être le «disque courant», python retourne "\\y".

>cd C:
>cd \mydironC\apath
>cd D:
>cd \mydironD\bpath
>cd C:
>cd
>C:\mydironC\apath
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.