【问题标题】:Batch get relative path from absolute path从绝对路径批量获取相对路径
【发布时间】:2023-03-22 17:04:02
【问题描述】:

如何将绝对路径批量转换为相对路径?我有一个目录A 的绝对路径和一个参考目录B,我需要A 相对于B 的路径。例如,下面的批处理脚本应该打印..\other\somedir\。

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

我试过relativePath=!absolutePath:%referencePath%=!,但这会产生绝对路径C:\Users\xyz\other\somedir\。

我需要类似于python函数os.path.relpath的东西:

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

我需要这个,因为我有一个带有类似于上述文件名的命令行参数的批处理文件。此批处理文件创建另一个批处理文件startup.bat,它设置一些环境变量并启动应用程序。 startup.bat 可以通过网络调用,所以我必须使用相对路径。使用绝对路径,环境变量会指向错误机器上的文件。

【问题讨论】:

    标签: windows batch-file cmd path


    【解决方案1】:

    好吧,通常我强烈建议不要对文件或目录路径进行字符串操作,因为它很容易出错。但是对于像这样不依赖于现有路径的任务,似乎没有办法。

    无论如何,为了以可靠的方式这样做,必须考虑以下问题:

    • Windows 使用不区分大小写的路径,因此也以这种方式进行所有路径比较!
    • 避免子字符串替换,因为=-signs 和其他一些字符很麻烦!
    • 确保正确解析提供的路径,使用for-loop 元变量的~f-修饰符!这确保了输入路径确实是绝对的,它们不包含双重分隔符(\\),并且没有带有. 和.. 的序列(如abc\..\.\def,相当于def),这使得比较变得困难。
    • 注意路径可能以丑陋的方式提供,例如尾随 \ 或 \.、错误的引号(如 abc\"def ghi"\jkl)或使用错误的路径分隔符(/ 而不是 \,这是 Windows 标准)。

    好的,让我们转向我编写的脚本,用于推导公共路径和两个绝对路径之间的相对路径,关于所有上述项目(参见rem cmets):

    @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
    

    请注意,此方法不支持 UNC 路径。

    【讨论】:

      【解决方案2】:

      @MichaelS 的解决方案不错,但是有问题:

      • 如果子目录的名称与其父目录相同或相似,则不起作用。
      • setlocal 不带 endlocal
      • 脚本中没有相同路径条件
      • 没有命令行界面 (CLI)

      改进版如下:(另存为setComRelPath.bat)(你需要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%
      

      如果您想查看它的功能,请取消注释 echo 行

      【讨论】:

        【解决方案3】:

        这是一个快速的'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!
        

        如果文件夹位于不同的驱动器上,这不会为您提供正确的输出,因此您必须自己处理这种特殊情况。

        编辑(评论):嗯,这不可能那么难,你自己想不出来。如果 / 和 \ 混合使用(这是一个坏主意 - 我们在 Windows 上!Windows 意味着路径中的 \,UNIX 等意味着路径中的 /)你应该将 / 替换为:

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

        如果路径相等,则无事可做:

        IF %referencePath%==%absolutePath% (
            SET complRelPath=.\
            GOTO WHATEVER
        )
        

        【讨论】:

        • 我不得不切换referencePath和absolutePath,但它工作。谢谢你:)
        • 如果两条路径相同,您的示例将创建一个无限循环,如果斜杠和反斜杠混合使用,则会产生奇怪的输出。我可以编辑您的解决方案并添加我的修复吗?还是为此创建一个新答案更好?
        • 我认为这是一个误解:我确实自己创建了一个解决方案,并询问我是否应该发布一个新答案或用我的解决方案编辑你的答案。
        • 即使在您编辑之后也有一些错误:如果目录名称在其中一个路径中重复,则输出错误,例如,xyz 在 `C:\Users\xyz\ 中重复项目\xyz`。此外,如果路径是一些但一个路径有尾部反斜杠而另一个没有反斜杠,则批处理脚本将永远运行。
        • @pdchill,Windows 中不允许使用反斜杠作为字符。它们是分隔符,这意味着您不应该以一个结束路径,因为它没有被用作分隔符!另请注意,还有其他三个答案,因此不要只关注其中一个,而应考虑测试其他答案。这些响应者在竭尽全力帮助您之后,同样值得您考虑。
        【解决方案4】:

        ……还有一个利用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
        

        这显然只适用于实际存在的路径

        【讨论】:

          【解决方案5】:

          虽然我完全理解你想要做什么,但我不喜欢这种方法。

          相反,为什么不使用你的环境路径,我们将在路径中搜索相关文件。

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

          【讨论】:

            【解决方案6】:
            @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
            

            如果您能告诉我们您想要的输出是什么,将会有所帮助。告诉我们您当前代码的输出是什么,并且暗示这不是您所期望的,然后如何使用其他平台获得某些东西并不是特别有用。

            问题是您试图在一个连续冒号的字符串中替换一个包含冒号的字符串。 cmd` 感到困惑,因为它不知道三个冒号中的哪个冒号。

            此解决方案受到限制,因为它假定要删除的路径部分正是referencepath 的父目录。在没有更多信息的情况下,据我准备猜测......

            【讨论】:

            • 我的问题开头说“例如,以下批处理脚本应该打印 ..\other\somedir\。”所以想要的输出是`..\other\somedir`。
            • 公平的。你会希望我用我的眼镜,而不是只戴,嗯?
            • 是否可以取消删除路径正好是父目录的限制?我的一些用例有更深的引用路径,比如C:\Users\xyz\project\data\ ,所以需要去掉project\data\ 。
            猜你喜欢
            • 1970-01-01
            • 2022-06-10
            • 2011-08-11
            • 2010-09-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-02-07
            相关资源
            最近更新 更多