Scripting Games 2013: Event 3 Notes

Wow, it is hard to believe that we are now halfway through the Scripting Games! As the events have progressed, I have seen a lot of improvement with the techniques as well as seeing new techniques that continue to impress me. On the flip side, I have seen some mistakes or assumptions when coding that cause a potential 5 star script to be a 2 or 3 star script. The best part about all of this is that we are all (yes, even the judges) learning new things that can only help to improve everyone’s scripting knowledge.

With that, it is time to take a look at the things that could be improved upon or should be avoided altogether.

-Filter vs. Where-Object

This was probably the biggest issue I have seen with the submissions for Event 3. Too many submissions were taking the output of Get-WMIObject and piping the results into Where-Object to get all of the local hard drives. The sooner you get used to working with –Filter, the better off you will be working with scripts and one liners to query WMI on both local and remote systems. When you use the Filter parameter, especially on remote systems, the filtering is performed on the remote system and only the objects that you filtered for will be passed back to the local system.

Some people claim that it is an ugly approach and I disagree. If I am looking for only hard drives, which one looks worse:

Filter

drivetype=3

Where-Object

$_.drivetype –eq 3

To me, the obvious answer is using Filter and supplying the WQL statement. Sure the statements may get complicated, but so could using the Where-Object. And if you might not be sure as to what type of WQL statement to use, simply look it up. There are plenty of resources available that you can use to figure out the proper query.

I’ve also heard that performance was the reason for using Where-Object locally vs. –Filter and that it isn’t much better to consider using the –Filter parameter. Lets compare performance just to see.

Filter

(Measure-Command {
    Get-WmiObject -Class Win32_LogicalDisk -Filter 'drivetype=3'
}).TotalMilliseconds

image

 

Where-Object

(Measure-Command {
    Get-WmiObject -Class Win32_LogicalDisk | Where {
        $_.driveType -eq 3
    }
}).TotalMilliseconds

image

About a 50 millisecond difference. Pretty good difference to me. Some might disagree, but the fact is that using –Filter has better performance. So why is that the case? Let’s take a closer look at each command. If you know PowerShell, you know that Where-Object takes the input from whatever command is before it in the pipeline and starts performing a comparison against the object in the pipeline based on the data you provide.

Get-WmiObject -Class Win32_LogicalDisk | Where {
    Write-Verbose $_ -Verbose
    $_.driveType -eq 3
}
 

image

As you can see, each object is being looked at when just using Where-Object. I wonder what happens when I use the –Filter parameter…

Get-WmiObject -Class Win32_LogicalDisk -Filter 'drivetype=3' | Where {
    Write-Verbose $_ -Verbose
}

image

Instead of going through 4 items, I only see the 3 that are actual hard drives. This is the efficiency of using a parameter on ANY cmdlet vs. using Where-Object.

So bottom line: –Filter is not only a best practice for Get-WMIObject, but it is also more efficient and provides better performance.

Hard Coded Paths

Something that was driving me crazy during this event, especially on the beginner side was the use of hard coded paths to output the html file. When you use a path such as C:\smith\awesomefolder as the directory, you cannot expect it to work for me. I shouldn’t have to modify your code to make it work for me. It should work without any issues. The solution for this would be to use something like $pwd, or also in some scripts I saw (good job, by the way!), $Env:TEMP  or $Env:USERPROFILE. There are other possibilities, but the main thing is that you must make sure that it is universal to the everyone who uses it.

.Net Types

This is the PowerShell scripting games, not the .Net games. Don’t use [DateTime]::now, when there is a perfectly good cmdlet called Get-Date that will accomplish the same thing. I even saw this: [environment]::MachineName being used. Really? You can get the same thing out of $Env:Computername.

Another person took the time to create their own network checker using System.Net.NetworkInformation.Ping. The same thing can be done by using Test-Connection.

Unless there is compelling need to use this (haven’t seen it yet) to use a .Net type to accomplish your goal, stick with the cmdlets and you will be better off.

Over Commenting

This is pretty self explanatory. We don’t need a 2-3 line comment talking about how you are querying WMI on a local system and then continuing this trend for each and every line of code. Not every line of code requires a comment, let along multiple lines of commenting. Keep it simple and clean and everything will be Ok.

Some Final Points

  • Consider use of [pscustomobject] vs. Net-Object PSObject for V3 code
  • Stop using back ticks as line breaks.
  • If the event says to do a one line, then you should shoot for a one liner and avoid using semicolons because it is no longer a one liner.
  • No more concatenating (Ex: “this” + $something + “is great!”). We are not in the vbscripting games.
  • Also avoid using += to append lines to an existing variable. Use a Here-String instead.

Keep up the great submissions and I am looking forward to seeing what you do with Event 4!

Posted in powershell, Scripting Games 2013 | Tagged , , , | 3 Comments

Scripting Games 2013: Event 2 Notes

I spent some time last week and this weekend to compile a list of notes of what I have seen with the Event 2 submissions that should show improvement. I touched up on some items with my previous article where I picked out some submissions that I liked and didn’t quite like but wanted to touch on a few more things. Some of this feels like a repeat of last week and even last years games, but that is Ok. This is all about learning and as long as everyone takes what all of the judges have been writing about, then there will be nothing but great improvements during the course of the games.

