I have no idea what I'm doing.

Wednesday, November 27, 2013

TRUE PowerShell command history persistence (i.e. Bash up arrow), surviving multiple sessions, syncing across multiple PCs, and a table full of fiiixins

A day of celebration is at hand.  For centuries Windows administrators have been living crippled and hellish existences.  Laughed at by some, mocked by others, and ignored by the rest.  All throughout the 19th and 20th century (as well as the entire 5th Age of Paladine), the command line history of each PowerShell session was eradicated upon exit.  Vaporized - trashed - hit over the head, tossed into a pickup truck, taken to an abandoned forest just off the highway, tossed into a pit, and burned till nothing was left but the smokey tears of sysadmins all across the world.

Sure, PowerShell had that 'hash tag + hit TAB' crap, but they've never had 'up arrow' access to the command history.  Well, not anymore...

To get started, you'll need three things:
  1. A script to backup and restore your command history each time you start/exit a PowerShell instance.  (don't worry, the execution time is negligible)
  2. This backup/restore script will require the PSReadLine module.
  3. The easiest way to get the PSReadLine module is to install PsGet.

Let's do this in reverse...

Step 1:  Install PsGet here: http://psget.net/.  For the security conscious people, alternate methods of installation are available, OR you take the security-lite approach and download/inspect the files before execution.
(new-object Net.WebClient).DownloadString("http://psget.net/GetPsGet.ps1") | iex

Step 2:  Install/Import the PSReadLine module.
install-module PSReadline

Step 3:  Add a script to your profile that loads/saves your command history.  Be sure to have the path ready (HistoryDirPath).  For me, I like to put his into a dropbox location and share it amongst my other workstations.  Or if you want to have some level of separation, change the file name (HistoryFileName)
$MaximumHistoryCount = 31KB
$ImportedHistoryCount = 0
$HistoryDirPath = "c:\dropbox\powershell\history\"
$HistoryFileName = "history.clixml"
if (!(Test-Path $HistoryDirPath -PathType Container))
    {   New-Item $HistoryDirPath -ItemType Directory }
Register-EngineEvent PowerShell.Exiting –Action {           
        $TotalHistoryCount = 0
        Get-History | ? {$TotalHistoryCount++;$true}
        $RecentHistoryCount = $TotalHistoryCount - $ImportedHistoryCount
        $RecentHistory = Get-History -Count $RecentHistoryCount 
        if (!(Test-path ($HistoryDirPath + $HistoryFileName)))
            Get-History | Export-Clixml ($HistoryDirPath + $HistoryFileName)
            $OldHistory = Import-Clixml ($HistoryDirPath + $HistoryFileName) 
            $NewHistory = @($OldHistory + $RecentHistory)
            $NewHistory | Export-Clixml ($HistoryDirPath + $HistoryFileName)
if (Test-path ($HistoryDirPath + $HistoryFileName))
        Import-Clixml ($HistoryDirPath + $HistoryFileName) | ? {$count++;$true} |Add-History 
     Write-Host -Fore Green "`nLoaded $count history item(s).`n"
     $ImportedHistoryCount = $count

# Importing PSReadline must come AFTER you load your command history
if ($host.Name -eq 'ConsoleHost')
        Import-Module PSReadline

# if you don't already have this configured...
Set-PSReadlineKeyHandler -Key UpArrow -Function HistorySearchBackward
Set-PSReadlineKeyHandler -Key DownArrow -Function HistorySearchForward 

Your profile location will be something like:

Now for the fiiixins...

Redirect your default profile to your tricked-out profile.

Since you're putting your command history and profile on dropbox, don't forget to add your modules as well!

Setup a command line installer like Scoop or Chocolatey.

With one of these installer you're one line away from all sorts of cool tools, stuff like:  7zip, curl, grep, nano, git, openssh, vim, wget.  With Chocolately you can install goodies like Notepad++, Chrome, VLC, etc.

bunch of sources:


  1. I'm not sure why you use
    Get-History | ? {$TotalHistoryCount++;$true}
    to count the history items, when that basically outputs the entire history. using $false instead of $true is slightly better as it avoids unnecessary output, but how about simply:

  2. Also (I was posting before - blogspot's openid support is horrible), I don't understand why you go to such lengths to extract only the recent history from the current session. It would make sense if you want to only append to the history file, but as it is an XML format, and you need to read all of it anyway, why not just dump all the available history every time?

    If we get rid of the test for the history directory (I'm storing my history in the user's home, like bash does), not modify the maximum history (PS v3 has 4096 by default, which is good enough for me), and all the other unneeded code, we are left with these 3 lines - which work great (note the piping to out-null to suppress the reg-event output):

    $HistoryFilePath = Join-Path ([Environment]::GetFolderPath('UserProfile')) .ps_history
    Register-EngineEvent PowerShell.Exiting –Action { Get-History | Export-Clixml $HistoryFilePath } | out-null
    if (Test-path $HistoryFilePath) { Import-Clixml $HistoryFilePath | Add-History }

    So in summary:
    1. Thanks for a great introduction to persistent history
    2. Shorter code - especially in the profile script (which tends to accumulate junk) is better.

  3. I know this is old but I am just amazed that I cannot find anything on a great new feature in Powershell 5! Ctrl+R (bck-i-search like bash) lets you search your history ACROSS sessons. Yay!