ScriptBlock Flexibility in PowerShell

5 minute read

I decided to start sharing some of my submissions for the 2011 Microsoft Scripting Games that received decent ratings. This is my entry for Advanced Event 3, where the idea was to create a script for event log gathering. The unique part is how a single scriptblock is invoked two different ways. I don’t know that I would always integrate this into a single function, but it’s an interesting example of how flexible scriptblocks can be.

Any code that needs to reach out and touch other computers is going to benefit from PowerShell remoting. The more computers you have to connect to, the more efficient it is to use remoting. I wanted to utilize remoting the way I would in my own production environment, but I also wanted the option to use the built-in remoting capabilities found in the Get-WinEvent cmdlet. Not everyone is lucky enough to have remoting configured everywhere.

I created a scriptblock that was essentially a self-contained script with parameters. All of the actual “work” is handled in this scriptblock. The rest of the function is figuring out how to launch that scriptblock.

$ScriptBlock = {
        
      Param
      (
        [String]$Name,
        [Int]$NumOfEvents,
        [Int]$NumOfDays,
        [Int]$ID,
        [Int]$Level
      )
        
      $filter = @{LogName=""}
            
      if ($ID)
      {
        $filter.Add("ID",$ID)
      }
        
      if ($Level)
      {
        $filter.Add("Level",$Level)
      }
      
      if (Test-Connection -ComputerName $Name -Count 1 -Quiet)
      {
        $logNames = Get-WinEvent -ComputerName $Name -ListLog * | 
          Where-Object {$_.RecordCount -and $_.IsEnabled}
        
        foreach ($log in $lognames)
        {
          $filter.LogName = $log.LogName
            
          Get-WinEvent -ComputerName $Name `
                       -FilterHashTable $filter `
                       -MaxEvents $NumOfEvents `
                       -ErrorAction SilentlyContinue |
            Where-Object {$_.TimeCreated -ge (Get-Date).AddDays(-$NumOfDays).Date}
        }
      }
    }

With the script-within-a-script ready, I just needed to launch it appropriately. The Get-LoggedEvent function has a UseRemoting switch. When this switch is specified, I want to use Invoke-Command to take advantage of PowerShell’s fan-out remoting capabilities. I pass Invoke-Command the array of names, the scriptblock and the arguments to be passed to the scriptblock. The scriptblock is given ‘localhost’ for the computer name, and Invoke-Command takes care of running a copy of the scriptblock on each computer.

if ($UseRemoting)
    {
      Invoke-Command -ComputerName $ComputerName `
                     -ScriptBlock $ScriptBlock `
                     -ArgumentList 'localhost', `
                                   $NumberOfEvents, `
                                   $NumberOfDays, `
                                   $EventID, `
                                   $LevelTable[$Severity]  
    }

If UseRemoting is not specified, I need to unwrap the array of computer names and call the scriptblock once for each computer name. This time, I need to pass the actual computer name to the scriptblock, because the built-in remoting capabilities of Get-WinEvent will be used instead of letting Invoke-Command run the scriptblock locally on each system.

else
    {
        foreach ($Name in $ComputerName)
        {
          &$ScriptBlock -Name $Name `
                        -NumOfEvents $NumberOfEvents `
                        -NumOfDays $NumberOfDays `
                        -ID $EventID `
                        -Level $LevelTable[$Severity]
        }
    }

With any script, there are always improvements that can be made. Bartek Bielawski (who ended up winning the 2011 Scripting Games) made a good suggestion that I use splatting for those long lists of parameters instead of backticks. He also pointed out that I don’t need to specify false as a default value for switch parameters because the value will already default to false.

All of my entries for the 2011 Scripting Games can be found at PoshCode.

Complete Script:

# -----------------------------------------------------------------------------
# Script: Get-LoggedEvent.ps1
# Author: Jason Hofferle
# Date: 04/06/2011
# Version: 1.0.0
# Comments: This script is based around the Get-LoggedEvent function. This 
#  function was designed with the intent that gathering a list of computer 
#  names and formatting output would be done by other cmdlets in the pipeline. 
#  These type of tasks gain so much speed through PowerShell remoting that an 
#  optional parameter was added that will use fan-out remoting instead of 
#  querying each computer individually.
# -----------------------------------------------------------------------------

Function Get-LoggedEvent
{
  [CmdletBinding()]
  Param
  (
    [String[]]
    $ComputerName = $Env:ComputerName,
       
    [Int]
    $NumberOfEvents = 1,
       
    [Int]
    $NumberOfDays = 0,
        
    [Int]
    $EventID,

    [ValidateSet("Critical","Error","Warning","Informational")]
    [String]
    $Severity,
        
    [Switch]
    $UseRemoting = $false
  )

  Begin
  {
    # For converting human-friendly text to numbers used in query        
    $LevelTable = @{
      Critical=1
      Error=2
      Warning=3
      Informational=4}
    
    # Specify a scriptblock that can be run locally, or passed to
    # Invoke-Command when using PowerShell remoting.        
    $ScriptBlock = {
        
      Param
      (
        [String]$Name,
        [Int]$NumOfEvents,
        [Int]$NumOfDays,
        [Int]$ID,
        [Int]$Level
      )
        
      $filter = @{LogName=""}
            
      if ($ID)
      {
        $filter.Add("ID",$ID)
      }
        
      if ($Level)
      {
        $filter.Add("Level",$Level)
      }
      
      if (Test-Connection -ComputerName $Name -Count 1 -Quiet)
      {
        $logNames = Get-WinEvent -ComputerName $Name -ListLog * | 
          Where-Object {$_.RecordCount -and $_.IsEnabled}
        
        foreach ($log in $lognames)
        {
          $filter.LogName = $log.LogName
            
          Get-WinEvent -ComputerName $Name `
                       -FilterHashTable $filter `
                       -MaxEvents $NumOfEvents `
                       -ErrorAction SilentlyContinue |
            Where-Object {$_.TimeCreated -ge (Get-Date).AddDays(-$NumOfDays).Date}
        }
      }
    }
  }

  Process
  {
    if ($UseRemoting)
    {
      # Pass the entire array of computer names to Invoke-Command
      Invoke-Command -ComputerName $ComputerName `
                     -ScriptBlock $ScriptBlock `
                     -ArgumentList 'localhost', `
                                   $NumberOfEvents, `
                                   $NumberOfDays, `
                                   $EventID, `
                                   $LevelTable[$Severity]  
    }
    else
    {
        # Unwrap $ComputerName array and invoke the scriptblock for each computer
        foreach ($Name in $ComputerName)
        {
          &$ScriptBlock -Name $Name `
                        -NumOfEvents $NumberOfEvents `
                        -NumOfDays $NumberOfDays `
                        -ID $EventID `
                        -Level $LevelTable[$Severity]
        }
    }
  }
  <#
      .Synopsis
      Returns recent events from local and remote computers.
      
      .Description
      The Get-LoggedEvent function queries event logs and event trace logs and
      returns the most recent events from each log.
           
      .parameter ComputerName
      Gets the event information on the specified computers.
      The default is the local computer name.
        
      .parameter NumberOfEvents
      Specifies the number of events to return from each log.
      The default is to return only the latest event.
        
      .parameter NumberOfDays
      Specifies the number of past days to query for events.
      The default is 0, which only returns events logged today.
        
      .parameter EventID
      Gets only the events with the specified event ID.
      The default is all events.
      
      .parameter Severity
      Gets only the events with the specified Level. Valid values are Critical,
      Error, Warning and Informational.
      The default is all types.
      
      .parameter UseRemoting
      Instead of querying each computer individually, use PowerShell remoting
      to connect to the remote systems specified in the ComputerName property.
      The default is to not use remoting.
        
      .Example
      Get-LoggedEvent
        
      Description
      -----------
      This command gets the latest event from each event log on the local
      computer, if there have been events logged today.
      
      .Example
      Get-LoggedEvent -NumberOfEvents 10 -NumberOfDays 3 -Severity Warning
        
      Description
      -----------
      This command gets the latest 10 Warning events from each event log that
      have been logged in the last three days.
      
      .Example
      Get-LoggedEvent -ComputerName DC1,DC2,WIN7 -UseRemoting
        
      Description
      -----------
      This command gets event information from the three computers specified
      using PowerShell remoting.
  #>
}