When is a one-liner not a one-liner?

The answer is when someone uses a semicolon “;” and assumes that counts as a one-liner. Sorry folks, but this doesn’t work. A one-liner is something that is a complete continuation of a string of commands. In PowerShell, a semicolon is a line break which means that the one-liner that was being put together has now became a multi-one-liner.

Here is what not to do:

$OS = Get-WMIObject -Class Win32_OperatingSystem ;$ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem; New-Object PSObject -Property @{Computername=$ComputerSystem.Name;OS=$Os.Caption}

It seems like a one-liner, but with the semicolons, it is nothing more than a one-liner lie. In fact, the only time here that the semicolon is legal is with the New-Object cmdlet that takes a hash table as input.

A one liner is not required for the Scripting Games in the beginner event. It is a nice to have, but it is also a risk because if you try to force it as such, it could come back to bite you when it comes to voting. My advice, split the code up on lines because not only will it prevent possible issues in the code, it is going to look tons better!

Formatting your code

Moving from one-liners to proper code formatting so it can easily be read by not only the voting community, but also applies to production scripts that may have to be read 6 months from by someone else or even the future you. Think to yourself, “Will I know exactly what is going on here in 6 months?”. If you cannot answer this question or answer No, then stop, go back and take the time to re-format the code so it is more readable.

Here are some bullet points to think about when writing code:

  • Space out your code; separate by code blocks so it is not just one line after another of code
  • Use the proper line continuation characters: commas“,”, opening square bracket “{“ and pipe symbol “|”; please for the love of everything good, do not use a backtick “`” as a line continuation. This is also a lie and must be stopped!
  • Indent your code where needed. Examples would be when you use a natural line break (first bullet point) or during a ForEach/Where block and If/Else block. Doing so will make it easier to read and organize your code.

Splatting instead of back ticks for parameters

Splatting is not only great for working with various cmdlets and making input a lot easier when in a loop, but it is also a smart idea to make your code easier to read and avoiding the use of back ticks when working with multiple parameters.

Send-MailMessage -To user@domain.com `
                 -From user1@domain.com `
                 -Subject Test `
                 -SMTPServer server@domain.com `
                 -Body "This is a test"

This is not a good way of handling the parameters in a script. Sure, it is better than having them on one line and navigating that mess, but this still doesn’t fit a good practice. This is where splatting comes into play.

$email = @{
    To = 'user@domain.com'
    From = 'user1@domain.com'
    Subject = 'Test'
    SMTPServer = 'server.domain.com'
}
...
#Some code
...
#Add the output into the body of the email
$email.Body = $outputText
Send-MailMessage @email

I can define most of my parameters for Send-MailMessage up front and once I have the data available for my Body, I can add that into my $email hash table before calling Send-MailMessage and supplying the hash table into the cmdlet (using “@” to splat) and the command will run just like usual.

Enough with $ErrorActionPreference

I have seen a lot of people using $ErrorActionPreference up front in their submissions as a catch all for error handling. Sure, some people are using Stop or Continue (this is already the default). This should never be touched at all in a script unless there is an excellent reason to do so (so far there hasn’t been a reason to do so). Each cmdlet has a common parameter called –ErrorAction that accepts Continue, Inquire, Stop and SilentlyContinue that you can use. Unless you need to suppress error messages for some reason, then I would recommend using Stop and put this command in a Try/Catch statement to handle any errors that come up.

Format* fail

The last thing that I want to go over is the use of Format* cmdlets as a final step to output data to the user. Bad, bad idea! What you are doing is ruining the object for the user in a way that they cannot manipulate the output either through Sort or Where-Object and cannot even output this to a file ( it can be outputted to a .txt file, but it will be just like what is on screen, truncated and all).

Get-Service | Format-Table | Sort Name

image

Doesn’t work out so good, does it? If only the Format-Table wasn’t there, then I could have sorted this out.

Maybe this will work for a CSV file instead?

Get-Service | Format-Table | Export-Csv -NoTypeInformation Report.csv

image

No errors, so it had to have worked correctly, right? Wrong!

image

If you can read this, then you are a better person than me. Smile The same goes with Format-List as well. Format* cmdlets should never be used in a script and should be left for a user to make the decision themselves. It will save you the hassle of being asked why the output stinks and also save you from losing points in a submission.

That’s it for now. Hopefully you can use these tips not only for the Scripting Games but also for “real world” scripts in your environment as well!

Posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 | Tagged , , | 2 Comments

Scripting Games 2013: Event 2 ‘Favorite’ and ‘Not So Favorite’

Event 2 is in the books and with that, it is time to take a look at all of the scripts submitted and make the difficult decisions as to which ones I liked and which ones I didn’t quite like. Remember, just because a script landed on my ‘Not so Favorite’ list doesn’t mean it was terrible. It was just that I felt that there were some things here and there that could have been looked at a little differently.

So without further ado, lets dig into the submissions!

Advanced Category – Not So Favorite Submission

<#
.SYNOPSIS
    This script runs an inventory of OS, CPU cores and installed RAM on a list of computers
.DESCRIPTION
    .
.PARAMETER FilePath
    The path to the computer list text file
 
 
.EXAMPLE
        .\inventory.ps1 C:\Computernames.txt
.NOTES
    Author: Posh_London
    Date:   May 2013    
#>
 
# == Define params == #
 
param([Parameter(Mandatory=$true)][string]$filepath)
 
$output = @()
$Servers = gc $filepath
$Servers | % {
	$processor = gwmi win32_processor -computername $_
	$os = gwmi win32_operatingsystem -computername $_
	$m = gwmi win32_computersystem -computername $_
	$memGB = $m.totalphysicalmemory/1gb
	$memory = [math]::round($memGB)
	$osout = $os.caption
	$cores = $processor.numberofcores
	$servername = $m.name
	$output +="$ServerName   $osout    $cores CPU Cores       $memory GB RAM"
	$output +="`n"
 
	}
 
write-host $output -foregroundcolor "yellow"
  1. Line 1: Help text is lacking good examples and a Description
  2. Line 19: FilePath is not really a good choice for Parameters, especially when you might want to pass Computernames through to it. Computername would be the better choice in this case.
  3. Line 19: Lack of pipeline support for this really hurts the submissions ability to pass content from a text file through to the script.
  4. Really should be a function if in the Advanced category; makes it more of a usable tool
  5. Line 19: Instead of using Mandatory for the parameter, consider setting a default value such as $Env:Computername
  6. Line 22,23: Alias are bad news! Don’t use them
  7. Line 23-37: No object at all is being outputted at all! In fact, you can’t even output this to a file because Write-Host is being used or do things such as sorting or manipulating of the objects. Recommend that the user read up outputting objects using New-Object to create custom objects that can then be exported to files or manipulated.

Advanced Category – Favorite Submission

<#
.SYNOPSIS
   Get inventory data for specified computer system.
.DESCRIPTION
   Get inventory data for provided host using wmi.
   Data proccessing use multithreading and support using timeouts in case of wmi problems.
   Target computer system must be reacheble using ICMP Echo.
   Provide ComputerName specified by user and HostName used by OS. Also provide OS version, CPU and memory info.
.PARAMETER ComputerName
   Specifies the target computer for data query.
.PARAMETER ThrottleLimit
   Specifies the maximum number of WMI operations that can be executed simultaneously
.PARAMETER Timeout
   Specifies the maximum time in second command can run in background before terminating this thread.
.PARAMETER ShowProgress
   Show progress bar information
.EXAMPLE
   PS > Get-AssetInfo -ComputerName test1
 
   ComputerName : hp-test1
   OSCaption    : Microsoft Windows 8 Enterprise
   Memory       : 5,93 GB
   Cores        : 2
   Sockets      : 1
 
   Description
   -----------
   Query information ablout computer test1
.EXAMPLE
   PS > Get-AssetInfo -ComputerName test1 -Credential (get-credential) | fromat-list * -force
 
   ComputerName   : hp-test1
   OSCaption      : Microsoft Windows 8 Enterprise
   OSVersion      : 6.2.9200
   Cores          : 2
   OSServicePack  : 0
   Memory         : 5,93 GB
   Sockets        : 1
   PSComputerName : test1
   Description
   -----------
   Query information ablout computer test1 using alternate credentials
.EXAMPLE
   PS > get-content C:\complist.txt | Get-AssetInfo -ThrottleLimit 100 -Timeout 60 -ShowProgress
 
   Description
   -----------
   Query information about computers in file C:\complist.txt using 100 thread at time with 60 sec timeout and showing progressbar
.NOTES
   Required: Powershell 2.0
   Info: WMI prefered over CIM as there no speed advantage using cimsessions in multitheating against old systems.
