Skip to content

Passing a provider-name-prefixed path to Set-Location makes $PWD retain the prefix #10522

Closed

Description

Perhaps related: #9293

Steps to reproduce

Run the following on Windows (works analogously on Unix):

Set-Location Microsoft.PowerShell.Core\FileSystem::C:\Windows
"$PWD" | Should -Be C:\Windows

Note: A real-life scenario in which this could occur is if you pipe the output from a New-Item -Type Directory call to Set-Location, e.g.:

New-Item -Type Directory $HOME -Force | Set-Location

Expected behavior

The test should pass.

Actual behavior

The test fails:

Expected strings to be the same, but they were different.
Expected length: 10
Actual length:   48
Strings differ at index 0.
Expected: 'C:\Windows'
But was:  'Microsoft....'

That is, $PWD stringified (via .Path) to 'Microsoft.PowerShell.Core\FileSystem::C:\Windows', not the expected C:\Windows, as also reflected in the prompt string afterwards (via $executionContext.SessionState.Path.CurrentLocation).

While $PWD.ProviderPath does reflect the unprefixed (but always native) fileysystem path, relying on "$PWD" or $PWD.Path to do that too is very common.

Environment data

PowerShell Core 7.0.0-preview.3

Activity

  1. added
    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a
    on Sep 12, 2019
  2. iSazonov commented on Sep 13, 2019

    @iSazonov
    Collaborator

    The same for Get-Location:

     Set-Location Microsoft.PowerShell.Core\FileSystem::C:\Windows
    PS Microsoft.PowerShell.Core\FileSystem::C:\Windows> $pwd
    
    Path
    ----
    Microsoft.PowerShell.Core\FileSystem::C:\Windows
    
    PS Microsoft.PowerShell.Core\FileSystem::C:\Windows> Get-Location
    
    Path
    ----
    Microsoft.PowerShell.Core\FileSystem::C:\Windows

    They return [PathInfo] and it is PathInfo.ToString() behavior.
    Changing this would be a breaking change.
    Also it is not clear why path without prefix is expected behavior.

  3. mklement0 commented on Sep 13, 2019

    @mklement0
    ContributorAuthor

    Also it is not clear why path without prefix is expected behavior.

    To quote from the OP, emphasis added:

    While $PWD.ProviderPath does reflect the unprefixed (but always native) fileysystem path, relying on "$PWD" or $PWD.Path to do that [show the unprefixed full path] too is very common.

    I don't think anyone expects - nor should they - that the way they specify the location determines how that location is reported.

    E.g, if I do Set-Location ./tmp in order to change to, say, C:\foo\tmp, I expect both the prompt string and Get-Location to reflect C:\foo\tmp, not ./tmp.

    The same applies to passing a provider-prefixed path: it is just a way to identify the target item on input, which is unrelated to the presentation on output, which should be standardized.

    This is a breaking change in the same way that any bug fix is a breaking change: it corrects unexpected behavior.

  4. iSazonov commented on Sep 13, 2019

    @iSazonov
    Collaborator

    I don't think anyone expects - nor should they - that the way they specify the location determines how that location is reported.

    If I want to work with native filesystem paths I use native filesystem paths. If I want provider prefixes I expect that they will be preserved, including completions.

  5. mklement0 commented on Sep 13, 2019

    @mklement0
    ContributorAuthor

    Ilya (@iSazonov) :

    In this case you're not working with one path form of the other.

    Instead, you're using acceptable variations in the syntactic form of an argument to target a known item.

    To put it in concrete terms:

    Set-Location ./tmp  # from C:\foo
    Set-Location C:\foo\tmp
    Set-Location Microsoft.PowerShell.Core\FileSystem::C:\foo\tmp
    

    These are 3 different ways to target the same location - the variations in how you identified that location to Set-Location should have no impact whatsoever on the cmdlet's output / behavior (or on related cmdlets, such as Get-Location).

  6. dkaszews commented on Aug 24, 2022

    @dkaszews
    Contributor

    Posh-Git seems to work, I checked that updating the prompt to the following is sufficient:

    function prompt {
        $pathInfo = $ExecutionContext.SessionState.Path.CurrentLocation
        # Simplify provider path like `Microsoft.PowerShell.Core\FileSystem::C:\Windows`
        $currentPath = $pathInfo.Drive ? $pathInfo.Path : $pathInfo.ProviderPath
        "PS ${currentPath}$('>' * ($nestedPromptLevel + 1)) "
    }

    As mentioned in #17949 , updating the prompt is the best way to go as most people just care about the visual "bug" and it is not a breaking change.

  7. dkaszews commented on Aug 24, 2022

    @dkaszews
    Contributor

    Best to extend Resolve-Path with -SimplifyProvider and -SimplifyHome and use them in default prompt. That way there is no breaking change and you opt into the replacement in a script if you want.

  8. jhoneill commented on Aug 24, 2022

    @jhoneill

    This has come up again in 17949

    There is plainly a gap between what people EXPECT when working with a file system, and what has been implemented

    #17949 was basically

    • New-Item (mkdir) and Get-Item / Get-ChildItem add a PSPath property to System.IO.DirectoryInfo objects (it isn't added via type data so the .GetDirectories() method yields objects without it)
    • When object with a PSPath property is piped to set-location, that is used in preference to converting the object to string (ToString() gives the fullname property)
    • Therefore: if $d is a directory obtained by a cmdlet cd $d converts $d to a string and uses the full name as a path, but $d | cd uses the PSPath as a literal path.

    Dominik Kowalski (@dkaszews) Changing the prompt to hide that his happened is NOT a good idea
    Try this (on Windows)

    Get-Item $HOME | Set-Location
    Set-location \   
    

    On linux / or \ is considered to be a drive so it doesn't have the same problem but on windows \ or anything specified relative to \ fails.

    BUT you could use the prompt to fix it.

    if ((-not $ExecutionContext.SessionState.Path.CurrentLocation.Provider.Drive) -and 
        (     $ExecutionContext.SessionState.Path.CurrentLocation.Provider.Name -eq 'FileSystem')) {
              Set-Location $ExecutionContext.SessionState.Path.CurrentLocation.ProviderPath
    }
    

    There are cases where Set-Location cannot reliably and uniquely choose a drive (e.g. should changing into the temp directory, go to Temp: or the path within the native file file system. Or should HKEY_CLASSES_ROOT go to HKEY_LOCAL_MACHINE\SOFTWARE\Classes)

    Note that for the registry get-item hklm: | cd allows cd '..\HKEY_Classes_root' which isn't possible with cd HKLM: (but again breaks cd \)

    So I think

    1. If the user / script author takes the trouble to use a PSPath they want to the location to be set to a PS path. And in the case of something like Microsoft.PowerShell.Core\Registry::HKEY_CLASSES_ROOT there is no default drive to use,
    2. If the user passes a file system object via the pipeline selecting the PSPath is a surprise. Making Fullname an alias for path would solve this problem. Where changing an explicit PSPath is making a dangerous assumption, in the case of Set-Location assuming the user wants to use the PSPath and not the fullName for the filesystem is making a wrong one.
  9. iSazonov commented on Aug 24, 2022

    @iSazonov
    Collaborator

    Dominik Kowalski (@dkaszews) Changing the prompt to hide that his happened is NOT a good idea
    Try this (on Windows)

    It is not "hide". It is fundamentally follow PowerShell provider conceptions. We should use these fundamental conceptions right.

    Set-location \ is not a prove. It demonstrate a bug we should fix.

  10. jhoneill commented on Aug 24, 2022

    @jhoneill

    Dominik Kowalski (@dkaszews) Changing the prompt to hide that his happened is NOT a good idea
    Try this (on Windows)

    It is not "hide". It is fundamentally follow PowerShell provider conceptions. We should use these fundamental conceptions right.

    Set-location \ is not a prove. It demonstrate a bug we should fix.

    we might need to accept an amicable disagreement :-)

    Set-location \ does demonstrate that current location is set to different places, one impacted by a bug, one not
    And (on windows at least) a path starting \ means "from the root of the current drive" but PowerShell lets us set the current location to somewhere with no drive like Microsoft.PowerShell.Core\Registry::HKEY_CLASSES_ROOT or Microsoft.PowerShell.Core\FileSystem::\MachineName\c$\Windows` what does "" or "root of the drive" mean then ?

    Your comment above

    If I want to work with native filesystem paths I use native filesystem paths. If I want provider prefixes I expect that they will be preserved, including completions.

    Is what I think, although you might have changed your mind since then :-) Having the location using the provider and the prompt showing path as the provider sees it seems like masking a problem to me and there are places where it doesn't give the right answer - if I connect a PS Drive to a network share...

    new-psdrive S -PSProvider FileSystem -Root  "\\MachineName\c$" 
    S:
     get-item windows | cd
    

    I end up connected to Microsoft.PowerShell.Core\FileSystem::\\MachineName\c$\Windows
    Showing me the provider path \\MachineName\c$\Windows as a prompt instead of S: hasn't fixed the problem. Like the registry I can cd past the root of drive S: to a different share or even a different server but CD \ fails. (any fix for cd \ needs to cope with UNC paths).

    And if I do get-item temp:\ | cd it puts me in the
    Microsoft.PowerShell.Core\FileSystem::C:\Users\ME\AppData\Local\Temp>

    Showing me a prompt of C:\Users\ME\AppData\Local\Temp is both correct and unexpected - I expect to be on the temp: drive But if there is come kind of lookup which translates that to TEMP: (and the UNC to S:) then if I do get-item $env:temp | cd that will be wrong getting temp: instead of c:\users...

    In all these examples it is the way set-location prefers PSPath to the fullname property which is the problem

  11. iSazonov commented on Aug 24, 2022

    @iSazonov
    Collaborator

    James O'Neill (@jhoneill) You can find a lot of issues like you have mentioned. There are fundamental reasons for this:

    • The fundamental concepts of PowerShell have no formal description.
    • They are not implemented exactly as they should be according to the previous point.

    I tried to put it in an RFC, but that would have led to a huge redesign of PowerShell providers and public APIs - it was rejected.

  12. jhoneill commented on Aug 29, 2022

    @jhoneill

    Also worth noting was an experimental feature to convert PSPaths to filesystem ones was in 7.2 and previews of 7.3 but has been dropped #17670

  13. mklement0 commented on Aug 30, 2022

    @mklement0
    ContributorAuthor

    Good point, James O'Neill (@jhoneill):

    Given that the current binding of provider items via the pipeline happens by the .PSPath property value only, the drive information is already lost by the time Set-Location sees the parameter value - and guessing a drive indeed sounds like a bad idea.

    Thus, the solution is to bind such items as objects, where a drive-letter-based path can be reconstructed via the .PSDrive property. I think such a change would bring sanity to Set-Location's pipeline behavior without breaking anything.

    Here's a proof of concept, Set-LocationProperly, which wraps Set-Location with the desired behavior.
    From what I can tell, it should work with all providers and PowerShell-only drives, so that all of the following commands should work as intended; if no drive information is present, the behavior is as before, but with file-system items that would then only happen with raw UNC paths:

    • Get-Item / | Set-LocationPropertly
    • $PWD | Set-LocationProperly
    • Get-Item HKCU:\Console | Set-LocationProperly
    function Set-LocationProperly {
    
      [CmdletBinding(DefaultParameterSetName = 'Path', PositionalBinding = $false)]
      param(
        # Note: I think it would make more sense to interpret *strings* from the pipline as *literal* paths, in which
        #       case ValueFromPipeline could be removed, but that would at least technically be a breaking change.
        [Parameter(ParameterSetName = 'Path', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [string] $Path,
        [Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [Alias('lp')]
        [object] $LiteralPath,
        [switch] $PassThru,
        [Parameter(ParameterSetName = 'Stack')]
        [string] $StackName
      )
      
      Set-StrictMode -Version 1
      
      if ($pscmdlet.ParameterSetName -eq 'Stack') {
        Set-Location -StackName $StackName -PassThru:$PassThru
      }
      elseif ($pscmdlet.ParameterSetName -eq 'Path') {
        Set-Location -Path $Path -PassThru:$PassThru
      }
      else {
        $resolvedPath =
        if ($LiteralPath.PSDrive -and ($IsWindows -or $LiteralPath.PSDrive.Name -ne '/')) {
          $providerPath = $LiteralPath.PSPath -replace '^.+?::'
          $driveRoot = (Get-PSDrive $LiteralPath.PSDrive.Name).Root
          Join-Path ($LiteralPath.PSDrive.Name + ':') $providerPath.Remove(0, $driveRoot.Length)
        }
        elseif ($LiteralPath -is [System.IO.DirectoryInfo]) {
          $LiteralPath.FullName
        }
        elseif ($LiteralPath.PSPath) {
          $LiteralPath.PSPath
        }
        else { 
          # Note: Despite ValueFromPipelineByPropertyName, the objects with such properties bind *as a whole*,
          #       due to also being ValueFromPipeline as [object]s, so the properties must be tested for explicitly.
          if ($LiteralPath.LiteralPath) {
            $LiteralPath.LiteralPath
          }
          elseif ($LiteralPath.lp) {
            $LiteralPath.lp
          }
          else { # Otherwise: simply stringify the input object.
            # Note: This also covers [System.Management.Automation.PathInfo], such as reported by $PWD / Get-Location,
            #       which stringifiy as their .Path property, expressed in terms of PowerShell drives.
            "$LiteralPath"
          }
        }
        Set-Location -PassThru:$PassThru -LiteralPath $resolvedPath
      }
      
    }

    Note:

    • The above quietly ignores multiple input objects and only acts on the last one. It wouldn't be hard to implement a warning or emulate the current Set-Location behavior:
    • By contrast, Set-Location somewhat pointlessly changes the directory for each input object, which means that the last (existing) input path "wins", in effect, with preceding nonexisting ones causing statement-terminating errors.
      • This behavior has some justification: conceivably, you could call Set-Location -PassThru in a pipeline in order to perform subsequent operations in the same pipeline in a distinct directory each.
  14. iSazonov commented on Aug 31, 2022

    @iSazonov
    Collaborator

    Impossible without #5785

  15. jhoneill commented on Aug 31, 2022

    @jhoneill

    Michael Klement (@mklement0) - good points too.
    I see 2 issues with set-Location first what is intended when a provider path is provided, and where there are behaviour differences are they bugs or intentional and second the pipe behaviour does seem intended which can be fixed by making FullName an alias for Path, (and dodging the other issues).

  16. mklement0 commented on Aug 31, 2022

    @mklement0
    ContributorAuthor

    Ilya (@iSazonov), a rewrite of the file-system provider will be most welcome, if and when it happens (the issue was created in 2018).

    In the meantime, it looks to me that the above solution works, despite being a bit kludgy.

    James O'Neill (@jhoneill):

    Quick terminology clarification: provider path (perhaps better: provider-native path) usually refers to a path that is native to a given provider, and which PowerShell can't be expected to understand without a provider prefix; e.g., HKEY_CURRENT_USER\Console vs. registry::HKEY_CURRENT_USER\Console - I'l call the latter a provider-qualified path.

    In the absence of a provider prefix and a drive spec at the start of a path, PowerShell interprets a path as relative to the current location of whatever provider underlies the current location, so that while a file-system location (directory) is the current location (as is typical), Get-ChildItem HKEY_CURRENT_USER\Console (presumably in vain) looks for an file or directory named Console in a HKEY_CURRENT_USER subdirectory, for instance.

    Ambiguity is avoided in one of two ways:

    • Prefix a provider-native path with the name of the provider to make it a provider-qualified path, e.g. registry::HKEY_CURRENT_USER\Console
    • Use a drive-based path, e.g, HKCU:\Console

    Note that UNC file paths (e.g, \\localhost\c$) are an edge case: they aren't drive-based, so if a provider other than the file-system provider underlies the current location, they are not recognized as file-system paths (try Set-Location function:; Get-Item \\localhost\c$). As an aside: Curiously, on Unix only, / and \ are recognized as file-system paths even in a location such as function: - even though there is no drive spec involved and / and \ should refer to the root location of the current drive (technically, / is a drive on Unix, but in the absence of a : following it, it can't be distinguished from referring to the current location's root); but perhaps this inconsistency is for the better in practice.

    With all this in mind:

    making FullName an alias for Path

    Note that this would limit the solution to (a) file-system paths and (b) wouldn't respect PowerShell-only drives such as Get-Item SomeCustomDrive:foo.txt | Set-Location

    What the function above does is the following, which, from what I can tell, provides consistent behavior in line with user expectations:

    • If an object with provider ETS properties (.PSPath, ...) is passed:

      • If, in addition to .PSPath, it has a .PSDrive property: translate the provider-qualified path (provider prefix + provider-native path) back into a drive-based path, as the presence of .PSDrive unambiguously implies that a drive-based path was used on input.

      • Otherwise, if there's no drive information: Use the .PSPath property, as currently, resulting in a full, provider-qualified path.

        • In the context of file-system paths, this will then only happen with UNC paths.

          • While the resulting $PWD.Path value (and therefore the prompt string) will still be somewhat scary-looking (e.g. Microsoft.PowerShell.Core\FileSystem::\\localhost\c$), there is justification for this behavior, given that a UNC path is ambiguous, as explained above.
          • That said - and this is a separate issue - in practice it would make more sense to unambiguously treat native UNC paths (those without a provider prefix) as file-system paths, just like a /-prefixed path is already unambiguously considered a file-system path on Unix.
        • In the context of other providers - those that optionally allow addressing an item without a drive spec - you'd only get the provider-prefixed path if you explicitly avoided a drive spec, such as Get-Item registry::HKEY_CURRENT_USER\Console | Set-Location (whereas Get-Item HKCU:\Console | Set-Location would preserve the drive).

    • All other cases would result in the same behavior as currently.

  17. SteveL-MSFT commented on Sep 19, 2022

    @SteveL-MSFT
    Member

    For the initial scenario, couldn't we more simply fix this by adding a noteproperty called Path to the output of New-Item?

  18. mklement0 commented on Sep 19, 2022

    @mklement0
    ContributorAuthor

    Steve Lee (@SteveL-MSFT), the larger issue here is that Set-Location should accept any [System.IO.DirectoryInfo] instance piped to it and honor its PS drive information, if present, not just if emitted by New-Item.

    Thus, the better solution is to fix Set-Location, as previously described, which requires modifying it to accept object input.

    The irony here is that providing a [System.IO.DirectoryInfo] without provider instance properties currently actually works as expected, albeit by definition only ever with file-system-native paths, and only with file paths that don't contain [ (that string pipeline input binds to -Path instead of -LiteralPath is unfortunate, but that's a separate problem):

    # OK, due to effectively stripping the PS* provider properties 
    # and the *stringified*  [System.IO.DirectoryInfo] instance - which is its .FullName property - 
    # binding to Set-Location's -Path property
    Get-Item $HOME | % { [System.IO.DirectoryInfo] $_.FullName } | Set-Location

    By contrast, Get-Item $HOME | Set-Location, as reported in #9293, causes the same problem described in the initial post.

  19. SteveL-MSFT commented on Dec 7, 2022

    @SteveL-MSFT
    Member

    The @PowerShell/wg-powershell-cmdlets discussed this and since this is not a change in behavior and has existed for a long time, we recommend Won't Fix

  20. ghost closed this as completedon Dec 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-FileSystem-Providerspecific to the FileSystem providerIssue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get aResolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.WG-Cmdletsgeneral cmdlet issues

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions