A Peek into a PowerShell Class (and a Script Module / Advanced Function example)

Wrapping up PowerShell class, I always like to conclude with a big finish. This week, it's a script module that contains two "advanced functions" (or, if you prefer, "script cmdlets"). You'll see examples of debug trace code, error handling, support for the -confirm and -whatif switches, comment-based help, "private" functions within a script module, and so on. Both Get-OSInfo and Reboot-Windows are functional (and the use of "Reboot" as a verb was deliberate, to demonstrate PowerShell's warning message about nonstandard verbs when you import the module). Drop this into \Documents\WindowsPowerShell\Modules\Tools\Tools.psm1, and then run Import-Module tools to give them a try.

#requires -Version 2
#$DebugPreference = 'Continue'
 
function Get-OSInfo {
 
.SYNOPSIS
Gets information about the operating system
.DESCRIPTION
Get-OSInfo retrieves operating system information including
BIOS serial number, service pack version, and so on.
.PARAMETER computername
Specifies one or more computer names to query. Accepts
pipeline input.
.PARAMETER logfile
The path and filename of a file to log failed computers into.
.EXAMPLE
Get-OSInfo -computername localhost
.EXAMPLE
Get-Content names.txt | Get-OSInfo
#>
    [CmdletBinding()]
    param (
        [Parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True,
        ValueFromPipelineByPropertyName=$True)]
        [Alias('host')]
        [string[]]$computername,
         
        [Parameter(Mandatory=$True)]
        [string]$logfile
    )
    BEGIN {
        Write-Verbose "Inside Get-OSWorker BEGIN block"
        $inputFromPipeline = $true
        if ($psBoundParameters.containskey('computername')) {
            $inputFromPipeline = $false
        }
        del $logfile -ea silentlycontinue
    }
    PROCESS {
        if ($inputFromPipeline) {
            OSWorker -computername $computername -logfile $logfile
        } else {
            foreach ($name in $computername) {
                OSWorker -comp $name -log $logfile
            }
        }
    }
    END {}
}
 
function OSWorker {
    param (
        [string]$computername,
        [string]$logfile
    )
    Write-Debug "Inside OSWorker"
    Write-Debug "`$computername is $computername"
    Write-Debug "`$logfile is $logfile"
    $continue = $True
    try {
        Write-Debug "Attempting WMI call"
        $os = Get-WmiObject -class Win32_OperatingSystem -ea Stop -comp $computername
    } catch {
        $continue = $false
        Write-Debug "WMI call failed"
        $computer | out-file $logfile -append
    }
    if ($continue) {
        Write-Debug "Attempting 2nd WMI call"
        $bios = Get-WmiObject -class Win32_BIOS -comp $computername
        $obj = New-Object -TypeName PSObject
        $obj | Add-Member -MemberType NoteProperty -Name ComputerName -value ($computername)
        $obj | Add-Member -MemberType NoteProperty -Name OSBuild -value ($os.buildnumber)
        $obj | Add-Member -MemberType NoteProperty -Name OSDescription -value ($os.caption)
        $obj | Add-Member -MemberType NoteProperty -Name SPVersion -value ($os.servicepackmajorversion)
        $obj | Add-Member -MemberType NoteProperty -Name BIOSSerial -value ($bios.serialnumber)
        Write-Output $obj
    } else {
        Write-Debug "Not attempting 2nd WMI call"
    }
}
 
