In this post, I'll discuss how to handle file paths in Pascal in a cross-platform manner. I have newly written the following functions, because I often encountered problems with the Delphi and FreePascal own appropriate functions in terms of cross-platform difficulties. So I reinvented the wheel out of necessity, but for the cross-platform special case, where each build variant must also support the each other path convention. Specifically, I'll focus on converting between relative and absolute paths, expanding relative paths, including handling UNC paths and so on. I'll also provide an in-depth analysis of each function, detailing the logic and edge cases they handle. So, let's get started.
IsPathSeparator
The IsPathSeparator
function checks if a character is a path separator. It considers '/' and '\' as path separators. This fundamental function forms the basis for parsing the paths in the following functions, and is crucial for ensuring cross-platform compatibility. It's implemented as a function rather than a check like aChar in ['/','\']
due to some code generator bugs in recent trunk versions of FreePascal when handling sets in conditions.
function IsPathSeparator(const aChar:AnsiChar):Boolean; inline;
begin
case aChar of
'/','\':begin
result:=true;
end;
else begin
result:=false;
end;
end;
end;
ExpandRelativePath
The ExpandRelativePath
function converts a relative path to an absolute path, accounting for different path styles used across various platforms. It accepts a relative path and an optional base path. The function begins by checking if the relative path is already an absolute path. It recognizes both Unix-style and Windows-style absolute paths, including UNC paths. It then appends the relative path to the base path. The function also handles edge cases like multiple consecutive path separators and trailing separators.
function ExpandRelativePath(const aRelativePath:RawByteString;const aBasePath:RawByteString=''):RawByteString;
var InputIndex,OutputIndex:Int32;
InputPath:RawByteString;
PathSeparator:AnsiChar;
begin
if (length(aRelativePath)>0) and
(IsPathSeparator(aRelativePath[1]) or
((length(aRelativePath)>1) and
(aRelativePath[1] in ['a'..'z','A'..'Z']) and
(aRelativePath[2]=':'))) then begin
InputPath:=aRelativePath;
end else begin
if (length(aBasePath)>1) and not IsPathSeparator(aBasePath[length(aBasePath)]) then begin
PathSeparator:=#0;
for InputIndex:=1 to length(aBasePath) do begin
if IsPathSeparator(aBasePath[InputIndex]) then begin
PathSeparator:=aBasePath[InputIndex];
break;
end;
end;
if PathSeparator=#0 then begin
for InputIndex:=1 to length(aRelativePath) do begin
if IsPathSeparator(aRelativePath[InputIndex]) then begin
PathSeparator:=aRelativePath[InputIndex];
break;
end;
end;
if PathSeparator=#0 then begin
PathSeparator:='/';
end;
end;
InputPath:=aBasePath+PathSeparator;
end else begin
InputPath:=aBasePath;
end;
InputPath:=InputPath+aRelativePath;
end;
result:=InputPath;
InputIndex:=1;
OutputIndex:=1;
while InputIndex<=length(InputPath) do begin
if (((InputIndex+1)<=length(InputPath)) and (InputPath[InputIndex]='.') and IsPathSeparator(InputPath[InputIndex+1])) or
((InputIndex=length(InputPath)) and (InputPath[InputIndex]='.')) then begin
inc(InputIndex,2);
if OutputIndex=1 then begin
inc(OutputIndex,2);
end;
end else if (((InputIndex+1)<=length(InputPath)) and (InputPath[InputIndex]='.') and (InputPath[InputIndex+1]='.')) and
((((InputIndex+2)<=length(InputPath)) and IsPathSeparator(InputPath[InputIndex+2])) or
((InputIndex+1)=length(InputPath))) then begin
inc(InputIndex,3);
if OutputIndex=1 then begin
inc(OutputIndex,3);
end else if OutputIndex>1 then begin
dec(OutputIndex,2);
while (OutputIndex>1) and not IsPathSeparator(result[OutputIndex]) do begin
dec(OutputIndex);
end;
inc(OutputIndex);
end;
end else if IsPathSeparator(InputPath[InputIndex]) then begin
if (InputIndex=1) and
((InputIndex+1)<=length(InputPath)) and
IsPathSeparator(InputPath[InputIndex+1]) and
((length(InputPath)=2) or
(((InputIndex+2)<=Length(InputPath)) and not IsPathSeparator(InputPath[InputIndex+2]))) then begin
result[OutputIndex]:=InputPath[InputIndex];
result[OutputIndex+1]:=InputPath[InputIndex+1];
inc(InputIndex,2);
inc(OutputIndex,2);
end else begin
if (OutputIndex=1) or ((OutputIndex>1) and not IsPathSeparator(result[OutputIndex-1])) then begin
result[OutputIndex]:=InputPath[InputIndex];
inc(OutputIndex);
end;
inc(InputIndex);
end;
end else begin
while (InputIndex<=length(InputPath)) and not IsPathSeparator(InputPath[InputIndex]) do begin
result[OutputIndex]:=InputPath[InputIndex];
inc(InputIndex);
inc(OutputIndex);
end;
if InputIndex<=length(InputPath) then begin
result[OutputIndex]:=InputPath[InputIndex];
inc(InputIndex);
inc(OutputIndex);
end;
end;
end;
SetLength(result,OutputIndex-1);
end;
ConvertPathToRelative
The ConvertPathToRelative
function converts an absolute path (including UNC paths) to a relative path with respect to a base path. This function is more complex as it needs to find the common prefix of the absolute path and the base path, and then compute the relative path. The function handles edge cases like trailing separators in the base path and different path separators in the input paths, ensuring cross-platform compatibility.
function ConvertPathToRelative(aAbsolutePath,aBasePath:RawByteString):RawByteString;
var AbsolutePathIndex,BasePathIndex:Int32;
PathSeparator:AnsiChar;
begin
if length(aBasePath)=0 then begin
result:=aAbsolutePath;
end else begin
aAbsolutePath:=ExpandRelativePath(aAbsolutePath);
aBasePath:=ExpandRelativePath(aBasePath);
PathSeparator:=#0;
for BasePathIndex:=1 to length(aBasePath) do begin
if IsPathSeparator(aBasePath[BasePathIndex]) then begin
PathSeparator:=aBasePath[BasePathIndex];
break;
end;
end;
if PathSeparator=#0 then begin
for AbsolutePathIndex:=1 to length(aAbsolutePath) do begin
if IsPathSeparator(aAbsolutePath[AbsolutePathIndex]) then begin
PathSeparator:=aAbsolutePath[AbsolutePathIndex];
break;
end;
end;
if PathSeparator=#0 then begin
PathSeparator:='/';
end;
end;
if length(aBasePath)>1 then begin
if IsPathSeparator(aBasePath[length(aBasePath)]) then begin
if (length(aAbsolutePath)>1) and IsPathSeparator(aAbsolutePath[length(aAbsolutePath)]) then begin
if (aAbsolutePath=aBasePath) and (aAbsolutePath[1]<>'.') and (aBasePath[1]<>'.') then begin
result:='.'+PathSeparator;
exit;
end;
end;
end else begin
aBasePath:=aBasePath+PathSeparator;
end;
end;
AbsolutePathIndex:=1;
BasePathIndex:=1;
while (BasePathIndex<=Length(aBasePath)) and
(AbsolutePathIndex<=Length(aAbsolutePath)) and
((aBasePath[BasePathIndex]=aAbsolutePath[AbsolutePathIndex]) or
(IsPathSeparator(aBasePath[BasePathIndex]) and IsPathSeparator(aAbsolutePath[AbsolutePathIndex]))) do begin
inc(AbsolutePathIndex);
inc(BasePathIndex);
end;
if ((BasePathIndex<=length(aBasePath)) and not IsPathSeparator(aBasePath[BasePathIndex])) or
((AbsolutePathIndex<=length(aAbsolutePath)) and not IsPathSeparator(aAbsolutePath[AbsolutePathIndex])) then begin
while (BasePathIndex>1) and not IsPathSeparator(aBasePath[BasePathIndex-1]) do begin
dec(AbsolutePathIndex);
dec(BasePathIndex);
end;
end;
if BasePathIndex<=Length(aBasePath) then begin
result:='';
while BasePathIndex<=Length(aBasePath) do begin
if IsPathSeparator(aBasePath[BasePathIndex]) then begin
result:=result+'..'+PathSeparator;
end;
inc(BasePathIndex);
end;
end else begin
result:='.'+PathSeparator;
end;
if AbsolutePathIndex<=length(aAbsolutePath) then begin
result:=result+copy(aAbsolutePath,AbsolutePathIndex,(length(aAbsolutePath)-AbsolutePathIndex)+1);
end;
end;
end;
Analysis
Let's delve deeper into the analysis of each function:
The IsPathSeparator
function is straightforward. It checks whether a given character is a path separator, either '/' or '\'. This utility function plays a crucial role in other functions where parsing of the paths is needed.
The ExpandRelativePath
function handles a variety of scenarios:
- It can process relative paths, absolute paths, and UNC paths, which is crucial for cross-platform compatibility.
- It handles both Unix-style ('/') and Windows-style ('\') path separators, supporting different file systems.
- It can deal with path components like . (current directory) and .. (parent directory).
- It correctly adds a path separator if the base path doesn't have a trailing separator.
- It handles case sensitivity properly. For simplicity and cross-platform compatibility, case sensitivity is ignored as well as the fact that under *nix '\' can be a valid part of a filename.
The ConvertPathToRelative
function also handles various edge cases:
- It starts by expanding the absolute path and the base path, ensuring they are in a consistent format.
- Then, it finds the common prefix of the two paths. If the characters at the current position are not equal, it moves back to the last path separator.
- It constructs the relative path by appending '..' for each remaining segment in the base path and the remaining part of the absolute path.
- It handles case sensitivity properly. For simplicity and cross-platform compatibility, case sensitivity is ignored as well as the fact that under *nix '\' can be a valid part of a filename.
Overall, these functions are robust and handle a variety of common and edge cases. They can handle both Unix-style and Windows-style paths, including UNC paths. They do not interact with the file system, i.e., they don't check whether the paths exist, nor do they handle file permissions or other file system-specific issues. Their focus is purely on manipulating path strings in a cross-platform manner.
That's a wrap on cross-platform file paths in Pascal. I hope this post illuminated new insights for you. Happy coding!