Win32_OperatingSystem

Powershell – Truth about default formatting

Have you ever ran a powershell cmdlet and stare at the output, and ever wondered, what makes Powershell display the results in such a way? Is there any hidden configuration that guides the powershell engine to display results in a specific format for one cmdlet and in a different format for another cmdlet??

Read along. This blog will answer your curiosity !

 

Example: Below is the output of Get-service

get-service

 

Below is the output of Get-Process

get-process

 

The answer is Yes. The Powershell Engine is indeed guided by a configuration file that tells powershell on how to display the results. Or we call it as the “Default rules for Formatting” (not an official name :))

You will find the configuration files in the Powershell Installation Folder. Use the “$pshome” variable to find out the Powershell Installation Folder.

PS E:\Work\Powershell\scripts> $pshome
C:\Windows\System32\WindowsPowerShell\v1.0

Change the directory to the Powershell Installation directory and you must be able to find a file named “DotNetTypes.format.ps1xml”.

pshome

Please be cautious, not to edit the file. As it will break the signature and the PowerShell will not be able to use it anymore.

If you want to find out the default rules that are applied, then simply open the file and search for the cmdlet.

Example: If I want to know the default rules for the “Get-Service” cmdlet, I search the file for the keyword “service”. Note that the keyword “Service” should be enclosed within the “Name” tags. That is the correct one. As per the below image.

dotnettypes

 

You can double confirm if it is the correct branch, using the “<TypeName>” tag. This value should equate to the TypeName when you do a Get-Member of that cmdlet.

Example: “System.ServiceProcess.ServiceController” for Get-Service

Now what you are looking in the file are set of directions that the Powershell Engine follows. If you can see in the preceding lines, we can see a <TableControl> tag, which says, that the output should be in the form of table. Next few lines specifies the attributes of the table, such as Width and Height.

 

When you run Get-Service, here’s what happens:

  1. The cmdlet places objects of the type System.ServiceProcess.ServiceController into the pipeline.
  2. At the end of the pipeline is an invisible cmdlet called Out-Default. It is always there, and it’s job is to pick up what ever objects are in the pipeline after all the commands have run.
  3. Out-Default passes the objects to Out-Host, because the PowerShell console is designed to use the screen (called the host) as it’s default form of output.
  4. Most of the Out- cmdlets are incapable of working with normal objects. Instead, they are designed to work with special formatting instructions. So when Out-Host sees that it has been handed normal objects, it passes them to the formatting system.
  5. The formatting system looks at the type of the object and follow an internal set of formatting rules. It uses those rules to produce formatting instructions, which are passed back to Out-Host.
  6. Once Out-Host sees that it has formatting instructions, it follows those instructions  to construct the onscreen display.

So when you run the below cmdlet,

 Get-Process | Out-File process.txt

The out-File will see the normal objects. It will pass them to the formatting system, which will create formatting instructions and then passes back them to the Out-File. The Out-File then constructs the file based on those formatting instructions.

Below are the formatting rules:

  • First Rule: The System looks to see if the type of object it is dealing with has a predefined view. That is what you saw in the DotNetTypes.format.ps1xml.  There are other .format.ps1xml files installed with Powershell, and they are loaded when the powershell starts. You can create your own predefined views as well.

 

  • Second Rule: If the system is not able to find a predefined view, then it will look to see if anyone has declared a “default display property set” for that type of object. You can find that in a configuration file called- “Types.ps1xml” (under the Powershell Home directory)

 

defaultpropertyset

Go back to Powershell and Run:

Get-WmiObject win32_operatingsystem

get-wmiobject

I guess, now you know from where these entries came from. The properties you see are present because they are listed as defaults in the Types.ps1xml. If the formatting system finds a “default display property set”, it will use those set of properties.

 

  • Third Rule: It is about what kind of output to create. If the formatting system will display four or fewer properties, it will use a table. If there are five or more properties, then it will display the results as a list. This is why the Win32_OperatingSystem object was not displayed as a table, as it contained six properties, triggering a list. The theory is that more than 4 properties might not fit into a table, without truncating information.
Advertisement

Powershell – Writing a Filtering Function

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.

Powershell – Writing a parameterized function

As a system administrator or an ITPro we need to run cmdlets to administer our environment. Using powershell, administrating gets pretty easy. But if you have to run same cmdlets again and again for multiple boxes, the process may be tiring or irritating. We need a way  to tell the powershell to do tasks repeatedly, without our intervention (unless it is necessary), and a way to achieve modularity among the tasks that we perform. This can be achieved using functions.

We will be talking about three different types of functions in a three part series. In this blog we will discuss about the simplest form of a function called – Parameterized function.

Lets look at the two cmdlets below:

Get-WmiObject Win32_OperatingSystem -ComputerName vaio | select @{l='ComputerName';e={$_.__SERVER}}, BuildNumber, ServicePackMajorVersion
Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName vaio | select @{l='SysDriveFree(MB)';e={$_.Freespace/1MB -as [int]}}

