74

I'm trying to process a list of files that may or may not be up to date and may or may not yet exist. In doing so, I need to resolve the full path of an item, even though the item may be specified with relative paths. However, Resolve-Path prints an error when used with a non-existant file.

For example, What's the simplest, cleanest way to resolve ".\newdir\newfile.txt" to "C:\Current\Working\Directory\newdir\newfile.txt" in Powershell?

Note that System.IO.Path's static method use with the process's working directory - which isn't the powershell current location.

Sabuncu
  • 5,095
  • 5
  • 55
  • 89
Eamon Nerbonne
  • 47,023
  • 20
  • 101
  • 166

13 Answers13

119

You want:

c:\path\exists\> $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(".\nonexist\foo.txt")

returns:

c:\path\exists\nonexists\foo.txt

This has the advantage of working with PSPaths, not native filesystem paths. A PSPath may not map 1-1 to a filesystem path, for example if you mount a psdrive with a multi-letter drive name.

What's a pspath?

ps c:\> new-psdrive temp filesystem c:\temp
...
ps c:\> cd temp:
ps temp:\> 

temp:\ is a drive-qualified pspath that maps to a win32 (native) path of c:\temp.

-Oisin

x0n
  • 51,312
  • 7
  • 89
  • 111
  • 1
    Nice. I guess I saw some similar function when looking through Reflector to ResolvePathCommand. Going through code is good for inspiration ;) – stej Jun 14 '10 at 21:49
  • 1
    Many thanks! That command may be a little longwinded, but it's *exactly* what I was looking for. – Eamon Nerbonne Jun 15 '10 at 07:45
  • 17
    OMG, how is it that the `Resolve-Path` cmdlet doesn't have a flag to not test that path as well. – John Leidegren Nov 21 '14 at 20:25
  • @JohnLeidegren Probably because Resolve-Path resolves paths, and we're looking for an unresolved path. – x0n Nov 12 '15 at 17:40
  • 3
    @x0n According to the [Approved Verbs](https://msdn.microsoft.com/en-us/library/ms714428(v=vs.85).aspx) page, all Resolve means is, "Maps a shorthand representation of a resource to a more complete representation." It says nothing about checking for the resource's existence. – jpmc26 Jun 29 '16 at 22:01
  • 2
    ps>[io.path]::GetFullPath("./somedir") returns "D:\bla-bla-folder\somedir" – Vladislav Jun 02 '17 at 14:10
  • 1
    @Vladislav It's not that simple - that only works for win32 paths, not powershell paths (they are different things) http://www.nivot.org/post/2010/03/05/PowerShellThePatchworkOfPathsPSPathsAndProviderPaths – x0n Jul 18 '17 at 19:35
  • @x0n, what do mean in "powershell paths"? "..win32" - i tested on my 64bit windows 7 – Vladislav Jul 19 '17 at 07:55
  • @Vladislav powershell drives are not the same as windows drives. The have the same name(s) by default, but they don't have to. Read my blog post: http://www.nivot.org/post/2010/03/05/PowerShellThePatchworkOfPathsPSPathsAndProviderPaths – x0n Jul 20 '17 at 18:14
  • 1
    Finally, I have a proper implementation that works with relative normalized paths and absolute on paths that don't exist and on drives that don't exist, and on any powershell provider. https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-proper_drive_provider_path_from_pspath – Luiz Felipe Oct 26 '22 at 17:06
48

When Resolve-Path fails due to the file not existing, the fully resolved path is accessible from the thrown error object.

You can use a function like the following to fix Resolve-Path and make it work like you expect.

function Force-Resolve-Path {
    <#
    .SYNOPSIS
        Calls Resolve-Path but works for files that don't exist.
    .REMARKS
        From http://devhawk.net/blog/2010/1/22/fixing-powershells-busted-resolve-path-cmdlet
    #>
    param (
        [string] $FileName
    )

    $FileName = Resolve-Path $FileName -ErrorAction SilentlyContinue `
                                       -ErrorVariable _frperror
    if (-not($FileName)) {
        $FileName = $_frperror[0].TargetObject
    }

    return $FileName
}
information_interchange
  • 2,538
  • 6
  • 31
  • 49
joshuapoehls
  • 32,695
  • 11
  • 50
  • 61
23

I think you're on the right path. Just use [Environment]::CurrentDirectory to set .NET's notion of the process's current dir e.g.:

[Environment]::CurrentDirectory = $pwd
[IO.Path]::GetFullPath(".\xyz")
Keith Hill
  • 194,368
  • 42
  • 353
  • 369
  • 2
    Right, but that has side-effects and a bit of nasty interop I'd like to avoid - I'd kind of hoped that this 'd be something powershell (which is a shell scripting language, after all) could handle natively... Anyhow, +1 for an actual option! – Eamon Nerbonne Jun 14 '10 at 19:41
  • 1
    It also has the problem whereby the $pwd may not actually be a real path on the filesystem either; for example if you mount a psdrive with a multi-letter drive name. – x0n Jun 14 '10 at 21:13
  • 3
    Good point. Of course, you can grab the .NET current dir first, then set it to a filesystem provider path (not use $pwd) and then reset current dir back to its original value. – Keith Hill Jun 15 '10 at 17:22
  • If you are going to change the process CWD, you might as well use the internal infrastructure of the powershell, it has a nice stack of paths that doesn't change the process CWD. https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-normalize_relative_path – Luiz Felipe Oct 26 '22 at 17:15
11
Join-Path (Resolve-Path .) newdir\newfile.txt
Max Toro
  • 28,282
  • 11
  • 76
  • 114
  • You can also find more examples in the [documentation](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/join-path?view=powershell-5.1) – Alx Nov 23 '17 at 15:48
  • 5
    Only works if assuming 'newdir' is relative. `Join-Path (Resolve-Path .) F:\thisuer\specified\absolutepath\inparameter` – user2864740 May 23 '19 at 22:14
7

This has the advantage of not having to set the CLR Environment's current directory:

[IO.Path]::Combine($pwd,"non\existing\path")

NOTE

This is not functionally equivalent to x0n's answer. System.IO.Path.Combine only combines string path segments. Its main utility is keeping the developer from having to worry about slashes. GetUnresolvedProviderPathFromPSPath will traverse the input path relative to the present working directory, according to the .'s and ..'s.

Community
  • 1
  • 1
Ronnie Overby
  • 45,287
  • 73
  • 267
  • 346
  • Nice n short! Is this exactly equivalent to the `GetUnresolvedProviderPathFromPSPath`-based solution, or are there subtle differences? – Eamon Nerbonne May 20 '14 at 13:37
  • 2
    Not really a viable solution. It assumes the psdrive has the same name as the provider backing store's. You can have a drive in powershell called "stuff:\" that is mapped to "c:\" for example, this solution would pass the psdrive name instead of the win32 name which would fail – x0n Nov 12 '15 at 17:44
  • @x0n thanks for bringing up that interesting caveat. – Ronnie Overby Nov 14 '15 at 12:12
  • There are very subtle differences https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-combine_pwd_then_getfullpath and not so subtle https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-only_unresolved_provider_path_from_pspath – Luiz Felipe Oct 26 '22 at 17:13
2

I've found that the following works well enough.

$workingDirectory = Convert-Path (Resolve-Path -path ".")
$newFile = "newDir\newFile.txt"
Do-Something-With "$workingDirectory\$newFile"

Convert-Path can be used to get the path as a string, although this is not always the case. See this entry on COnvert-Path for more details.

savagent
  • 2,114
  • 1
  • 13
  • 10
0
function Get-FullName()
{
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $True)] [object[]] $Path
    )
    Begin{
        $Path = @($Path);
    }
    Process{
        foreach($p in $Path)
        {
            if($p -eq $null -or $p -match '^\s*$'){$p = [IO.Path]::GetFullPath(".");}
            elseif($p -is [System.IO.FileInfo]){$p = $p.FullName;}
            else{$p = [IO.Path]::GetFullPath($p);}
            $p;
        }
    }
}
user133580
  • 1,479
  • 1
  • 19
  • 28
Just a learner
  • 26,690
  • 50
  • 155
  • 234
  • This only works if using the psdrive names are the same as the backing drive name. Also only works for win32 file paths and will fail on every other provider. – x0n Jul 18 '17 at 17:46
0

I ended up with this code in my case. I needed to create a file later in the the script, so this code presumes you have write access to the target folder.

$File = ".\newdir\newfile.txt"
If (Test-Path $File) {
    $Resolved = (Resolve-Path $File).Path
} else {
    New-Item $File -ItemType File | Out-Null
    $Resolved = (Resolve-Path $File).Path
    Remove-Item $File
}

I also enclosed New-Item in try..catch block, but that goes out of this question.

Igor
  • 1,349
  • 12
  • 25
  • This is bad, it modifies the Filesystem and doesn't work with providers like the registry. Look here for a proper implementation that I'm going to submit to be a parameter for "Resolve-Path" https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-proper_drive_provider_path_from_pspath – Luiz Felipe Oct 26 '22 at 17:10
0

I had a similar issue where I needed to find the folder 3 levels up from a folder that does not exist yet to determine the name for a new folder I wanted to create... It's complicated. Anyway, this is what I ended up doing:

($path -split "\\" | select -SkipLast 3) -join "\\"
4thex
  • 1,094
  • 1
  • 9
  • 21
  • That's not relevant, but its simple actually. just do `Split-Path -Parent (Split-Path -Parent (Split-Path -Parent 'a/b/c/d'))` – Luiz Felipe Aug 09 '22 at 20:00
-2

There is an accepted answer here, but it is quite lengthy and there is a simpler alternative available.

In any recent version of Powershell, you can use Test-Path -IsValid -Path 'C:\Probably Fake\Path.txt'

This simply verifies that there are no illegal characters in the path and that the path could be used to store a file. If the target doesn't exist, Test-Path won't care in this instance -- it's only being asked to test if the provided path is potentially valid.

Rain
  • 153
  • 5
  • 5
    This *tests* a path for validity; I want to resolve it into an absolute path (e.g. for normalization purposes, or to be able to store the path and reuse it in another context with a different environment). – Eamon Nerbonne Nov 14 '17 at 08:17
-2

Both most popular answers don't work correctly on paths on not existing drives.

function NormalizePath($filename)
{
    $filename += '\'
    $filename = $filename -replace '\\(\.?\\)+','\'
    while ($filename -match '\\([^\\.]|\.[^\\.]|\.\.[^\\])[^\\]*\\\.\.\\') {
        $filename = $filename -replace '\\([^\\.]|\.[^\\.]|\.\.[^\\])[^\\]*\\\.\.\\','\'
    }
    return $filename.TrimEnd('\')
}
Ilyan
  • 175
  • 6
  • It would be helpful if you explained what this is doing. This is not something that's going to be obvious what's going on at first glance, worse so for someone not well-versed in regex. Also, when I pass the sample path in the question to this function, it just returns the same path. Passing `'C:\'` returns `'C:'`, which means something different. I get the correct result for `'C:\Dir1\..\File1'`, but `'Dir1\..\File1'` is returned unmodified and `'Dir1\Dir2\..\File1'` just becomes `'Dir1\File1'`, not an absolute path. Is this intended to be used before one of those answers' solutions? – Lance U. Matthews Apr 02 '20 at 20:10
  • 1
    Also: manipulating structured strings via regex is notoriously tricky. A solution like this might easily have nasty corner cases; and a it looks like @BACON already found a few. Even if you can *do* it correctly, it's going to be almost impossible to *verify* it's correct; a huge test set would be a good start. – Eamon Nerbonne Apr 03 '20 at 20:48
-3

You can just set the -errorAction to "SilentlyContinue" and use Resolve-Path

5 >  (Resolve-Path .\AllFilerData.xml -ea 0).Path
C:\Users\Andy.Schneider\Documents\WindowsPowerShell\Scripts\AllFilerData.xml

6 >  (Resolve-Path .\DoesNotExist -ea 0).Path

7 >
Andy Schneider
  • 8,516
  • 6
  • 36
  • 52
-5

Check if the file exists before resolving:

if(Test-Path .\newdir\newfile.txt) { (Resolve-Path .\newdir\newfile.txt).Path }
shane carvalho
  • 130
  • 1
  • 10