1
votes

How can I convert an absolute path to a relative path in batch? I have an absolute path to a directory A and a reference directory B, and I need the path to A relative to B. As example, the following batch script should print ..\other\somedir\.

setlocal enabledelayedexpansion
set referencePath=C:\Users\xyz\project\
set absolutePath=C:\Users\xyz\other\somedir\
set relativePath=...
echo %relativePath%

I tried relativePath=!absolutePath:%referencePath%=!, but this yields the absolute path C:\Users\xyz\other\somedir\.

I need something similar to the python function os.path.relpath:

>>> os.path.relpath("C:\\Users\\xyz\\other\\somedir", "C:\\Users\\xyz\\project\\")
"..\\other\\somedir"

I need this because I have a batch file with command line arguments similar to the above file names. This batch file creates another batch file startup.bat which sets some environment variables and starts an application. The startup.bat may be called over network, so I have to use relative paths. With absolute paths, the environment variables would point to the files on the wrong machine.

6

6 Answers

2
votes

Here is a quick'n'dirty hack:

@echo off
setlocal enabledelayedexpansion
set referencePath=C:\Users\xyz\project\
set absolutePath=C:\Users\xyz\other\somedir\
set relativePath=
:LOOP
for /F "tokens=1 delims=\" %%a in ("%referencePath%") do (set ref=%%a)
for /F "tokens=1 delims=\" %%a in ("%absolutePath%") do (set rel=%%a)
if /i !ref!==!rel! (
    set referencePath=!referencePath:%ref%\=!
    set absolutePath=!absolutePath:%rel%\=!
    goto LOOP
)
:RELLOOP
for /F "tokens=1 delims=\" %%a in ("%absolutePath%") do (
    set absolutePath=!absolutePath:%%a\=!
    set relativePath=!relativePath!..\
)
if not "%absolutePath%"=="" goto RELLOOP
set complRelPath=%relativePath%%referencePath%
echo !complRelPath!

This won't give you propper output if the folders are on different drives so you'll have to handle this special case yourself.

EDIT (comment): Well, this can't be that hard that you couldn't figure it out yourself. If / and \ are mixed (which is a bad idea - we are on Windows! Windows means \ in paths, UNIX etc. means / in paths) you should replace / by :

SET referencePath=%referencePath:/=\%
SET absolutePath=%absolutePath:/=\%

If the paths are equal, you have nothing to do so:

IF %referencePath%==%absolutePath% (
    SET complRelPath=.\
    GOTO WHATEVER
)
2
votes
@ECHO OFF
setlocal enabledelayedexpansion
set referencePath=C:\Users\xyz\project\
set absolutePath=C:\Users\xyz\other\somedir\
FOR %%a IN ("%absolutepath%") DO FOR %%r IN ("%referencepath%.") DO (
 SET "abspath=%%~pa"
 SET "relativepath=!abspath:%%~pr=..\!"
)
echo %relativePath%

GOTO :EOF

It would be of assistance if you were to tell us what your desired output is. Telling us what the output of your current code is, and that implicitly that's not what you expect, and then how to obtain something using some other platform is not particularly helpful.

The problem is that you are attempting to replace a string containing a colon within a string contining a colon. cmd` gets confused as it doesn't know which colon of the three is which.

This solution is resticted, since it assumes that the part of the path to be removed is exactly the parent directory of referencepath. In the absence of more information, it's as far as I'm prepared to guess...

1
votes

…and an example which leverages PowerShell:

@Echo Off
Set "referencePath=C:\Users\xyz\project"
Set "absolutePath=C:\Users\xyz\other\somedir"
Set "relativePath="
Set "_="
If /I Not "%CD%"=="%referencePath%" (Set "_=T"
    PushD "%referencePath%" 2>Nul || Exit /B)
For /F "Delims=" %%A In ('
    PowerShell -C "Resolve-Path -LiteralPath '%absolutePath%' -Relative"
') Do Set "relativePath=%%A"
If Defined _ PopD
If Defined relativePath Echo %relativePath%
Pause

This obviously only works with actual existing paths

1
votes

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.

0
votes

Though I understand exactly what you want to do, I do not like this method.

Instead, why not use your environment path, where we will search for the relevant file in path.

@echo off
for %%g in ("bin\startup-batch.cmd") do @set "pathTobat=%%~$PATH:g"
echo "%pathTobat%"
0
votes

The solution of @MichaelS is good, but has problems:

  • If subdirectories have same or similar names as their parents, it doesn't work.
  • setlocal without endlocal
  • Same-path condition not in script
  • No command-line-interface (CLI)

Improved version follows: (save as setComRelPath.bat) (you need strlen.cmd from https://ss64.com/nt/syntax-strlen.html)

@echo off

rem Usage: call setCompRelPath.bat C:\A\B C:\i\am\here
rem        echo "%compRelPath%"
rem 
rem Alternernatively,
rem        set position_target=D:\44_Projekte\imagine\
rem        set position_now=%CD%\
rem        call setCompRelPath.bat
rem        echo "%compRelPath%"

if not [%1]==[] set position_target=%1
if not [%2]==[] set position_now=%2

set __absolutePath=%position_now%
set __referencePath=%position_target%

if %__referencePath%==%__absolutePath% (
    set complRelPath=.\
    exit /b
)

rem echo __referencePath=%__referencePath%
rem echo __absolutePath=%__absolutePath%
set relativePath=
:LOOP
for /F "tokens=1 delims=\" %%a in ("%__referencePath%") do (set ref=%%a)
for /F "tokens=1 delims=\" %%a in ("%__absolutePath%") do (set rel=%%a)
if /i not %ref%==%rel% goto RELLOOP

call strlen.cmd "x%ref%" _strlen
call set __referencePath=%%__referencePath:~%_strlen%%%
call set __absolutePath=%%__absolutePath:~%_strlen%%%
rem echo abs^> %__absolutePath%
goto LOOP

:RELLOOP
for /F "tokens=1 delims=\" %%a in ("%__absolutePath%") do call :SUB_relpath %%a
if not "%__absolutePath%"=="" goto RELLOOP
goto FIN

:SUB_relpath
set ARG=%1
call strlen.cmd "x%ARG%" _strlen
rem echo abs: %__absolutePath% // ARG=%ARG% // rel: %relativePath%
call set __absolutePath=%%__absolutePath:~%_strlen%%%
set relativePath=%relativePath%..\
exit /b

:FIN
set compRelPath=%relativePath%%__referencePath%

If you want to see it functioning, uncomment the echo lines