#>
function Get-AssetInfo
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias('DNSHostName','PSComputerName')]
        [string[]]
        $ComputerName,
 
        [Parameter(Position=1)]
        [ValidateRange(1,65535)]
        [int32]
        $ThrottleLimit = 32,
 
        [Parameter(Position=2)]
        [ValidateRange(1,65535)]
        [int32]
        $Timeout = 120,
 
        [Parameter(Position=3)]
        [switch]
        $ShowProgress,
 
        [Parameter(Position=4)]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )
 
    Begin
    {
 
        Write-Verbose -Message 'Creating local hostname list'
        $IPAddresses = [net.dns]::GetHostAddresses($env:COMPUTERNAME) | Select-Object -ExpandProperty IpAddressToString
        $HostNames = $IPAddresses | ForEach-Object {
            try {
                [net.dns]::GetHostByAddress($_)
            } catch {
                # We do not care about errors here...
            }
        } | Select-Object -ExpandProperty HostName -Unique
        $LocalHost = @('', '.', 'localhost', $env:COMPUTERNAME, '::1', '127.0.0.1') + $IPAddresses + $HostNames
 
        Write-Verbose -Message 'Creating initial variables'
        $runspacetimers = [HashTable]::Synchronized(@{})
        $runspaces = New-Object -TypeName System.Collections.ArrayList
        $bgRunspaceCounter = 0
 
        Write-Verbose -Message 'Creating Initial Session State'
        $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        foreach ($ExternalVariable in ('runspacetimers', 'Credential', 'LocalHost'))
        {
            Write-Verbose -Message "Adding variable $ExternalVariable to initial session state"
            $iss.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $ExternalVariable, (Get-Variable -Name $ExternalVariable -ValueOnly), ''))
        }
 
        Write-Verbose -Message 'Creating runspace pool'
        $rp = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $ThrottleLimit, $iss, $Host)
        $rp.Open()
 
        Write-Verbose -Message 'Defining background runspaces scriptblock'
        $ScriptBlock = {
            [CmdletBinding()]
            Param
            (
                [Parameter(Position=0)]
                [string]
                $ComputerName,
 
                [Parameter(Position=1)]
                [int]
                $bgRunspaceID
            )
            $runspacetimers.$bgRunspaceID = Get-Date
 
            if (Test-Connection -ComputerName $ComputerName -Quiet -Count 1 -ErrorAction SilentlyContinue)
            {
                try
                {
                    Write-Verbose -Message "WMI Query: $ComputerName"
                    $WMIHast = @{
                        ComputerName = $ComputerName
                        ErrorAction = 'Stop'
                    }
                    if ($LocalHost -notcontains $ComputerName)
                    {
                        $WMIHast.Credential = $Credential
                    }
 
                    $WMICompSystem = Get-WmiObject @WMIHast -Class Win32_ComputerSystem
                    $WMIOS = Get-WmiObject @WMIHast -Class Win32_OperatingSystem
                    $WMIProc = Get-WmiObject @WMIHast -Class Win32_Processor
 
                    if (@($WMIProc)[0].NumberOfCores) #Modern OS
                    {
                        $Sockets = @($WMIProc).Count
                        $Cores = ($WMIProc | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
                    }
                    else #Legacy OS
                    {
                        $Sockets = @($WMIProc | Select-Object -Property SocketDesignation -Unique).Count
                        $Cores = @($WMIProc).Count
                    }
 
                    #region Create custom output object
                    #Due to some bug setting scriptblock directly as value can cause 'NullReferenceException' in v3 host
                    $MethodOptions = @{
                        Name = 'ToString'
                        MemberType = 'ScriptMethod'
                        PassThru = $true
                        Force = $true
                        Value = [ScriptBlock]::Create(@"
                            "{0:N1} {1}" -f @(
                                switch -Regex ([math]::Log(`$this,1024)) {
                                    ^0 {
                                        (`$this / 1), ' B'
                                    }
                                    ^1 {
                                        (`$this / 1KB), 'KB'
                                    }
                                    ^2 {
                                        (`$this / 1MB), 'MB'
                                    }
                                    ^3 {
                                        (`$this / 1GB), 'GB'
                                    }
                                    ^4 {
                                        (`$this / 1TB), 'TB'
                                    }
                                    default {
                                        (`$this / 1PB), 'PB'
                                    }
                                }
                            )
"@
                        )
                    }
 
                    $myObject = New-Object -TypeName PSObject -Property @{
                        'PSComputerName' = $ComputerName
                        'ComputerName' = $WMICompSystem.DNSHostName
                        'OSCaption' = $WMIOS.Caption
                        'OSServicePack' = $WMIOS.ServicePackMajorVersion
                        'OSVersion' = $WMIOS.Version
                        'Memory' = $WMICompSystem.TotalPhysicalMemory | Add-Member @MethodOptions
                        'Cores' = $Cores
                        'Sockets' = $Sockets
                    }
 
                    $myObject.PSObject.TypeNames.Insert(0,'My.Asset.Info')
                    $defaultProperties = @('ComputerName','OSCaption', 'Memory', 'Cores', 'Sockets')
                    $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultProperties)
                    $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
                    $myObject | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    #endregion
 
                    Write-Output -InputObject $myObject
                }
                catch
                {
                    Write-Warning -Message ('{0}: {1}' -f $ComputerName, $_.Exception.Message)
                }
            }
            else
            {
                Write-Warning -Message ("{0}: Unavailable" -f $ComputerName)
            }
        }
 
        function Get-Result
        {
            [CmdletBinding()]
            Param
            (
                [switch]$Wait
            )
            do
            {
                $More = $false
                foreach ($runspace in $runspaces)
                {
                    $StartTime = $runspacetimers.($runspace.ID)
                    if ($runspace.Handle.isCompleted)
                    {
                        Write-Verbose -Message ('Thread done for {0}' -f $runspace.IObject)
                        $runspace.PowerShell.EndInvoke($runspace.Handle)
                        $runspace.PowerShell.Dispose()
                        $runspace.PowerShell = $null
                        $runspace.Handle = $null
                    }
                    elseif ($runspace.Handle -ne $null)
                    {
                        $More = $true
                    }
                    if ($Timeout -and $StartTime)
                    {
                        if ((New-TimeSpan -Start $StartTime).TotalSeconds -ge $Timeout -and $runspace.PowerShell)
                        {
                            Write-Warning -Message ('Timeout {0}' -f $runspace.IObject)
                            $runspace.PowerShell.Dispose()
                            $runspace.PowerShell = $null
                            $runspace.Handle = $null
                        }
                    }
                }
                if ($More -and $PSBoundParameters['Wait'])
                {
                    Start-Sleep -Milliseconds 100
                }
                foreach ($threat in $runspaces.Clone())
                {
                    if ( -not $threat.handle)
                    {
                        Write-Verbose -Message ('Removing {0} from runspaces' -f $threat.IObject)
                        $runspaces.Remove($threat)
                    }
                }
                if ($ShowProgress)
                {
                    $ProgressSplatting = @{
                        Activity = 'Getting asset info'
                        Status = '{0} of {1} total threads done' -f ($bgRunspaceCounter - $runspaces.Count), $bgRunspaceCounter
                        PercentComplete = ($bgRunspaceCounter - $runspaces.Count) / $bgRunspaceCounter * 100
                    }
                    Write-Progress @ProgressSplatting
                }
            }
            while ($More -and $PSBoundParameters['Wait'])
        }
    }
    Process
    {
        foreach ($Computer in $ComputerName)
        {
            $bgRunspaceCounter++
            $psCMD = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddParameter('bgRunspaceID',$bgRunspaceCounter).AddParameter('ComputerName',$Computer)
            $psCMD.RunspacePool = $rp
 
            Write-Verbose -Message ('Starting {0}' -f $Computer)
            [void]$runspaces.Add(@{
                Handle = $psCMD.BeginInvoke()
                PowerShell = $psCMD
                IObject = $Computer
                ID = $bgRunspaceCounter
           })
           Get-Result
        }
    }
 
    End
    {
        Get-Result -Wait
        if ($ShowProgress)
        {
            Write-Progress -Activity 'Getting asset info' -Status 'Done' -Completed
        }
        Write-Verbose -Message "Closing runspace pool"
        $rp.Close()
        $rp.Dispose()
    }
}
  1. Excellent use of help. Shows multiple examples of using this function and clearly defines parameters
  2. Line 58-69: Great use of parameter attributes to handle various situations. I like the use of [Alias()] with the ByPropertyName attribute for the pipeline. This allows you to pipe a call from Get-ADComputer directly into this function and it will chug right along to perform the query.
  3. Line 82,83: Great use of handling the Credential parameter. By doing it this way, you have a few ways to handle the Credential input (domain\username, username, pscredential object or just use the –Credential param and allow it to prompt for input.
  4. Use of  [net.dns]::GetHostByAddress() was unnecessary as the WMI call would return a usable hostname for the object output.
  5. I will give kudos to the use of custom runspaces to make a more efficient query. Great job at handling the runspaces and the implementation of a timer to handle runspaces that might be hung up.
  6. Handling of the credential against local system issue was nicely done to avoid issues.
  7. Use of splat tables to pass into cmdlet is great.
  8. Line 206-210: Smart use of enhancing the output of the object. Haven’t seen most of this done before.

 

Beginner Category – Not So Favorite Submission

#Pulls IPs from txt file.  Gets Machine Name, Number of Physical CPUs, Number of cores per CPU, RAM in GBs, and Windows OS install
ipcsv C:\ScriptingGames\Event2\IPList.txt | gwmi Win32_ComputerSystem | 
    Format-List Name,NumberOfProcessors, NumberOfLogicalProcessors, 
    @{Name="RAM"; Expression={[math]::round($($_.TotalPhysicalMemory/1GB), 2)}} ;
    gwmi win32_OperatingSystem |Format-list name

Code formatted so it can be viewed better

  1. This isn’t a CSV file, so I am unsure as to why Import-Csv is being used for the list of IPs.
  2. Get-WMIObject doesn’t support pipeline input at all. Check Get-Help Get-Wmiobject –Parameter Computername. You can also check out my article on using Trace-Command to troubleshoot pipeline issues here. This causes the line to fail instantly. Instead, use a ForEach loop and use the –Computername parameter with the current item in the loop.
  3. Format-List is a no-no. Once you use a Format* cmdlet, you have lost all ability to use this object for anything such as sorting, outputting to a file, etc… See this article for more information.
  4. A better approach would have been to save the output of each WMI call to variables and then use New-Object to output a custom object that lists all of the information as well as allowing the use to pipe the object to another cmdlet or export to a file.
  5. The second pipe to Get-WMIObject will fail pretty much like the first; again this should have been used with the –Computername parameter and saved to a variable for future object outputting.

 

Beginner Category – Favorite Submission

Get-Content -LiteralPath 'C:\IPLIST.txt' | ForEach-Object {
    (Get-WmiObject -Class "Win32_ComputerSystem" -ComputerName $_ | 
    Select-Object -Property Name,@{n="Total Memory (GB)";e={$_.TotalPhysicalMemory / 1GB}},
    @{n="Cores";e={$_.NumberOfLogicalProcessors}},@{n="Processors";e={$_.NumberOfProcessors}} | 
    Add-Member -Name "OS Version" -Value $(Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_ | 
    Select-Object -ExpandProperty Caption) -MemberType NoteProperty -PassThru)
}

Code formatted so it can be viewed better

  1. Great use of a one-liner approach to solve this issue
  2. Like the use of ForEach-Object to iterate through each of the items in the text file.
  3. Custom objects with the Select-Object @{n=’name’;e={$_}} allows for object output that can be sorted, exported to a file, etc…
  4. Clever use of Add-Member to handle the Caption property in the current object

 

That’s it for Event 2! I will say that it was very tough deciding on which submissions I wanted as ‘Favorite’ as there were some great submissions, which meant that I really had to be picky about what I liked. Same for the ‘Not So Favorites’, it is never easy to find one that I just don’t like as much as another, but it goes with the territory.

I always appreciate feedback on how you think I did with picking the submissions. Disagree with my decisions? Let me know! I am just one person out of many and as we all know, not everyone has the same opinions on what they think is a better or not so good solution. As long as we can provide a learning experience, then we all win in this competition!

Posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 | Tagged , , | 3 Comments

Tips on Implementing Pipeline Support

Something that I was seeing during the first event of the Scripting Games was the use (or misuse) of implementing pipeline support for a parameter in a function or script. While most people did do this correctly, I did see a decent number of people do some things that would never work at all if someone attempted to pipe data into their function. I want to clarify/expand on some things that I talked about in a previous article that should be done and why some of the methods being used will not work like you think.

We all know that being able to pass objects (not text!) through the pipeline with PowerShell is just amazing and very powerful. Doing this by taking output from another cmdlet and then streaming it into another command which allows us to chain commands very seamlessly without effort. Doing this will also throttle the amount of memory that is being allocated (in most cases) that the current session is using for the commands.

Getting started…

Want to know more about the pipeline? Then do the right thing and explore PowerShell’s awesome help system with the following command:

Get-Help about_pipelines

What you may not know is how to properly implement this to get the benefit of the pipeline in your functions. And by this, I am talking about the Begin, Process and End blocks in the code. I am going to show initially some mistakes that could be made with this implementation and how to overcome them.

First off, how do I allow my parameter to accept pipeline input? By specifying one of the following parameter attributes:

  1. ValueFromPipeline
    1. Accepts values of the same type expected by the parameter or that can be converted to the type that the parameter is expecting.
  2. ValueFromPipelineByPropertyName
    1. Accepts values of the same type expected by the parameter but must also be of the same name as the parameter accepting pipeline input.

Now with that out of the way, lets look at what you might expect to see:

    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Computername
    )

The Computername parameter allows for pipeline support by value of something that is a string or a collection of strings. If we were accepting pipeline that has the same name as Computername (or any defined Aliases with the [Alias()] attribute, we would use the following:

    Param (
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias('IPAddress','__Server','CN')]
        [string[]]$Computername
    )

This allows me to do something like pipe the output of a WMI query using Get-WMIObject into a function and it would grab the __Server property of the object and use it in the pipeline of the function. Pretty cool stuff! Please make sure that if you use the *ByPropertyName attribute, that there is actually a property in the object either supports it or you are using an Alias attribute that has the property that will map to whatever the incoming object has.

Now on to the main point of this article which is setting up the guts of the function to process this correctly.

Begin, Process and End with no pipeline support

First off, if you are not accepting pipeline input, you really have no need to use Begin, Process and End because frankly, it is doing nothing for you other than just taking up space in your code. I know that people may be doing this as a way to organize their code, but there is a better way that I will show you in a moment.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter()]
        [string[]]$Computername
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }
    Process {
        Write-Verbose "Stuff in Process block to perform"
        ForEach ($Computer in $Computername) {
            $Computer
        }
    }

    End {
        Write-Verbose "Final work in End block"
    }
}

image

This is really a false sense of the blocks working as they are just going in the order provided in the code. Instead, take advantage of the PowerShell V3 ISE and its ability to use code folding with regions to organize your code accordingly.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter()]
        [string[]]$Computername
    )
    #region Initialization code
    Write-Verbose "Initialize stuff in Begin block"
    #endregion Initialization code

    #region Process data
    Write-Verbose "Stuff in Process block to perform"
    ForEach ($Computer in $Computername) {
        $Computer
    }
    #endregion Process data

    #region Finalize everything
    Write-Verbose "Final work in End block"
    #endregion Finalize everything
}

image

Same output, but now without the Begin, Process and End blocks. I’ll repeat it again, if you don’t allow for pipeline input, then just stick with using #region/#endregion tags to organize your code (you should also do this regardless of pipeline input or not).

Pipeline support with no Process block support

Ok, so what happens if we do specify a parameter that has pipeline support but has NO Process block? This was something common I saw during Event 1 and will show you what happens when trying to run a command that is setup this way.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Computername
    )
    Write-Verbose "Initialize stuff in Begin block"

    Write-Verbose "Stuff in Process block to perform"
    ForEach ($Computer in $Computername) {
        $Computer
    }

    Write-Verbose "Final work in End block"
}

What do you think will happen when I run this with pipeline input? Will it process everything? Will it process nothing? Lets find out!

image

If you thought that it would only show the last item in the pipeline, then you are the winner! What is happening is that without the Process block, the behavior is similar to what we would expect from the End block.

The way to do it…

So what is the proper way to accomplish this, let me show you that now with the following example.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Computername
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }

    Process {
        Write-Verbose "Stuff in Process block to perform"
        ForEach ($Computer in $Computername) {
            $Computer
        }
    }

    End {
        Write-Verbose "Final work in End block"
    }
}

image

Works like a champ now. But take a look at something here. The Write-Verbose statement runs for each item that is processed in the pipeline. What does this mean? Well, it means that you have to be careful about what is put in the Process block as it will run each and every time for each item being passed through the pipeline. In other words, don’t try to create the same file to write to with output or creating your main array that will hold data in it such as this example:

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Computername
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }

    Process {
        $report = @()
        Write-Verbose "Stuff in Process block to perform"
        ForEach ($Computer in $Computername) {
            $report += $Computer
        }
    }

    End {
        Write-Verbose "Final work in End block"
        $Report
    }
}

image

All of that data collected was overwritten with each item. I also saw something similar to this on a few submissions during Event 1. Be careful about not making this mistake!

Do I need all of these Begin, Process and End blocks?

With all of this information presented to you, does this mean that you only have to specify a Process block in your function? Well, yes and no. Yes if all you have is pipeline stuff to process and have no need to initialize anything else in the beginning. If you do have things to spin up, then add a Begin block to handle that, otherwise your function will fail when being run like this:

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Computername
    )
    Write-Verbose "Initialize stuff in Begin block"
    Process {
        Write-Verbose "Stuff in Process block to perform"
        ForEach ($Computer in $Computername) {
            $Computer
        }
    }
}

 

The function will actually load into memory without issue, but check out what happens when you attempt to run the function.

image

All seems well until we get to the Process piece. Instead of being read as a Process block, it is misinterpreted as Get-Process which obviously fails. Point here is keep everything in the Begin,Process and End blocks if you have need for them.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Computername
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }
    Process {
        Write-Verbose "Stuff in Process block to perform"
        ForEach ($Computer in $Computername) {
            $Computer
        }
    }
}

image

Much better!

Multiple parameters that accept pipeline input

Now for something a little different. I saw at least one submission that had multiple parameters with pipeline input and wondered how was that going to work (turns out not so well!). See this example:

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Name,
        [parameter(ValueFromPipeline=$True)]
        [string[]]$Directory
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }

    Process {
        Write-Verbose "Process block"
        Write-Host "Name: $Name"
        Write-Host "Directory: $Directory"
    }

    End {
        Write-Verbose "Final work in End block"
        $Report
    }
}

Instead of the usual numbers into the pipeline, I am going to use Get-ChildItem and pipe that into my function to see what happens.

image

Weird, isn’t it? It will process the same value for each parameter just because it accepted pipeline input. How do we get around this issue? Use the PipelineValueByPropertyName attribute instead.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipelineByPropertyName=$True)]
        [string[]]$Name,
        [parameter(ValueFromPipelineByPropertyName=$True)]
        [string[]]$Directory
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }

    Process {
        Write-Verbose "Process block"
        Write-Host "Name: $Name"
        Write-Host "Directory: $Directory"
    }

    End {
        Write-Verbose "Final work in End block"
        $Report
    }
}

image

Now we are able to pull two separate values with 2 parameters that accept pipeline input. Another option would be to use ParameterSets, but that would mean that you would only have one parameter or the other to accept pipeline input and wouldn’t have the output that I have above allowing the use of multiple parameters to accept input AND use that in the function side by side.

One last thing, take care when using both ValueFromPipeline and …ByPropertyName with multiple parameters as it can cause some craziness in the output.

Function Get-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [string[]]$Name,
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [string[]]$Directory
    )
    Begin {
        Write-Verbose "Initialize stuff in Begin block"
    }

    Process {
        Write-Verbose "Process block"
        Write-Host "Name: $Name"
        Write-Host "Directory: $Directory"
    }

    End {
        Write-Verbose "Final work in End block"
    }
}

image

In fact, this completely freaks out the Directory parameter and doesn’t actually bind to anything. This is due to the order of binding when you use both of these attributes.

Order of Parameter Binding Process From Pipeline

  1. Bind parameter by Value with same Type (No Coercion)
  2. Bind parameter by Value with type conversion (Coercion)
  3. Bind parameter by PropertyName with same Type (No Coercion)
  4. Bind parameter by PropertyName with type conversion (Coercion)

You can use Trace-Command to dig deeper into this and really see what is happening. Working with Trace-Command can be complicated and reading all of the output can certainly be overwhelming, so use at your own discretion!

This is a little bonus content on working with Trace-Command and seeing where the parameter binding is taking place as well as when Coercion and No Coercion is taking place.  I’ll be covering 6 one-liners to highlight specific items with the parameter binding.

The baseline that I will be using is for a timestamp that is a string and then another object that has a [datetime] type that will be piped into 5 functions to show each method of binding.

#String time
$nonType = New-Object PSObject -prop @{Datetime = "5:00 PM"}
#[datetime] type 
$Type = New-Object PSObject -prop @{Datetime = [datetime]"5:00 PM"}

Looking at how parameter binding handles different types

Here we will look at a simple function that accepts pipeline input by PropertyName to handle incoming data.

Function Get-Something_PropName {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipelineByPropertyName=$True)]
        [datetime[]]$Datetime
    )
    Process {$Datetime}
}

First lets run my variable with the Type [datetime] already defined and see where the parameter binding takes place:

Trace-Command parameterbinding {$Type | get-Something_PropName} -PSHost

image

You can see that it started out at line 5 by first seeing if it can pass the NO COERCION with the ByPropertyName attribute by validating that it is the type of [datetime] with the result being SUCCESSFUL.

Next up: the non-type property for the datetime parameter.

Trace-Command parameterbinding {$nonType | get-Something_PropName} -PSHost

image

Remember where the NO COERCION worked on the last run because the property was of the same type as the parameter requirement? Well, it doesn’t work out so well with my string value of “5:00 PM”. You can see where it doesn’t pass with a SKIPPED. Next up is the attempt to cast the input (COERCION) as the [datetime] type so it can match what the $DateTime parameter is requiring. This is done using the [System.Management.Automation.ArgumentTypeConverterAttribute] and in this case, it is SUCCESSFUL.

As a side note, I will be using the $nonType variable from here on out to show each time how it fails the NO COERCION attempt before the COERCION attempt.

Working with the [Alias()] attribute

Writing advanced functions means that support pipelining means also potentially using the [Alias()] parameter attribute to handle other properties that the parameter doesn’t have. This is more important when working with ByPropertyName.

Function Get-Something_PropName_NoAlias {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipelineByPropertyName=$True)]
        [datetime[]]$Date
    )
    Process {$Date}
}
Trace-Command parameterbinding {$nonType | get-Something_PropName_NoAlias} -PSHost

image

Well, that is certainly interesting. If you look at line 4 here, it shows an arg of System.Management.Automation.PSCustomObject which isn’t all that useful. This is because the property being passed is DateTime while the parameter of this function is Date. The PropertyByName completely fails because it has no idea about the incoming data. So will ByValue work instead? Lets find out.

Trace-Command parameterbinding {$nonType | get-Something_Value_NoAlias} -PSHost

image

A little better this time around, but still a failure. Since it is ByValue, the parameter doesn’t care about what the name is of the object being passed through. It does see the input as a hash table with the data viewable, but still fails because it is neither the type of [datetime] nor can it be converted to the type as well. Just for run, lets pass a single integer into this and see how it works out.

Trace-Command parameterbinding {1 | get-Something_Value_NoAlias} -PSHost

image

Obviously it was never going to be of the [datetime] type, but it was easily converted into a [datetime] type object so it was able to bind to the parameter even without the alias attribute.

Ok, now we are going to add an Alias attribute for ‘DateTime’ to handle the incoming object.

Function Get-Something_PropName_Alias {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipelineByPropertyName=$True)]
        [Alias('DateTime')]
        [datetime[]]$Date
    )
    Process {$Date}
}
Trace-Command parameterbinding {$nonType | get-Something_PropName_Alias} -PSHost

image

As expected with the nontype input, the first check fails and then, thanks to the alias that handles the DateTime parameter, is successful with the COERCION check. Rather than show what would happen with the ByValue attempt, what do you think will happen this time around? HINT: History will repeat itself.

Working with both ByPropertyName and ByValue and Aliases

Up until now, I have been working with either ByValue or ByPropertyName, but never actually combining both into a function. That changes with the following example. Here I will have both configured as well as setting an Alias to show what happens both with the nontype input.

Function Get-Something_PropName_Value_Alias {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias("Datetime")]
        [datetime]$Date
    )
    Process {$Date}
}

image

This shows a perfect example of the list I showed on how the parameter attempts to bind the incoming data. First it attempts the ByValue and ByPropertyName with NO COERCION (matching the type of object to parameter) and then proceeding to the type conversion (COERCION) with ByPropertyName before finally succeeding with the ByValue type conversion.

Doing what I did earlier by adding a 1 instead of the $nonType shows a different result by succeeding on the ValueFromPipeline WITH COERCION.

image

If using No Aliases in your function for parameters, expect some issues as well. If you are passing an object into your function that doesn’t have the same property name as your parameter, then it will fail regardless of how the pipeline attributes are set. The only way it would succeed is if you pass a single object (vs. an object that has multiple properties) that will work for the ByValue and is either of the same type or can be converted to the required type.

That wraps up this post on implementing pipeline support as well as taking a swim into using Trace-Command to debug parameter binding. Hopefully this has provided you enough information to feel better prepared to implementing pipeline support as well as troubleshooting when it fails.

Posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 | Tagged , , , | 8 Comments