function Reboot-Windows {
 
.SYNOPSIS
Restarts, logs off, shutsdown, or powers off one or
more Windows servers.
.DESCRIPTION
Uses the WMI class Win32_OperatingSystem, which has a
Win32Shutdown() method.
.PARAMETER computername
The computer name(s) to target. Accepts pipeline input.
.PARAMETER method
LogOff, Shutdown, PowerOff, Reboot. The operation is not
forced, which means an application can cancel the operation.
.EXAMPLE
Reboot-Windows -computername SERVERDC1 -method PowerOff
#>
    [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')]
    param (
        [Parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True,
        ValueFromPipelineByPropertyName=$True)]
        [Alias('host')]
        [string[]]$computername,
         
        [Parameter(Mandatory=$True)]
        [ValidateSet('LogOff','Shutdown','Reboot','PowerOff')]
        [string]$method
    )
    BEGIN {
        $inputFromPipeline = $true
        if ($psBoundParameters.containskey('computername')) {
            $inputFromPipeline = $false
        }
        Switch ($method) {
            'LogOff' { $param = 0 }
            'Shutdown' { $param = 1 }
            'Reboot' { $param = 2 }
            'PowerOff' { $param = 8 }
        }
    }
    PROCESS {
        if ($inputFromPipeline) {
            if ($pscmdlet.ShouldProcess($computername)) {
                RebootWorker -computername $computername $param
            }
        } else {
            foreach ($name in $computername) {
                if ($pscmdlet.ShouldProcess($computername)) {
                    RebootWorker $name $param
                }
            }
        }
    }
    END {}   
}
 
function RebootWorker {
    param($computername,$param)
    Get-WmiObject -class Win32_OperatingSystem -comp $computername |
    Invoke-WmiMethod -name Win32Shutdown -arg $param
}
 
New-Alias goi Get-OSInfo
 
Export-ModuleMember -function Reboot-Windows
Export-ModuleMember -function Get-OSInfo
Export-ModuleMember -alias goi

You're welcome to extend and add to these functions, or even change them completely. As in-class examples, they're obviously exhibiting a lot of different things. For example, we discussed the fact that well-written (and consistently used) Write-Debug statements could sometimes serve in lieu of inline comments, as well as serving as trace code, which is why you don't see any inline comments in these examples. However, in the course of class, we didn't get around to adding Write-Debug everywhere.

Discuss this Blog Entry 3

on Nov 9, 2010
@Bartek, you're absolutely right. You get a warning every time. It's annoying - and a reason to NOT use non-standard verbs!

As for my choice of syntax, keep in mind that in PowerShell there are usually a lot of ways to do anything. I find that more beginners do well with the syntax I used, in part because it breaks each property addition out onto a separate command/line. There's nothing wrong with the syntax you propose. It's actually a bit more concise - but "concise" can also be "confusing" to shell newcomers (something everyone should keep in mind as they blog). Since I spend a ton of time training, my syntax concerns are usually around readability and cognition, and not about concise code. It's the same reason you won't see me use the "%" alias very often, or the "?" alias. I should actually be better than I am about spelling out parameters and not using positional ones, actually.

on Nov 16, 2010
Hi Don,
I'm new to Powershell, and after my 1st course, and following your blogs and demonstrations, I've been addicted to Powershell. Just wanted to say thank you for this excellent example of using Advanced Functions, as well as the different techniques you threw in, like checking if the input is coming from the pipeline or input parameter.
I do have a quesiton if you can clarify for me. you have this example:
Get-Content names.txt | Get-OSInfo

But in the script, you declared the $logfile as a Mandatory parameter, shouldn't the script fail, if you are missing the a filename to capture the fail computers?
[Parameter(Mandatory=$True)]
[string]$logfile
From the looks of it, I was expecting Get-Content names.txt | Get-OSInfo -logfile c:\error.txt

Thanks and Great work as always.
-Kane










Bartek Bielawski (not verified)
on Nov 5, 2010
Don, you export-modulemember that uses non-standard verb.. I would expect a warning each time module is loaded.
I also don't see any reason for using V1:
$obj | Add-Member
method of creating custom object instead of
New-Object PSObject -Property @{}





Please or Register to post comments.

What's PowerShell with a Purpose Blog?

Don Jones demystifies Windows PowerShell.

Blog Archive

Sponsored Introduction Continue on to (or wait seconds) ×