Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Friday Fun: A PowerShell Tickler

Posted on May 17, 2013May 23, 2013

elmoI spend a lot of my day in the PowerShell console. As you might imagine, I often have a lot going on and sometimes it is a challenge to keep on top of everything. So I thought I could use PowerShell to help out. I created a PowerShell tickler system. Way back in the day a tickler system was something that would give you a reminder about an impending event or activity. What I decided I wanted was a tickler that would display whenever I started PowerShell.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

This doesn't replace my calendar and task list, but it let's me see events I don't want to miss right from PowerShell. As I worked through this idea I ended up with a PowerShell module, MyTickle.psm1, that has a number of functions to managing the tickle events, as I call them. From PowerShell I can get events, add, set and remove. I thought the module would make a good Friday Fun post because it certainly isn't a high impact project but it offers some ideas on building a module and functions that I hope you'll find useful.

The module for right now is a single file. Here's the file and below I'll talk about.

#requires -version 3.0

#  ****************************************************************
#  * 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.             *
#  ****************************************************************

#MYTICKLE.PSM1
#Last updated 5/17/2013

#region Define module variables
#This should be the WindowsPowerShell folder under your Documents folder
$profileDir = Split-Path $profile

#the path to the tickle csv file
$TicklePath = Join-Path -Path $profileDir -ChildPath "mytickler.csv"

#the default number of days to display
$TickleDefaultDays = 7 
#endregion

#region Define module functions

Function Get-TickleEvent {

<#

.Synopsis
Get Tickle Events
.Description
Get tickle events by id or name. The default behavior is to get all events in
date order. You can specify a range of ID numbers and use wildcards with the 
event names. Use the Expired switch to get all tickle events that have already 
occurred. 

The command will not throw an exception if no matching tickle events are found.

#>

[cmdletbinding(DefaultParameterSetname="ALL")]
Param(
[Parameter(Position=0,ParameterSetName="ID")]
[int[]]$Id,
[Parameter(Position=0,ParameterSetName="Name")]
[string]$Name,
[Parameter(Position=0,ParameterSetName="Expired")]
[switch]$Expired,
[ValidateScript({Test-Path $_} )]
[string]$Path=$TicklePath
)

Write-Verbose "Importing events from $Path"

Switch ($pscmdlet.ParameterSetName) {
 "ID"      {
            Write-Verbose "by ID" 
            $filter = [scriptblock]::Create("`$_.id -in `$id")  }
 "Name"    { 
            Write-Verbose "by Name"
            $filter = [scriptblock]::Create("`$_.Event -like `$Name") }
 "Expired" { 
            Write-Verbose "by Expiration"
            $filter = [scriptblock]::Create("`$_.Date -lt (Get-Date)") }
 "All"     { 
            Write-Verbose "All"
            $filter = [scriptblock]::Create("`$_ -match '\w+'") }

} 

#import CSV and cast properties to correct type
Import-CSV -Path $Path | 
Select @{Name="ID";Expression={[int]$_.ID}},
@{Name="Date";Expression={[datetime]$_.Date}},
Event,Comment | where $Filter | Sort date

} #Get-TickleEvent

