Skip to content

Commit 84feb09

Browse files
ericchansenCopilot
andcommitted
feat: initial PSCommandHelper module
PowerShell 7 module that detects bash/Linux commands and suggests PowerShell equivalents with educational explanations. Uses the CommandNotFoundAction hook for non-invasive, learn-by-doing experience. - 75+ bash-to-PowerShell command mappings - Colorful emoji-rich suggestions with explanations - Get-CommandMapping for browsing/searching the mapping table - Enable/Disable toggle for the hook - One-command installer with PROFILE integration - 10 Pester 5 tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0 parents  commit 84feb09

11 files changed

Lines changed: 637 additions & 0 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore common files
2+
*.bak
3+
*.tmp
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@{
2+
RootModule = 'PSCommandHelper.psm1'
3+
ModuleVersion = '0.1.0'
4+
GUID = 'a3f8b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c'
5+
Author = 'Eric Hansen'
6+
Description = 'Learn PowerShell by doing. Detects bash/Linux commands and suggests PowerShell equivalents with explanations.'
7+
PowerShellVersion = '7.0'
8+
9+
FunctionsToExport = @(
10+
'Enable-PSCommandHelper'
11+
'Disable-PSCommandHelper'
12+
'Get-CommandMapping'
13+
)
14+
15+
CmdletsToExport = @()
16+
VariablesToExport = @()
17+
AliasesToExport = @()
18+
19+
PrivateData = @{
20+
PSData = @{
21+
Tags = @('PowerShell', 'Learning', 'Bash', 'Linux', 'Helper', 'Education')
22+
ProjectUri = ''
23+
}
24+
}
25+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PSCommandHelper - Root Module
2+
# Dot-source all private and public functions
3+
4+
$Private = @(Get-ChildItem -Path "$PSScriptRoot\Private\*.ps1" -ErrorAction SilentlyContinue)
5+
$Public = @(Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" -ErrorAction SilentlyContinue)
6+
7+
foreach ($file in @($Private + $Public)) {
8+
try {
9+
. $file.FullName
10+
}
11+
catch {
12+
Write-Error "Failed to import $($file.FullName): $_"
13+
}
14+
}
15+
16+
# Export only public functions
17+
Export-ModuleMember -Function $Public.BaseName

PSCommandHelper/Private/CommandMap.ps1

Lines changed: 100 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
function Format-Suggestion {
2+
[CmdletBinding()]
3+
param(
4+
[Parameter(Mandatory)]
5+
[hashtable]$Mapping,
6+
7+
[Parameter(Mandatory)]
8+
[string]$OriginalCommand
9+
)
10+
11+
$esc = [char]27
12+
13+
# Colors
14+
$reset = "$esc[0m"
15+
$bold = "$esc[1m"
16+
$dim = "$esc[2m"
17+
$yellow = "$esc[33m"
18+
$green = "$esc[32m"
19+
$cyan = "$esc[36m"
20+
$magenta = "$esc[35m"
21+
$bgDark = "$esc[48;5;236m"
22+
23+
$divider = "$dim$('' * 60)$reset"
24+
25+
Write-Host ""
26+
Write-Host $divider
27+
Write-Host " 💡 $yellow${bold}PSCommandHelper$reset"
28+
Write-Host ""
29+
Write-Host " $dim You typed:$reset $yellow$OriginalCommand$reset"
30+
Write-Host " $dim Try this:$reset $green${bold}$($Mapping.PowerShell)$reset"
31+
Write-Host ""
32+
Write-Host " $cyan$($Mapping.Explanation)$reset"
33+
34+
if ($Mapping.Example) {
35+
Write-Host ""
36+
Write-Host " ${dim}Example:$reset"
37+
Write-Host " $magenta$bold> $($Mapping.Example)$reset"
38+
}
39+
40+
Write-Host $divider
41+
Write-Host ""
42+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
function Disable-PSCommandHelper {
2+
<#
3+
.SYNOPSIS
4+
Deactivates the PSCommandHelper command-not-found handler.
5+
.DESCRIPTION
6+
Removes the CommandNotFoundAction handler that was registered by Enable-PSCommandHelper.
7+
#>
8+
[CmdletBinding()]
9+
param()
10+
11+
if ($script:PSCommandHelperHandler) {
12+
$current = $ExecutionContext.InvokeCommand.CommandNotFoundAction
13+
if ($current) {
14+
$newHandler = [Delegate]::Remove($current, $script:PSCommandHelperHandler)
15+
$ExecutionContext.InvokeCommand.CommandNotFoundAction = $newHandler
16+
}
17+
$script:PSCommandHelperHandler = $null
18+
Write-Host "🔴 PSCommandHelper disabled." -ForegroundColor Yellow
19+
}
20+
else {
21+
Write-Verbose "PSCommandHelper is not currently enabled."
22+
}
23+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
function Enable-PSCommandHelper {
2+
<#
3+
.SYNOPSIS
4+
Activates the PSCommandHelper to suggest PowerShell equivalents for bash commands.
5+
.DESCRIPTION
6+
Registers a CommandNotFoundAction handler in PowerShell 7+ that intercepts
7+
unrecognized commands, matches them against a bash-to-PowerShell mapping table,
8+
and displays a colorful educational suggestion.
9+
#>
10+
[CmdletBinding()]
11+
param()
12+
13+
if ($PSVersionTable.PSVersion.Major -lt 7) {
14+
Write-Warning "PSCommandHelper requires PowerShell 7 or later. Current version: $($PSVersionTable.PSVersion)"
15+
return
16+
}
17+
18+
# Prevent double-registration
19+
if ($script:PSCommandHelperHandler) {
20+
Write-Verbose "PSCommandHelper is already enabled."
21+
return
22+
}
23+
24+
# Pre-load data and function references for the closure
25+
# (the handler runs outside module scope, so private functions aren't visible)
26+
$map = Get-BashToPowerShellMap
27+
$formatFunc = Get-Command Format-Suggestion
28+
29+
$handler = {
30+
param($sender, [System.Management.Automation.CommandLookupEventArgs]$eventArgs)
31+
32+
$commandName = $eventArgs.CommandName
33+
34+
$matched = $null
35+
36+
# Exact match first
37+
$matched = $map | Where-Object { $_.Bash -eq $commandName } | Select-Object -First 1
38+
39+
# Fallback: match on the base command word
40+
if (-not $matched) {
41+
$baseCmd = ($commandName -split '\s+')[0]
42+
$matched = $map | Where-Object { $_.Bash -eq $baseCmd } | Select-Object -First 1
43+
}
44+
45+
if ($matched) {
46+
& $formatFunc -Mapping $matched -OriginalCommand $commandName
47+
}
48+
}
49+
50+
$handler = $handler.GetNewClosure()
51+
52+
# Cast to the proper delegate type for CommandNotFoundAction
53+
$typedHandler = $handler -as [System.EventHandler[System.Management.Automation.CommandLookupEventArgs]]
54+
55+
$current = $ExecutionContext.InvokeCommand.CommandNotFoundAction
56+
if ($current) {
57+
$ExecutionContext.InvokeCommand.CommandNotFoundAction = [Delegate]::Combine($current, $typedHandler)
58+
}
59+
else {
60+
$ExecutionContext.InvokeCommand.CommandNotFoundAction = $typedHandler
61+
}
62+
63+
# Store typed delegate reference for clean removal
64+
$script:PSCommandHelperHandler = $typedHandler
65+
66+
Write-Host "✅ PSCommandHelper enabled. Type a bash command to see the PowerShell equivalent!" -ForegroundColor Green
67+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
function Get-CommandMapping {
2+
<#
3+
.SYNOPSIS
4+
Browse or search the bash-to-PowerShell command mapping table.
5+
.DESCRIPTION
6+
Lists all available bash → PowerShell command mappings, or filters them by a search term.
7+
Useful for proactive learning — browse the table to discover PowerShell equivalents.
8+
.PARAMETER Search
9+
Optional search term to filter mappings. Searches both bash and PowerShell columns.
10+
.EXAMPLE
11+
Get-CommandMapping
12+
Lists all mappings.
13+
.EXAMPLE
14+
Get-CommandMapping -Search "grep"
15+
Shows mappings related to grep.
16+
.EXAMPLE
17+
Get-CommandMapping -Search "file"
18+
Shows mappings with "file" in any field.
19+
#>
20+
[CmdletBinding()]
21+
param(
22+
[Parameter(Position = 0)]
23+
[string]$Search
24+
)
25+
26+
$map = Get-BashToPowerShellMap
27+
28+
if ($Search) {
29+
$map = $map | Where-Object {
30+
$_.Bash -like "*$Search*" -or
31+
$_.PowerShell -like "*$Search*" -or
32+
$_.Explanation -like "*$Search*"
33+
}
34+
}
35+
36+
if (-not $map) {
37+
Write-Host "No mappings found for '$Search'." -ForegroundColor Yellow
38+
return
39+
}
40+
41+
$esc = [char]27
42+
$reset = "$esc[0m"
43+
$bold = "$esc[1m"
44+
$yellow = "$esc[33m"
45+
$green = "$esc[32m"
46+
$cyan = "$esc[36m"
47+
$dim = "$esc[2m"
48+
49+
Write-Host ""
50+
Write-Host " ${bold}📖 Bash → PowerShell Mappings$reset"
51+
if ($Search) {
52+
Write-Host " ${dim}Filtered by: '$Search'$reset"
53+
}
54+
Write-Host " $dim$('' * 56)$reset"
55+
56+
foreach ($entry in $map) {
57+
Write-Host " $yellow$($entry.Bash.PadRight(20))$reset$green${bold}$($entry.PowerShell)$reset"
58+
Write-Host " $dim$($entry.Explanation)$reset"
59+
Write-Host ""
60+
}
61+
62+
Write-Host " $dim$($map.Count) mapping(s) shown.$reset"
63+
Write-Host ""
64+
}

README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# PSCommandHelper
2+
3+
> Learn PowerShell by doing. When you type a bash command that doesn't exist in PowerShell, PSCommandHelper suggests the PowerShell equivalent — with an explanation.
4+
5+
## What it does
6+
7+
When you type a bash/Linux command in PowerShell 7 that doesn't resolve (like `rm -rf`, `grep`, `curl`, etc.), PSCommandHelper intercepts the error and shows you:
8+
9+
```
10+
────────────────────────────────────────────────────────────
11+
💡 PSCommandHelper
12+
13+
You typed: rm -rf
14+
Try this: Remove-Item -Recurse -Force
15+
16+
Remove-Item deletes files/folders. -Recurse handles subdirectories, -Force skips confirmation.
17+
18+
Example:
19+
> Remove-Item ./build -Recurse -Force
20+
────────────────────────────────────────────────────────────
21+
```
22+
23+
It **does not** run the command for you — the goal is to help you learn, not to auto-correct.
24+
25+
## Installation
26+
27+
### Quick install
28+
29+
```powershell
30+
git clone <repo-url> powershell-helper
31+
cd powershell-helper
32+
.\install.ps1
33+
```
34+
35+
This copies the module to your `Documents\PowerShell\Modules` folder and adds it to your `$PROFILE`.
36+
37+
### Manual install
38+
39+
1. Copy the `PSCommandHelper` folder to a directory in your `$env:PSModulePath`
40+
2. Add these lines to your `$PROFILE`:
41+
42+
```powershell
43+
Import-Module PSCommandHelper
44+
Enable-PSCommandHelper
45+
```
46+
47+
## Usage
48+
49+
Once installed, just use PowerShell normally. When you type a bash command that isn't recognized, you'll see a helpful suggestion.
50+
51+
### Browse all mappings
52+
53+
```powershell
54+
Get-CommandMapping
55+
```
56+
57+
### Search for a specific command
58+
59+
```powershell
60+
Get-CommandMapping -Search "grep"
61+
Get-CommandMapping -Search "file"
62+
```
63+
64+
### Temporarily disable
65+
66+
```powershell
67+
Disable-PSCommandHelper
68+
```
69+
70+
### Re-enable
71+
72+
```powershell
73+
Enable-PSCommandHelper
74+
```
75+
76+
## Covered commands
77+
78+
The built-in mapping table covers **75+ bash commands** across these categories:
79+
80+
| Category | Examples |
81+
|----------|----------|
82+
| **File operations** | `rm`, `cp`, `mv`, `mkdir`, `touch`, `cat`, `ls`, `find`, `chmod`, `ln -s` |
83+
| **Text processing** | `grep`, `sed`, `awk`, `head`, `tail`, `wc`, `sort`, `uniq`, `cut`, `tr`, `diff` |
84+
| **System/process** | `ps`, `kill`, `top`, `df`, `du`, `env`, `export`, `which`, `whoami` |
85+
| **Networking** | `curl`, `wget`, `ping`, `ifconfig`, `netstat`, `ssh`, `scp`, `nslookup` |
86+
| **Shell/misc** | `echo`, `clear`, `history`, `alias`, `man`, `sudo`, `source`, `tar`, `zip` |
87+
| **Piping/redirection** | `> file`, `>> file`, `2>&1`, `/dev/null` |
88+
89+
## Requirements
90+
91+
- **PowerShell 7.0+** (uses `CommandNotFoundAction` which is not available in Windows PowerShell 5.1)
92+
93+
## Running tests
94+
95+
```powershell
96+
Invoke-Pester ./tests/PSCommandHelper.Tests.ps1
97+
```
98+
99+
## How it works
100+
101+
PowerShell 7 exposes the `$ExecutionContext.InvokeCommand.CommandNotFoundAction` event. When the shell can't find a command by name, this event fires **before** the error is shown. PSCommandHelper registers a handler that:
102+
103+
1. Receives the unrecognized command name
104+
2. Looks it up in a hashtable of bash → PowerShell mappings
105+
3. Displays a colorful, educational suggestion
106+
4. Lets the original error propagate (so you know the command didn't run)
107+
108+
## License
109+
110+
MIT

0 commit comments

Comments
 (0)