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?
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:
Why Path.Combine doesn't add the Path.DirectorySeparatorChar after the drive designator?
Path.Combine() does not add directory separator after drive letter
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');
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;
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.
Related
The environment is Windows 7 Pro and Delphi 7.
Windows.CreateDirectory() is failing to create multiple folders in a very long path that is well under the path length limit. GetLastError() returns ERROR_PATH_NOT_FOUND.
The failure is the same on an ESXi virtual machine, as well as a native Win7 workstation and physical disk. A similar failure occurs with Windows.MoveFile().
The long path in the code below is created correctly in a CMD window as a pasted parameter to MKDIR.
My work-around is creating this long path piece-meal. I split the path at the '\' character into a string array. Then I loop through the array and build the cumulative path from each element. The loop correctly builds the full path without an error.
I have no clue why the Win32 function fails to create a valid long path.
var
arrDstPath : TStringArray;
begin
// --------------
// failing method
// --------------
strDstPath := 'C:\Duplicate Files\my customer recovered data\desktop\my customer name\application data\gtek\gtupdate\aupdate\channels\ch_u3\html\images\';
if (Windows.CreateDirectory(pchar(strDstPath),nil) = false) then
Result := Windows.GetLastError; // #3 is returned
if (DirectoryExists(strNewPath) = false) then
Result := ERROR_PATH_NOT_FOUND;
// -----------------
// successful method
// -----------------
strNewPath := '';
LibSplitToArray(arrDstPath,'\',strDstPath);
for intIdx := 0 to High(arrDstPath) do
begin
strNewPath := strNewPath + arrDstPath[intIdx] + '\';
Windows.CreateDirectory(PChar(strNewPath), nil);
end;
if (DirectoryExists(strDstPath) = false) then // compare to original path string
begin
Result := ERROR_PATH_NOT_FOUND;
Exit;
end;
Actually, the official documentation for the CreateDirectory function describes what's going on. Since the function fails, your instinct should be to have a look at the section describing the return value, which states:
ERROR_ALREADY_EXISTS
The specified directory already exists.
ERROR_PATH_NOT_FOUND
One or more intermediate directories do not exist; this function will only create the final directory in the path.
I assume you got ERROR_PATH_NOT_FOUND, and the documentation suggests a probable reason: you are attempting to create several levels of subdirectories at once, which the function doesn't support.
Fortunately, the Delphi RTL has the ForceDirectories function that can create subdirectories recursively. (How can I Create folders recursively in Delphi?)
In Delphi 2010 and later, you can also use TDirectory.CreateDirectory from IOUtils.pas. Internally, this calls ForceDirectories.
I'm totally new to Delphi I guess learning by doing something should be ok (My hope!)
My Idea was because I often have to re-create the same tasks:
creating always the same directorys which contains sometimes files and sometimes leaved empty...
So my conclusion was to automate it in some way.
Assume a Memo containing the following:
config.xml|enc
/skin
/data/defines.dat:blub
/temp
The Basepath where all the stuff above should be created inside:
C:\users\BGates\test
":blub" is just placeholders e.g. :blub can contain any text which comes from another memo in my application which means that later defines.dat is filled with the text blub contains...
As you can see sometimes I use | and sometimes : for the placeholder...
So from the information above I would like to parse the contents of the memo to create a directory structure like this:
C:\users\BGates\test\
config.xml
skin
data
defines.dat (while defines.dat will contain the stuff which comes from blub)
temp
My problem is the parsing of the memo esspecially how to decide its a folder or its a folder in another folder, then its a file in the root or a file inside of a folder and so on...
Well it might be there is an easier way (I was reading about csv files and such but then? My tool would be hard to understand for someone using it which doesn't know how a csv file needs to look like) while my example above feels maybe familier to them...
Could someone show me please an example how to parse it in a correct (best practice) way So I could learn from it?
There are routines in the SysUtils unit that make file path parsing a lot easier. Have a look at ExtractFileName and ExtractFilePath, for starters. Also, if you're using a recent version of Delphi, (D2010 or any of the XE line,) the IOUtils unit contains a set of helper methods under the TPath record that simplify working with paths.
For example, if I wanted to deal with the line /data/defines.dat:blub, I'd do something like this:
function NormalizePath(const name: string): string;
begin
result := StringReplace(name, '/', '\', [rfReplaceAll]);
end;
procedure ProcessLine(line: string);
var
path, filename, data: string;
colonPos: integer;
begin
colonPos := pos(':', line);
if colonPos > 0 then
begin
data := copy(line, colonPos + 1);
delete(line, colonPos, MAXINT);
end;
line := TPath.Combine(BASE_PATH, normalizePath(line));
if ExtractFileExt(line) = '' then
path := line
else begin
path := ExtractFilePath(line);
filename := line;
end;
ForceDirectories(path); //ensure that the folder exists
if filename <> '' then
TFile.WriteAllText(filename, data);
end;
Note: I just wrote this off the top of my head. It may contain bugs. Don't trust it without testing it first. Also, this uses functionality from IOUtils, and things will be a little bit trickier if you don't have it in your version of Delphi. But this should give you the general idea of how to deal with the problem you're trying to solve.
Further to this answered question I have another sticky problem. My coding is Free Pascal but Delphi solutions will work probably.
In brief, I have a string value of concatenated paths that is formed by taking a source directory and recreating that tree in a destination directory. e.g.
C:\SourceDir\SubDirA
becomes
F:\DestinationDir\SourceDir\SubDirA.
However, the solution I have for the Linux version of my program (as posted in the link above) doesn't quite work with Windows version because I end up with :
F:\DestionationDir\C:SourceDir\SubDirA.
which is invalid.
So I came up with this "only run in Windows" code to remove the central drive letterof the reformed path, but leave the initial one at the start by saying "Look at the string starting from the 4th character in from the left. If you find 'C:', delete it" so that the path becomes F:\DestinationDir\SourceDir\SubDirA.
{$IFDEF Windows} // Only do this for the Windows version
k := posex('C:', FinalisedDestDir, 4); // Find 'C:' in the middle of the concatanated path and return its position as k
Delete(FinalisedDestDir, k, 2); // Delete the 2 chars 'C:' of 'C:\' if found, leaving the '\' to keep the path valid
{$ENDIF}
Now, that works fine IF the C: is the source of the chosen directory. But obviously if the user is copying data from another drive (such as E:, F:, G: or whatever else drive up to Z:) it will not work.
So my question is, how do I code it so that it says "if any drive letter a: to z: is found after the 4th character from the left, delete it"? Whilst any solution that works "will do", ideally I need a fast solution. The best solution would be to not have it in there in the first place, but given the solution I posted in reply to my earlier post, I can't work out how not to have it in, due to the procedure I use to form it.
Here is a code I use in my application:
function CombinePath(const BaseDir, Path: string): string;
begin
if IsPathDelimiter(Path, 1) then
Result := ExcludeTrailingBackSlash(BaseDir) + Path else
Result := IncludeTrailingBackSlash(BaseDir) + Path;
end;
function MapRootPath(const Path, NewPath: string): string;
var
Drive, RelativePath: string;
begin
Drive := ExtractFileDrive(Path); // e.g: "C:"
RelativePath := ExtractRelativePath(Drive, Path); // e.g: "Program Files\MyApp"
Result := CombinePath(NewPath, RelativePath);
end;
Usage:
ShowMessage(MapRootPath('C:\SourceDir\SubDirA', 'F:\DestionationDir'));
// result is "F:\DestionationDir\SourceDir\SubDirA"
I offer you two solutions:
Normalize your paths before concatenating folders
You know the saying, prevention is always better then the cure. Why don't you "normalize" paths before you do your concatentations? This gives you the chance to:
Delete the drive letter from the path if the name starts with a path. If the second char in the string is : and the third is \ you know it's a Windows path containing a drive letter. You may delete all of the first 3 characters.
Deal with UNC names, you didn't mention those: \\ComputerName\ShareName\SubFolder
Fix slashes so they conform to your current platform
Remove the drive letter later
This is ugly (because you shouldn't get into this situation in the first place), but you can always look for :\ - not for C:. The : is not valid in folder or file names on windows, so if you find it you know it's preceded by exactly one char, and that's the DRIVE letter. Get the index for :\, substract `, delete 2 chars from that index.
I don't know freepascal but for this problem use regular expression such as [A-Za-z]\: to find such string. I see from freepascal wiki that it supports regular expressions http://wiki.freepascal.org/Regexpr.
You might also see if some of the SysUtils functions can help, including:
ExpandFileName()
IncludeTrailingSlashes()
etc..
Putting your path in a "normalized" form - like these functions might be able to do for you - makes it trivial to convert between Linux and Windows path conventions.
Just a thought...
Thanks for all the help with this. Special thanks to kobik who's code sample is very clear and easy to follow. That is certainly one way to do it, but whilst I was waiting for replies I came up with the following which also seems to work quite well for me :
type
TRange = 'A'..'Z';
...
{$IFDEF Windows}
// Due to the nonsenseories of Windows, we have to allow for driver lettering.
for DriveLetter in TRange do
begin
k := posex(DriveLetter+':', FinalisedDestDir, 4); // Find e.g 'C:' in the middle of the concatanated path and return its position, leaving the first 'C:\' at the start in place
Delete(FinalisedDestDir, k, 2); // Delete 'C:' of 'C:\' if found, leaving the '\'
end;
{$ENDIF}
I am trying to list all the directories in a given directory. I have this code:
var
srec: TSearchRec;
begin
// folder is some absolute path of a folder
if FindFirst(folder + PathDelim + '*', faDirectory, srec) = 0 then
try
repeat
if (srec.Name <> '.') and (srec.Name <> '..') then
ShowMessage(srec.Name);
until FindNext(srec) <> 0;
finally
FindClose(srec);
end;
But for some reason I get messages about file names instead of directories only. I thought that using faDirectory would make FindFirst and family only return names of directories. What am I doing wrong? If I change it to
if FindFirst(folder, faDirectory, srec) = 0 then
Then it only shows the name of folder but not as an absolute path (relative to folder + '/..') and quits afterwards.
I realise that I can check if it is a directory by making sure that (srec.Attr and faDirectory) = faDirectory but I feel like that's doing things in a roundabout way and there should be a proper way of doing it.
If you are using delphi xe, check the TDirectory.GetDirectories function.
The SysUtils.FindFirst Documentation has the answer to your issue.
function FindFirst(const Path: string; Attr: Integer; var F: TSearchRec): Integer;
The Attr parameter specifies the special files to include in addition
to all normal files. Choose from these file attribute constants when
specifying the Attr parameter.
You could do something like this:
var
Dir: string;
begin
for Dir in TDirectory.GetDirectories('c:\') do
ShowMessage(Dir);
end;
You has to use the filter to remove files
some small changes to your code
your code:
folder + PathDelim + '*'
change to
folder + PathDelim + '*.'
What is the best/easiest way to check if a certain path points to a drive's root?
I guess I could just check if path name ends with '\' or ':', or if the path is only 2 or three characters in length, but I was hoping there was some kind of standard "IsDriveRoot" function to check this.
Tx
UPDATE:
After searching through the Delphi help file I found the ExtractFileDrive() function which returns the drive portion of any given path.
Using that function I gues it's easy to write a little function to check if the original path is the same as the result of ExtractFileDrive(), which would mean that the original path had to be the drive's root.
Function IsDriveRoot(APath: string): Boolean;
begin
Result := ((Length(APath) = 2) and (ExtractFileDrive(APath) = APath))
or ((Length(APath) = 3) and ((ExtractFileDrive(APath) + '\') = APath));
end;
or
Function IsDriveRoot(APath: string): Boolean;
begin
Result := ((Length(APath) = 2) and (Copy(APath,2,1) = ':'))
or ((Length(APath) = 3) and (Copy(APath,3,1) = '\'));
end;
Something like that should do it....
I actually think the second example is simpler, and will probably end up using that one.
Thanks again to all who responded :)
It seems GetVolumePathName can be quite helpful in your case.
You could utilize GetDriveType() call:
if GetDriveType(PChar(path)) <> DRIVE_NO_ROOT_DIR then
...