Getting Started - A Deeper Dive Into Functions

Part 3 Review

At the end of Part 3, we created the following functions:

function Get-OSInfo {

    $osInfo = Get-WmiObject -Class Win32_OperatingSystem

    $versionText = Get-VersionText -version $osInfo.Version.Split('.')[0]

    Write-Host "You're running $versionText"

}


function Get-VersionText {
    param($version)

    Switch ($version) {

        10{$versionText = 'Windows 10'}

        6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

        6.2 {$versionText = 'Windows 8 or Server 2012'}

        6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

        6.0 {$versionText = 'Windows Vista or Server 2008'}

        5.2 {$versionText = 'Windows Server 2003/2003 R2'}

        5.1 {$versionText = 'Windows XP'}

        5.0 {$versionText = 'Windows 2000'}

    }

    Return $versionText

}

Get-OSInfo

Get-VersionText issue explored

Let's go ahead and run that script in the ISE (or copy and paste it into the console).

Here's the output I get:

Stay in the console pane if you're in the ISE, and let's run the following command:

Get-VersionText -version 6

Here's the output:

This presents a problem! The way we coded the version we send to Get-VersionText takes only the first part of the version number before the "." in the version returned from the Get-WMIObject command. To see this, run the following commands:

$osInfo = Get-WmiObject -Class Win32_OperatingSystem
$osInfo

The version we're sending the Get-VersionText function is gathered using the following code:

$osInfo.Version.Split('.')[0]

For me, running Windows 10, that is fine! But if you are running Windows 7, it would have just been 6! That would not have returned the correct version (as we saw above). The version we need to match correctly to display the text for Windows 7 is 6.1.

The fix

Let's look at a possible solution.  We need to get the numbers before the last decimal point in the Version property. There are two string manipulation methods that can help us out here. They are SubString and LastIndexOf.

SubString takes two arguments to display a specific part of the string based on the numerical values you specify. 

LastIndexOf takes one argument as the text to look for the last instance of. It then returns the numerical value of where it matches. 

Here is the solution to store the correct version number to look up:

$versionNumber = $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.'))

If you want to see what LastIndexOf does on its own (with value of '.') run:

$osInfo.Version.LastIndexOf('.')

So we're running $osInfo.Version.SubString(0,4) when we use  $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.')) **And the version is 10.x.xxx)**

That's why we use LastIndexOf and not a hard set numerical value. Let's look at setting up a test to see that in action with a different version number. 

$testVersion = '6.1.1337'
$testVersion.SubString(0,4)
$testVersion.SubString(0,$testVersion.LastIndexOf('.'))

You should see the following results:

Notice that if we used (0,4) we got an extra '.' at the end as a result. But if we use LastIndexOf, it returns the correct number for SubString to use every time for what we need.

Getting the functions to work

To get the code from Part 3 to work now, we'll have to modify the Get-OSInfo function to add the new line to get the version number, and then use that variable when calling the Get-VersionText function.

We'll also want to declare the parameter type in Get-VersionText for the version parameter to [double].

The reason we want to set the type for this parameter is so that it will take 10.0, and essentially make it 10. However, it will also take 10.1 or 6.1 and keep it at that value. To see that, use the following commands:

[double]10.0
[double]10.1

Perfect, that's just what we want to make sure our Switch statement works in the Get-VersionText function for Windows 10. Here's the code that will work:

function Get-OSInfo {

    $osInfo = Get-WmiObject -Class Win32_OperatingSystem

    $versionNumber = $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.'))
    $versionText = Get-VersionText -version $versionNumber

    Write-Host "You're running $versionText"

}


function Get-VersionText {
    param([double]$version)

    Switch ($version) {

        10{$versionText = 'Windows 10'}

        6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

        6.2 {$versionText = 'Windows 8 or Server 2012'}

        6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

        6.0 {$versionText = 'Windows Vista or Server 2008'}

        5.2 {$versionText = 'Windows Server 2003/2003 R2'}

        5.1 {$versionText = 'Windows XP'}

        5.0 {$versionText = 'Windows 2000'}

    }

    Return $versionText

}

