Post

PowerShell - Canonical Paths and Case-Sensitivity


PowerShell is not case-sensitive.

Windows is not case-sensitive.

Generally, these statements do not matter when you’re working with PowerShell and Windows. However, as .Net Core and PowerShell Core move towards supporting *nix platforms, this has the potential of causing problems.

Most PowerShell cmdlets, will ingest the path parameter as provided. For example, Set-Location C:\wINDows\sYsTEm32 works just as well as Set-Location C:\Widows\System32. This behavior can spell trouble if incorrect path information is persisted with your code and roams between Windows and *nix systems.

The following are two concerete example of this behavior causing problems that I have seen:

  1. A StackOverflow user recently asked if there was an easier way to override cd or Set-Location to properly resolve a path to correct casing that’s on the disk. svn info was throwing an error when running against C:\svn\MYDIR as opposed to C:\svn\MyDir.

  2. Topics in PowerShell-Docs repo interlink to other topics. Some of these links are all lower-cased. However, most of the markdown files are named using title or camel case. For example, link to Functions about topic is done using about_functions.md. However, the file being referenced is named about_Functions.md. On Windows, the link is perfectly valid. However, once pushed to GitHub or to a linux system, the link is invalid due to case-sensitivity.

Possible Solution

This is not a new problem, and there are a few solutions and workarounds available on the internet. The general approach is to take a path and query the file system APIs to get the canonical path as it is stored on the disk. The canonical path can then be used for whatever operations and comparisons that need to be done.

The following is a one possible solution. It examines a path, and recursively determines the actual casing of the path via System.IO.DirectoryInfo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Function Get-CanonicalPath
{
    param($currentPath)

    $pathInfo = [System.IO.DirectoryInfo]$currentPath
    $parent = $pathInfo.Parent

    # if parent is null, we're at the end of the path, e.g. C:\ portion in C:\winDows\SySTEM32
    if($null -eq $parent)
    {
        return $pathInfo.Name
    }

    # recursively get the canonical, properly cased, path of parent of current path
    $ParentCanonicalPath = Get-CanonicalPath $parent
    
    # If the current path is a directory, get the proper path using .GetDirectories()
    # else get it using .GetFiles()    
    $LeafCanonicalPath = if($currentPath -is [System.IO.DirectoryInfo])
                            {
                                $parent.GetDirectories($pathInfo.Name).Name
                            }
                            else
                            {
                                $parent.GetFiles($pathInfo.Name).Name
                            }

    # combine the parent and leaf paths and return.
    return Join-Path $ParentCanonicalPath $LeafCanonicalPath
}

This works in most cases, but can definitely use some refinement. Such checking to make sure that the path exists and that it’s not a UNC.

Plug it into your $PROFILE and make it part of your workflow when working against cross-platform codebase.