We are discussing on how to write a Powershell Function. This is part 2 of the three part series. Please check my blog on Parameterized function, which is the part 1 of this series.
The second type of function is called a Filtering Function or also called as Pipeline Function.
- You can accept one kind of information through the pipeline. This might be computer names, processes, or any other kind of single kind of information.
- What ever you accept through the pipeline can come as a single object, or multiple objects can be piped in. You will write one (or many) commands that execute against each piped-in object, no matter how many there are.
- You can designate additional parameters for other input elements. The values provided to these parameters will be used for each execution of your commands.
Below is an example of a Filtering function: function Get-ServerInfo { BEGIN{} PROCESS{ $computername = $_ $os = Get-WmiObject Win32_OperatingSystem -ComputerName $computername $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computername $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computername $obj | Add-Member -MemberType NoteProperty -Name BuildNumber -Value ($os.BuildNumber) $obj | Add-Member -MemberType NoteProperty -Name SPVersion -Value ($os.ServicePackMajorVersion) $obj | Add-Member -MemberType NoteProperty -Name SysDriveFree -Value ($disk.free / 1MB -as [int]) Write-Output $obj } END{} } Get-Content names.txt | Get-ServerInfo | Format-Table -AutoSize
Here is what we did:
- We have added a BEGIN, PROCESS and END block
- What ever is in the BEGIN block will execute the first time this function is called in the pipeline; the END block will execute when the function is almost finished.
- The PROCESS script block will execute one time for each object that is piped into the function. (If you do not pipe any input then PROCESS block will execute once)
- This script expects computer names to be piped in, so if you pipe four names, the PROCESS block will execute four times. Each time, the “$_” placeholder will be automatically populated with a new object from the pipeline.
- The last line of the script shows how you would execute this function: pipe a bunch of string objects to it.
Adding a parameter to a filtering function
What if we want the cmdlet to accept computer names either from the pipeline or from a parameter? In other words, you want both of theses to work:
Get-Content names.txt | Get-ServerInfo Get-ServerInfo -computername (Get-Content names.txt)
Rite now the function does not accept parameters, so lets include a parameter.
function Get-ServerInfo { param ( [String] $computername ) BEGIN{} PROCESS{ $computername = $_ $os = Get-WmiObject Win32_OperatingSystem -ComputerName $computername $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computername $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computername $obj | Add-Member -MemberType NoteProperty -Name BuildNumber -Value ($os.BuildNumber) $obj | Add-Member -MemberType NoteProperty -Name SPVersion -Value ($os.ServicePackMajorVersion) $obj | Add-Member -MemberType NoteProperty -Name SysDriveFree -Value ($disk.free / 1MB -as [int]) Write-Output $obj } END{} } Get-Content names.txt | Get-ServerInfo | Format-Table -AutoSize
However, we run into a problem. The original way of running the command – which is included at the bottom if the script listing – will continue to work. But the other way of running the function does not work.
Get-ServerInfo -computername vaio Get-WmiObject : Cannot validate argument on parameter 'ComputerName'. The argument is null or empty. Supply an argument that is not null or empty and then try the command again. At line:12 char:65 + $os = Get-WmiObject Win32_OperatingSystem -ComputerName $computername + ~~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [Get-WmiObject], ParameterBindingValidationExcepti on + FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.GetW miObjectCommand Get-WmiObject : Cannot validate argument on parameter 'ComputerName'. The argument is null or empty. Supply an argument that is not null or empty and then try the command again. At line:14 char:87 + ... -ComputerName $computername + ~~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [Get-WmiObject], ParameterBindingValidationExcepti on + FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.GetW miObjectCommand
This is because, all of the code in the function lives within the PROCESS block. We are taking the computer name from the $_ placeholder, which is populated with an object from the pipeline input. Except that we did not pipe anything in, so the PROCESS block only executes once, and $_ never contains anything, so $computername never contains anything, so nothing works.
Breaking the function into two parts
So, to fix the issue, we need to break the function into two pieces. The main working part can stand alone and can be used whether you are getting input from the pipeline or from a parameter, called as worker function. The second part will be the public function that you want people to actually use. It’s job is to determine, where the input is coming from and to pass the one computer name at a time to the worker function.
We have two things to keep in mind:
- When input comes from the pipeline, the shell will enumerate through the objects automatically, allowing you to work with one at a time. You can pass those objects, as they are processed, to the worker function.
- When the input comes form a parameter, you may gave either one object or many objects, but the PROCESS script block will only execute once regardless. So you will have to manually enumerate the parameter so that you can get to each object, one at a time.
We have a Powershell’s built in variable called $PSBoundParameters, and it contains each parameter that was manually specified. It has a ContainsKey() method that will let you test to see if a particular parameter was used or not.
function GetServerInfoWork { param ( [String]$computername ) $os = Get-WmiObject Win32_OperatingSystem -ComputerName $computername $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computername $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computername $obj | Add-Member -MemberType NoteProperty -Name BuildNumber -Value ($os.BuildNumber) $obj | Add-Member -MemberType NoteProperty -Name SPVersion -Value ($os.ServicePackMajorVersion) $obj | Add-Member -MemberType NoteProperty -Name SysDriveFree -Value ($disk.free / 1MB -as [int]) Write-Output $obj } function Get-ServerInfo { param( [String[]]$computername ) BEGIN{ $usedParameter = $false if($PSBoundParameters.ContainsKey('computername')){ $usedParameter = $true } } PROCESS{ if($usedParameter) { foreach($computer in $computername) { GetServerInfoWork -computername $computer }else { GetServerInfoWork -computername $_ } } } END{} } Get-ServerInfo -verbose -computername (Get-Content E:\Work\Powershell\scripts\names.txt.txt) Get-Content E:\Work\Powershell\scripts\names.txt.txt | Get-ServerInfo | Format-Table -AutoSize
Here is what we did :
- We started by pulling most of the function code into a worker function.
- In the public function, we declared the parameter to accept multiple stings
- In the BEGIN block, which execute first, we want to see if the input is coming from the pipeline or via the parameter. We start by assuming that the input came from the pipeline, setting $usedParameter to $false. Then test the $PSBoundParameters variable, and if it does indeed contain the comptername key, then we know that the -computername parameter was used, so we set the $usedParameter to $true.
- Even though the $usedParameter is created in the BEGIN block, it will be accessible in the PROCESS and END block.
- In the PROCESS block, we check the variable to see what to do.
- We use a foreach loop to enumerate the parameter input, passing each object to the worker function one at a time.
- If the input came from the pipeline, the PROCESS block is already handling the enumeration, so we let it do its job and pass the $_ placeholder’s contents to the workers function.