Powershell: Can't do a simple cd to a folder?

Created on 4 Jun 2020  路  13Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

```powershell

Open powershell, type cd C:\Program Files\

Press enter. Fails.

Expected behavior

I expect that a command line tool can actually do simple operations like changing a directory without exploding.

Actual behavior

Throws 'invalid argument exception'

Environment data

Windows 10

2020-06-04 09_37_41-Windows PowerShell

Issue-Question Resolution-Answered

Most helpful comment

@iSazonov is there any need?
If you type rather than tab complete a path with a space in, then CMD knows that cd takes only 1 parameter and everything after "cd[space]" is the directory to change to. dir takes parameters and CMD can't pull the same trick. Running notepad C:\program files\foo\foo.ini won't join the two parts of the path together. CD only works because cmd, inherited (via command.com) the cp/m trick of doing some actions inside the command line handler, PSReadline could do it, but "quote things with spaces in" is pretty widely understood and quickly learned whereas creating an exception which applies to a single command sets bad expectations.

All 13 comments

this works if you do

cd C:\pr then hit tab (to tab complete - a feature that I believe came in one of the more recent versions of PowerShell) where it will wrap this to the correct path which is 'C:\Program Files\' in PowerShell and has been the case since v1

Specifying an argument to a command that has spaces in it can be accomplished in two ways. First, you can quote the arg e.g.:

cd 'C:\Program Files\'

In fact, if you type C:\Prog<tab>, PowerShell will tab-complete the path and add the quotes for you. You can also escape the path e.g.:

cd C:\Program` Files

Now you could argue that this one command could be made a wee bit smarter and not require either quoting or escaping. This can be done. In the PSCX extension, we supply a Set-LocationEx that works the way you expect. Here's the impl of that function - it uses $UnboundArguments to construct the space-separated path. It also allows you to cd to a file path - it cd's to the file's dir.

Bash gives "too many arguments" if the name has a space and no quotes. Most shells need quotes around most parameters with spaces in. However CMD is odd because cd program files doesn't need quotes , but Dir program files won't work unless quotes are added. I'd take a guess that hasn't changed since OS/2 (and then NT3.1) allowed spaces in file names. It's odd that works anywhere.

I guess we could implement the magic for Set-Location. This will block adding new positional parameters but it will work with great UX if user pastes such path. Cons - users will expect such behavior for other cmdlet too which is not always possible.
Another thought is to make PowerShell so smart to understand current paste context and if it is a path than quote inserted string.

@iSazonov is there any need?
If you type rather than tab complete a path with a space in, then CMD knows that cd takes only 1 parameter and everything after "cd[space]" is the directory to change to. dir takes parameters and CMD can't pull the same trick. Running notepad C:\program files\foo\foo.ini won't join the two parts of the path together. CD only works because cmd, inherited (via command.com) the cp/m trick of doing some actions inside the command line handler, PSReadline could do it, but "quote things with spaces in" is pretty widely understood and quickly learned whereas creating an exception which applies to a single command sets bad expectations.

Yeah I don't really see a need to fix this. It behaves according to all expectations from existing commands. The one exception that was created over 20 years ago isn't a good enough reason to kludge things into working IMO.

I agree that this doesn't need to be "fixed" in PowerShell. If someone really wants this, there are ways they can implement the functionality.

it uses $UnboundArguments to construct the space-separated path.

I see, it uses [Parameter(ValueFromRemainingArguments=$true)] [string[]] $UnboundArguments and then $UnboundArguments -join ' '. But this collapses consecutive spaces, unlike CD in cmd.exe.

C:\temp>MKDIR "hey  there"

C:\temp>CD hey  there

C:\temp\hey  there>

How would one implement a PowerShell CD command that preserves the number of spaces?

Alternatively, you could pick out the path from $MyInvocation.Line. Beware though that the Line can have other command before and/or after your cd command e.g. get-date; mycd C:\Foo Bar; get-uptime

Alternatively, you could pick out the path from $MyInvocation.Line. Beware though that the Line can have other command before and/or after your cd command e.g. get-date; mycd C:\Foo Bar; get-uptime

