Create a PowerShell Function to Display System Uptime

Recently I saw a post on Twitter about a PowerShell script to display system uptime. Since I’m always curious to see how people are using PowerShell I followed the link the script, which was posted on the TechNet Script Gallery site. As expected the author used Windows Management Instrumentation (WMI) to query the Win32_OperatingSystem class.

This class includes a property that indicates when the computer last booted. If you use Get-CIMInstance, this value is automatically formatted into as a datetime value. I appreciated that he used a Timespan object to display the uptime. But I did have one comment about his code, and something that I see quite often. His original code may be updated by the time you read this. By the way, the author was very receptive to my suggestions and open to me about using his code as a learning device, as he is still learning PowerShell. Even if you don’t have a need for the end result, the process I want to explore from command to tool should be very useful.
To begin, here’s the original code.

$Reboot = Get-CimInstance Win32_OperatingSystem -ComputerName chi-dc04 | Select-Object CSName,LastBootUpTime
$date = Get-Date
New-TimeSpan -Start $Reboot.LastBootUpTime -End $Date |
Select-Object @{Label = "System Name"; Expression = {$Reboot.CSName}},
@{Label = "Last Reboot Time"; Expression = {$Reboot.LastBootUpTime}},Days,Hours,Minutes,Seconds |
Format-Table -AutoSize

PowerShell Function to Display System Uptime

Creating a PowerShell Function

The one major drawback to this example is that it includes formatting via the Format-Table cmdlet. This code can be turned into a useful tool, which is what I intend to show you, but including formatting limits you as you’ll see in a moment. First, let’s turn this into a simple function. I have a set of commands that work just fine from the prompt. I can easily paste them inside a function, turn the computername into a parameter and I’m done!

Function Get-MyUptime {
Param([string]$Computername = $env:COMPUTERNAME)
$Reboot = Get-CimInstance Win32_OperatingSystem -ComputerName $computername |
Select-Object CSName,LastBootUpTime
$Date = Get-Date
New-TimeSpan -Start $Reboot.LastBootUpTime -End $Date |
Select-Object @{Label = "System Name"; Expression = {$Reboot.CSName}},
@{Label = "Last Reboot Time"; Expression = {$Reboot.LastBootUpTime}},Days,Hours,Minutes,Seconds |
Format-Table -AutoSize
} #end function

A quick word on names: don’t re-invent the wheel. You should be able to use a common parameter name that you see elsewhere. If your parameter needs to reflect the name of a computer, then use Computername since that is commonly used in other cmdlets. Don’t use something like System or MachineName. Doing so makes your command less intuitive. If you need a parameter like SysName say to support your corporate culture, you can add it as an alias. You’ll see an example of that later. Aren’t sure about your parameter name choice? Ask PowerShell for other cmdlets that might be using it:

get-command -ParameterName computername

The name of your command should use one of the standard verbs. Run the Get-Verb cmdlet to discover them. The noun should be singular and reflective of the “thing” you are working with. To avoid possible naming collisions you might consider using a noun prefix to reflect you or your organization.
With my function, it becomes much easier to get system uptime.

get-myuptime "chi-dc01"
"chi-dc01","chi-dc04","chi-core01" |  foreach {Get-MyUptime $_}

For the most part, this looks OK.
120914 1941 Scriptingwi2
But what happens when you try to do something like sort the output?

"chi-dc01","chi-dc04","chi-core01" | foreach {Get-MyUptime $_} | Sort Days,Hours,Minutes

Remember, the default sort is ascending which isn’t what I got.
120914 1941 Scriptingwi3
Why? Ask PowerShell.

get-myuptime "chi-dc01" | get-member

120914 1941 Scriptingwi4
This is why you never include format cmdlets in our scripts and functions. They write formatting objects to the pipeline. The only thing you can do is pipe to Out-File or Out-Printer. Instead, write your function so that it writes objects to the pipeline. Then if you need to format it, run the command with Format-Table. Maybe tomorrow you need Format-List or to export to a CSV file. If you include formatting inside your function, you’re done. That’s all your command will ever be able to do.

With that in mind here’s my revised function.

Function Get-MyUptime {
Param([string[]]$Computername = $env:COMPUTERNAME)
$Reboots = Get-CimInstance Win32_OperatingSystem -ComputerName $computername | Select-Object CSName,LastBootUpTime
$Date = Get-Date
Foreach ($reboot in $reboots) {
    New-TimeSpan -Start $Reboot.LastBootUpTime -End $Date |
    Select-Object @{Name = "SystemName"; Expression = {$Reboot.CSName}},
    @{Name = "LastRebootTime"; Expression = {$Reboot.LastBootUpTime}},Days,Hours,Minutes,Seconds
}
} #end function

While I was removing Format-Table I made a few other changes. First, I modified the Computername parameter so it could accept a collection of computer names.

Param([string[]]$Computername = $env:COMPUTERNAME)

The parameter is expecting a string and inserting [] tells PowerShell to expect multiple values, usually separated by commas. Now I can run the command like this:

get-myuptime "chi-dc01","chi-dc04","chi-core01"

This works because the Computername parameter from Get-CimInstance, also takes an array of strings for a computer name. This also means that when this part of the function runs:

$Reboots = Get-CimInstance Win32_OperatingSystem -ComputerName $computername | Select-Object CSName,LastBootUpTime

The $Reboots variable will be an array or collection of CIM instances. This means I will need to process each one individually in order to get the uptime. That’s why I’m using a ForEach enumerator.

Foreach ($reboot in $reboots) {
    New-TimeSpan -Start $Reboot.LastBootUpTime -End $Date |
    Select-Object @{Name = "SystemName"; Expression = {$Reboot.CSName}},
    @{Name = "LastRebootTime"; Expression = {$Reboot.LastBootUpTime}},Days,Hours,Minutes,Seconds
}

If you compare these two versions closely you’ll see that I changed the name of the variable holding the Get-CimInstance command from $Reboot to $Reboots. I did this so that I wouldn’t have to revise the timespan code that already worked. I simply wrapped it in a ForEach enumerator and used the same variable name.

I also changed the custom hash table in Select-Object that defines new properties. While you can use Label and Name interchangeably, I prefer to use Name because it helps remind me that I am defining a property name and not a formatting label. That is also why I modified the names to remove the spaces. My function is going to write an object to the pipeline and it is much easier to work with property names without spaces.
My result is a simple object
120914 1941 Scriptingwi5
Need formatting? Use PowerShell.
120914 1941 Scriptingwi6
Now that I have objects, I can sort, filter or whatever I need.

get-myuptime "chi-dc01","chi-dc04","chi-core01" |
where {$_.days -le 7} |
Sort LastRebootTime |
format-table

I want to only see servers that have been up for 7 days or less, sorted on the last reboot time.
120914 1941 Scriptingwi7
Look how far we’ve come from a few lines of PowerShell commands to a re-usable function. And we’re not done yet! There’s much more that I want to show you but that will have to wait for another article. In the meantime, I would encourage you to get your PowerShell functions at least to this level. Think writing objects to the pipeline and leave formatting to PowerShell.