Monday, 15 September 2014

Join-Path Fails When Local Drive Letter Doesn’t Exist

One of the dangers of being a .Net developer that also writes PowerShell is that you can make assumptions about a feature in PowerShell and how it might map on to some underlying .Net construct. Take the Join-Path cmdlet for example which concatenates two path segments using the OS specific path separator to give you a new, combined path segment. This sounds a awful lot like the .Net method Path.Combine():

PS C:\> Join-Path 'C:\Hello' 'World'

Just like Path.Combine() I can even combine paths that don’t exist yet (or perhaps ever will), such as in the example above:

PS C:\> dir C:\Hello
Get-ChildItem : Cannot find path 'C:\Hello' because it does not exist.

As if to prove a point I’m on a train and so not attached to a network as I write this and yet I can format UNC style paths with aplomb too:

PS C:\> Join-Path '\\Hello\World' 'Again'

So far so good. But where the similarity ends is with local drive mappings as I (and more recently my colleague) found out when testing our deployment script on our local machine. In our deployment script I created a common function using a PowerShell module that would set some key variables to identify the core installation and data folders, e.g.

function Define-Environment([string] $environment,
                            [bool] $isDevMachine)
  switch ($environment)
      $global:deploymentRoot = 'E:\Apps\MyApp'
      . . .
    . . .

  $global:logFolder = Join-Path $deploymentRoot 'Log'
  . . .

  if ($isDevMachine)
    $global:deploymentRoot = 'D:\Dev\Test'
    $global:logFolder = 'D:\Logs'
    . . .

To allow us to test the deployment on our own local machines as much as possible (for every environment - test, demo, staging, etc.) the scripts have a switch that overrides the target environment where necessary, i.e. the deployment folders and service account. That was originally done by appending a custom bit of code at the end of the Define-Environment function [1] to keep it separate from the main block of configuration code.

However, when it came to testing a deployment locally it failed complaining about some drive or other that didn’t exist. My immediate thought was that something was not being correctly overridden because the paths wouldn’t be used until much later during the file copy phase, right? Wrong, it appears that Join-Path never validates a path per-se, but does validate a drive letter, if it is specified.

For example, I do have a C: drive, but no folder called “Hello”, and no Z: drive either:

PS C:\> Join-Path 'C:\Hello' 'World'
PS C:\> Join-Path 'Z:\Hello' 'World'
Join-Path : Cannot find drive. A drive with the name 'Z' does not exist.

If you really do want to validate the entire path then you can use the -Resolve switch:

PS C:\> Join-Path 'C:\Hello' 'World' -Resolve
Join-Path : Cannot find path 'C:\Hello\World' because it does not exist.

This behaviour seems strangely inconsistent to me and I’m certainly not the only one as there is a bug report on the Microsoft Connect web site about the behaviour. Working in The Enterprise means we are stuck with PowerShell V2 on our servers and so I haven’t checked what the state of this behaviour is on later versions PowerShell; hopefully they’ll add a -NoResolve switch or something similar to allow us to disable the check for all local paths.

Oh, and as far as our Define-Environment function goes, obviously I just refactored it so that the overriding happened before any Join-Path calls were made. It added a little extra complexity to the code in a few places but nothing too onerous.

[1] I can’t remember now why it was appended, but the folder structure was slightly different for some reason and so it wasn’t quite as simple as just overriding the root folder. Perhaps there was another folder we didn’t override, or something like that.


  1. I don't think it's as inconsistent as it might first appear.
    It's quite a normal thing to do to construct paths that do not yet exist *in order to create them*.
    Referring to a drive letter that does not exist will almost always be an error if used, though.
    Obviously there are exceptions to that, as you've found - and I agree that the validation should be opt-in (or at least opt-out!) - but I think the two types of validation are different things,

    1. @PhilNash Sorry, but what are the two types of validation to which you are referring? When I create a path either I wanted it (all) validated or not. If I want bits of it validated surely I should use Split-Path and Test-Path and do it manually?

      In other words, why is an invalid UNC share name tolerated but not a local drive mapping when they are both absolute path roots?