Last active
July 11, 2025 11:56
-
-
Save emilwojcik93/cd5a85621b572dbc3fcc258c60f604bc to your computer and use it in GitHub Desktop.
Auto-find & add executables to PATH. Prioritizes WinGet locations, falls back to common Windows dirs. Handles duplicates, force override. No admin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
WinGet Executable PATH Manager - Intelligent executable discovery and PATH configuration for Windows | |
.DESCRIPTION | |
This comprehensive PowerShell script provides automated executable discovery and PATH management | |
with intelligent priority-based searching. It systematically searches through WinGet registry entries, | |
WinGet filesystem locations, and common Windows installation directories to locate executables, | |
then manages Windows PATH entries with sophisticated conflict resolution and force override capabilities. | |
The script operates without administrative privileges by modifying only the User PATH environment | |
variable, making it safe for standard user accounts while providing immediate session updates. | |
KEY FEATURES: | |
============ | |
• Priority-based executable search starting with WinGet-managed applications | |
• Comprehensive fallback to standard Windows installation directories | |
• Advanced PATH/alias existence validation with manual verification | |
• Intelligent PATH management with duplicate detection and removal | |
• Force override capability for repairing broken or outdated PATH entries | |
• Real-time session environment refresh for immediate command availability | |
• Progress reporting with detailed status updates during searches | |
• Extensive verbose logging for troubleshooting and debugging | |
• Non-administrative operation (modifies User PATH only) | |
• Modular function design for programmatic usage | |
SEARCH PRIORITY ORDER: | |
===================== | |
1. WinGet Registry Entries: | |
- TargetFullPath (direct executable paths) | |
- InstallLocation (package directories) | |
- Recursive subdirectory searches | |
2. WinGet Filesystem Locations: | |
- %LOCALAPPDATA%\Microsoft\WinGet\Links (symlinks) | |
- %LOCALAPPDATA%\Microsoft\WinGet\Packages (package cache) | |
3. Common Windows Directories: | |
- %LOCALAPPDATA%\Programs | |
- %ProgramFiles% and %ProgramFiles(x86)% | |
- %USERPROFILE%\AppData\Local\Programs | |
- Package manager directories (Scoop, Chocolatey) | |
- System directories (%SYSTEMROOT%\System32, %SYSTEMROOT%) | |
SUPPORTED PACKAGE MANAGERS: | |
=========================== | |
• WinGet (Microsoft's official package manager) | |
• Scoop (user-level package manager) | |
• Chocolatey (community package manager) | |
• Manual installations in standard directories | |
SAFETY FEATURES: | |
================ | |
• Non-destructive PATH modifications (User scope only) | |
• Duplicate entry detection and prevention | |
• Existing entry validation before modification | |
• Force override with explicit user consent | |
• Comprehensive error handling and graceful degradation | |
• Session-only environment updates with persistent changes | |
.PARAMETER ExeName | |
Specifies the name of the executable file to locate and configure in PATH. | |
Must include the file extension (e.g., "docker.exe", "git.exe", "python.exe"). | |
The script will search for this exact filename across all configured locations, | |
starting with WinGet registry entries and falling back to common directories. | |
.PARAMETER Force | |
Forces the overwrite of existing PATH entries even if the executable is already | |
available as a command, alias, or PATH entry. This is useful for: | |
• Repairing broken PATH entries that point to non-existent locations | |
• Updating PATH to point to newer versions of software | |
• Resolving conflicts between multiple installations | |
• Ensuring WinGet-managed versions take priority over other installations | |
.PARAMETER Verbose | |
Enables comprehensive verbose output for detailed logging and troubleshooting. | |
Provides information about: | |
• Search progress through each directory | |
• Registry query results and matches | |
• PATH analysis and modification decisions | |
• Error details and access permission issues | |
• Performance metrics and timing information | |
.OUTPUTS | |
System.Collections.Hashtable | |
Returns a hashtable containing operation results with the following properties: | |
• Success (Boolean): Whether the operation completed successfully | |
• Action (String): Description of the action taken | |
• ExecutablePath (String): Full path to the located executable | |
• DirectoryPath (String): Directory path added to PATH (if applicable) | |
• AddedToPath (Boolean): Whether a new PATH entry was created | |
• Source (String): Location source (WinGet-Registry, WinGet-FileSystem, Common-Location) | |
• SymlinkPath (String): WinGet symlink path if available | |
.NOTES | |
File Name : Find-ExecutableAndSetPath.ps1 | |
Author : GitHub Copilot Assistant | |
Prerequisite : PowerShell 5.1 or later | |
Copyright : Free to use and modify | |
REQUIREMENTS: | |
• Windows 10/11 or Windows Server 2016+ | |
• PowerShell 5.1 or PowerShell 7+ | |
• Standard user account (no administrative privileges required) | |
• WinGet installed (optional but recommended for best results) | |
REGISTRY ACCESS: | |
The script reads from HKEY_CURRENT_USER registry hive only, which does not | |
require administrative privileges. It specifically queries: | |
• HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* | |
ENVIRONMENT MODIFICATIONS: | |
• Modifies User PATH environment variable only (not System PATH) | |
• Changes are persistent across sessions | |
• Immediate effect in current PowerShell session | |
• New terminal windows will inherit the updated PATH | |
.LINK | |
https://github.com/microsoft/winget-cli | |
.LINK | |
https://docs.microsoft.com/en-us/windows/package-manager/ | |
.EXAMPLE | |
.\Find-ExecutableAndSetPath.ps1 -ExeName "docker.exe" | |
Searches for docker.exe starting with WinGet locations, then common directories. | |
If found, adds the containing directory to the User PATH environment variable. | |
Provides standard output with success/failure status. | |
.EXAMPLE | |
.\Find-ExecutableAndSetPath.ps1 -ExeName "git.exe" -Verbose | |
Performs the same search for git.exe but with comprehensive verbose logging. | |
Shows detailed information about each search location, registry queries, | |
and PATH modification decisions. | |
.EXAMPLE | |
.\Find-ExecutableAndSetPath.ps1 -ExeName "python.exe" -Force | |
Forces the addition of python.exe to PATH even if it's already available | |
as a command. Useful for ensuring the WinGet-managed version takes priority | |
over other installations like Microsoft Store Python. | |
.EXAMPLE | |
Set-ExecutableInPath -ExeName "node.exe" -Force -ShowProgress $true | |
Uses the script as a module to configure node.exe with progress reporting | |
and force override enabled. Returns a detailed result object for | |
programmatic usage. | |
.EXAMPLE | |
Find-ExecutableLocation -ExeName "code.exe" -ShowProgress $false | |
Performs only the search operation without modifying PATH. | |
Returns location information for Visual Studio Code executable | |
without progress bar display. | |
.EXAMPLE | |
Test-ExecutableInPath -ExeName "docker.exe" | |
Checks if docker.exe is currently available as a command, alias, or PATH entry | |
without performing any modifications. Returns detailed availability status. | |
#> | |
param( | |
[Parameter(Mandatory=$false)] | |
[string]$ExeName, | |
[switch]$Force | |
) | |
# Function 1: Find executable in WinGet and common locations | |
function Find-ExecutableLocation { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$ExeName, | |
[switch]$ShowProgress = $true | |
) | |
# Define search locations in priority order | |
$wingetLocations = @( | |
"$env:LOCALAPPDATA\Microsoft\WinGet\Links", | |
"$env:LOCALAPPDATA\Microsoft\WinGet\Packages" | |
) | |
$commonLocations = @( | |
"$env:LOCALAPPDATA\Programs", | |
"$env:ProgramFiles", | |
"${env:ProgramFiles(x86)}", | |
"$env:USERPROFILE\AppData\Local\Programs", | |
"$env:USERPROFILE\scoop\apps", | |
"$env:ChocolateyInstall\lib", | |
"$env:LOCALAPPDATA\Microsoft\WindowsApps", | |
"$env:SYSTEMROOT\System32", | |
"$env:SYSTEMROOT" | |
) | |
$allLocations = $wingetLocations + $commonLocations | |
$totalLocations = $allLocations.Count | |
$currentLocation = 0 | |
Write-Verbose "Starting search for $ExeName in $totalLocations locations" | |
# First check WinGet registry for direct paths | |
Write-Verbose "Checking WinGet registry entries..." | |
if ($ShowProgress) { | |
Write-Progress -Activity "Searching for $ExeName" -Status "Checking WinGet registry" -PercentComplete 0 | |
} | |
try { | |
$wingetPackages = Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | | |
Where-Object { $_.WinGetPackageIdentifier } | |
foreach ($package in $wingetPackages) { | |
# Check TargetFullPath first (direct executable path) | |
if ($package.TargetFullPath -and $package.TargetFullPath.EndsWith($ExeName)) { | |
Write-Verbose "Found in WinGet registry TargetFullPath: $($package.TargetFullPath)" | |
if ($ShowProgress) { Write-Progress -Activity "Searching for $ExeName" -Completed } | |
return @{ | |
ExecutablePath = $package.TargetFullPath | |
DirectoryPath = Split-Path $package.TargetFullPath -Parent | |
Source = "WinGet-Registry-Target" | |
SymlinkPath = $package.SymlinkFullPath | |
} | |
} | |
# Check InstallLocation | |
if ($package.InstallLocation -and (Test-Path $package.InstallLocation)) { | |
$exePath = Join-Path $package.InstallLocation $ExeName | |
if (Test-Path $exePath) { | |
Write-Verbose "Found in WinGet registry InstallLocation: $exePath" | |
if ($ShowProgress) { Write-Progress -Activity "Searching for $ExeName" -Completed } | |
return @{ | |
ExecutablePath = $exePath | |
DirectoryPath = $package.InstallLocation | |
Source = "WinGet-Registry-Install" | |
SymlinkPath = $package.SymlinkFullPath | |
} | |
} | |
# Search subdirectories in install location | |
$foundExe = Get-ChildItem -Path $package.InstallLocation -Filter $ExeName -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 | |
if ($foundExe) { | |
Write-Verbose "Found in WinGet registry subdirectory: $($foundExe.FullName)" | |
if ($ShowProgress) { Write-Progress -Activity "Searching for $ExeName" -Completed } | |
return @{ | |
ExecutablePath = $foundExe.FullName | |
DirectoryPath = $foundExe.DirectoryName | |
Source = "WinGet-Registry-Subdir" | |
SymlinkPath = $package.SymlinkFullPath | |
} | |
} | |
} | |
} | |
} | |
catch { | |
Write-Verbose "Error checking WinGet registry: $($_.Exception.Message)" | |
} | |
# Search in file system locations | |
foreach ($location in $allLocations) { | |
$currentLocation++ | |
$percentComplete = [math]::Round(($currentLocation / $totalLocations) * 100) | |
if (-not (Test-Path $location -ErrorAction SilentlyContinue)) { | |
Write-Verbose "Skipping non-existent location: $location" | |
continue | |
} | |
$locationSource = if ($location -in $wingetLocations) { "WinGet-FileSystem" } else { "Common-Location" } | |
$status = "Searching in: $location" | |
if ($ShowProgress) { | |
Write-Progress -Activity "Searching for $ExeName" -Status $status -PercentComplete $percentComplete | |
} | |
Write-Verbose "Searching in: $location" | |
try { | |
# Direct path check first (faster) | |
$directPath = Join-Path $location $ExeName | |
if (Test-Path $directPath) { | |
Write-Verbose "Found direct path: $directPath" | |
if ($ShowProgress) { Write-Progress -Activity "Searching for $ExeName" -Completed } | |
return @{ | |
ExecutablePath = $directPath | |
DirectoryPath = $location | |
Source = $locationSource | |
SymlinkPath = $null | |
} | |
} | |
# Recursive search | |
$foundExe = Get-ChildItem -Path $location -Filter $ExeName -Recurse -ErrorAction Stop | Select-Object -First 1 | |
if ($foundExe) { | |
Write-Verbose "Found recursive: $($foundExe.FullName)" | |
if ($ShowProgress) { Write-Progress -Activity "Searching for $ExeName" -Completed } | |
return @{ | |
ExecutablePath = $foundExe.FullName | |
DirectoryPath = $foundExe.DirectoryName | |
Source = $locationSource | |
SymlinkPath = $null | |
} | |
} | |
} | |
catch { | |
Write-Verbose "Access denied or error in $location`: $($_.Exception.Message)" | |
} | |
} | |
if ($ShowProgress) { Write-Progress -Activity "Searching for $ExeName" -Completed } | |
Write-Verbose "$ExeName not found in any location" | |
return $null | |
} | |
# Function 2: Check if executable exists in PATH or as alias | |
function Test-ExecutableInPath { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$ExeName | |
) | |
$result = @{ | |
ExistsInPath = $false | |
ExistsAsAlias = $false | |
ExistsAsCommand = $false | |
PathLocation = $null | |
AliasDefinition = $null | |
WorkingPath = $null | |
} | |
Write-Verbose "Checking if $ExeName exists in PATH or as alias..." | |
# Check if it's available as a command (which includes PATH and aliases) | |
$command = Get-Command $ExeName -ErrorAction SilentlyContinue | |
if ($command) { | |
$result.ExistsAsCommand = $true | |
$result.WorkingPath = $command.Source | |
switch ($command.CommandType) { | |
'Application' { | |
$result.ExistsInPath = $true | |
$result.PathLocation = $command.Source | |
Write-Verbose "$ExeName found in PATH: $($command.Source)" | |
} | |
'Alias' { | |
$result.ExistsAsAlias = $true | |
$result.AliasDefinition = $command.Definition | |
Write-Verbose "$ExeName found as alias: $($command.Definition)" | |
} | |
default { | |
Write-Verbose "$ExeName found as $($command.CommandType): $($command.Source)" | |
} | |
} | |
} | |
# Double-check PATH manually | |
if (-not $result.ExistsInPath) { | |
$pathDirs = $env:Path -split ';' | Where-Object { $_ -and (Test-Path $_ -ErrorAction SilentlyContinue) } | |
foreach ($dir in $pathDirs) { | |
$fullPath = Join-Path $dir $ExeName | |
if (Test-Path $fullPath -ErrorAction SilentlyContinue) { | |
$result.ExistsInPath = $true | |
$result.PathLocation = $fullPath | |
Write-Verbose "$ExeName found manually in PATH: $fullPath" | |
break | |
} | |
} | |
} | |
return $result | |
} | |
# Function 3: Add directory to PATH | |
function Add-DirectoryToPath { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$DirectoryPath, | |
[switch]$Force | |
) | |
Write-Verbose "Adding directory to PATH: $DirectoryPath" | |
# Get current user PATH | |
$userPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User) | |
$pathDirs = $userPath -split ';' | Where-Object { $_ } | |
# Check if directory already exists in PATH | |
$existsInPath = $pathDirs | Where-Object { $_.TrimEnd('\') -eq $DirectoryPath.TrimEnd('\') } | |
if ($existsInPath -and -not $Force) { | |
Write-Verbose "Directory already exists in PATH" | |
return @{ | |
Added = $false | |
Reason = "Already exists in PATH" | |
UpdatedPath = $userPath | |
} | |
} | |
# Remove existing entry if Force is used | |
if ($existsInPath -and $Force) { | |
Write-Verbose "Force flag used - removing existing PATH entry" | |
$pathDirs = $pathDirs | Where-Object { $_.TrimEnd('\') -ne $DirectoryPath.TrimEnd('\') } | |
} | |
# Add new directory | |
$newPathDirs = $pathDirs + $DirectoryPath | |
$newPath = $newPathDirs -join ';' | |
# Set environment variable | |
[System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User) | |
# Update current session | |
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine) + ";" + $newPath | |
Write-Verbose "Successfully added to PATH: $DirectoryPath" | |
return @{ | |
Added = $true | |
Reason = if ($Force) { "Force added" } else { "Added normally" } | |
UpdatedPath = $newPath | |
} | |
} | |
# Function 4: Main function to set executable in PATH | |
function Set-ExecutableInPath { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$ExeName, | |
[switch]$Force, | |
[switch]$ShowProgress = $true | |
) | |
Write-Host "Processing executable: $ExeName" -ForegroundColor Cyan | |
# Step 1: Check if executable already works | |
Write-Verbose "Step 1: Checking current availability..." | |
$pathCheck = Test-ExecutableInPath -ExeName $ExeName | |
if ($pathCheck.ExistsAsCommand -and -not $Force) { | |
Write-Host "[OK] $ExeName is already available" -ForegroundColor Green | |
if ($pathCheck.ExistsInPath) { | |
Write-Host " Location: $($pathCheck.PathLocation)" -ForegroundColor Gray | |
} elseif ($pathCheck.ExistsAsAlias) { | |
Write-Host " Alias: $($pathCheck.AliasDefinition)" -ForegroundColor Gray | |
} | |
return @{ | |
Success = $true | |
Action = "Already available" | |
ExecutablePath = $pathCheck.WorkingPath | |
AddedToPath = $false | |
} | |
} | |
# Step 2: Find executable location | |
Write-Verbose "Step 2: Searching for executable..." | |
$location = Find-ExecutableLocation -ExeName $ExeName -ShowProgress:$ShowProgress | |
if (-not $location) { | |
Write-Host "[ERROR] $ExeName not found in any location" -ForegroundColor Red | |
return @{ | |
Success = $false | |
Action = "Not found" | |
ExecutablePath = $null | |
AddedToPath = $false | |
} | |
} | |
Write-Host "[OK] Found $ExeName" -ForegroundColor Green | |
Write-Host " Location: $($location.ExecutablePath)" -ForegroundColor Gray | |
Write-Host " Source: $($location.Source)" -ForegroundColor Gray | |
# Step 3: Add to PATH if needed | |
Write-Verbose "Step 3: Adding directory to PATH..." | |
$pathResult = Add-DirectoryToPath -DirectoryPath $location.DirectoryPath -Force:$Force | |
if ($pathResult.Added) { | |
Write-Host "[OK] Added to PATH: $($location.DirectoryPath)" -ForegroundColor Green | |
} else { | |
Write-Host "[INFO] PATH not modified: $($pathResult.Reason)" -ForegroundColor Yellow | |
} | |
return @{ | |
Success = $true | |
Action = if ($pathResult.Added) { "Found and added to PATH" } else { "Found but not added" } | |
ExecutablePath = $location.ExecutablePath | |
DirectoryPath = $location.DirectoryPath | |
AddedToPath = $pathResult.Added | |
Source = $location.Source | |
SymlinkPath = $location.SymlinkPath | |
} | |
} | |
# Function 5: Refresh environment variables | |
function Update-SessionEnvironment { | |
Write-Verbose "Refreshing environment variables for current session..." | |
$machinePath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine) | |
$userPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User) | |
$env:Path = "$machinePath;$userPath" | |
Write-Host "[OK] Environment variables refreshed" -ForegroundColor Green | |
} | |
# Main execution logic | |
if ($ExeName) { | |
$result = Set-ExecutableInPath -ExeName $ExeName -Force:$Force -ShowProgress:$true | |
if ($result.Success) { | |
Write-Host "`nSummary:" -ForegroundColor Cyan | |
Write-Host " Action: $($result.Action)" -ForegroundColor White | |
Write-Host " Executable: $($result.ExecutablePath)" -ForegroundColor White | |
if ($result.AddedToPath) { | |
Write-Host " Added to PATH: $($result.DirectoryPath)" -ForegroundColor White | |
Write-Host "`n[INFO] You may need to restart your terminal for PATH changes to take effect" -ForegroundColor Yellow | |
} | |
} | |
Update-SessionEnvironment | |
} else { | |
Write-Host "WinGet Executable PATH Manager" -ForegroundColor Cyan | |
Write-Host "Usage examples:" -ForegroundColor White | |
Write-Host " .\Find-ExecutableAndSetPath.ps1 -ExeName 'docker.exe' -Verbose" -ForegroundColor Gray | |
Write-Host " Set-ExecutableInPath -ExeName 'git.exe' -Force" -ForegroundColor Gray | |
Write-Host " Find-ExecutableLocation -ExeName 'python.exe'" -ForegroundColor Gray | |
Write-Host " Test-ExecutableInPath -ExeName 'node.exe'" -ForegroundColor Gray | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Find-ExecutableAndSetPath.ps1
This PowerShell script intelligently finds and configures executables in Windows PATH with priority-based searching. It starts with WinGet-managed applications and falls back to common Windows installation directories, automatically managing PATH entries without requiring administrative privileges.
Usage
Options
-ExeName <String>
: The name of the executable file to locate and configure in PATH. Must include file extension (e.g., "docker.exe", "git.exe", "python.exe").-Force
: Forces overwrite of existing PATH entries even if the executable is already available. Useful for repairing broken PATH entries or ensuring WinGet versions take priority.-Verbose
: Enables comprehensive verbose output for detailed logging and troubleshooting.Running the Script from the Internet:
Use
Invoke-RestMethod
to download and execute the script. Here is how you can do it:Note
If it doesn't work, then try to Set-ExecutionPolicy via PowerShell (Admin)
Note
To execute the script from the Internet with additional parameters, please run
Example of issue
Example of execution
Example of execution with verbose output and force flag
Example of execution for non-existent executable
Key Features
Search Priority Order
Safety Features
Functions
Find-ExecutableLocation
ExeName
: The executable name to search forShowProgress
: Enable/disable progress bar display$location = Find-ExecutableLocation -ExeName "docker.exe" -ShowProgress $true
Test-ExecutableInPath
ExeName
: The executable name to check$pathStatus = Test-ExecutableInPath -ExeName "git.exe"
Add-DirectoryToPath
DirectoryPath
: The directory path to add to PATHForce
: Force overwrite existing entriesAdd-DirectoryToPath -DirectoryPath "C:\Tools\bin" -Force
Set-ExecutableInPath
ExeName
: The executable name to configureForce
: Force override existing PATH entriesShowProgress
: Enable progress reportingSet-ExecutableInPath -ExeName "python.exe" -Force -ShowProgress $true
Update-SessionEnvironment
Update-SessionEnvironment
Supported Package Managers
Requirements
Related Links