.cmd scripts launch in an external window when:
PowerShell can't pipe the script's stdout when this happens.
Specifically, this happens to me when invoking commands installed via npm / nodejs. npm installs commands with a bash script -- no file extension -- and a .cmd script in the same directory. The following invocation triggers this behavior:
& "$PSScriptRoot\node_modules\.bin\webpack"
Create two files in the current directory (I did it in C:\Users\abradley\Desktop)
"echoFoo": create an empty file
"echoFoo.cmd":
@echo foo
Then run the following in PowerShell:
# Using Pester assertions to show what's expected
./echoFoo | should -be 'foo'
../Desktop/echoFoo | should -be 'foo'
../../../Users/abradley/Desktop/echoFoo | should -be 'foo'
C:/Users/abradley/Desktop/echoFoo | should -be 'foo' # <-- Error: Cannot run a document in the middle of a pipeline
C:/Users/abradley/Desktop/echoFoo # <-- External cmd window pops up to run this script
All 4 assertions in the example above should succeed; output of 5th invocation should appear in same console.
Fourth assertion fails; fifth invocation runs in an external window.
> $PSVersionTable
Name Value
---- -----
PSVersion 6.1.0-rc.1
PSEdition Core
GitCommitId 6.1.0-rc.1
OS Microsoft Windows 10.0.17134
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
@SteveL-MSFT It seems this important omission.
Good find, @cspotcode.
To add some more details:
Windows PowerShell is affected too.
_All_ executable extensions (as per $env:PATHEXT) are affected.
Here's a quick way to reproduce the problem, using sample file t and dummy executables with the same filename root for all executable extensions, in sequence:
Set-Location $env:TEMP
Push-Location $env:TEMP
$null > '.\t'
$env:PATHEXT -split ';' | % {
ri t.*
$n = "t$_"; '' | set-content -enc ascii $n
$fn = (rvpa $n).path;
$fnne = $fn -replace '\..+$'
"$_`: Invoking '$fnne' with '$fn' present..."
& $fnne
read-host 'Press Enter to test next extension'
ri $fn
}
Note that only .com, .exe, .cmd and .bat files actually _execute_ - albeit unexpectedly in a _new_ window, as described - whereas the other extensions pop up a dialog for selecting an application, as if you had double-clicked / called Invoke-Item on a _document_ not associated with an application.
In the case where a file is not executable, PowerShell automatically does a ShellExecute (equivalent to explicitly calling Invoke-Item) which leaves it to the desktop shell to determine how to "run" the file. If you try it with a .txt file, it should open notepad on Windows or textedit on macOS. So this is expected. In the case where there's no extension that makes it executable, it leaves it to the shell to take care of it.
In the case where you have file and file.cmd, it will make an exact match first so it finds file and not file.cmd. So it does a ShellExecute on file and the shell seems to find file.cmd and executes that but to do so it needs to open a new console window. You can see the same behavior with Win+R and give the absolute path to echoFoo (sans extension).
This all seems to be working as designed and (I think) only the edge case of having two files with same name differing by extension brings unintuitive results.
@SteveL-MSFT It runs the .cmd correctly using a relative path but not an absolute path.
In both situations, I have file and file.cmd. Using a relative path, it runs the .cmd in the same console window. Using an absolute path, it runs the .cmd in a new console window.
C:\Users\abradley\Desktop\foo runs in new console window.
.\foo runs in same console window.
So I'm still pretty sure this is a bug.
@cspotcode to be clear, I'm not saying the experience couldn't be improved, but is having two files the same name one without an extension and one with executable extension common? I would be open to reviewing a community submitted PR, but this doesn't seem critical to fix vs other issues.
You said it was deferring to ShellExecute, and that was causing it to open a new window. Yet with a relative path it does not open a new window. So the behavior is different from what you described, correct? I'm just trying to understand why the behavior is different for an absolute path, when it already does the correct thing for a relative path.
This will happen for anything installed via npm, the nodejs package manager. npm's installation mechanism always creates both a .cmd script and an extensionless bash script for every module that declares a binary.
For example, it affects TypeScript. The example I cited in my original post is webpack, another popular tool.
npm installs binaries into a project-local .bin directory so it's common to invoke binaries at this path. And to avoid issues where I need to cd back to the root of my project, I use $PSScriptRoot to get the right path. Here's an invocation that's affected by this issue:
& "$PSScriptRoot\node_modules\.bin\tsc" # Compile TypeScript
(sorry if I sound repetitive; I'm just trying to ensure we're on the same page and that I'm not missing anything)
When given a relative path, a search is used to find a match. However, when an absolute path is given, it's a literal.
@SteveL-MSFT: @cspotcode has demonstrated that there's an inconsistency, and that its effects surface in a common use case.
When given a relative path, a search is used to find a match. However, when an absolute path is given, it's a literal.
What search? The only way to resolve a relative path is relative to the current filesystem location; and a relative path such as .\.bin\webpack is a literal too.
In any event, there's no good reason to treat targeting a file by relative path differently than by absolute path.
What makes sense _conceptually_:
Let _direct invocation_ / invocation via & (or even .) / Start-Process - typically used for _invoking executables_ - _prioritize_ finding _executable_ files, which means that if no extension is specified, to start looking for files with one of the extensions in $env:PATHEXT _first_.
Start-Process already does that with both relative and full paths.& currently only does it for _relative_ paths, so we must fix it to do the same with absolute ones.Let Invoke-Item - typically used for opening _documents_ - _prioritize_ finding extension-less files _as specified_, with a _fallback_ to looking for executables.
Invoke-Item functions currently, but I doubt that someone relied on the old behavior, as it is mostly useless: Currently, it behaves like direct invocation does with absolute paths even with relative paths (so, unlike direct invocation, it is internally consistent, but arguably consistently does the wrong thing).Now that we know that having file and file.cmd in the same folder _does_ happen in practice, and fairly commonly (npm), this separation would allow targeting either file in a predictable manner.
I haven't looked into how to convince ShellExecute to treat a given file path _as-is_, without looking for variants with implied executable extensions, but I hope that's a solvable problem.
Agree that npm is a sufficient scenario that warrants more investigation into this.
I'm having issues with using absolute and relative paths for running phpunit. If I use a relative path like .\vendor\bin\phpunit it works as expected, but if I use the absolute path c:/Users/xxx/vendor/bin/phpunit it opens a new cmd window which run the command and then closes itself.
Most helpful comment
@SteveL-MSFT It runs the .cmd correctly using a relative path but not an absolute path.
In both situations, I have
fileandfile.cmd. Using a relative path, it runs the .cmd in the same console window. Using an absolute path, it runs the .cmd in a new console window.C:\Users\abradley\Desktop\fooruns in new console window..\fooruns in same console window.So I'm still pretty sure this is a bug.