Function Set-TickleEvent {

<#

.Synopsis
Set a tickle event
.Description
This command will update the settings for a tickle event. You can update the
event name, date or comment. The easiest way to use this is to pipe an tickle
event to this command.

#>

[cmdletbinding(SupportsShouldProcess,DefaultParameterSetName="Inputobject")]
Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter the tickle event id",ParameterSetName="ID")]
[int]$Id,
[Parameter(Position=1,ValueFromPipeline,ParameterSetname="Inputobject")]
[object]$Inputobject,
[datetime]$Date,
[string]$Event,
[string]$Comment,
[ValidateScript({Test-Path $_} )]
[string]$Path=$TicklePath,
[switch]$Passthru
)
Begin {
    write-verbose "Using $($PSCmdlet.ParameterSetName) parameter set"
}
Process {

#if ID only then get event from CSV
Switch ($pscmdlet.ParameterSetName) {
 "ID" {
    Write-Verbose "Getting tickle event id $ID"
    $myevent = Get-TickleEvent -id $id
   }
 "Inputobject" {
    Write-Verbose "Modifying inputobject"
    $myevent = $Inputobject
 }
} #switch

#verify we have an event to work with
if ($myevent) {
    #modify the tickle event object
    write-verbose ($myevent | out-string)

    if ($Date) {
      Write-Verbose "Setting date to $date"
      $myevent.date = $Date
    }
    if ($Event) {
      Write-Verbose "Setting event to $event"
      $myevent.event = $Event
    }
    if ($comment) {
      Write-verbose "Setting comment to $comment"
      $myevent.comment = $comment
    }
    write-verbose "Revised: $($myevent | out-string)"

    #find all lines in the CSV except the matching event
    $otherevents = get-content -path $Path | where {$_ -notmatch "^""$($myevent.id)"} 
    #remove it
    $otherevents | Out-File -FilePath $Path -Encoding ascii 

    #append the revised event to the csv file
    $myevent | Export-Csv -Path $Path -Encoding ASCII -Append -NoTypeInformation

    if ($passthru) {
        $myevent
    }
}
else {
    Write-Warning "Failed to find a valid tickle event"
}

} #process

} #Set-TickleEvent

Function Add-TickleEvent {

<#
.Synopsis
Add a tickle event
.Description
This command will create a new tickle event. If the CSV file referenced by the
TicklePath variable does not exist, it will be created. You must enter an event 
name and date.
#>

[cmdletbinding(SupportsShouldProcess)]

Param (
[Parameter(Position=0,ValueFromPipelineByPropertyName,Mandatory,HelpMessage="Enter the name of the event")]
[string]$Event,
[Parameter(Position=1,ValueFromPipelineByPropertyName,Mandatory,HelpMessage="Enter the datetime for the event")]
[datetime]$Date,
[Parameter(Position=2,ValueFromPipelineByPropertyName)]
[string]$Comment,
[ValidateNotNullorEmpty()]
[string]$Path=$TicklePath,
[switch]$Passthru
)

Begin {
    #verify the path and create the file if not found
    if (! (Test-Path $Path)) {
        Write-Verbose "Creating a new file: $Path"
        Try {
         '"id","Date","Event","Comment"' | 
         Out-File -FilePath $Path -Encoding ascii -ErrorAction Stop
        }
        Catch {
            Write-Warning "Failed to create $Path"
            Write-Warning $_.Exception.Message
            $NoFile = $True
        }
    }
}

Process {
if ($NoFile) {
    Write-Verbose "No CSV file found."
    #bail out of the command
    Return
}

#get last id and add 1 to it
[int]$last = Import-Csv -Path $Path | 
Sort {$_.id -AS [int]} | Select -last 1 -expand id
[int]$id = $last+1

$hash=[ordered]@{
  ID = $id
  Date = $date
  Event = $event
  Comment = $comment
}
Write-Verbose "Adding new event"
Write-Verbose ($hash | out-string)

$myevent = [pscustomobject]$hash
$myevent | Export-Csv -Path $Path -Append -Encoding ASCII -NoTypeInformation
if ($passthru) {
    $myevent
}
} #process

} #Add-TickleEvent

Function Remove-TickleEvent {
<#
.Synopsis
Remove a tickle event
.Description
Remove one or more events from the tickle file. This will overwrite the current
file so you might want to back it up first with Backup-TickleFile.
.Example
PS C:\> get-ticklevent -expired | remove-tickleevent
#>

[cmdletbinding(SupportsShouldProcess,DefaultParameterSetName="Inputobject")]
Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter the tickle event id",ParameterSetName="ID")]
[int]$Id,
[Parameter(Position=1,ValueFromPipeline,ParameterSetname="Inputobject")]
[object]$Inputobject,
[ValidateScript({Test-Path $_} )]
[string]$Path=$TicklePath
)

