Skip to content

Instantly share code, notes, and snippets.

@matfax
Last active July 31, 2025 14:11
Show Gist options
  • Select an option

  • Save matfax/e61e84cf186681ad1f9ac3b25f517df4 to your computer and use it in GitHub Desktop.

Select an option

Save matfax/e61e84cf186681ad1f9ac3b25f517df4 to your computer and use it in GitHub Desktop.
PowerShell CommandNotFoundHandler with WSL and Git Alias Fallback
# PowerShell Profile - Add this to your $PROFILE file
# Enable experimental features if needed (PowerShell 7.0-7.4)
if ($PSVersionTable.PSVersion -lt [Version]"7.5.0") {
try {
Enable-ExperimentalFeature PSFeedbackProvider -ErrorAction SilentlyContinue
Enable-ExperimentalFeature PSCommandNotFoundSuggestion -ErrorAction SilentlyContinue
} catch {
# Features may already be enabled or not available
}
}
# Initialize caches
if ($null -eq $script:gitAliases) {
$script:gitAliases = @()
}
if ($null -eq $script:wslCommandCache) {
$script:wslCommandCache = @{}
}
# Hybrid CommandNotFoundAction Handler
$ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = {
param($CommandName, $CommandLookupEventArgs)
# Git Aliases
if ($script:gitAliases.Count -eq 0) {
try {
$script:gitAliases = (git config --global -l) |
Where-Object { $_ -match '^alias\.(.+?)=' } |
ForEach-Object { $matches[1] }
} catch {
$script:gitAliases = @()
}
}
if ($CommandName -in $script:gitAliases) {
Write-Host "Git Alias Found: Running 'git $CommandName'" -ForegroundColor Cyan
$CommandLookupEventArgs.CommandScriptBlock = [scriptblock]::Create("git $CommandName")
$CommandLookupEventArgs.StopSearch = $true
return
}
# WSL Fallback with Caching
try {
# Check cache first
if (-not $script:wslCommandCache.ContainsKey($CommandName)) {
# Get the full path to the command using bash with proper environment
$whichOutput = wsl.exe bash -l -c "which $CommandName" 2>$null
if ($LASTEXITCODE -eq 0 -and $whichOutput) {
# Split by newlines and take the first path (the one that would execute)
$firstPath = ($whichOutput -split "`n" | Where-Object { $_.Trim() } | Select-Object -First 1)
if ($firstPath) {
$script:wslCommandCache[$CommandName] = $firstPath.Trim()
} else {
$script:wslCommandCache[$CommandName] = $null
}
} else {
$script:wslCommandCache[$CommandName] = $null
}
}
$cachedPath = $script:wslCommandCache[$CommandName]
if ($cachedPath) {
# Define a list of potentially destructive commands to block
$unsafeWslCommands = @("rm", "mkfs", "fdisk", "dd", "chmod", "chown")
if ($CommandName -in $unsafeWslCommands) {
Write-Warning "WSL command '$CommandName' is potentially unsafe and was blocked."
$CommandLookupEventArgs.StopSearch = $true
return
}
Write-Host "PowerShell command not found. Running from WSL..." -ForegroundColor Cyan
# Define the script block to execute the command in WSL using cached path
$CommandLookupEventArgs.CommandScriptBlock = {
# Force the result into an array using @() to prevent strings from being split during splatting
$wslArgs = @($args | ForEach-Object {
# Check if an argument is a Windows path (e.g., C:\...)
if ($_ -match '^[A-Za-z]:') {
# Convert the Windows path to a WSL path (e.g., /mnt/c/...)
$driveLetter = $_.Substring(0,1).ToLower()
$path = $_.Substring(2) -replace '\\', '/'
"/mnt/$driveLetter$path"
} else {
# Pass other arguments through unchanged
$_
}
})
# Execute using the cached full path
wsl.exe -- $cachedPath @wslArgs
}.GetNewClosure()
$CommandLookupEventArgs.StopSearch = $true
return
}
} catch {
# WSL not available or an error occurred, continue to built-in suggestions
}
# Let PowerShell's built-in suggestion system work if no other handler has run
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment