Well, usually I strongly recommend not to do string manipulation on file or directory paths, because it is quite prone to failures. But for a task like this, which does not rely on existing paths, there appears no way around.
Anyway, for doing so in a reliable fashion, the following issues must be considered:
- Windows uses case-insensitive paths, so do all path comparisons in such manner as well!
- Avoid sub-string substitution, because it is troublesome with
=
-signs and a few other characters!
- Ensure to properly resolve the provided paths, using the
~f
-modifier of for
-loop meta-variables! This ensures that the input paths are really absolute, they do not contain doubled separators (\\
), and there are no sequences with .
and ..
(like abc\..\.\def
, which is equivalent to def
), that make comparison difficult.
- Regard that paths may be provided in an ugly way, like with trailing
\
or \.
, bad quotation (like abc\"def ghi"\jkl
), or using wrong path separators (/
instead of \
, which is the Windows standard).
Alright, so let us turn to a script that I wrote for deriving the common path and the relative path between two absolute paths, regarding all of the said items (see the rem
comments):
@echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define constants here:
set "referencePath=C:\Users\"xyz"\dummy\..\.\project"
set "absolutePath=C:\Users\"xyz"\other\\somedir\"
rem /* At first resolve input paths, including correction of bad quotes and wrong separators,
rem and avoidance of trailing separators (`\`) and also other unwanted suffixes (`\.`): */
set "referencePath=%referencePath:"=%"
set "absolutePath=%absolutePath:"=%"
for %%P in ("%referencePath:/=\%") do for %%Q in ("%%~fP.") do set "referencePath=%%~fQ"
for %%P in ("%absolutePath:/=\%") do for %%Q in ("%%~fP.") do set "absolutePath=%%~fQ"
rem // Initially clean up (pseudo-)array variables:
for /F "delims==" %%V in ('2^> nul ^(set "$ref[" ^& set "$abs["^)') do set "%%V="
rem // Split paths into their elements and store them in arrays:
set /A "#ref=0" & for %%J in ("%referencePath:\=" "%") do if not "%%~J"=="" (
set /A "#ref+=1" & setlocal EnableDelayedExpansion
for %%I in (!#ref!) do endlocal & set "$ref[%%I]=%%~J"
)
set /A "#abs=0" & for %%J in ("%absolutePath:\=" "%") do if not "%%~J"=="" (
set /A "#abs+=1" & setlocal EnableDelayedExpansion
for %%I in (!#abs!) do endlocal & set "$abs[%%I]=%%~J"
)
rem /* Determine the common root path by comparing and rejoining the array elements;
rem also build the relative path herein: */
set "commonPath=\" & set "relativePath=." & set "flag=#" & set /A "#cmn=#ref+1"
for /L %%I in (1,1,%#abs%) do (
if defined flag (
set "flag=" & if defined $ref[%%I] (
setlocal EnableDelayedExpansion
if /I "!$abs[%%I]!"=="!$ref[%%I]!" for %%J in ("!commonPath!\!$ref[%%I]!") do (
endlocal & set "commonPath=%%~J" & set "flag=#" & set /A "#cmn=%%I+1"
)
)
)
if not defined flag (
setlocal EnableDelayedExpansion & for %%J in ("!relativePath!\!$abs[%%I]!") do (
endlocal & set "relativePath=%%~J"
)
)
)
rem // Complete the relative path by preceding enough level-up (`..`) items:
for /L %%I in (%#cmn%,1,%#ref%) do (
setlocal EnableDelayedExpansion & for %%J in (".\.!relativePath!") do (
endlocal & set "relativePath=%%~J"
)
)
set "relativePath=%relativePath:*\=%" & if "%commonPath:~-1%"==":" set "commonPath=%commonPath%\"
set "commonPath=%commonPath:~2%" & if not defined commonPath set "relativePath=%absolutePath%"
rem // Eventually return results:
set "referencePath"
set "absolutePath"
set "commonPath" 2> nul || echo commonPath=
set "relativePath"
endlocal
exit /B
Note that this approach does not support UNC paths.