A History Browsing Prompt

Fri, Jul 8, 2005 5-minute read

Now that we’ve come up to speed with the new beta, it’s time to continue our journey into customizing the prompt. It’s been almost a month since I promised it, and a lot of you are already doing great things with your custom profile.

For reference sake, this is our current prompt function, as defined in profile-custom.msh:

function prompt  
   $currentDirectory = Get-Location  
   "MSH $currentDirectory >"  

That prompt looks like:

MSH C:\Documents and Settings\Lee >

Ideally, we want it to act more like this:

MSH:1 C:\Documents and Settings\Lee ># some command
MSH:2 C:\Documents and Settings\Lee ># some other command
MSH:3 C:\Documents and Settings\Lee >"Hello World"
Hello World
MSH:4 C:\Documents and Settings\Lee ># yet another command
MSH:5 C:\Documents and Settings\Lee >invoke-history 3
"Hello World"
Hello World

By giving the history ID in the prompt, you can re-invoke those commands without having to use your keyboard’s arrow keys to cycle through the history buffer. Notice how I was able to use the “invoke-history” cmdlet to execute the command I saw on that line.

So how do we go about updating the prompt to include the line number?

Largely, the secret lays in the “get-history” cmdlet. Try it now, and see what it shows:

MSH:6 C:\Documents and Settings\Lee >get-history
Id $_ CommandLine
-- -- -----------
1  C  # some command
2  C  # some other command
3  C  "Hello World"
4  C  # yet another command
5  C  "Hello World"

At first blush, it seems as though the simplest way to get your history count would be something like this, knowing that get-history returns a list of history items. We already know how to get the count of items in a list:

MSH:7 C:\Documents and Settings\Lee >(get-history).Count

That’s a great first guess, but it unfortunately suffers from two major flaws, and a minor flaw.

Major Flaw 1: get-history’s -count parameter.

By default, get-history only returns the last 30 items in your history. You could work around that problem by specifying some huge value (ie: 9999) for the count parameter, but there is a cleaner solution: leverage the Id of the last command executed.

Major Flaw 2: get-history doesn’t always return an array.

Since cmdlets can write any number of objects to the pipeline, the infrastructure decides how to output the results on a case-by-case basis. The first time you call get-history from a clean shell, you have no history. In that situation, the cmdlet returns no items – so the shell doesn’t write anything out. The second time you call get-history (from the clean shell,) your history contains only one item. In that case, the shell writes the individual object out. From the third time on, your history contains two or more items. The get-history cmdlet dutifully writes them out, and the shell finally returns them as an array:

MSH:1 C:\Documents and Settings\Lee >get-history
MSH:2 C:\Documents and Settings\Lee >get-history
Id $_ CommandLine
-- -- -----------
1  C  get-history

MSH:3 C:\Documents and Settings\Lee >get-history
Id $_ CommandLine
-- -- -----------
1  C  get-history
2  C  get-history

Of course, we have a way of working around this as well, or I wouldn’t bring it up!

Minor Flaw 3: We need to display a history Id from the future.

The Id of the last command (or total history count) isn’t exactly what you want. If you use the Id of the last command (or total history count,) the number will not represent the command you are about to execute. In that case, you won’t be able to use the numbers as parameters to invoke-history.

Here’s the final solution that works around these problems, heavily commented for clarity:

function prompt  
   ## Get the history. Since the history may be either empty,  
   ## a single item or an array, the @() syntax ensures  
   ## that Monad treats it as an array  
   $history = @(get-history)  
   ## If there are any items in the history, find out the  
   ## Id of the final one.  
   ## Monad defaults the $lastId variable to '0' if this  
   ## code doesn't execute.  
   if($history.Count -gt 0)  
      $lastItem = $history[$history.Count - 1]  
      $lastId = $lastItem.Id  

   ## The command that we're currently entering on the prompt  
   ## will be next in the history. Because of that, we'll  
   ## take the last history Id and add one to it.  
   $nextCommand = $lastId + 1

   ## Get the current location  
   $currentDirectory = get-location

   ## And create a prompt that shows the command number,  
   ## and current location  
   "MSH:$nextCommand $currentDirectory >"  

That gets you a very functional history-browsing prompt, and is just about the one that I use on all of my machines. In an upcoming post, I’ll talk about some other things you might want to do with your prompt.

And for those of you who are becoming more familiar with the shell, one of my older questions still stands:

What idioms from other languages / shells do you find the hardest to un-train yourself from? For example, “cd..” is a valid command in DOS, but not MSH. What have you found the hardest to discover? What did you do to try and discover it? I’m looking for things like,

“I couldn’t figure out how to use a hash table, so I typed get-command *hashtable*”

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]