6

So when I run

TPath.Combine('c:', 'myfile.txt');

in Delphi XE2 then I get 'C:myfile.txt' in return. This is not what I expect and it is not a valid path in windows. I would expect TPath.Combine to be a call to the windows API ( http://msdn.microsoft.com/en-us/library/fyy7a5kt%28v=vs.110%29.aspx ) or to have the same behaviour as the API.

Is there something I'm doing wrong? Can I "fix" the behaviour of TPath.Combine? Or do I have to search all uses in my code and replace it with a string concatenation with a '\' in between?

TLama
  • 75,147
  • 17
  • 214
  • 392
user3331950
  • 119
  • 1
  • 6
  • 1
    Small nitpick, .NET is not Windows API (And Delphi is not the same as .NET ;) ) – whosrdaddy Sep 04 '14 at 16:04
  • 3
    The Win32 API function is [`PathCombine()`](http://msdn.microsoft.com/en-us/library/windows/desktop/bb773571.aspx) – Remy Lebeau Sep 04 '14 at 16:18
  • The documentation for `Path.Combine` that you link to says it would also return `c:myfile.txt`: "If path1 is not a drive reference (that is, "C:" or "D:") and does not end with a valid separator character as defined in DirectorySeparatorChar, AltDirectorySeparatorChar, or VolumeSeparatorChar, DirectorySeparatorChar is appended to path1 before concatenation." –  Sep 04 '14 at 16:23
  • @ hvd: No, you are wrong. Please read the specification again. – user3331950 Sep 05 '14 at 07:01
  • @hvd read it correctly, and of course you can always execute the function call so see what it returns – David Heffernan Sep 05 '14 at 07:09
  • @user3331950 I did read it again, and I don't understand how you could misinterpret it. path1 ("c:") is a drive reference *and* ends with a valid separator character (VolumeSeparatorChar). Therefore, DirectorySeparatorChar is not appended to path1 before concatenation. The result is that "c:" and "myfile.txt" are combined to form "c:myfile.txt". (If you try it, make sure you don't try it on an online C# compiler: the behaviour on Linux with Mono will be different, because ":" is not a drive separator there.) –  Sep 05 '14 at 07:31
  • @hvd You are right. My fault. – user3331950 Sep 05 '14 at 09:57
  • note, seems the example shown at the related Win32 function is either wrong or that function works differently (and adds the backslash to the drive designator) from the .NET and Delphi ones – George Birbilis Jul 25 '22 at 14:52

4 Answers4

11

I think that the behaviour is correct, and as designed. There is a difference between C:myfile.txt and C:\myfile.txt. The Windows documentation calls this out quite explicitly:

If a file name begins with only a disk designator but not the backslash after the colon, it is interpreted as a relative path to the current directory on the drive with the specified letter. Note that the current directory may or may not be the root directory depending on what it was set to during the most recent "change directory" operation on that disk. Examples of this format are as follows:

  • "C:tmp.txt" refers to a file named "tmp.txt" in the current directory on drive C.
  • "C:tempdir\tmp.txt" refers to a file in a subdirectory to the current directory on drive C.

If the RTL function TPath.Combine added a separator after a drive designator, then there would be no way for you to use TPath.Combine to produce a path like "C:tmp.txt". So, if you want a directory separator, you'll need to supply one yourself:

TPath.Combine('c:\', 'myfile.txt');

Note that the .net framework method Path.Combine on which the Delphi RTL class TPath is loosely modelled behaves the same was the the Delphi RTL equivalent.

Related:

Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • since PathCombineA of Win32 was mentioned in comments, seems the example shown at the related Win32 function is either wrong or that function works differently from the .NET and the Delphi ones (adds the backslash to the drive designator) - https://learn.microsoft.com/el-gr/windows/win32/api/shlwapi/nf-shlwapi-pathcombinea – George Birbilis Jul 25 '22 at 14:54
1

When combining folder names and folder files it is always good (if you don't want the default behaviour when a drive designator is given as a path) to put folder name through IncludeTrailingPathDelimiter method. This method will add trailing delimiter to your path if there isn't one

TPath.Combine(IncludeTrailingPathDelimiter('c:'), 'myfile.txt');
George Birbilis
  • 2,782
  • 2
  • 33
  • 35
SilverWarior
  • 7,372
  • 2
  • 16
  • 22
  • 1
    The purpose of functions like `TPath.Combine()` is to handle the slashes for you so you DO NOT have to insert them manually. – Remy Lebeau Sep 04 '14 at 16:07
  • 1
    @RemyLebeau Well, the system cannot handle this for you because the system cannot read your mind and know whether or not you want one here – David Heffernan Sep 04 '14 at 16:40
  • Documentation doesen't specify anything about TPath.Combine to automatically add Trailing slashes. – SilverWarior Sep 05 '14 at 06:27
  • @SilverWarior You are missing the point that Remy is attempting to make. His point is that the raise d'etre for `TPath.Combine` is to obviate the need for you to worry about trailing slashes. – David Heffernan Sep 05 '14 at 15:17
  • @DavidHeffernan I don't realy undestand what you mean with your last comment – SilverWarior Sep 05 '14 at 15:45
  • What Remy means is that you wouldn't need to write `TPath.Combine(IncludeTrailingPathDelimiter('mydir'), 'myfile.txt')` and could write instead `TPath.Combine('mydir', 'myfile.txt')` – David Heffernan Sep 05 '14 at 15:58
  • What Remy means is that you shouldn't have to add trailing path delimiter by yourself. Generally when using TPath record you don't have to. But when first path provided is just 'c:' then TPath.combine method doesen't work correctly as it doesen't add "DirectorySeparatorChar" between the two paths provided as it should. I just checked the code to see how it works and where it fails. I have also isued a QC report to the Embarcadero about this bug. See QC 127407 or http://qc.embarcadero.com/wc/qcmain.aspx?d=127407 – SilverWarior Sep 06 '14 at 13:42
  • It's not a bug though. The function behaves as designed. And yes, that's what Remy meant. Which is why he criticised your answer. – David Heffernan Sep 06 '14 at 20:13
  • 1
    Then I would say its design is flawed becouse you don't get the resutls you would have expeced. As for my answer it is a workaround for this specific problem. – SilverWarior Sep 07 '14 at 12:15
  • Typical dev behaviour if you ask me -> works as designed without acknowledging the design might not be what users are expecting. To *me* it sounds logical for TPath.Combine to add the delimiter and use `.\` for relative paths rather than `c:`. – Remko Feb 20 '17 at 08:58
0

The Path.Combine and CombinePath functions have problems with very long path names, furthermore when the path is not physically on the drive (but for example in a zip file) then it also doesn't work. This implementation works for me:

function ExpandFileNameEx(const BasePath, RelativeFileName: string): string;
var
  p:integer;
  s,folder:string;
begin
  { Check if Relative file name is a fully qualified path: }
  if (pos(':\', RelativeFileName) > 0) or (copy(RelativeFileName,1,2) = '\\') then
    Result := RelativeFileName

  { Check if Relative file name is a root path assignment: }
  else if copy(RelativeFileName,1,1) = '\' then
  begin
    Result := IncludeTrailingPathDelimiter(ExtractFileDrive(BasePath))
             + Copy(RelativeFileName,2,Length(RelativeFileName));
  end else
  begin
    { Check all sub paths in Relative file name: }
    Result := BasePath;
    s := RelativeFileName;
    repeat
      p := pos('\', s);
      if p > 0 then
      begin
        folder := Copy(s,1,p-1);
        Delete(s, 1,p);
      end else
      begin
        folder := s;
        s := '';
      end;

      if folder <> EmptyStr then
      begin
        if Folder = '..' then
          Result := ExtractFileDir(Result)
        else if Folder = '.' then
          { No action }
        else
          Result := IncludeTrailingPathDelimiter(Result) + Folder;
      end;
    until p = 0;
  end;
end;
Ramon Speets
  • 99
  • 1
  • 3
0

That is why you should always use IncludeTrailingBackslash (or IncludeTrailingPathDelimiter if you're a masochist):

  • Before: TPath.Combine('c:', 'myfile.txt');
  • After: IncludeTrailingBackslash('c:')+'myfile.txt';

It avoids the unknown nuances that someone else was thinking, or edge cases they didn't bother to handle, or quirks they didn't bother to document, when they designed their abstraction.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219