Process {

    #if ID only then get event from CSV
    Switch ($pscmdlet.ParameterSetName) {
     "ID" {
        Write-Verbose "Getting tickle event id $ID"
        $myevent = Get-TickleEvent -id $id
       }
     "Inputobject" {
        Write-Verbose "Identifying inputobject"
        $myevent = $Inputobject
     }
    } #switch

    #verify we have an event to work with
    if ($myevent) {
        Write-Verbose "Removing event"
        Write-Verbose ($myEvent | Out-String)
        if ($pscmdlet.ShouldProcess(($myEvent | Out-String))) {
        #find all lines in the CSV except the matching event
        $otherevents = Get-Content -path $Path | where {$_ -notmatch "^""$($myevent.id)"} 
        #remove it
        $otherevents | Out-File -FilePath $Path -Encoding ascii 
        }
    } #if myevent

} #process

} #Remove-TickleEvent

Function Show-TickleEvent {
<#
.Synopsis
Display Tickle events in the console
.Description
This command gets upcoming tickle events and writes them to the console using
Write-Host. Use this command in your PowerShell profile script.
#>

[cmdletbinding()]
Param(
[Parameter(Position=0)]
[ValidateScript({Test-Path $_})]
[string]$Path=$TicklePath,
[Parameter(Position=1)]
[ValidateScript({$_ -ge 1})]
[int]$Days = $TickleDefaultDays
)

#import events from CSV file
$events = Import-Csv -Path $Path

#get upcoming events within 7 days sorted by date
$upcoming = $events | 
where {
 #get the timespan between today and the event date
 $ts = (New-TimeSpan -Start (Get-Date) -end $_.Date).TotalHours 
 #find events less than the default days value and greater than 0
 Write-Verbose $ts
 $ts -le ($Days*24) -AND $ts -gt 0
 } |
Add-Member -MemberType ScriptProperty -Name Countdown -value {New-TimeSpan -start (Get-Date) -end $this.date} -PassThru -force|
sort CountDown

if ($upcoming) {
#how wide should the box be?
#get the length of the longest line
$l = 0
foreach ($item in $upcoming) {
 #turn countdown into a string without the milliseconds
  $count = $item.countdown.ToString()
  $time = $count.Substring(0,$count.lastindexof("."))
  #add the time as a new property
  $item | Add-Member -MemberType Noteproperty -name Time -Value $time
  $a = "$($item.event) $($item.Date) [$time]".length
  if ($a -gt $l) {$l = $a}
  $b = $item.comment.Length
  if ($b -gt $l) {$l = $b}
}

[int]$width = $l+5

$header="* Reminders $((Get-Date).ToShortDateString())"

#display events
Write-Host "`r"
Write-host "$($header.padright($width,"*"))" -ForegroundColor Cyan
Write-Host "*$(' '*($width-2))*" -ForegroundColor Cyan

foreach ($event in $upcoming) {

  if ($event.countdown.totalhours -le 24) {
    $color = "Red"
  }
  elseif ($event.countdown.totalhours -le 48) {
    $color = "Yellow"
  }
  else {
    $color = "Green"
  }

  #define the message string
  $line1 = "* $($event.event) $($event.Date) [$($event.time)]"
  if ($event.comment -match "\w+") {
   $line2 = "* $($event.Comment)"
   $line3 = "*"
  }
  else {
   $line2 = "*"
   $line3 = $null
  }

$msg = @"
$($line1.padRight($width-1))*
$($line2.padright($width-1))*
"@

if ($line3) {
    #if there was a comment add a third line that is blank
    $msg+="`n$($line3.padright($width-1))*"
}

  Write-Host $msg -ForegroundColor $color

} #foreach

Write-host ("*"*$width) -ForegroundColor Cyan
Write-Host "`r"
} #if upcoming events found
else {
  $msg = @"

**********************
* No event reminders *
**********************

"@
  Write-host $msg -foregroundcolor Green
}

} #Show-TickleEvent

Function Backup-TickleFile {
<#
.Synopsis
Create a backup of the tickle file
.Description
This command will create a backup copy of the tickle CSV file. The default path
is the same directory as the tickle file. You might want to backup the tickle
file before removing any events.
#>

[cmdletbinding(SupportsShouldProcess)]
Param(
[ValidateScript({Test-Path $_} )]
[string]$Path=$TicklePath,
[ValidateScript({Test-Path $_} )]
[string]$Destination = (Split-Path $TicklePath),
[switch]$Passthru
)

Try {
    $ticklefile = Get-Item -Path $path
    $backup = Join-Path -path $Destination -ChildPath "$($ticklefile.basename).bak"
    Write-Verbose "Copying $path to $backup"
    $ticklefile | Copy-Item  -Destination $backup -ErrorAction Stop -PassThru:$Passthru
}
Catch {
    Write-Warning "Failed to backup file"
    Write-Warning $_.exception.message
}
} #Backup-TickleFile

#endregion

#region Define module aliases
Set-Alias -Name gte -value Get-TickleEvent
Set-Alias -name ate -Value Add-TickleEvent
Set-Alias -name rte -Value Remove-TickleEvent
Set-Alias -name ste -Value Set-TickleEvent
Set-Alias -name shte -Value Show-TickleEvent
Set-Alias -name btf -Value Backup-Ticklefile
#endregion

Export-ModuleMember -Function * -Variable TicklePath,TickleDefaultDays -Alias *

You should be able to copy the code from the WordPress plugin and paste it into a script file locally. You can call it whatever you want just remember to use a .psm1 file extension. The module uses some PowerShell 3.0 features like ordered hashtables but you could revise to have it run in PowerShell 2.0. Fundamentally it should work in both versions.

The events are stored in a CSV file that I reference with a module variable, $TicklePath. The default is a file called mytickler.csv which will be in your WindowsPowerShell folder under Documents. The module also defines a variable called $TickleDefaultDays, with a default value of 7. This displayed events to those that fall within that range. To use, I added these lines to my PowerShell profile.

import-module c:\scripts\mytickle.psm1
show-tickleevent

The result, is that when I launch a new PowerShell session I see something like this (the message about help updates is from something else so disregard):
show-tickle

Here's how it works.

The Show-TickleEvent function imports events from the CSV file that will happen within the next 7 days. Each object also gets an additional property that is a timespan object for how much time remains. The function then parses event information and constructs a "box" around the events.

#how wide should the box be?
#get the length of the longest line
$l = 0
foreach ($item in $upcoming) {
 #turn countdown into a string without the milliseconds
  $count = $item.countdown.ToString()
  $time = $count.Substring(0,$count.lastindexof("."))
  #add the time as a new property
  $item | Add-Member -MemberType Noteproperty -name Time -Value $time
  $a = "$($item.event) $($item.Date) [$time]".length
  if ($a -gt $l) {$l = $a}
  $b = $item.comment.Length
  if ($b -gt $l) {$l = $b}
}

[int]$width = $l+5

$header="* Reminders $((Get-Date).ToShortDateString())"

#display events
Write-Host "`r"
Write-Host "$($header.padright($width,"*"))" -ForegroundColor Cyan
Write-Host "*$(' '*($width-2))*" -ForegroundColor Cyan

#get upcoming events within 7 days sorted by date
$upcoming = $events |
where {
#get the timespan between today and the event date
$ts = (New-TimeSpan -Start (Get-Date) -end $_.Date).TotalHours
#find events less than the default days value and greater than 0
Write-Verbose $ts
$ts -le ($Days*24) -AND $ts -gt 0
} |
Add-Member -MemberType ScriptProperty -Name Countdown -value {New-TimeSpan -start (Get-Date) -end $this.date} -PassThru -force |
sort CountDown

I set a foreground color depending on how imminent the event is and then write each event to the console, wrapped in my border.

