In PowerShell there is a callback ("Action") that is called when a command lookup fails through normal means. The purpose of this callback is to allow you to add your own command-resolution, and there are a lot of community hacks out there already:
- You could add support for new command types, like
- You could implement a PSScriptPath separate from the
$Env:Path
variable. - You could even add support for binary numeric literals
To be clear: some of those options are awesome -- and in particular, for developers, the git aliases and WSL commands are potentially really helpful. There's nothing wrong with any of that.
However, I don't think I'd want any of them to replace the default error message in PowerShell.
I'm going to show you one here which I think might lead to something generally useful. It's based on the way zsh and some other shells implement typo detection. If you type ehco
as a command, it will offer to create an alias for you from that to echo
, and you can accept, and run that command just once, or define an alias that will persist. That seems like it would be useful, so ...
First, we need a way to find a similar command. I've written a simple Find-ClosestCommand below. It's based on an edit distance algorithm
called Levenshtein distance
. I found several implementations of this in PowerShell, the fastest was
this one from @gravejester's Communary.PASM module. Of course, using edit distance might end up being too simple, in the long run, but for an experiment, it will work.
As background: edit distance measures the difference between two strings based on the number of edits
(insertion, deletion, or substitution) needed to make them identical. Find-ClosestCommand
computes the edit distance from
the failed command to each command name available on the system, and returns just one command: the one with
the smallest edit distance (or the first one it finds that is only one edit away).
Given Find-ClosestCommand
we can implement a CommandNotFoundAction
quite simply, but first,
The callback has two parameters:
- The name of the command which was not found
- A
CommandLookupEventArgs
where you can set theCommand
if you're able to resolve it
The action is expected to try to find a command, and if it does, to set the Command
propert and the StopSearch
property on the CommandLookupEventArgs
-- after which PowerShell will simply run that command (with all the parameters in the original command line). That is, it could be invisible to the user (and is, in the implementations mentioned above).
Also, if PowerShell doesn't find a matching command, it searches again with .\
prefixed, as part of error handling, because if it finds a file in the local path, it will show a different error.
Rather than doing something like insulting the user, we want to try to help with typos and bad guesses.
The simplest possible action would just be this:
$ExecutionContext.InvokeCommand.CommandNotFoundAction = {
param($Name, $EventArgs)
$EventArgs.Command = Find-ClosestCommand $CommandName
$EventArgs.StopSearch = $true
}
The problem is, PowerShell is going to run whatever you find -- and in some cases, what you find might not match very well.
In order to avoid accidentally formatting a hard drive or something crazy like that, we want that confirmation from the user
like what zsh does.
For something like this, I like to use a switch
statement wrapped around a PromptForChoice. Check out my CommandNotFoundAction below.
Basically, we're just calling Get-ClosestCommand and then asking the user whether the command we found is the right one, and whether they want to remember that typo for the future.
Because this is really just an example on a blog post, I did not do anything to persist the aliases, but we could easily:
- Add a
aliases.ps1
file in the home profile folder - Dot-source that from the profile
- Append lines to it whenever the user chooses
Always
Trying to come up with deliberate typos for testing was frustrating, but in the process I have already found a few improvements that should be made:
- It should prefer missing initializations (i.e.
sc
for Set-Content) before anything else. - There should be a limit on the distance. E.g. If the distance is greater than the input, there's basically no similarity, and we shouldn't prompt.