.netwindowspowershellfilesystemsmax-path

How can I get PowerShell's Get-ChildItem command to list files in a path longer than 260 characters?


When using PowerShell 5.1 and earlier in Windows and trying the list the contents of a folder using the Get-ChildItem command - where the folder path is longer than 260 characters - results in an "ItemNotFoundException" or the error "The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.".

When I attempt to bypass this limitation by prepending "\\?\", it simply doesn't return anything at all, and if I instead try to look up the directory with [System.IO.DirectoryInfo], I get the same "too long" error as before.

In Windows Server 2016 and Windows 10, I seem to be able to get around this by setting the DWORD "LongPathsEnabled" to 1 under the registry key "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem", but this doesn't work for systems in our environment running older versions of Windows or PowerShell.

I'd expect PowerShell/.NET to accept long paths when I prepend "\\?\", but it still insists that the path is too long, even though the below-linked Microsoft article suggests that this should work: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation


Solution

  • I posted this question purely because I figured out a good answer, that doesn't require loading native Win32 DLLs via P/Invoke, doesn't require setting any registry keys, doesn't require falling back to calling any native executables, and appears to work all the way back to Windows Server 2012R2 (I don't have anything older to test on).

    The short answer:

    Run the following in your current PowerShell session or prepend it in your scripts (this may not work in future versions of PowerShell/NET, as it uses internal and undocumented APIs):

    [System.AppContext]::SetSwitch('Switch.System.IO.UseLegacyPathHandling', $false)
    [System.AppContext]::SetSwitch('Switch.System.IO.BlockLongPaths', $false)
    [System.Type]::GetType('System.AppContextSwitches').GetField('_useLegacyPathHandling', [System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::NonPublic).SetValue($null, 0)
    [System.Type]::GetType('System.AppContextSwitches').GetField('_blockLongPaths', [System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::NonPublic).SetValue($null, 0)
    

    The explanation:

    After poking around at a decompiled view of "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll" in ILSpy for a couple of hours, I determined that .NET APIs like System.IO.DirectoryInfo call the internal static method System.IO.Path.NormalizePath, which performs initial validation of the file path you supply.

    This then checks an internal boolean System.AppContextSwitches.UseLegacyPathHandling, and if this is set to true, it will simply refuse to accept paths longer than 260 characters, even if you use the documented "\\?\" prefix!

    The hard bit is then disabling "UseLegacyPathHandling". Normally, you'd add a line like <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false" /> to your application's "app.config" (to be honest, this probably also works for PowerShell), but if you want to be able to do this at runtime without modifying the system you're on, it turns out to be a pain.

    Ideally, simply running [System.AppContext]::SetSwitch('Switch.System.IO.UseLegacyPathHandling', $false) should work, except that .NET tries to be clever and cache the values of these switches, such that if they've ever been accessed already, they're essentially stuck at whatever value they were set to when they were first accessed.

    This is where it gets ugly: In the snippet above, I reset the cached values back to 0, which the internal System.AppContextSwitches.GetCachedSwitchValue method (technically actually GetCachedSwitchValueInternal) uses to signify that the value of the switch hasn't been cached, and should be looked up via the public method AppContext.TryGetSwitch.

    Only then - in my experience - does it actually honour the values you set via System.AppContext.SetSwitch (the syntax is [System.AppContext]::SetSwitch in PowerShell).

    Enjoy!