Get-OSInfo

Go ahead and run that code! Looks like it worked for me.

Don't worry if you don't understand what exactly [double] means yet. I'll be going over types later in this post!

Adding CMDlet-like functionality to your functions

What if I told you there's this one weird trick to writing better functions? Yeah... I'd hate me too. There is one thing you can do, however, that will graduate a function from basic to advanced. That would be adding [cmdletbinding()] to the very top of of your scripts and/or functions. (It must be at the top, except for where there is comment based help).

When you use [cmdletbinding()] you need to also include the param() statement. This is true even if your function doesn't require parameters.

What does it do, exactly?

By simply adding [cmdletbinding()] you enable extra functionality for:

  • The ability to add -Verbose and -Debug when calling your functions/scripts.
  • Add the -whatif and -confirm options to your scripts (although, this requires a bit of extra work, but is doable).
  • Extra parameter options for using advanced parameters. These can really come in handy.

Simple advanced function example

function Invoke-SuperAdvancedFunctionality {
[cmdletbinding()]
param()

    Write-Verbose "This function has now graduated!"

}

Let's go ahead and enter that in PowerShell (run via ISE or paste into console), and then call it by using:

Invoke-SuperAdvancedFunctionality

Alright... nothing happened!

Well that's because the line we added is Write-Verbose, not Write-Output or Write-Host. This seems like a good time to go over...

Write-Verbose vs. Write-Debug vs. Write-Host vs. Write-Output

Write-Host

This command is generally frowned upon in the PowerShell community. Write-Host merely outputs text via the current running PowerShell host (be it the ISE or console). 

Pros

  • Easy way to work through scripts while outputting information to yourself.
  • Can use formatting features (`t for [TAB] and `n for new line to name a couple), and colors! 
  • Can present users of your scripts/functions with a nice way to have data presented to them

Cons

  • Some people in the PowerShell community may scoff at you.
  • Can be confused with Write-Output, which should be used differently

Example

Write-Host 'This is an example of Write-Host!' -ForegroundColor White -BackgroundColor DarkBlue

More information
Use the command: 

Get-Help Write-Host -Detailed

Write-Output

This command sends what you specify to the next command in the pipeline. What this means is that if it is the last command used, you'll see the value of Write-Output in the console. You can use Write-Output to return information from a function, or even to set a variable with a command.

Pros

  • Can be used to pass objects to the pipeline in PowerShell.

Cons

  • Can be used to pass objects to the pipeline in PowerShell! (yup can be a con too)
    • Be careful with this! If your intention is to merely display information, do not use Write-Output (especially in a function) as you may clutter the pipeline with random information that could cause some unintentional results.
  • Cannot format the text in any way (as it is an object and information passed to the pipeline).

Example

Write-Output "This is a test"
$test = Write-Output "This is a test"
$test

The first line will simply output the text to the console. The second line shows how it can be used to pass information to the pipeline. The third line will display the value of the $test variable.

Let's see what happens if we run the following commands with Write-Host...

$test = Write-Host "This is a test"
$test

Since Write-Host does not pass information to the pipeline, and merely outputs text to the host, it does not store the result in the variable. The command is simply executed and the output written to the console only.

More information

Use the command:

Get-Help Write-Output -Detailed

Write-Verbose

Write-Verbose is enabled when you use a function or script with [cmdletbinding()] specified at the top. You use it by calling the function or script with the -Verbose flag.

Let's go back to our function Invoke-SuperAdvancedFunctionality from earlier, and call it with the -Verbose flag.

Invoke-SuperAdvancedFunctionality -Verbose

There we go! Now you see the text from the Write-Verbose command we used in the function.

Write-Debug

Write-Debug allows you to step through your code with breakpoints. It is also enabled by adding [cmdletbinding()] to your script or function.

Let's add the following lines to our function:

$processes = Get-Process
Write-Debug "`$processes contains $($processes.Count) processes"