#define the message string
  $line1 = "* $($event.event) $($event.Date) [$($event.time)]"
  if ($event.comment -match "\w+") {
   $line2 = "* $($event.Comment)"
   $line3 = "*"
  }
  else {
   $line2 = "*"
   $line3 = $null
  }

$msg = @"
$($line1.padRight($width-1))*
$($line2.padright($width-1))*
"@

if ($line3) {
    #if there was a comment add a third line that is blank
    $msg+="`n$($line3.padright($width-1))*"
}

  Write-Host $msg -ForegroundColor $color

I purposely used Write-Host so that I could color code events and because I didn't want the profile to write anything to the pipeline. Because the module is loaded at the start of my PowerShell session, I can always run Show-TickleEvent and event specify a different number of days. If I want objects, then I can use the Get-TickleEvent function which will import the csv events based on a criteria like ID or name. The function uses parameter sets and I create a filter scriptblock depending on the parameter set.

Switch ($pscmdlet.ParameterSetName) {
 "ID"      {
            Write-Verbose "by ID" 
            $filter = [scriptblock]::Create("`$_.id -in `$id")  }
 "Name"    { 
            Write-Verbose "by Name"
            $filter = [scriptblock]::Create("`$_.Event -like `$Name") }
 "Expired" { 
            Write-Verbose "by Expiration"
            $filter = [scriptblock]::Create("`$_.Date -lt (Get-Date)") }
 "All"     { 
            Write-Verbose "All"
            $filter = [scriptblock]::Create("`$_ -match '\w+'") }

}

When I import the CSV file, I add types to the properties because otherwise everything would be a string, and then pipe each object to my filter.

#import CSV and cast properties to correct type
Import-CSV -Path $Path | 
Select @{Name="ID";Expression={[int]$_.ID}},
@{Name="Date";Expression={[datetime]$_.Date}},
Event,Comment | where $Filter | Sort Date

These objects come in handy because they can be piped to Set-TickleEvent to modify values like event name, date or comment. Or I can pipe to Remove-TickleEvent to delete entries. The deletion process in essence finds all lines in the CSV file that don't start with the correct id and creates a new file using the same name.

if ($pscmdlet.ShouldProcess(($myEvent | Out-String))) {
  #find all lines in the CSV except the matching event
  $otherevents = Get-Content -path $Path | where {$_ -notmatch "^""$($myevent.id)"} 
  #remove it
  $otherevents | Out-File -FilePath $Path -Encoding ascii 
 }

Finally, after accidentally wiping out event files, I added a simple backup function copies the CSV file to the same directory but with a .BAK file extension. You could specify an alternate path, but it defaults to the WindowsPowerShell folder.

Hopefully I won't miss important events again, of course assuming I add them to my tickler file. I'll let you play with Add-TickleEvent to see how that works or you could always modify the CSV file with Notepad.

If you actually use this, I hope you'll let me know.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

3 thoughts on “Friday Fun: A PowerShell Tickler”

  1. Pingback: Microsoft Most Valuable Professional (MVP) – Best Posts of the Week around Windows Server, Exchange, SystemCenter and more – #29 - TechCenter - Blog - TechCenter – Dell Community
  2. Other (@dvsbobloblaw) says:
    May 22, 2013 at 6:55 pm

    I just started reading and already had to comment.

    “I spend a lot of my day in the PowerShell console.” It’s a great place to be.

  3. Harriett A. Frye says:
    June 20, 2013 at 1:23 am

    The solution that I came up with was to leverage the All Users Startup option in Windows to launch a script that would check to see if the folder that holds the PowerShell profile scripts exists. If it exists, the script terminates and all is well. This happens on every interactive login, but takes only a second. If the folder does not exist, the will kick off a creation of the scripts based upon a preset profile definition that includes the targeting & naming of the logs, starting the transcript, and loading the SharePoint module. The profile definition can be completely customized making this a viable approach for admins of other technologies, not just SharePoint.

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d