where answers one question: when I type this command, which file on disk actually runs? On a machine that has been in service for a while that is rarely obvious. A tool can sit in System32, again under a vendor’s install directory, and a third time as a store alias in WindowsApps, and the copy that wins is decided by the order of the PATH variable rather than by whichever one you installed last.
That makes where the first command to run whenever a tool behaves like the wrong version, a script works for you and not for a colleague, or an upgrade appears to have done nothing. It is also the fastest way to prove the opposite: that the command is not installed at all, which is a different fault with a different fix.
The reference page lists the parameters and three examples, and shows no output for any of them. This article fills that in, and then covers the part that catches people out: the same command typed into PowerShell runs something else entirely, produces no output, and reports success while doing it.
Applies to: Windows 10, Windows 11, Windows Server 2016 and later
Quick answer
From a Command Prompt. The first line of output is the copy that runs; anything after it is shadowed by that first one.
rem Every copy of python that is reachable, in the order PATH is searched
where python
rem No output at all, just an exit code: 0 found, 1 not found. This is the script form
where /q python
rem Size and timestamp for each hit, which tells two same-named binaries apart
where /t python
From PowerShell the same word does something completely different, so type the extension. Get-Command is the native answer and gives you more than a path.
# The .exe is not optional here. Bare 'where' is an alias for Where-Object
where.exe python
# Native equivalent: -All lists every candidate in the order PowerShell would pick them
Get-Command python -All
where python in PowerShell. It does not fail, it does not warn, and it does not print anything. It runs Where-Object with nothing to filter. Measured in PowerShell 7.4.6: zero objects returned, $? is True and $Error is untouched, so a script that checks for success is told everything went fine.
What where actually searches
Microsoft’s description is short and worth reading closely: by default where searches “the current directory and the paths that are specified in the PATH environment variable”. Two consequences follow from that sentence. A file sitting in whatever folder your prompt happens to be in can show up in the results even though it is not on PATH at all, and the order of the output mirrors the order of PATH rather than any notion of newest or best.
The second rule is about extensions: “If you do not specify a file name extension, the extensions listed in the PATHEXT environment variable are appended to the pattern by default.” So where python and where python.exe are not the same query. The first can also return a .bat or .cmd wrapper, which is frequently the thing that is really running.
rem Read your own list rather than assuming the default one
echo %PATHEXT%
rem Same command, two different questions. Compare the two result sets
where python
where python.exe
| Parameter | What it does |
|---|---|
/r <Dir> | Recursive search starting at the specified directory |
/q | Returns an exit code (0 for success, 1 for failure) without displaying the list of matched files |
/f | Displays the results in quotation marks |
/t | Displays the file size and the last modified date and time of each matched file |
$<ENV>:<Pattern> or <Path>:<Pattern> | Searches an environment variable’s paths, or one explicit directory, instead of PATH. Not to be combined with /r |
/? | Displays help at the command prompt |
where *.msc lists every management console on PATH and where /r C:\Tools *.exe walks a tree. The $ENV: form is the least known of them and often the most useful: where $public:*.* searches the paths held in the PUBLIC variable without touching PATH at all.
Before the first example
Four things to settle before running anything below.
1. Nothing here needs elevation. Every command on this page reads file locations and runs from an ordinary prompt. If you want to confirm which context you are in, that is what whoami and the access token covers.
2. Use a command that plausibly has more than one copy. python, node and git are the usual suspects on a developer workstation; on a plain server try notepad, which Microsoft’s own documentation uses to demonstrate exactly this. A command with a single copy proves nothing.
3. Run it from a neutral directory. Because the current directory is searched alongside PATH, testing from a folder that happens to contain a copy of the thing you are looking for will produce a result you cannot reproduce anywhere else.
cd \
where python
4. Decide which shell you are in and stay honest about it. In Command Prompt the word where is enough. In PowerShell it is not, and the reason is not cosmetic. Everything from the PowerShell section onward assumes you have read that part.
Practical examples
1. Find out which copy will actually run
The problem: A tool reports the wrong version. You upgraded it, the installer said it succeeded, and the command still behaves like the old build.
The solution: Stop guessing which install is in charge and read the search order directly. The first line is the winner; every line after it is a copy that PATH never reaches.
rem Output order mirrors PATH order, so line 1 is the copy that runs
where python
Three copies is an ordinary result on a working machine, and the upgrade has almost certainly landed in the second entry while the first one keeps winning. These three paths are not a capture from one particular box; they are the shapes you actually meet, and they are the exact input the parsing section further down is tested against.
C:\Python312\python.exe
C:\Program Files\Python312\python.exe
C:\Users\John\AppData\Local\Microsoft\WindowsApps\python.exe
WindowsApps entry is normally an App Execution Alias rather than the install you were thinking of. If that line comes first, that is what the name resolves to, and the version you actually installed further down the list never runs.
2. Prove a command is missing rather than misconfigured
The problem: A documented command returns “is not recognized”. The instinct is to start adding directories to PATH.
The solution: Check first. An empty result from where means the file is not on this machine, which is an install problem and not a PATH problem. Adding directories that contain nothing will not help.
rem An empty result here means the feature or tool is not installed at all
rem It does NOT mean something is missing from PATH
where dfsrmig
For a batch file, use /q instead. It prints nothing and sets errorlevel to 0 on success and 1 on failure, which is the documented behaviour and the only form worth putting in a script.
rem /q suppresses output entirely, so the exit code is the whole answer
where /q dfsrmig
if errorlevel 1 (
echo dfsrmig is not installed. Install the DFS Replication role service first.
exit /b 1
)
3. Tell two identical looking binaries apart
The problem: where returned two paths and both look plausible. You need to know whether they are the same file in two places or two different builds.
The solution: /t adds the size and the last modified timestamp to each line, which settles most cases in one glance without opening anything.
rem /t prefixes each result with size and last write time
where /t python.exe
When the sizes match and you need certainty rather than a strong hint, hash both files. Identical hashes mean one build in two locations, which is a PATH tidiness problem. Different hashes mean two builds, which is a versioning problem.
# Get-FileHash on each path that where returned
Get-FileHash 'C:\Python312\python.exe' -Algorithm SHA256
Get-FileHash 'C:\Program Files\Python312\python.exe' -Algorithm SHA256
4. Search somewhere other than PATH
The problem: You need to locate a file inside a known tree, or inside whatever directories some environment variable is pointing at, without permanently touching PATH.
The solution: where can look somewhere other than PATH in three ways, and two of them most people never meet: an explicit directory, the paths held inside an environment variable, or a recursive walk from a starting point.
rem One explicit directory, no recursion, no PATH involvement
where C:\Tools:*.exe
rem Search the paths held in an environment variable instead of PATH
where $public:*.*
rem Recursive from a starting point. Works against a UNC path too
where /r C:\Tools *.exe
where /r \\SRV-PROD-01\c$\Tools notepad.*
/r are mutually exclusive. Microsoft states that the $ENV: and Path: formats “should not be used with the /r command-line option”. Pick one or the other.
5. Use the result inside a script
The problem: You want the winning path in a variable so the rest of the script can call it by full path and stop depending on PATH order at all.
The solution: Capture the first line only, and split it correctly. The parsing section below shows why delims= is not optional here.
rem delims= disables splitting entirely, so a path containing spaces survives
rem goto :found stops after the first line, which is the copy that would run
setlocal
for /f "delims=" %%P in ('where python 2^>nul') do (
set "PYEXE=%%P"
goto :found
)
echo python was not found on PATH
exit /b 1
:found
echo Using: %PYEXE%
"%PYEXE%" --version
2^>nul inside the for /f command string needs the caret. for /f parses the quoted command before cmd runs it, so the redirection symbol has to be escaped or the whole line breaks.
Why where does something else in PowerShell
PowerShell does not look for an executable first. It walks a fixed list of command kinds and runs the first match it finds. Microsoft states the order plainly in about_Command_Precedence: alias, then function, then cmdlet, then external executable files. An executable is last.
PowerShell ships an alias named where, pointing at Where-Object. So where matches at step one and where.exe at step four is never reached. Microsoft’s own documentation uses this exact command as its worked example of the problem, and publishes the output of Get-Command where -All to show both of them side by side.
CommandType Name Version Source
----------- ---- ------- ------
Alias where -> Where-Object
Application where.exe 10.0.22621.1 C:\Windows\system32\where.exe
Both exist, and the alias is listed first because it wins. The same page gives the fix in one sentence: “For executables, you can include the file extension. For example, to run the executable version of where use where.exe.”
What makes this worth a section rather than a footnote is the failure mode. Where-Object takes a property name as its first positional parameter, so where python parses cleanly as “filter the incoming objects on a property called python”. There are no incoming objects, so it filters nothing and emits nothing.
# All of this measured in PowerShell 7.4.6
Get-Command where # Alias -> Where-Object
(Get-Command Where-Object).Parameters['Property'] # positional, position 0
$out = @(where python)
$out.Count # 0
$? # True
$Error.Count # unchanged
where python in PowerShell does not report that python is missing. It reports nothing, sets $? to True and adds nothing to $Error, because from PowerShell’s point of view the command succeeded. A script written as where python; if ($?) { ... } takes the success branch on a machine where python is not installed.
There is one documented exception to the precedence order worth knowing, because it makes the behaviour look inconsistent. If the alias lives in a module that has not been loaded yet, PowerShell runs the executable instead: “It doesn’t auto-load modules if it finds the external executable.” The where alias is built in, so it is always loaded and always wins, but a name that resolves to your executable on one machine can resolve to somebody’s module alias on another.
Get-Command: which copy wins, and what kind of thing it is
Get-Command is not a PowerShell spelling of where. It answers a broader question: of everything in this session that responds to this name, which one runs, and is it even a file? That covers cases where cannot see at all, because an alias or a function has no path on disk.
Two behaviours do most of the work. Without -All, it returns only the command that would actually run. With -All, it returns every candidate, and Microsoft is specific about the ordering: it “gets all commands with the specified name and returns them in execution precedence order”. The first row is the winner, the rest are shadowed.
# Just the winner, plus what kind of thing it is
Get-Command python
# Every candidate, first row first. Definition holds the path for an Application
Get-Command python -All | Format-Table CommandType, Name, Definition
# The path on its own, ready to put in a variable
(Get-Command python).Source
Microsoft’s example for -All is Notepad, which on a normal Windows install genuinely has two copies. This is their capture.
CommandType Name Definition
----------- ---- ----------
Application notepad.exe C:\WINDOWS\system32\notepad.exe
Application NOTEPAD.EXE C:\WINDOWS\NOTEPAD.EXE
The CommandType column is the part where cannot give you, and it has ten documented values. In practice four of them account for nearly everything you will meet.
| CommandType | What you are looking at |
|---|---|
Application | A non-PowerShell file found in $Env:PATH. This is the only type where can also find |
Alias | A second name for something else. Definition tells you what it points at |
Cmdlet | A compiled command from a module. No path on disk to inspect |
Function | Defined in a profile, a module or the session itself, and outranks any cmdlet of the same name |
Get-Command with no arguments does not list executables. Microsoft: “Without parameters, Get-Command gets all the cmdlets, functions, and aliases installed on the computer.” To include the contents of PATH you have to ask, either by naming the command or with Get-Command *, which “gets all types of commands, including all the non-PowerShell files in the PATH environment variable”.
Parsing the output without getting a wrong answer
where prints one path per line and nothing else, which makes it look like the easiest output on Windows to parse. It has exactly one hazard, and it is the oldest one on Windows: C:\Program Files.
Four candidates, each run over the three-line result from example 1.
| Approach | Measured result | Verdict |
|---|---|---|
for /f "tokens=1" | 2 of 3 lines correct | Truncates at the first space |
for /f "delims=" | 3 of 3 correct | Correct |
where /f then "delims=" | 0 of 3 correct | Quotes are kept as part of the value |
where /f then %%~P | 3 of 3 correct | Correct, but longer than just using delims= |
for /f "tokens=1" is correct on two of those three lines, and wrong on the one you care about. C:\Program Files\Python312\python.exe comes back as C:\Program. A test on a machine where every path happens to be space free passes cleanly and tells you nothing.
delims= with nothing after it switches splitting off, so the whole line becomes token 1. That is all this output needs. Reaching for /f to “handle the spaces” makes it worse, because the quotation marks it adds end up inside the variable unless you also strip them with %%~P.
rem Wrong: stops at the first space
for /f "tokens=1" %%P in ('where python') do echo [%%P]
rem Right: no splitting at all
for /f "delims=" %%P in ('where python') do echo [%%P]
rem Also right, if you already have /f output: %%~P removes the quotes
for /f "delims=" %%P in ('where /f python') do echo [%%~P]
In PowerShell there is no text to parse. Get-Command returns objects, so ask for the property and skip the whole problem.
# Source is the full path for an Application, and $null for a cmdlet or function
Get-Command python -All |
Where-Object CommandType -eq 'Application' |
ForEach-Object { "{0} {1}" -f $_.Name, $_.Source }
Where-Object used for what it is actually for, filtering objects in a pipeline. It is the same command that hijacks the word where two sections above. Both behaviours are correct; the trap is only that one word means two different things depending on which shell you typed it in.
Hidden gems
The same word, two shells, opposite answers, and no error either way. This is the one to remember. In Command Prompt where python answers the question. In PowerShell it filters an empty pipeline and returns nothing while reporting success. Our own PowerShell cheat sheet already lists Where as shorthand for Where-Object; this is what that row costs you when you forget it.
/q is the only form that belongs in a script. It writes nothing at all and leaves the answer in errorlevel, so there is no output to parse and nothing to go wrong with spaces or quoting. If all you need to know is present or absent, never capture output you are only going to throw away.
An empty result is a different diagnosis from a wrong result. Nothing found means the file is not on the machine, so PATH is irrelevant and the fix is an install. One or more results means the file exists and the question becomes which one is first. Six other articles on this site open with where <command> for exactly this reason: it separates the two before any time is spent on the wrong one.
/r accepts a UNC path. Microsoft’s own example is where /r \\computer1\c notepad.*, which turns where into a quick remote file search over an administrative share without copying anything or opening a session. Slow over a WAN, genuinely useful on a LAN.
Extensionless searches and explicit ones are different queries. Because PATHEXT is appended when you leave the extension off, where python can return a .bat or .cmd shim that where python.exe will never show you. When the two result sets differ, the difference is usually the thing that has been confusing you.
Cross-shell equivalent
Three shells, three spellings of the same question. The table is worth keeping next to you if you move between Windows servers and Linux boxes in the same day.
| Question | Command Prompt | PowerShell | bash |
|---|---|---|---|
| Which one runs? | where python (first line) | (Get-Command python).Source | command -v python |
| Show me all of them | where python | Get-Command python -All | type -a python |
| Exists? Exit code only | where /q python | if (Get-Command python -EA Ignore) | command -v python >/dev/null |
The PowerShell existence check deserves its own line, because the obvious ways of writing it are both wrong. Measured in 7.4.6: Get-Command on a name that does not resolve returns $null, leaves $? as True, and writes a CommandNotFoundException record into $Error. It is not terminating, so a plain try/catch around it never fires and the error quietly accumulates. Suppress it at the call instead.
# -ErrorAction Ignore is what keeps $Error clean. try/catch does not catch this
# unless you also add -ErrorAction Stop, which makes it terminating
if (Get-Command python -ErrorAction Ignore) {
'python is available'
} else {
'python is not installed'
}
type -a is the closest analogue to Get-Command -All, and bash has the same split that Windows does. Measured with two copies of one script on PATH: command -v returned only the first, type -a returned both in PATH order. And on a shell builtin the difference becomes total: type -a cd answers “cd is a shell builtin” while which cd finds nothing at all, for the same reason where.exe cannot see a PowerShell function. More of these pairings are in the Linux cheat sheet for Windows admins.
Where this matters
- An upgrade that appears to have done nothing. The new build installed correctly into a directory that sits later in PATH than the old one.
whereshows both in one line of output. - A script that works for you and not for a colleague. Same command name, different winning copy, because PATH is per user as well as per machine.
- “Command not recognized” on a server. Before touching PATH, confirm the file exists at all. On Windows Server most of these turn out to be a role service that was never installed.
- Scheduled tasks and services that behave unlike your session. They run with a different PATH, so the copy that wins there is not necessarily the one that wins for you. Resolve to a full path in the script and the question disappears.
- Porting a batch file to PowerShell. The
whereline survives the copy and paste, stops working, and does not say so. It is one of the few conversions that fails silently. - Auditing a machine you have just inherited.
whereon the handful of tools you expect, plusGet-Command -All, gives you the real toolchain rather than the documented one.
Tips and limitations
whereneeds no elevation, and running it elevated can change the answer, because the elevated session may carry a different PATH.- It reports what is reachable right now. A PATH edit made in the System Properties dialog does not reach an already open prompt; open a new one before trusting a negative result.
- The current directory is searched as well as PATH, so results can depend on where you were standing when you ran it.
- Output order follows PATH order. It is not sorted, not deduplicated, and says nothing about which copy is newest.
- In PowerShell always write
where.exe. In Command Prompt either form works, sowhere.exeis the safer habit for anything you might paste into the other shell later. - The
$ENV:andPath:search forms cannot be combined with/r. - Parse with
for /f "delims=". Anything that splits on spaces will truncate the firstProgram Filespath it meets. Get-Commandsees aliases, functions and cmdlets that have no file on disk. If it returns something andwhere.exereturns nothing, that is the explanation, not a fault.
Official documentation
- where | Microsoft Learn
- about_Command_Precedence | Microsoft Learn
- Get-Command | Microsoft Learn
- Where-Object | Microsoft Learn
- about_Aliases | Microsoft Learn
Related tools
- Hash Generator: when
wherereturns two paths of the same size, hashing both is what turns a strong hint into an answer about whether they are one build or two.
Related guides
- Windows command aliases with doskey and PowerShell: where the
wherealias comes from, and how to see every other alias that can shadow a command you meant to run. - whoami /all: read your Windows access token: the other half of “why did this behave differently for me”, when the answer is the token rather than PATH.
- findstr and find in Windows: how to filter the output of
wheresafely, and what/c:does with a path that contains spaces. - dfsrmig: migrate SYSVOL from FRS to DFSR: the clearest worked case of an empty
whereresult meaning “role service not installed” rather than “fix PATH”. - driverquery: inventory Windows drivers: another command whose fixed width output punishes anyone who splits it on spaces.
- bcdedit: read and edit the boot configuration: a case where PowerShell reinterprets your arguments before the tool ever sees them, exactly as it does with
where. - vssadmin: shadow copies and VSS writers: begins by confirming the binary exists, because its subcommand list differs between Windows versions.
- wbadmin: Windows Server Backup from the command line: another feature that is absent rather than misconfigured on most default installs.