50

I'd like to convert a path to a relative path in a PowerShell script. How do I do this using PowerShell?

For example:

Path to convert: c:\documents\mynicefiles\afile.txt
Reference path:  c:\documents
Result:          mynicefiles\afile.txt

And

Path to convert: c:\documents\myproject1\afile.txt
Reference path:  c:\documents\myproject2
Result:          ..\myproject1\afile.txt
Dave Hillier
  • 18,105
  • 9
  • 43
  • 87

6 Answers6

77

I found something built in, Resolve-Path:

Resolve-Path -Relative

This returns the path relative to the current location. A simple usage:

$root = "C:\Users\Dave\"
$current = "C:\Users\Dave\Documents\"
$tmp = Get-Location
Set-Location $root
Resolve-Path -relative $current
Set-Location $tmp
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dave Hillier
  • 18,105
  • 9
  • 43
  • 87
  • 28
    Rather than using a temporary variable, you can also use Push-Location and Pop-Location to set the location and then revert back to its original value. Same basic solution but without the temporary variable. – John Bledsoe Apr 14 '14 at 17:44
  • 4
    This will fail if the root directory doesn't exist yet. – Scott Aug 08 '18 at 16:53
  • 7
    Clever, but I don't like the side effect of changing your working directory (even though you switch it back) – Ohad Schneider Oct 25 '18 at 13:12
  • 5
    To make sure current location will be restored even when `Resolve-Path` throws an exception (depending on `$ErrorActionPreference`), wrap code in try/catch: `try{ push-location $root; resolve-path -relative $current } finally{ pop-location }` – zett42 Oct 09 '20 at 13:39
18

Using the built-in System.IO.Path.GetRelativePath is simpler than the accepted answer:

[System.IO.Path]::GetRelativePath($relativeTo, $path)
Dave Hillier
  • 18,105
  • 9
  • 43
  • 87
John Hall
  • 535
  • 5
  • 15
  • 1
    I'd disagree with the "safer" though it is simpler. Note: this only works in .Net 5.0 RC and .Net core – Dave Hillier Oct 27 '20 at 14:41
  • In Windows 10, it requires a separate download and install of Powershell Core in order to support this .Net Core method. The accepted answer works out of the box. – sw1337 Dec 29 '21 at 04:06
1

There is a good answer here, but it changes your current directory (it reverts back), but if you really need to isolate that process, code example below can help. It does the same thing, just in a new PowerShell instance.

function Get-RelativePath($path, $relativeTo) {
    $powershell = (Get-Process -PID $PID | Get-Item)
    if ([string]::IsNullOrEmpty($powershell)) {
        $powershell = "powershell.exe"
    }

    & $powershell -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "& { Set-Location `"$relativeTo`"; Resolve-Path `"$path`" -Relative}"
}

It's really slow though, you should really use the other version unless you absolutely have to use this.

Medeni Baykal
  • 4,223
  • 1
  • 28
  • 36
1

Sometimes I have multiple "roots" from which I want to generate relative file paths. I have found Resolve-Path -Relative unusable in this kind of situation. Changing a global setting, current location, in order to generate a relative file path seems error-prone and (if you're writing parallel code) possibly not thread-safe.

The following should work in early or recent versions of Powershell and Powershell Core, doesn't change your current directory, even temporarily, and is OS-independent and thread-safe.

It doesn't address the second example from OP (inserting .. as required.)

function Get-RelativePath {
    param($path, $relativeTo)
    # strip trailing slash
    $relativeTo = Join-Path `
                      (Split-Path -Parent $relativeTo) `
                      (Split-Path -Leaf $relativeTo)
    $relPath = Split-Path -Leaf $path
    $path = Split-Path -Parent $path
    do {    
        $leaf = Split-Path -Leaf $path
        $relPath = Join-Path $leaf $relPath
        $path = Split-Path -Parent $path
    } until (($path -eq $relativeTo) -Or ($path.Length -eq 0))
    $relPath
}

An example:

PS> $configRoot = 'C:\Users\P799634t\code\RMP\v2\AWD'
PS> $queryPath = 'C:\Users\P799634t\code\RMP\v2\AWD\config_queries\LOAD_UNQ_QUEUE_QUERY2.sql'
PS> Write-Host (Get-RelativePath $queryPath $configRoot)
config_queries\LOAD_UNQ_QUEUE_QUERY2.sql

It behaves reasonably when one file path is not a sub-path of the other:

PS> $root = 'C:\Users\P799634t\code\RMP\v2\AWD'
PS> $notRelated = 'E:\path\to\origami'
PS> Write-Host (Get-RelativePath $notRelated $root)
E:\path\to\origami
Nic
  • 1,518
  • 12
  • 26
0

A quick and easy way would be :

$current -replace [regex]::Escape($root), '.'

Or if you want the relative path from your actual current location

$path -replace [regex]::Escape((pwd).Path), '.'

This assumes all your paths are valid.

Dave Sexton
  • 10,768
  • 3
  • 42
  • 56
  • 1
    This doesn't work if $root is not a root of $current (see example 2 of Question) – PMF Mar 29 '19 at 16:05
-4

Here is an alternative approach

$pathToConvert1 = "c:\documents\mynicefiles\afile.txt"
$referencePath1 = "c:\documents"
$result1 = $pathToConvert1.Substring($referencePath1.Length + 1)
#$result1:  mynicefiles\afile.txt


And

$pathToConvert2 = "c:\documents\myproject1\afile.txt"
#$referencePath2 = "c:\documents\myproject2"
$result2 = "..\myproject" + [regex]::Replace($pathToConvert2 , ".*\d+", '')
#$result2:          ..\myproject\afile.txt

Note: in the second case ref path wasn't used.

yantaq
  • 3,968
  • 2
  • 33
  • 34