The function should now look like this:

function Invoke-SuperAdvancedFunctionality {
[cmdletbinding()]
param()

    Write-Verbose "This function has now graduated!"

    $processes = Get-Process

    Write-Debug "`$processes contains $($processes.Count) processes"

}

A quick note on strings and escape characters

In PowerShell, values surrounded by "double quotes" are called expanded strings. These allow variables to be resolved and displayed in the string. In this example I wanted to show what the actual variable name is (without resolving it), and also show the .Count property. To do this I first use the escape character of ` before $processes. The ` character tells PowerShell to not display the value of $processes, but to treat it like text.

I then wanted to show the .Count property. To get this to resolve you need to use $() to encapsulate the variable and property you're wanting to call. This is called a subexpression.

If you're just displaying text information in PowerShell, it's best to use single quotes.

Back to Write-Debug!

Now run:

Invoke-SuperAdvancedFunctionality -Verbose

The function will now show you the debugging information, and prompt you for options as to how to continue.

There are really no pros or cons to using Write-Verbose or Write-Debug. I wrote the above pros and cons for Write-Host and Write-Output slightly in jest.

The general use cases for Write-Verbose is when you want to display information. Typically it's to show someone else running your code that has added the -Verbose flag to derive more information.

As for Write-Debug, it's best used to troubleshoot and step through your scripts when you need to pause and check what a value is set to (or anything else you want to stop for). 

Advanced parameters and types

Let's start a new script in the ISE and save it as part4.ps1.

Add the following code:

function Get-WMIInfo {
    [cmdletbinding()]
    param(
    [parameter(Mandatory=$true)]
    [string]
    $lookup,
    [parameter(Mandatory=$false)]
    [double]
    $version,
    [parameter(Mandatory=$false)]
    [int]
    $typeNumber
    )

    Write-Debug "[$lookup] reference requested."

Now, with [cmdletbinding()], we can use some parameter options we couldn't before. One of those is (Mandatory=$true). What that does is requires that parameter to be set for the script or function to run. If it is not initially set, it will prompt you to set it when the code executes.

There are many other options available to you for advanced parameters. You can check those out here.

Types

You can specify type constraints in PowerShell. In the above code we set one for each parameter. 

[string]$lookup
[double]$version
[int]$typeNumber

What PowerShell does it looks for the values/arguments provided to match the datatypes. It they do not, you'll get a pretty error message. Here's an example error when you try to pass a string to the [int] type.

[int]$thisIsGoingToErrorOut = 'one is a number, right?'

Type list

[array]          -An array of values.
[bool]           - $true or $false values only.
[byte]           - An 8-bit unsigned integer.
[char]           - A unicode 16-bit character.
[decimal]     - A single-precision 32-bit floating point number.
[double]      - A double-precision 64-bit floating point number.
[hashtable] - This represents a hastable object in PowerShell. (keys paired with values)
[int]             - A 32-bit signed integer.
[single]       - A (lonely) single-precision 32-bit floating point number.
[string]       - A fixed-length string of Unicode characters.
[xml]           - This represents an XMLdocument object.
 

Using the Switch statement to handle parameter arguments

You can use a Switch statement to handle different arguments given to a parameter.

Let's finish the code for Get-WMIInfo. Paste the following after the Write-Debug command:

Switch ($lookup) {

        {$_ -eq 'osversion'} {

            Write-Debug "Looking up [$version]."

            Switch ($version) {

                10{$versionText = 'Windows 10'}

                6.3 {$versionText = 'Windows 8.1 or Server 2012 R2'}

                6.2 {$versionText = 'Windows 8 or Server 2012'}

                6.1 {$versionText = 'Windows 7 or Server 2008 R2'}

                6.0 {$versionText = 'Windows Vista or Server 2008'}

                5.2 {$versionText = 'Windows Server 2003/2003 R2'}

                5.1 {$versionText = 'Windows XP'}

                5.0 {$versionText = 'Windows 2000'}

                Default {$versionText = 'Unable to determine version!'}

            }

            Write-Debug "[$version] matched with text [$versionText]"

            Return $versionText

        }

        {$_ -eq 'drivenumber'} {


            Write-Debug "Looking up drive # [$typeNumber]"

            Switch ($typeNumber) {

                0 {$typeText = 'Unknown'}

                1 {$typeText = 'No Root Directory'}

                2 {$typeText = 'Removeable Disk'}

                3 {$typeText = 'Local Disk'}

                4 {$typeText = 'Network Drive'}

                5 {$typeText = 'Compact Disk'}

                6 {$typeText = 'RAM Disk'}

                Default {$typeText = "Invalid type number [$typeNumber]"}

            }

            Write-Debug "[$typeNumber] matched with text [$typeText]"

            Return $typeText

        }
    }
}

