Good, Better, Best

I’m very happy to see more and more people getting their hands dirty with Windows PowerShell.  A common challenge I see across different support forums is getting information from one part of a script to another. Very often the user has created a function and is attempting to use its output elsewhere in their script. Often I can tell the scripter is coming from a VBScript background or at least thinking that way. There’s nothing necessarily wrong with writing a PowerShell script in a VBScript style, but these scripts are missing out. Let me walk you through several iterations of a function to demonstrate.

Here’s a variation on a function I saw posted recently.

Function Get-Simple {
  
  $name=Read-Host "Enter a name"
  $password=Read-Host "Enter a password" -asSecureString
  $size=Read-Host "Enter a size"
  $server=Read-Host "Enter a server name"
 
}

In VBScript this would probably be a subroutine and is an acceptable way to modularize a repeatable block of code. However the first problem is one of scope. 

#this fails because the variables don't survive the scope
Get-Simple
Write-Host "Creating $name on $server" -foregroundcolor CYAN

The variables in the Get-Simple function only exist for the function scope. Once the function ends, the scope evaporates and so do all of the variables. Normally the best practice is to not access out of scope variables and to define any variable in a given scope before you use it. However if you know what you’re doing and plan carefully you can access out of scope variables. Here’s a revised version of the Get-Simple function.

Function Get-Simple2 {
  #assign variables to the parent scope
  #not a best practice but legal
  $script:name=Read-Host "Enter a name"
  $script:password=Read-Host "Enter a password" -asSecureString
  $script:size=Read-Host "Enter a size"
  $script:server=Read-Host "Enter a server name"
}

Now this function is a little more usable in my PowerShell script, but there’s still a potential issue.

#this now works, but the function is limited
Get-Simple2
Write-Host "Creating $name on $server" -foregroundcolor CYAN
#look what kind of value $i provide based on $size from the function
$i=$size*4
$i

If you run this code and entered a number like 5 when prompted for a size, PowerShell will set$i to a value of 5555 because it will treat 5 as a string. Read-Host returns strings and this trips many PowerShell beginners. The solution is to cast variables to the correct type. I’ll show you an example in a moment.

Again, there’s nothing necessarily wrong with this function, but it doesn’t take advantage of PowerShell’s object nature. Consider this improved version.

Function Get-Better {
  #let's create an object
  $name=Read-Host "Enter a name"
  $password=Read-Host "Enter a password" -asSecureString
  #cast $size as an integer
  [int]$size=Read-Host "Enter a size"
  $server=Read-Host "Enter a server name"
  
  #create an empty custom object
  $obj=New-Object PSObject
  
  #assign properties
  $obj | Add-Member -MemberType Noteproperty -name "Name" -value $name
  $obj | Add-Member -MemberType Noteproperty -name "Password" -value $password
  $obj | Add-Member -MemberType Noteproperty -name "Size" -value $size
  $obj | Add-Member -MemberType Noteproperty -name "Server" -value $server
 
  #write the object to the pipeline
  $obj
}

The function uses essentially the same Read-Host expressions. Notice that I’m casting $size as a [int]. Even though Read-Host will output a string, as long as I enter something that looks like a number PowerShell will attempt to convert it to the correct type. By the way, depending on your needs you might also have used types like [int64] or [double]. The main change to this function is that it will write an object to the pipeline. I create an empty generic object using New-Object and then pipe it to Add-Member to define properties. The property values come from the Read-Host commands. A function like this is much more PowerShell-like.

$var=Get-Better
 
Write-Host "Creating $($var.name) on $($var.server)" -foregroundcolor CYAN
#Now look what kind of value we get from $i
$i=$var.size*4
$i

PowerShell no longer has to rely on simple values. I have a rich object with properties of varying types that I can easily manipulate. I hope you can see how this is an improvement over the original function. But wait…there’s more.

When I write a function or even a script, I want it to be as flexible and easy to use as possible. Here’s one more improvement.

Function Get-Best {
 Param([string]$name=$(Read-Host "Enter a name"),     
       [Security.SecureString]$password=$(Read-Host "Enter a password" -asSecureString),
       [int]$size=1,
       [string]$server=$(Read-Host "Enter a server name")
       )
  
  #create an empty custom object
  $obj=New-Object PSObject
  
  #assign properties
  $obj | Add-Member -MemberType Noteproperty -name "Name" -value $name
  $obj | Add-Member -MemberType Noteproperty -name "Password" -value $password
  $obj | Add-Member -MemberType Noteproperty -name "Size" -value $size
  $obj | Add-Member -MemberType Noteproperty -name "Server" -value $server
 
  #write the object to the pipeline
  $obj
}

This function accepts parameters. I’ve cast each parameter to the appropriate type. This forces PowerShell to verify that the value is of the correct type. If not an exception will be raised and PowerShell will terminate the command. This is preferable to getting unexpected results from your function because an object is not the type you were expecting. Notice I’m also providing default values for each variable. In keeping with the original function I’ll prompt for a value if none is specified. The $size parameter has a default value of 1. If you run this code, the experience and results are essentially the same.

$var=Get-Best
 
#Now look what kind of value we get from $i
Write-Host "Creating $($var.name) on $($var.server)" -foregroundcolor CYAN
$i=$var.size*4
$i

However, if I already know what value I want to specify for a given parameter, all I need to do is specify it.

$var=Get-Best -size 5 -server "FILE01"
Write-Host "Creating $($var.name) on $($var.server) with a size of $($var.size)" -foregroundcolor CYAN
#returns Creating Jeff on FILE01 with a size of 5

This final version of my function has everything I need to fully take advantage of PowerShell. It writes an object to the pipeline that is easy to use and can be customized via input parameters, many with default values.

I hope you found this “object” lesson helpful. PowerShell v2.0 takes us even further but I’ll save that discussion for another day.

If you’d like to try out these functions, download a script file here.