I have two paths:
fred\\frog
and
..\\frag
I can join them together in PowerShell like this:
Any non-PowerShell path manipulation functions (such as those in System.IO.Path) will not be reliable from PowerShell because PowerShell's provider model allows PowerShell's current path to differ from what Windows thinks the process' working directory is.
Also, as you may have already discovered, PowerShell's Resolve-Path and Convert-Path cmdlets are useful for converting relative paths (those containing '..'s) to drive-qualified absolute paths but they fail if the path referenced does not exist.
The following very simple cmdlet should work for non-existant paths. It will convert 'fred\frog\..\frag' to 'd:\fred\frag' even if a 'fred' or 'frag' file or folder cannot be found (and the current PowerShell drive is 'd:').
function Get-AbsolutePath {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string[]]
$Path
)
process {
$Path | ForEach-Object {
$PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_)
}
}
}
You could also use Path.GetFullPath, although (as with Dan R's answer) this will give you the entire path. Usage would be as follows:
[IO.Path]::GetFullPath( "fred\frog\..\frag" )
or more interestingly
[IO.Path]::GetFullPath( (join-path "fred\frog" "..\frag") )
both of which yield the following (assuming your current directory is D:\):
D:\fred\frag
Note that this method does not attempt to determine whether fred or frag actually exist.
You can use a combination of pwd
, Join-Path
and [System.IO.Path]::GetFullPath
to get a fully qualified expanded path.
Since cd
(Set-Location
) doesn't change the process current working directory, simply passing a relative file name to a .NET API that doesn't understand PowerShell context, can have unintended side-effects, such as resolving to a path based off the initial working directory (not your current location).
What you do is you first qualify your path:
Join-Path (Join-Path (pwd) fred\frog) '..\frag'
This yields (given my current location):
C:\WINDOWS\system32\fred\frog\..\frag
With an absolute base, it is safe to call the .NET API GetFullPath
:
[System.IO.Path]::GetFullPath((Join-Path (Join-Path (pwd) fred\frog) '..\frag'))
Which gives you the fully qualified path and with the ..
removed:
C:\WINDOWS\system32\fred\frag
It's not complicated either, personally, I disdain the solutions that depend on external scripts for this, it's simple problem solved rather aptly by Join-Path
and pwd
(GetFullPath
is just to make it pretty). If you only want to keep only the relative part, you just add .Substring((pwd).Path.Trim('\').Length + 1)
and voila!
fred\frag
Thanks to @Dangph for pointing out the C:\
edge case.
This library is good: NDepend.Helpers.FileDirectoryPath.
EDIT: This is what I came up with:
[Reflection.Assembly]::LoadFrom("path\to\NDepend.Helpers.FileDirectoryPath.dll") | out-null
Function NormalizePath ($path)
{
if (-not $path.StartsWith('.\')) # FilePathRelative requires relative paths to begin with '.'
{
$path = ".\$path"
}
if ($path -eq '.\.') # FilePathRelative can't deal with this case
{
$result = '.'
}
else
{
$relPath = New-Object NDepend.Helpers.FileDirectoryPath.FilePathRelative($path)
$result = $relPath.Path
}
if ($result.StartsWith('.\')) # remove '.\'.
{
$result = $result.SubString(2)
}
$result
}
Call it like this:
> NormalizePath "fred\frog\..\frag"
fred\frag
Note that this snippet requires the path to the DLL. There is a trick you can use to find the folder containing the currently executing script, but in my case I had an environment variable I could use, so I just used that.
If you need to get rid of the .. portion, you can use a System.IO.DirectoryInfo object. Use 'fred\frog..\frag' in the constructor. The FullName property will give you the normalized directory name.
The only drawback is that it will give you the entire path (e.g. c:\test\fred\frag).
The expedient parts of the comments here combined such that they unify relative and absolute paths:
[System.IO.Directory]::SetCurrentDirectory($pwd)
[IO.Path]::GetFullPath($dapath)
Some samples:
$fps = '.', 'file.txt', '.\file.txt', '..\file.txt', 'c:\somewhere\file.txt'
$fps | % { [IO.Path]::GetFullPath($_) }
output:
C:\Users\thelonius\tests
C:\Users\thelonius\tests\file.txt
C:\Users\thelonius\tests\file.txt
C:\Users\thelonius\file.txt
c:\somewhere\file.txt