PowerShell 'Suggestion Mode'

Mon, Sep 8, 2008 3-minute read

One bit of feedback we frequently get is that PowerShell’s learning curve has some steeper bumps than we would like. Or simply, is strongly affected by habits learned from other languages or shells.

Interestingly enough, many of these problems aren’t new to us – we just don’t have a good way (aside from help) of exposing them to the user. This was something I touched on in the footnotes of a blog in 2005, and started implementing personally shortly after that. Here’s an example of its output:

[C:\temp]
PS:14 > "c:\temp\has space\test.ps1"
c:\temp\has space\test.ps1

Suggestion: Did you mean to run the command in quotes?  If so, try using & "<command>"

The core of this is implemented by a script, Get-TrainingSuggestion.ps1. It retrieves the last item from your history, and runs a bunch of regular expression comparisons against it. If it finds a match in the list of training rules, it outputs the suggestion that corresponds to that rule.

############################################################################## 
## Get-TrainingSuggestion.ps1 
## Retrieve a training suggestion (if applicable) for the last command 
## you typed. 
############################################################################## 
$history = get-history -Count 1 

$suggestions = @{ 
   "/\?"="To get help on a topic, use -? instead of /?.`nAlternatively, use get-help <command>."; 
   ".length"="Try using the .Count property instead of the .Length property.  Although .Length " + 
       "often works, .Count is the more consistent way to get the number of objects in a collection."; 
   ";[ \t]*$"="Semicolons are not required as line terminators in PowerShell.  Try your command without one."; 
   "Regex.*]::Match"="PowerShell's -match evaluator is much more efficent than calling it through the .NET API."; 
   "^`".*`"$"="Did you mean to run the command in quotes?  If so, try using & `"<command>`""; 
   "start "="Are you trying to run the program associated with that path?  If so, try invoke-item <path>"; 
   "%.*%"="To access environment variables, try `"`$env:variable`" instead of `"%variable%`""; 
   "ren[^ ]* [^ ]+ .*:\.*"="Rename-item takes only a name as its second argument.  Unless you want the " + 
       "name to have those path characters, do not include them."; 
   "dir.*/s.*"="To get a recursive directory listing, try `"dir . * -rec`"."; 
   "dir.*/ad"="To get a list of directories, try `"dir | where { `$_.PsIsContainer }`"" 
   "^get-childitem.*/s.*$"="To get a recursive directory listing, try `"get-childitem . * -rec`"."; 
   "^grep"="To search files for text patterns, use the match-string cmdlet."; 
} 

$helpMatches = "" 

if($history) 
{ 
   $lastCommand = $history.CommandLine 

   ## Get the suggestions from the baked list 
   foreach($key in $suggestions.Keys) 
   { 
    if($lastCommand -match $key) 
    { 
        $helpMatches += "`nSuggestion: " + $suggestions[$key] 
    } 
   } 
} 

$helpMatches 

To use this, simply update your PROMPT function to call Get-TrainingSuggestion.ps1.

function prompt
{
    $suggestion = Get-TrainingSuggestion
    if($suggestion)
    {
        Write-Host $suggestion
        Write-Host
    }

    "PS >"
}