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
...
Related
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.
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 need to do some searching in the filesystem and would like to present a progress indication.
A rough approximation of that is the number of directories traversed.
function GetSubDirCount(Path : String): integer;
var
Index : Integer;
Temp : String;
SearchRec : TSearchRec;
begin
Result:= 0;
Temp:= Path;
if Path[Length(Path)] <> SysUtils.PathDelim then begin
Path:= Path + SysUtils.PathDelim;
end;
Path:= Path + '*.';
Index:= FindFirst(Path, faDirectory, SearchRec);
while Index = 0 do begin
if (SearchRec.Name = '.') or (SearchRec.Name = '..') then begin
Index:= FindNext(SearchRec);
Continue;
end;
Inc(Result);
Result:= Result + GetSubDirCount(Temp + SysUtils.PathDelim + SearchRec.Name);
Index:= FindNext(SearchRec);
end;
FindClose(SearchRec);
end;
I currently use the above code, is there a faster way?
I'm only interested in the count.
If there's a really fast way to get the number of files as well that would be a bonus.
As you are not specifying the Delphi version you are using, I suggest the corresponding methods from IOUtils - namely TDirectory.GetDirectories and TDirectory.GetFiles as they are available in recent Delphi versions.
Update: It is probably not the fastest way to count the number of directories and files, but if the files shall be iterated later anyway, one could as well use the result of these functions for the iteration.
Minor improvement: use const in the parameter declaration.
ex:
function GetSubDirCount(const Path : String): integer;
As Rob points out, this will not work as Path is modified in the body. I would still use this approach however, and NOT modify path in the body. I'd have a local string var "Suffix", modify that (add optional pathdelim, and '*.'), and pass both to FindFirst:
FindFirst(Path+Suffix, faDirectory, SearchRec);
#Johan
Since the Windows code takes up most time, I suggest you apply the fixes suggested by other respondents, and update your code to use threads if your feel comfortable with that:
As soon as you retrieve a subdirectory put add it to a (thread safe) list
Have a thread look at that list and spawn worker threads do to the actual file processing per directory
Update your progress all the time: number of dirs found/handled. This will be a bit wobbly in the beginning, but at least you can start working while Windows is still 'finding'
The 'usual' warnings apply:
don't make your number of threads too large
if your file processing creates new files make sure your find routines don't choke on the new output files
How can I copy/extract part of a File path?
For example, say if I have this path: D:\Programs\Tools\Bin\Somefile.dat
how could I copy/extract it to make it like this:
C:\Users\Bin\Somefile.dat
or
C:\Users\Tools\Bin\Somefile.dat
or
C:\Users\Programs\Tools\Bin\Somefile.dat
Notice that the examples above are taking part of the original path, and changing it to another directory. I think this is called Expand name or something maybe??
PS, I already know about ExtractFileName and ExtractFilePath etc, the path anyway could be dynamic in that it wont be a hard coded path, but ever changing, so these functions are likely no good.
Thanks.
Here's a quick implementation that returns the TAIL of a path, including the specified number of elements. There's also a bit of demo of how to use it, and the results are exactly the ones you requested. Unfortunately I don't fully understand what transformations you're after: this might be exactly what you're after, or it might be something entirely wrong, that just happens to produce a result that looks like your sample:
program Project25;
{$APPTYPE CONSOLE}
uses
SysUtils;
function ExtractPathTail(const OriginalPath:string; const PathElemCount:Integer):string;
var i, start, found_delimiters: Integer;
begin
start := 0;
found_delimiters := 0;
for i:=Length(OriginalPath) downto 1 do
if OriginalPath[i] = '\' then
begin
Inc(found_delimiters);
if found_delimiters = PathElemCount then
begin
start := i;
Break;
end;
end;
if start = 0 then
raise Exception.Create('Original path is too short, unable to cut enough elements from the tail.') // mangled English to help SO's code formatter
else
Result := System.Copy(OriginalPath, start+1, MaxInt);
end;
const SamplePath = 'D:\Programs\Tools\Bin\Somefile.dat';
begin
try
WriteLn('C:\Users\' + ExtractPathTail(SamplePath, 2)); // prints: C:\Users\Bin\Somefile.dat
WriteLn('C:\Users\' + ExtractPathTail(SamplePath, 3)); // prints: C:\Users\Tools\Bin\Somefile.dat
WriteLn('C:\Users\Programs\' + ExtractPathTail(SamplePath, 3)); // prints: C:\Users\Programs\Tools\Bin\Somefile.dat
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Have you looked at the ExtractFileName function? all built in for you. Depending on where your paths/files are coming from of course, you may need the ExtractFilePath, or other related functions.
try using the PathAppend and PathExtractElements functions
I have to make a unix compatible windows delphi routine that confirms if a file name exists in filesystem exactly in same CaSe as wanted, e.g. "John.txt" is there, not "john.txt".
If I check "FileExists('john.txt')" its always true for John.txt and JOHN.TXT due windows .
How can I create "FileExistsCaseSensitive(myfile)" function to confirm a file is really what its supposed to be.
DELPHI Sysutils.FileExists uses the following function to see if file is there, how to change it to double check file name is on file system is lowercase and exists:
function FileAge(const FileName: string): Integer;
var
Handle: THandle;
FindData: TWin32FindData;
LocalFileTime: TFileTime;
begin
Handle := FindFirstFile(PChar(FileName), FindData);
if Handle <> INVALID_HANDLE_VALUE then
begin
Windows.FindClose(Handle);
if (FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
begin
FileTimeToLocalFileTime(FindData.ftLastWriteTime, LocalFileTime);
if FileTimeToDosDateTime(LocalFileTime, LongRec(Result).Hi,
LongRec(Result).Lo) then Exit;
end;
end;
Result := -1;
end;
function FileExistsEx(const FileName: string): Integer;
var
Handle: THandle;
FindData: TWin32FindData;
LocalFileTime: TFileTime;
begin
Handle := FindFirstFile(PChar(FileName), FindData);
if Handle <> INVALID_HANDLE_VALUE then
begin
Windows.FindClose(Handle);
if (FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
begin
FileTimeToLocalFileTime(FindData.ftLastWriteTime, LocalFileTime);
if FileTimeToDosDateTime(LocalFileTime, LongRec(Result).Hi, LongRec(Result).Lo) then
if AnsiSameStr(FindData.cFileName, ExtractFileName(FileName)) then Exit;
end;
end;
Result := -1;
end;
Tom, I'm also intrigued by your use case. I tend to agree with Motti that it would be counter intuitive and might strike your users as odd.
On windows file names are not case sensitive so I don't see what you can gain from treating file names as if they were case sensitive.
In any case you can't have two files named "John.txt" and "john.txt" and failing to find "John.txt" when "john.txt" exists will probably result in very puzzled users.
Trying to enforce case sensitivity in this context is un-intuitive and I can't see a viable use-case for it (if you have one I'll be happy to hear what it is).
I dealt with this issue a while back, and even if I'm sure that there are neater solutions out there, I just ended up doing an extra check to see if the given filename was equal to the name of the found file, using the case sensitive string comparer...
I ran into a similar problem using Java. Ultimately I ended up pulling up a list of the directory's contents (which loaded the correct case of filenames for each file) and then doing string compare on the filenames of each of the files.
It's an ugly hack, but it worked.
Edit: I tried doing what Banang describes but in Java at least, if you open up file "a.txt" you'r program will stubbornly report it as "a.txt" even if the underlying file system names it "A.txt".
You can implement the approach mention by Kris using Delphi's FindFirst and FindNext routines.
See this article