The first Switch statement evaluates $lookup. The value for $lookup is determined by what is set in the $lookup parameter. 

Let's look at the follow segment of code:

{$_ -eq 'osversion'} { 

What this does it is evaluates if $_ (in this case the value for lookup) matches 'osversion'.

If the condition is found to be true, it will run the code surrounded in {}'s.

In this case, the code would be the same code we used in Part 3 to lookup the OS version text. This would switch the value for $version,and attempt to match the version number.  

The default statement in the switch will execute if no values are matched. For the $version switch that would execute:

Default {$versionText = 'Unable to determine version!'}

The next evaluation point for $lookup is to see if it matches 'drivenumber'.

{$_ -eq 'drivenumber'} { 

If it matches drive number, then it proceeds to attempt to match the drive number received with the associated text.

Finishing the script for Part 4

Let's add the following code after the Get-WMIInfo function:

function Get-OSInfo {
    [cmdletbinding()]
    Param(
    [parameter(Mandatory=$false)]
    [boolean]
    $getDiskInfo = $false
    )

    Write-Verbose "Looking up OS Information..."

    $osInfo        = Get-WmiObject -Class Win32_OperatingSystem

    $versionNumber = $osInfo.Version.SubString(0,$osInfo.Version.LastIndexOf('.'))

    Write-Verbose "Looking up the matching windows edition for version #: [$versionNumber]"

    Write-Debug "Version number stored as [$versionNumber]"

    $versionText = Get-WMIInfo -lookup 'osversion' -version $versionNumber

    Write-Host `n"You're running $versionText"`n

    if ($getDiskInfo) {

        Write-Verbose "Gathing disk information via WMI..."

        $disks = Get-WmiObject -Class Win32_LogicalDisk

        if ($disks) {
            
            Write-Host `n"Disk information!"`n -ForegroundColor White -BackgroundColor Black
            
            Foreach ($disk in $disks) {

                Write-Host `n"Device ID    : $($disk.DeviceID)"
                Write-Host ("Free Space   : {0:N2} GB" -f $($disk.FreeSpace / 1GB))
                Write-Host ("Total Size   : {0:N2} GB" -f $($disk.Size / 1GB))
                Write-Host ("% Free       : {0:P0}   " -f $($disk.FreeSpace / $disk.Size))
                Write-Host "Volume Name  : $($disk.VolumeName)"
                Write-Host "Drive Type   : $(Get-WMIInfo -lookup 'drivenumber' -typeNumber $disk.DriveType)"`n

            }


        } else {

            Write-Host "Error getting disk info!"

        }

    }

}

Get-OSInfo -getDiskInfo $true

Execute the code, and you should see it both displays the OS information, as well as the information for any disks in your system.

Homework

  • After running the script we created, call the functions in different ways. Here's an example of one:

Get-OSInfo -getDiskInfo $true -Debug
Get-WMIInfo -lookup osversion -version 100
  • Review the code we used today.
    • What does the foreach statement do?
    • Take a look at the -f operator used to format a percentage (you can also see some PowerShell math in action)
Write-Host ("% Free : {0:P0} " -f $($disk.FreeSpace / $disk.Size))

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to Top]