How to normalize a path in PowerShell?

PowershellPath

Powershell Problem Overview


I have two paths:

fred\frog

and

..\frag

I can join them together in PowerShell like this:

join-path 'fred\frog' '..\frag'

That gives me this:

fred\frog\..\frag

But I don't want that. I want a normalized path without the double dots, like this:

fred\frag

How can I get that?

Powershell Solutions


Solution 1 - Powershell

You can expand ..\frag to its full path with resolve-path:

PS > resolve-path ..\frag 

Try to normalize the path using the combine() method:

[io.path]::Combine("fred\frog",(resolve-path ..\frag).path)

Solution 2 - Powershell

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 now 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, with the .. correctly resolved:

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

UPDATE

Thanks to @Dangph for pointing out the C:\ edge case.

Solution 3 - Powershell

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.

Solution 4 - Powershell

The accepted answer was a great help however it doesn't properly 'normalize' an absolute path too. Find below my derivative work which normalizes both absolute and relative paths.

function Get-AbsolutePath ($Path)
{
    # System.IO.Path.Combine has two properties making it necesarry here:
    #   1) correctly deals with situations where $Path (the second term) is an absolute path
    #   2) correctly deals with situations where $Path (the second term) is relative
    # (join-path) commandlet does not have this first property
    $Path = [System.IO.Path]::Combine( ((pwd).Path), ($Path) );

    # this piece strips out any relative path modifiers like '..' and '.'
    $Path = [System.IO.Path]::GetFullPath($Path);

    return $Path;
}

Solution 5 - Powershell

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($_)
        }
    }
}

Solution 6 - Powershell

If the path includes a qualifier (drive letter) then x0n's answer to Powershell: resolve path that might not exist? will normalize the path. If the path doesn't include the qualifier, it will still be normalized but will return the fully qualified path relative to the current directory, which may not be what you want.

$p = 'X:\fred\frog\..\frag'
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
X:\fred\frag

$p = '\fred\frog\..\frag'
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
C:\fred\frag

$p = 'fred\frog\..\frag'
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
C:\Users\WileCau\fred\frag

Solution 7 - Powershell

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.

Solution 8 - Powershell

This gives the full path:

(gci 'fred\frog\..\frag').FullName

This gives the path relative to the current directory:

(gci 'fred\frog\..\frag').FullName.Replace((gl).Path + '\', '')

For some reason they only work if frag is a file, not a directory.

Solution 9 - Powershell

Create a function. This function will normalize a path that does not exists on your system as well as not add drives letters.

function RemoveDotsInPath {
  [cmdletbinding()]
  Param( [Parameter(Position=0,  Mandatory=$true)] [string] $PathString = '' )
	
  $newPath = $PathString -creplace '(?<grp>[^\n\\]+\\)+(?<-grp>\.\.\\)+(?(grp)(?!))', ''
  return $newPath
}

Ex:

$a = 'fooA\obj\BusinessLayer\..\..\bin\BusinessLayer\foo.txt'
RemoveDotsInPath $a
'fooA\bin\BusinessLayer\foo.txt'

Thanks goes out to Oliver Schadlich for help in the RegEx.

Solution 10 - Powershell

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).

Solution 11 - Powershell

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

Solution 12 - Powershell

Well, one way would be:

Join-Path 'fred\frog' '..\frag'.Replace('..', '')

Wait, maybe I misunderstand the question. In your example, is frag a subfolder of frog?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiondan-gphView Question on Stackoverflow
Solution 1 - PowershellShay LevyView Answer on Stackoverflow
Solution 2 - PowershellJohn LeidegrenView Answer on Stackoverflow
Solution 3 - PowershellCharlieView Answer on Stackoverflow
Solution 4 - PowershellSean HannaView Answer on Stackoverflow
Solution 5 - PowershellJason StangroomeView Answer on Stackoverflow
Solution 6 - PowershellWileCauView Answer on Stackoverflow
Solution 7 - Powershelldan-gphView Answer on Stackoverflow
Solution 8 - Powershelldan-gphView Answer on Stackoverflow
Solution 9 - PowershellM.HubersView Answer on Stackoverflow
Solution 10 - PowershellDan RView Answer on Stackoverflow
Solution 11 - PowershellTNTView Answer on Stackoverflow
Solution 12 - PowershellEBGreenView Answer on Stackoverflow