Dave Hillier
Dave Hillier

Reputation: 19063

How to convert absolute path to relative path in PowerShell?

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

Upvotes: 53

Views: 38138

Answers (7)

zett42
zett42

Reputation: 27756

Since Powershell 7.4.0 you can use new Resolve-Path parameter -RelativeBasePath to do this in one line. The Set-Location workaround has finally become obsolete:

$pathToConvert = 'c:\documents\mynicefiles\afile.txt'
$referencePath = 'c:\documents'

Resolve-Path -Relative -Path $pathToConvert -RelativeBasePath $referencePath

Output:

.\mynicefiles\afile.txt

NOTE:

The arguments for -Path and -RelativeBasePath must both be existing paths! There is another restriction, which appears to be a bug in the current implementation though - the arguments must be filesystem paths.

The advantage of the .NET method System.IO.Path.GetRelativePath is that it works with non-existing paths as well.

Upvotes: 4

Kim
Kim

Reputation: 1686

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

Upvotes: 1

John Hall
John Hall

Reputation: 531

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

[System.IO.Path]::GetRelativePath($relativeTo, $path)

Upvotes: 18

Medeni Baykal
Medeni Baykal

Reputation: 4333

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.

Upvotes: 1

Dave Sexton
Dave Sexton

Reputation: 11188

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.

Upvotes: 0

yantaq
yantaq

Reputation: 4038

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.

Upvotes: -4

Dave Hillier
Dave Hillier

Reputation: 19063

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

Upvotes: 82

Related Questions