Por que Path.Combine não concatena corretamente os nomes de arquivos que começam com Path.DirectorySeparatorChar?

185

Na janela Imediata no Visual Studio:

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

Parece que os dois devem ser iguais.

O antigo FileSystemObject.BuildPath () não funcionou dessa maneira ...

Kris Erickson
fonte
@ Joe, estúpido está certo! Além disso, devo salientar que a função equivalente funciona muito bem no Node.JS ... Balançando a cabeça na Microsoft ...
NH.
2
@zwcloud Para o .NET Core / Standard, Path.Combine()é principalmente para compatibilidade com versões anteriores (com o comportamento existente). Você seria melhor usar Path.Join(): "Diferentemente do método Combine, o método Join não tenta fazer root no caminho retornado. (Ou seja, se path2 é um caminho absoluto, o método Join não descarta o caminho1 e retorna o caminho2 como o Combine método faz.) "
Stajs 17/09/19

Respostas:

205

Essa é uma pergunta filosófica (que talvez apenas a Microsoft possa responder de verdade), pois está fazendo exatamente o que a documentação diz.

System.IO.Path.Combine

"Se path2 contiver um caminho absoluto, este método retornará path2."

Aqui está o método Combine real da fonte .NET. Você pode ver que ele chama CombineNoChecks , que chama IsPathRooted no caminho2 e retorna esse caminho, se assim for:

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

Não sei qual é a lógica. Eu acho que a solução é remover (ou aparar) o DirectorySeparatorChar do início do segundo caminho; talvez escreva seu próprio método Combine que faça isso e depois chame Path.Combine ().

Ryan Lundy
fonte
Olhando para o código desmontado (veja meu post), você está certo de uma maneira.
Gulzar Nazim
7
Eu acho que funciona dessa maneira para permitir acesso fácil ao algoritmo "diretório de trabalho atual".
BCS
Parece funcionar como fazer uma sequência a cd (component)partir da linha de comando. Parece razoável para mim.
Adrian Ratnapala
11
Eu uso esse trim para obter o efeito desejado string strFilePath = Path.Combine (basePath, otherPath.TrimStart (new char [] {'\\', '/'}));
Matthew Lock,
3
Eu mudava o meu código de trabalho em Path.Combineapenas para ser seguro, mas, em seguida, ele quebrou .. É tão estúpido :)
SOTN
23

Este é o código desmontado do .NET Reflector para o método Path.Combine. Verifique a função IsPathRooted. Se o segundo caminho estiver enraizado (começa com um DirectorySeparatorChar), retorne o segundo caminho 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
fonte
23

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

Obviamente, todos os caminhos de 1 a 9 devem conter uma sequência equivalente no final. Aqui está o método PathCombine que eu criei:

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

Eu também acho que é bastante irritante que esse tratamento de string tenha que ser feito manualmente, e eu estaria interessado na razão por trás disso.

anhoppe
fonte
19

Na minha opinião, isso é um bug. O problema é que existem dois tipos diferentes de caminhos "absolutos". O caminho "d: \ mydir \ myfile.txt" é absoluto, o caminho "\ mydir \ myfile.txt" também é considerado "absoluto", mesmo faltando a letra da unidade. O comportamento correto, na minha opinião, seria preceder a letra da unidade do primeiro caminho quando o segundo caminho iniciar com o separador de diretório (e não é um caminho UNC). Eu recomendaria escrever sua própria função de wrapper auxiliar, com o comportamento desejado, se você precisar.

Cunha
fonte
7
Ele corresponde às especificações, mas também não é o que eu esperava.
dthrasher 26/09/09
@ Jake Isso não está evitando uma correção de bug; são várias pessoas que pensam muito sobre como fazer algo e depois se apegam ao que concordam. Além disso, observe a diferença entre a estrutura .Net (uma biblioteca que contém Path.Combine) e a linguagem C #.
Grault
9

Do MSDN :

Se um dos caminhos especificados for uma cadeia de comprimento zero, esse método retornará o outro caminho. Se path2 contiver um caminho absoluto, esse método retornará path2.

No seu exemplo, path2 é absoluto.

nickd
fonte
7

Seguindo o conselho de Christian Graus no blog "Coisas que odeio sobre a Microsoft", intitulado " Path.Combine é essencialmente inútil. ", Eis a minha solução:

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

Alguns aconselham que os namespaces colidam, ... eu fui Pathy, como um pouco, e para evitar colisões com namespaces System.IO.Path.

Editar : Adicionadas verificações de parâmetro nulo

ergohack
fonte
4

Este código deve fazer o truque:

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

Sem saber os detalhes reais, meu palpite é que ele tenta entrar como se você fosse um URI relativo. Por exemplo:

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

Isso significa que, quando você une um caminho com uma barra anterior, na verdade você está unindo uma base à outra; nesse caso, a segunda obtém precedência.

Elarson
fonte
Acho que as barras devem ser explicadas. Além disso, o que isso tem a ver com o .NET?
Peter Mortensen
3

Razão:

Seu segundo URL é considerado um caminho absoluto. O Combinemétodo retornará o último caminho apenas se o último caminho for um caminho absoluto.

Solução: basta remover a barra inicial /do seu segundo caminho ( /SecondPathpara SecondPath). Então funciona como você excluiu.

Amir Hossein Ahmadi
fonte
3

Isso realmente faz sentido, de alguma forma, considerando como os caminhos (relativos) são tratados normalmente:

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

A verdadeira questão é: Por que os caminhos, que começam com "\", são considerados "enraizados"? Isso também era novo para mim, mas funciona assim no Windows :

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

Se você deseja combinar os dois caminhos sem perder nenhum caminho, pode usar isto:

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

Ou com variáveis:

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

Ambos os casos retornam "C: \ test \ test".

Primeiro, avalio se o Path2 começa com / e, se for verdadeiro, retorne o Path2 sem o primeiro caractere. Caso contrário, retorne o Path2 completo.

Ferri
fonte
1
Provavelmente é mais seguro substituir o == @"\"cheque por uma Path.IsRooted()chamada, pois esse "\"não é o único caractere a ser considerado.
rumblefx0
0

Esses dois métodos devem evitar que você junte acidentalmente duas seqüências que possuem o delimitador.

    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
fonte
0

Isso \ significa "o diretório raiz da unidade atual". No seu exemplo, significa a pasta "test" no diretório raiz da unidade atual. Portanto, isso pode ser igual a "c: \ test".

Estevez
fonte
0

Remova a barra inicial ('\') no segundo parâmetro (caminho2) do Path.Combine.

shanmuga raja
fonte
A pergunta não está fazendo isso.
LarsTech 12/09/19
0

Eu usei a função agregada para forçar caminhos combinados como abaixo:

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

Como mencionado por Ryan, está fazendo exatamente o que a documentação diz.

Desde os tempos do DOS, o disco atual e o caminho atual são diferenciados. \é o caminho raiz, mas para o DISCO ATUAL.

Para cada " disco ", há um " caminho atual " separado . Se você alterar o disco usando cd D:, não altere o caminho atual para D:\, mas para: "D: \ qualquer \ foi \ o \ último \ caminho \ acessado \ neste \ disco" ...

Portanto, no Windows, um literal @"\x"significa: "CURRENTDISK: \ x". Portanto, Path.Combine(@"C:\x", @"\y")tem como segundo parâmetro um caminho raiz, não um parente, embora não em um disco conhecido ... E como não se sabe qual pode ser o «disco atual», o python retorna "\\y".

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