James O'Neill's Blog

January 28, 2012

Adding Persistent history to PowerShell

Filed under: Uncategorized — jamesone111 @ 7:19 pm

The Doctor: “You’re not are you? Tell me you’re not Archaeologists”
River Song: “Got a problem with Archaeologists ?”
The Doctor: “I’m a time traveller. I point and laugh at Archaeologists”

I’ve said to several people that my opinion of Linux has changed since I left Microsoft: after all I took a job with Microsoft because I rated their products and stayed there 10 years, I didn’t have much knowledge of Linux at the start of that stay and had no call to develop any during it, so my views I had was based on ignorance and supposition. After months of dealing with it on a daily basis I know how wrong I was. In reality Linux is uglier, more dis-functional and, frankly, retarded than I ever imagined it could be. Neither of our Linux advocates suggest it should be used anywhere but servers (just as well they both have iPhones and Xboxes which are about as far from the Open source ideal as it’s possible to go). But compare piping or tab expansion in PowerShell and Bash and you’re left in no doubt which one was designed 20 years ago. “You can only pipe text ? Really ? How … quaint”.

One of guys was trying to do something with Exchange from the command-line and threw down a gauntlet.
If PowerShell’s so bloody good why hasn’t it got persistent history”
OK. This is something which Bash has got going for it. How much work would it take to fix this ? Being PowerShell the answer is “a few minutes”. Actually the answer is “a lot less time than it takes to write a blog post about it”

First a little side track various people I know have a PowerShell prompt which looks like
[123]  PS C:\users\Fred>
Where 123 was the history ID. Type H (or history, or Get-History) and PowerShell shows you the previous commands, with their history ID, the command Invoke-History <id> (or ihy for short) runs the command.
I’d used PowerShell for ages before I discovered typing #<id>[Tab] inserts the history item into the command line. I kept saying “I’ll do that one day”, and like so many things I didn’t get round to it.
I already use the history ID I have this function in my profile
Function HowLong {
   <# .Synopsis Returns the time taken to run a command
      .Description By default returns the time taken to run the last command
     
.Parameter ID The history ID of an earlier item.
   #>

   param  ( [Parameter(ValueFromPipeLine=$true)]
           
$id = ($MyInvocation.HistoryId -1)
     
    )
  process {  foreach ($i in $id) {
                 (get-history $i).endexecutiontime.subtract(
                                 
(get-history ($i)).startexecutiontime).totalseconds
            }
          }
}
Once you know $MyInvocation.HistoryID gives the ID of the current item, it is easy to change the Prompt function to return something which contains it.

At the moment I find I’m jumping back and forth between PowerShell V2, and the CTP of V3 on my laptop
(and I can run PowerShell –Version 2 to launch a V2 version if I see something which I want to check between versions).
So I finally decided I would change the prompt function. This happened about the time I got the “why doesn’t the history get saved” question. Hmmm. Working with history in the Prompt function. Tick, tick, tick.  [Side track 2 In PowerShell the prompt isn’t a constant, it is the result of a function.  To see the function use the command type function:prompt]
So here is the prompt function I now have in my profile.
Function prompt {
  $hid = $myinvocation.historyID
  if ($hid -gt 1) {get-history ($myinvocation.historyID -1 ) |
                      convertto-csv | Select -last 1 >> $logfile
 
}
  $(if (test-path variable:/PSDebugContext) { '[DBG]: ' } else { '' }) + 
    "#$([math]::abs($hid)) PS$($PSVersionTable.psversion.major) " + $(Get-Location) + 
    $(if ($nestedpromptlevel -ge 1) { '>>' }) + '> '
}

The first part is new lines are new: get the history ID and if is greater than 1, get the previous history item, convert from an object to CSV format, discard the CSV header and append it to the file named in $logFile (I know I haven’t set it yet)

The second part is lifted from the prompt function found in the default profile, that reads
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
It’s actually one line but I’ve split it at the + signs for ease of reading.
I put the a # sign and the history ID before “PS” – when PowerShell starts the ID is –1 so I make sure it is the absolute value.
After “PS” I put the major version of PowerShell.
I’m particularly pleased with the #ID part in the non-ISE version of PowerShell double clicking on #ID selects it. My mouse is usually close enough to my keyboard that the keypad [Enter] key is within reach of my thumb so if I scroll up to look at something I did earlier, one flickity gesture (double-click, thumb enter, right click [tab]) has the command in the current command line.

So now I’m keeping a log, and all I need to do is to load that log my from Profile. PowerShell has an Add-History command and the on-line help talks about reading in the history from a CSV file so that was easy – I decided I would truncate the log when PowerShell started and also ensure that the file had the CSV header so here’s the reading friendly version of what’s in my profile.

