¿Por qué Path.Combine no concatena correctamente los nombres de archivo que comienzan con Path.DirectorySeparatorChar?

186

Desde la ventana Inmediato en Visual Studio:

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

Parece que ambos deberían ser lo mismo.

El viejo FileSystemObject.BuildPath () no funcionó de esta manera ...

Kris Erickson
fuente
@ Joe, estúpido tiene razón! Además, debo señalar que la función equivalente funciona bien en Node.JS ... Sacudiendo mi cabeza en Microsoft ...
NH.
2
@zwcloud Para .NET Core / Standard, Path.Combine()es principalmente para la compatibilidad con versiones anteriores (con el comportamiento existente). Sería mejor usar Path.Join(): "A diferencia del método Combinar, el método Join no intenta enraizar la ruta devuelta. (Es decir, si path2 es una ruta absoluta, el método Join no descarta path1 y devuelve path2 como Combine método lo hace.) "
Stajs

Respuestas:

205

Esta es una especie de pregunta filosófica (que tal vez solo Microsoft realmente pueda responder), ya que está haciendo exactamente lo que dice la documentación.

System.IO.Path.Combine

"Si path2 contiene una ruta absoluta, este método devuelve path2".

Aquí está el método de combinación real de la fuente .NET. Puede ver que llama a CombineNoChecks , que luego llama a IsPathRooted en path2 y devuelve esa ruta si es así:

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

No sé cuál es la razón. Supongo que la solución es quitar (o Recortar) DirectorySeparatorChar desde el comienzo de la segunda ruta; tal vez escriba su propio método Combine que haga eso y luego llame a Path.Combine ().

Ryan Lundy
fuente
Mirando el código desmontado (revise mi publicación), tiene razón en cierto sentido.
Gulzar Nazim
77
Supongo que funciona de esa manera para permitir un fácil acceso al algoritmo "directorio de trabajo actual".
BCS
Parece funcionar como hacer una secuencia cd (component)desde la línea de comandos. A mí me parece razonable.
Adrian Ratnapala
11
Utilizo este recorte para obtener el efecto deseado string strFilePath = Path.Combine (basePath, otherPath.TrimStart (new char [] {'\\', '/'}));
Matthew Lock
3
Path.Combine
Cambié
23

Este es el código desmontado de .NET Reflector para el método Path.Combine. Verifique la función IsPathRooted. Si la segunda ruta está enraizada (comienza con un DirectorySeparatorChar), devuelva la segunda ruta tal como está.

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;
}
Gulzar Nazim
fuente
23

Quería resolver este problema:

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

Por supuesto, todas las rutas 1-9 deben contener una cadena equivalente al final. Aquí está el método PathCombine que se me ocurrió:

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

También creo que es bastante molesto que este manejo de cadenas se haga manualmente, y me interesaría la razón detrás de esto.

anhoppe
fuente
19

En mi opinión esto es un error. El problema es que hay dos tipos diferentes de rutas "absolutas". La ruta "d: \ mydir \ myfile.txt" es absoluta, la ruta "\ mydir \ myfile.txt" también se considera "absoluta" a pesar de que le falta la letra de la unidad. El comportamiento correcto, en mi opinión, sería anteponer la letra de la unidad de la primera ruta cuando la segunda ruta comienza con el separador de directorio (y no es una ruta UNC). Recomendaría escribir su propia función de envoltura auxiliar que tiene el comportamiento que desea si lo necesita.

Cuña
fuente
77
Coincide con las especificaciones, pero tampoco es lo que esperaba.
dthrasher
@Jake Eso no es evitar una corrección de errores; son varias personas que piensan mucho sobre cómo hacer algo, y luego se adhieren a lo que acuerden. Además, tenga en cuenta la diferencia entre el marco .Net (una biblioteca que contiene Path.Combine) y el lenguaje C #.
Grault
9

De MSDN :

Si una de las rutas especificadas es una cadena de longitud cero, este método devuelve la otra ruta. Si path2 contiene una ruta absoluta, este método devuelve path2.

En su ejemplo, path2 es absoluto.

nickd
fuente
7

Siguiendo el consejo de Christian Graus en su blog "Cosas que odio sobre Microsoft" titulado " Path.Combine es esencialmente inútil ", aquí está mi solución:

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

Algunos aconsejan que los espacios de nombres deben colisionar, ... Fui con Pathyun poco y para evitar la colisión del espacio de nombres System.IO.Path.

Editar : se agregaron verificaciones de parámetros nulos

ergohack
fuente
4

Este código debería hacer el truco:

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

Sin conocer los detalles reales, supongo que hace un intento de unirse como si pudiera unirse a URI relativos. Por ejemplo:

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

Esto significa que cuando une una ruta con una barra inclinada anterior, en realidad está uniendo una base a otra, en cuyo caso la segunda tiene prioridad.

Elarson
fuente
Creo que las barras diagonales deberían explicarse. Además, ¿qué tiene esto que ver con .NET?
Peter Mortensen
3

Razón:

Su segunda URL se considera una ruta absoluta, la Combine método solo devolverá la última ruta si la última ruta es absoluta.

Solución: simplemente elimine la barra diagonal inicial /de su segunda ruta ( /SecondPatha SecondPath). Entonces funciona como lo exceptuaste.

Amir Hossein Ahmadi
fuente
3

En realidad, esto tiene sentido, de alguna manera, considerando cómo se tratan las rutas (relativas) generalmente:

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 verdadera pregunta es: ¿por qué los caminos, que comienzan con "\", se consideran "enraizados"? Esto también era nuevo para mí, pero funciona de esa manera en Windows :

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

Si desea combinar ambas rutas sin perder ninguna ruta, puede usar esto:

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

O con variables:

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

Ambos casos devuelven "C: \ test \ test".

Primero, evalúo si Path2 comienza con / y si es cierto, devuelvo Path2 sin el primer carácter. De lo contrario, devuelva el Path2 completo.

Ferri
fuente
1
Probablemente sea más seguro reemplazar el == @"\"cheque por una Path.IsRooted()llamada, ya "\"que no es el único personaje a tener en cuenta.
rumblefx0
0

Estos dos métodos deberían salvarlo de unir accidentalmente dos cadenas que tienen el delimitador en ellas.

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

Esto significa "el directorio raíz de la unidad actual". En su ejemplo, significa la carpeta "prueba" en el directorio raíz de la unidad actual. Entonces, esto puede ser igual a "c: \ test".

Estevez
fuente
0

Elimine la barra diagonal inicial ('\') en el segundo parámetro (ruta2) de Path.Combine.

shanmuga raja
fuente
La pregunta no es preguntar esto.
LarsTech
0

Utilicé la función de agregado para forzar la combinación de rutas de la siguiente manera:

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

Como mencionó Ryan, está haciendo exactamente lo que dice la documentación.

Desde tiempos de DOS, se distinguen el disco actual y la ruta actual. \es la ruta raíz, pero para el DISCO ACTUAL.

Para cada " disco " hay una " ruta actual " separada . Si cambia el disco usando cd D:, no cambia la ruta actual aD:\ , sino a: "D: \ lo que sea \ fue \ la \ última \ ruta \ accedido \ en \ este \ disco" ...

Entonces, en Windows, un literal @"\x"significa: "CURRENTDISK: \ x". Por lo tanto, Path.Combine(@"C:\x", @"\y")tiene como segundo parámetro una ruta raíz, no un pariente, aunque no en un disco conocido ... Y dado que no se sabe cuál podría ser el «disco actual», Python regresa "\\y".

>cd C:
>cd \mydironC\apath
>cd D:
>cd \mydironD\bpath
>cd C:
>cd
>C:\mydironC\apath
ilias iliadis
fuente