Yeah here's a really basic and flimsy version (partially because it's not carefully written and partially because it uses a non-public API)

Set-LocationProxy (click to expand)

using namespace System.Management.Automation
using namespace System.Management.Automation.Language

function Set-LocationProxy {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromRemainingArguments)]
        [string[]] $UnboundArgs
    )
    process {
        $extent = [InvocationInfo].
            GetProperty('ScriptPosition', [System.Reflection.BindingFlags]'NonPublic, Instance').
            GetValue($MyInvocation)

        $ast = [Parser]::ParseInput(
            $extent.StartScriptPosition.GetFullScript(),
            $extent.File,
            [ref] $null,
            [ref] $null)

        $commandAst = $ast.Find(
            {
                param($a) end {
                    return $a -is [CommandAst] -and
                        $a.Extent.StartOffset -eq $extent.StartOffset -and
                        $a.Extent.EndOffset -eq $extent.EndOffset
                }
            },
            <# searchNestedScriptBlocks: #> $true)

        $binding = [StaticParameterBinder]::BindCommand($commandAst)
        $elements = $binding.BoundParameters['UnboundArgs'].Value.Elements
        $spacesBefore = [int[]]::new($elements.Count)
        for ($i = 1; $i -lt $spacesBefore.Length; $i++) {
            $prev = $elements[$i - 1]
            $current = $elements[$i]

            # Doesn't account for comments and probably other things that would skew this.
            $spacesBefore[$i] = $current.Extent.StartOffset - $prev.Extent.EndOffset
        }

        $sb = [System.Text.StringBuilder]::new()
        for ($i = 0; $i -lt $UnboundArgs.Length; $i++) {
            $spacesToAdd = $spacesBefore[$i]
            if ($spacesToAdd) {
                $null = $sb.Append(' '[0], $spacesToAdd)
            }

            $null = $sb.Append($UnboundArgs[$i])
        }

        return $sb.ToString()
    }
}

gotta say that I find it hilarious that I'd need to use that giant script, figure out how to run it etc just so that I can copy and paste a directory path properly into a command line tool.

it's 2020, tools should do stuff for people, not the opposite.

Too bad that this does not work:

New-Alias -Name cd -Force -Option AllScope -Value "Set-Location --%"

Although I suppose such a feature would make PowerShell scripts even harder to parse without running them, as any command might then turn out to be an alias that changes how the rest of the line has to be parsed.

If the exceptional cd syntax is only for interactive use, then perhaps it would be best implemented in PSReadLine as a transformation that happens before PowerShell proper parses the command. If I understand correctly, PowerShell calls PSConsoleHostReadLine only to read commands, not to read parameters nor in Read-Host.

gotta say that I find it hilarious that I'd need to use that giant script, figure out how to run it etc just so that I can copy and paste a directory path properly into a command line tool.

Where are you copying the path from?
Also whilst this may not be a current feature it is something that I do think should be included in the wider PowerShell experience, but it should not be added in here, it should be added within PSReadLine Module (which is shipped with PowerShell anyway)

it's 2020, tools should do stuff for people, not the opposite.
Yes, but the right tool needs to be where the changes get made, and directly in PowerShell is not IMO the right place for this.

Too bad that this does not work:

New-Alias -Name cd -Force -Option AllScope -Value "Set-Location --%"

Although I suppose such a feature would make PowerShell scripts even harder to parse without running them, as any command might then turn out to be an alias that changes how the rest of the line has to be parsed.

If the exceptional cd syntax is only for interactive use, then perhaps it would be best implemented in PSReadLine as a transformation that happens before PowerShell proper parses the command. If I understand correctly, PowerShell calls PSConsoleHostReadLine only to read commands, not to read parameters.

Totally agree and I started to comment something similar, but then rebooted and can't find the recovered tab with that comment. But I have raised this over in PSReadline module in https://github.com/PowerShell/PSReadLine/issues/1593 to be continued to be tracked as a UX improvement, which I do believe to be better lived in PSReadline than within the PowerShell Engine

Was this page helpful?
0 / 5 - 0 ratings