We can run them in the shell. The problem is that these two commands will return two independent sets of results. Another problem is that the computer name is hard coded.

To achieve modulatity, we can break down the task into 3 steps:

  • We need to get a computer name or several computer names from someplace.
  • We have to retrieve the Win32_OperatingSystem WMI Class.
  • We have to retrieve the Win32_LogicalDisk WMI Class.

Trying to get all the output into a single table

$computername = 'vaio'

$os = Get-WmiObject Win32_OperatingSystem -ComputerName vaio | select @{l='ComputerName';e={$_.__SERVER}}, BuildNumber, ServicePackMajorVersion

$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName vaio | select @{l='SysDriveFree(MB)';e={$_.Freespace/1MB -as [int]}}

Write-Host "Computer`tBuildNumber`tSPVersion`tFreeSpace"
Write-Host "========`t==========`t==========`t========="
Write-Host ("{0}`t{1}`t{2}`t{3}" -f ($os.ComputerName),($os.BuildNumber),($os.ServicePackMajorVersion),($disk.'SysDriveFree(MB)'))

The output format is not that great, but the information is what we need.

What we did in the above code was:

  • We have kept the computer name in a variable.
  • We are using Write-Host to create a table header.
  • We are using -f to format the table row.

Wrapping the code in a function

We want to modularize our code into function, which is a self-contained, stand alone unit of work that we can distribute easily. An easy way make a function is to wrap it in a function declaration, as shown here:

function Get-ServerInfo {

$computername = 'vaio'

$os = Get-WmiObject Win32_OperatingSystem -ComputerName vaio | select @{l='ComputerName';e={$_.__SERVER}}, BuildNumber, ServicePackMajorVersion

$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName vaio | select @{l='SysDriveFree(MB)';e={$_.Freespace/1MB -as [int]}}

Write-Host "Computer`tBuildNumber`tSPVersion`tFreeSpace"
 Write-Host "========`t==========`t==========`t========="
 Write-Host ("{0}`t{1}`t{2}`t{3}" -f ($os.ComputerName),($os.BuildNumber),($os.ServicePackMajorVersion),($disk.'SysDriveFree(MB)'))

}

Parameterizing the function

We need to change something in the function, as we do not want the computer name to always be ‘vaio’. The solution is to change the $computername from a variable into a parameter, which we can do by adding it to a Param() block, at the top of the function.

function Get-ServerInfo {

param {
 $computername = 'localhost'
 }

$os = Get-WmiObject Win32_OperatingSystem -ComputerName vaio | select @{l='ComputerName';e={$_.__SERVER}}, BuildNumber, ServicePackMajorVersion

$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName vaio | select @{l='SysDriveFree(MB)';e={$_.Freespace/1MB -as [int]}}

Write-Host "Computer`tBuildNumber`tSPVersion`tFreeSpace"
 Write-Host "========`t==========`t==========`t========="
 Write-Host ("{0}`t{1}`t{2}`t{3}" -f ($os.ComputerName),($os.BuildNumber),($os.ServicePackMajorVersion),($disk.'SysDriveFree(MB)'))

}

You can see that, we have set the $computername equal to ‘localhost’. That will now serve as a default value. If someone runs this function without specifying a computer name, the function will target ‘localhost’, which is usually a safe operation.

Now, the function can be given any alternative computer name in any of these ways:

Get-ServerInfo -computername vaio

Get-ServerInfo -comp vaio

Get-ServerInfo vaio

Returning Objects from function

Powershell does not really like text – it likes Objects. What we have been doing so far in our function is attempting to format our output as a text table. By doing so we are working against Powershell’s native capabilities, which we are making it harder to get the output we want. That means, we need to stop trying to output texts and instead output objects.

Because we are getting information from two places – Win32_OperatingSystem and Win32_LogicalDisk, we cannot output either of the objects that we got back from WMI. Instead we will create a new blank object [PSObject], that we can use to combine our four pieces of information. We simply need to create a PSObject and add the information in the form of properties.

function Get-ServerInfo {

param (
 $computername = 'localhost'
 )

 $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


}

Get-ServerInfo | Format-Table -AutoSize

Here is what we did:

  • We have simplifying the WMI queries.
  • After querying the information, we create a new PSObject and put  it in a variable.
  • To add information to that object, we pipe the object to Add-Member four times.
  • By using, Write-Output, we are writing the final object into the pipeline.
  • The above code also shows the command we can use to call the function.

The above function is infinitely flexible, because it outputs objects instead of text. Example, all of the below are legitimate ways of using the function.

Get-ServerInfo | Format-Table -AutoSize
Get-ServerInfo -computername vaio | Export-Csv info.csv
Get-ServerInfo -comp localhost | ConvertTo-Html | Out-File info.html