$MaximumHistoryCount = 2048
$Global:logfile = "$env:USERPROFILE\Documents\windowsPowershell\log.csv"
$truncateLogLines = 100
$History = @()
$History += '#TYPE Microsoft.PowerShell.Commands.HistoryInfo'
$History += '"Id","CommandLine","ExecutionStatus","StartExecutionTime","EndExecutionTime"'
if (Test-Path $logfile) {$history += (get-content $LogFile)[-$truncateLogLines..-1] | where {$_ -match '^"\d+"'} }
$history > $logfile
$History | select -Unique  |
         
 Convertfrom-csv -errorAction SilentlyContinue |
         
 Add-History -errorAction SilentlyContinue

UPDATE Copying this code into the blog page and splitting the last $history line to fit, something went wrong and the
select -unique went astray. Oops.
It’s there because hitting enter doesn’t advance the History count, or run anything but does cause the prompt function to re-run. Now I’ve had to look it again it occurs to me it would be better to have select –unique in the (get-content $logfile) rather in the Add-history section as this would remove duplicates before truncating.

So … increase the history count, from the default of 64 (writing this I found that in V3 ctp 2 the default is 4096). Set a Global variable to be the path to the log file, and make it obvious what the length is I will truncate the log to.
Then build an array of strings named history. Put the CSV header information into $history, and if the log file exists put up to the truncate limit of lines into $history as well. Write $history back to the log file and pipe it into add history, hide any lines which won’t parse correctly. Incidentally those who like really long lines of PowerShell could recode all lines with $history in them into one line. So a couple of lines in the prompt function and between 3 and 9 lines in the profile depending on how you write them all in it’s less than a dozen lines. This blog post has taken a good couple of hours, and I don’t the code in 10 to 15 minutes.

image

Oh , and one thing I really like – when I launch PowerShell –Version 2 inside Version 3, it imports the history giving easy access to the commands I just used without needing to cut and paste.

If you’re a Bash user and didn’t storm off in a huff after my initial rudeness I’d like to set a good natured challenge. A non-compiled enhancement to bash I can load automatically which gives it tab expansion on par with PowerShell’s (OK, PowerShell has an unfair advantage completing parameters, so just command names and file names). And in case you wondered about the quote at the top of the post from one of Stephen Moffat’s Dr Who episodes. You see, “I Know PowerShell. I point and laugh at Bash users.”

About these ads

5 Comments

  1. Very nice. I didnt know about the #num history trick, very cool.
    Also since logfile is global, I think maybe I’d make it something a little more verbose, like PSConsoleLogFile mostly because its not uncommon for me to use logfile in testing.
    Thanks!

    Comment by jrich — January 29, 2012 @ 4:29 pm

  2. Well, what would you like to pipe? Objects? You see, programs do not return objects neither do files. The whole point is to pipe output from one program to the input of another program. As long as the program can handle the input, what’s the problem?

    Powershell is not a “shell” anymore as it does not just call programs but also provides those cmdlets, which let’s face it, are scripts. Rewrite all your cmdlets to – uh I dont know: Haskell programs, python scripts whatever – and you’d have the same functionality.

    Comment by feuermonster — January 31, 2012 @ 11:50 am

    • @feuermonster Of course programs return objects. That’s the whole point. Outlook retuns mail message objects like this
      $ol = New-Object -ComObject outlook.application
      $ns = $ol.GetNamespace(“MAPI”)
      $Folder = $ns.GetDefaultFolder(6).folders.item(“Foo”)
      $Folder.Items | foreach { Do something with mail messages }

      The problem is “As long as your program can handle the input”. If you only had programs which could only write mail messages to a flat text file (not something like XML where an object can be reconstructed) and then terminate, you’d have to write something to parse mail messages, and if you needed to sort them by date and then format a list of subjects, you’d need to write all that in your program. Good luck with that.

      Who said a shell can only run “dumb” programs ? Sure Powershell does more than other shells , that’s rather the point isn’t it. If you’re stuck with a legacy shell you need to add a scripting language and to do a lot of programming to plug the gaps.

      Comment by jamesone111 — February 4, 2012 @ 8:18 pm

      • >Who said a shell can only run “dumb” programs ?

        My point is, that the powershell runs scripts rather than standalone programs (because with standalone programs powershell is afaik as dumb as any other shell). Shells are meant to be replacable at least if they support basic shell operations. (i.e foo | bar should do the same independant of what unix shell I’m using). Which means that I classify PowerShell as some sort of a interactive scripting interpreter (like firing up clisp or ghci etc. etc.)

        But I admit that this is just my definition of a shell and I have nothing against powershell in particular (It makes my life easier).

        Comment by feuermonster — February 6, 2012 @ 2:26 pm

  3. [...] history to PowerShell February 1, 2012 robertrieglerwien Leave a comment Go to comments http://jamesone111.wordpress.com/2012/01/28/adding-persistent-history-to-powershell/ Share this:PrintEmailLike this:LikeBe the first to like this post. Categories: Developement [...]

    Pingback by Adding Persistent history to PowerShell « MS Tech BLOG — February 1, 2012 @ 8:45 pm


RSS feed for comments on this post.

The Rubric Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: