Register for Semperis' Hybrid Identity Protection (HIP) Conference - June 30 - July 1 Register for Semperis' Hybrid Identity Protection (HIP) Conference - June 30 - July 1
PowerShell

Create a Custom Tool to Find History with PowerShell

Not too long ago we published an article and PowerShell function you could use to copy a PowerShell command. You could use this technique to quickly build your own tools based on existing commands. Let me give you another example that’s a bit more detailed. First, there must be a need. For me, I want a quick way to get command history, but I want to limit my history to unique entries.

For example, here are the last 10 commands I have run using Get-History.

Using the Get-History cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
Using the Get-History cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)

In case you were wondering, the gmb command is an alias for one of my functions to display battery life. You’ll see that I have some duplicate commands. I’d prefer to have a unique list, which I can do like this:

get-history -count 10 | select -Unique

It works.

Creating a unique list with PowerShell's Get-History cmdlet. (Image Credit: Jeff Hicks)
Creating a unique list with PowerShell’s Get-History cmdlet. (Image Credit: Jeff Hicks)

But I don’t want to have to always type all of this. I’m happy with Get-History. I just need to add some functionality to it. So I’ll use my Copy-Command tool to create a new version of Get-History, where I’ll call Get-MyHistory. If you are following along, run this in the PowerShell ISE.

copy-command Get-History Get-MyHistory

Without making any changes, the Get-MyHistory function is identical to Get-History because at its core, all I am doing is passing parameters to the underlying command.

Process {
    Get-History @PSBoundParameters
} #process

I am splatting the automatic variable $PSBoundParameters to Get-History. My copy includes all the same parameters as well as complete comment-based help. Eventually, I will go back and modify the help to reference anything new that I add and to remove some things, such as the online help link to Get-History.

Remember, the purpose of creating my own tool is to simplify what I have to type at the prompt. I will also stress that this is a tool for me to use interactively, not something I would include in a script. If I did, I would have to make sure to include the Get-MyHistory function script.

Defining Parameters

Although my initial need was to find unique results, I don’t want to hard-code myself into a corner. There might be situations where I want to use my new tools to grab everything. We can do this by adding a switch parameter.

Param(

    [Parameter(Position=0, ValueFromPipeline=$true)]
    [ValidateRange(1, 9223372036854775807)]
    [long[]]$Id,

    [Parameter(Position=1)]
    [ValidateRange(0, 32767)]
    [int]$Count,

    #parameters that I added
    [switch]$Unique,
    [regex]$Pattern
)

I also want an easy way to find previous commands using a regular expression pattern. Depending on my needs, I can remove existing parameters, add validation tests or set default values.

Begin Scriptblock

Code in the Begin block runs once, before any pipelined objects are processed, although that’s not really an issue with this command. What I prefer to do is insert some Write-Verbose commands that will provide some debug and trace information. Some of these commands are auto-generated when you run Copy-Command.

Begin {
    #insert verbose messages to provide debugging and tracing information
    Write-Verbose "Starting $($MyInvocation.Mycommand)"
    Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)"
    Write-Verbose ($PSBoundParameters | out-string)

    #remove Unique and Pattern parameters if used since they are not part of Get-History
    if ($Unique) {
        $PSBoundParameters.Remove("Unique") | Out-Null
    }
    if ($Pattern) {
        $PSBoundParameters.Remove("Pattern") | Out-Null
    }
} #begin

Assuming you intend to splat $PSBoundParameters to the original command, you have to pay attention to this variable. In this instance, Get-History isn’t designed to use –Unique or –Pattern. If I try to run Get-History with them, PowerShell will complain and throw an exception. That’s why you see me removing them from $PSBoundParameters. The variables still exist, so if I use –Pattern, I can still reference $Pattern in my function. They just aren’t part of $PSBoundParameters.

Process Scriptblock

In the Process scriptblock, I can grab history commands using the same parameters, ID and Count.

$all = Get-History @PSBoundParameters

If I’m searching for a command based on a pattern, I filter the results.

    if ($Pattern) {
        #use v4 Where method for performance purposes
        Write-Verbose "Searching for commandlines matching $pattern"
        $all = $all.where({$_.commandline -match $pattern})
    }

Finally, I filter for unique history items if requested.

if ($Unique) {
        Write-Verbose "Selecting unique items"
        $all = $all | Select-Object -Unique 
    }

All that remains is to write the results to the pipeline.

$all | Add-Member -MemberType ScriptProperty -Name "Runtime" -value {$this.EndExecutionTime - $this.StartExecutionTime} -PassThru -force

I also decided I might want to know how long a command took to run so I added an additional property that is calculated at run time.

End Scriptblock

Code in the End scriptblock runs once after everything has been processed. In my case, I don’t have any cleanup to do, so I display a verbose message.

End {
   
    Write-Verbose "Ending $($MyInvocation.Mycommand)"

} #end

The Final Tool

When I load the function into my PowerShell session, I have a new tool at my disposal.

Get-MyHistory Help. (Image Credit: Jeff Hicks)
Get-MyHistory Help. (Image Credit: Jeff Hicks)

I can run it like Get-History.

Executing Get-MyHistory. (Image Credit: Jeff Hicks)
Executing Get-MyHistory. (Image Credit: Jeff Hicks)

Or I can use my new parameters.

Using parameters with Get-MyHistory. (Image Credit: Jeff Hicks)
Using parameters with Get-MyHistory. (Image Credit: Jeff Hicks)

The additional property is present but not part of the default display.

Here’s my result.

Out-GridView for Get-MyHistory results. (Image Credit: Jeff Hicks)
Out-GridView for Get-MyHistory results. (Image Credit: Jeff Hicks)

Without too much difficulty I created a new tool to get command history the way I want it. For the record, here’s the complete command.

#requires -version 4.0

<#
This is a copy of:

CommandType Name        ModuleName               
----------- ----        ----------               
Cmdlet      Get-History Microsoft.PowerShell.Core

Created: 5/18/2015
Author : Jeff

  ****************************************************************
  * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
  * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
  * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
  * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
  ****************************************************************
#>


Function Get-MyHistory {
<#

.SYNOPSIS

Gets a list of the commands entered during the current session.


.DESCRIPTION

The Get-MyHistory cmdlet gets session history, that is, the list of commands entered during the current session. This is a modified version of Get-History.

Windows PowerShell automatically maintains a history of each session. The number of entries in the session history is determined by the value of the $MaximumHistoryCount preference variable. Beginning in Windows PowerShell 3.0, the default value is 4096.

You can save the session history in XML or CSV format. By default, history files are saved in the home directory, but you can save the file in any location.

For more information about the history features in Windows PowerShell, see about_History (http://go.microsoft.com/fwlink/?LinkID=113233).


.PARAMETER Count

Displays the specified number of the most recent history entries. By, default, Get-MyHistory gets all entries in the session history. If you use both the Count and Id parameters in a command, the display ends with the command that is specified by the Id parameter.

In Windows PowerShell 2.0, by default, Get-MyHistory gets the 32 most recent entries.

.PARAMETER Id

Specifies the ID number of an entry in the session history. Get-MyHistory gets only the specified entry. If you use both the Id and Count parameters in a command, Get-MyHistory gets the most recent entries ending with the entry specified by the Id parameter.

.PARAMETER Pattern
A regular expression pattern to match something in the commandline.

.EXAMPLE

PS C:\>Get-MyHistory -unique
This command gets the unique entries in the session history. The default display shows each command and its ID, which indicates the order of execution as well as the command's start and stop time.

.EXAMPLE

PS C:\>Get-MyHistory -pattern "service"
This command gets entries in the command history that include "service". 

.NOTES

The session history is a list of the commands entered during the session. The session history represents the order of execution, the status, and the start and end times of the command. As you enter each command, Windows PowerShell adds it to the history so that you can reuse it. For more information about the command history, see about_History (http://go.microsoft.com/fwlink/?LinkID=113233).

Beginning in Windows PowerShell 3.0, the default value of the $MaximumHistoryCount preference variable is 4096. In Windows PowerShell 2.0, the default value is 64. For more information about the $MaximumHistoryCount variable, see about_Preference_Variables (http://go.microsoft.com/fwlink/?LinkID=113248).

Learn more about PowerShell:
http://jdhitsolutions.com/blog/essential-powershell-resources/

  ****************************************************************
  * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
  * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
  * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
  * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
  ****************************************************************

.INPUTS

Int64


.OUTPUTS

Microsoft.PowerShell.Commands.HistoryInfo


.LINK
Add-History
Clear-History
Invoke-History

.LINK

about_History

#>
[CmdletBinding()]
Param(

    [Parameter(Position=0, ValueFromPipeline=$true)]
    [ValidateRange(1, 9223372036854775807)]
    [long[]]$Id,

    [Parameter(Position=1)]
    [ValidateRange(0, 32767)]
    [int]$Count,

    #parameters that I added
    [switch]$Unique,
    [regex]$Pattern
)

Begin {
    #insert verbose messages to provide debugging and tracing information
    Write-Verbose "Starting $($MyInvocation.Mycommand)"
    Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)"
    Write-Verbose ($PSBoundParameters | out-string)

    #remove Unique and Pattern parameters if used since they are not part of Get-History
    if ($Unique) {
        $PSBoundParameters.Remove("Unique") | Out-Null
    }
    if ($Pattern) {
        $PSBoundParameters.Remove("Pattern") | Out-Null
    }
} #begin

Process {

    #splat bound parameters to Get-History
    $all = Get-History @PSBoundParameters
    
    if ($Pattern) {
        #use v4 Where method for performance purposes
        Write-Verbose "Searching for commandlines matching $pattern"
        $all = $all.where({$_.commandline -match $pattern})
    }

    if ($Unique) {
        Write-Verbose "Selecting unique items"
        $all = $all | Select-Object -Unique 
    }

    #write results to pipeline and add a new property
    $all | Add-Member -MemberType ScriptProperty -Name "Runtime" -value {$this.EndExecutionTime - $this.StartExecutionTime} -PassThru -force
   

} #process

End {
   
    Write-Verbose "Ending $($MyInvocation.Mycommand)"

} #end

} #end function Get-MyHistory

#define an optional alias
Set-Alias -Name gmh -Value Get-MyHistory

I’m really liking the ability to copy commands and create my own tools. I have a few more that will be sharing with you. And if you’ve built something cool, I hope you’ll let everyone know in the article comments.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

Register for the Hybrid Identity Protection (HIP) Europe Conference!

Hybrid Identity Protection (HIP) Europe 2021 - Virtual Conference

Mobile workforces, cloud applications, and digitalization are changing every aspect of the modern enterprise. And with radical transformation come new business risks. Hybrid Identity Protection (HIP) is the premier educational forum for identity-centric practitioners. At the inaugural HIP Europe, join your local IAM experts and Microsoft MVPs to learn all the latest from the Hybrid Identity world.