Toolmaking in the trenches: Functions

11 minute read

This post kicks off a series of blog post that I’ll be doing on how to get started building PowerShell tools in between issue resolution, phone calls, and whatever else comes your way. This is going to be just the bare minimum you need to know to be able to successfully build tools for yourself and your colleagues. No frills or teasers, but I will be careful to mention areas that can be expanded on.

For this series I’m also going to make a few assumptions since you are here and reading this post: you definitely know some PowerShell and have written a few scripts, but you probably don’t have time to develop a whole module for every little thing you administer. If that sounds like you, then you are in the right place becaues those aren’t roadblocks! I want to introduce you to the simple process I used for building tools in PowerShell that I could easily share and use.

Functions

A function in PowerShell is a custom cmdlet. Nothing fancy, unless you want it to be. The first place Microsoft wants you to start is to check the help on functions:

Get-Help about_functions

Or check the online docs.

But man, that is kind of long and dry. You should instead read this long post that I’m writing on PowerShell functions! No. Lets jump right into useful information and forget all this commentary.

What makes a function

The first thing to know about functions is that they are designed to accomplish a single task, though they often involve many sub-tasks that are strung together to simplify a complex task.

If you read my last post on Building Tools Makes Better Scripts then you are hopefully familiar with my simple checklist for determining if code is ripe for converting to a tool.

To shamelessly plagarize myself, if you do either of these things, you have code that can be converted into a function:

  1. You copy and paste a section of code from one script to another
  2. You often type the same or similar one-liner when doing normal administrative tasks

Another simple example I have is for signing scripts. To sign a PowerShell script, you use Set-AuthenticodeSignature but you need the path of the script, the certificate you are using to sign it, and a timestamp server. Why is that so complicated? Here’s what that line of code might look like:

Set-AuthenticodeSignature -FilePath 'C:\GIT\Repo\src\script.ps1' -Certificate (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | ?{$_.subject -like '*theposhwolf*'}) -TimeStampServer 'http://timestamp.comodoca.com/authenticode'

Maybe signing scripts for security isn’t actually worth it… Well, write a function! I’ll use this example to walk you through building a simple function.

Naming your function

Best practice says to name your function in the same way that all the builtin cmdlets are named: Verb-Noun, using an approved verb, of course:

Get-Verb

But, by all means, call your functions whatever the heck you want. Of course I recommend sticking to best practice in case you ever get around to sharing your stuff.

I should also mention that if you don’t like those options, read up on function aliases. You can name your function properly and have an alias be whatever you want and still be within best practice as long as you don’t use aliases in your scripts, just at an interactive prompt.

For my script signing example, this one should be: Sign-Script, but you’ll notice that Sign isn’t an approved verb… So since I believe in best practice, I’ll call it Set-ScriptSignature and create an aliase for Sign-Script.

So with our name, we have the start of our function. We just put the Function keyword in front of our name and add curly braces, and voilà, a function is born:

Function Set-ScriptSignature{

}

It just doesn’t do anything yet.

Accepting input as parameters

In a PowerShell function, you can depend on parameter positioning and use the automatic $args array that is constructed out of all of the parameters, or you can pretend like I never mentioned that and go straight to named parameters. Thats a better idea.

To accept input on a PowerShell function, we need a Param() block. In the param block, we tell PowerShell what we are looking for.

Remember from before, I mentioned that Set-AuthenticodeSignature requires some input, specifically the script path, a certificate, and a timestamp server. So I’m going to be really original and call them:

Function Set-ScriptSignature{
    Param(
        $FilePath,
        $Certificate,
        $TimestampServer
    )
}

Now since your timestamp server and certificate probably won’t change very often, you can just include them in your script. But hardcoding is always a bad idea, you say? Well, if we include them like this:

Function Set-ScriptSignature{
    Param(
        $FilePath,
        $Certificate = (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert),
        $TimestampServer = 'http://timestamp.comodoca.com/authenticode'
    )
}

Then those hardcoded values are now default values and can still be changed by passing that parameter a different value, but if no value is passed to -TimestampServer, for example, then it is handed the stated default value of: http://timestamp.comodoca.com/authenticode.

Doing stuff with your input

Ok, so input is added, now to do stuff with the things!

We want to run that same Set-AuthenticodeSignature command from before, so why don’t we just throw it in there and specify our parameters as the values for its parameters?

Function Set-ScriptSignature{
    Param(
        $FilePath,
        $Certificate = (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert),
        $TimestampServer = 'http://timestamp.comodoca.com/authenticode'
    )
    Set-AuthenticodeSignature -FilePath $FilePath -Certificate $Certificate -TimestampServer $TimestampServer
}

Oh! And don’t forget to add an alias at the bottom, outside of your function:

Set-Alias -Name Sign-Script -Value Set-ScriptSignature

Our function, it lives! But now what do I do with it? Personally, I make a habit of storing all of my functions in a single file with the same name as the function itself, so in this case, I’d throw it in a Set-ScriptSignature.ps1 file. Next post I’ll talk about how easy it is to create your own personal module with this approach.

Using your function without a module

In any PowerShell session, you can dot source your .ps1 file that has your function in it:

. C:\GIT\repo\src\Set-ScriptSignature.ps1

Or if you wanted to add it to your $Profile, you can do the same there. But I don’t want to go too much into this here because I have a module approach that works much better. So check out my next post!

Now you can call your function with your alias:

Sign-Script C:\GIT\repo\src\script.ps1

So much easier.

Advanced steps

If all you care about is getting your function to work, then skip this section! I personally can’t stand to call that function complete without some additions.

Parameter validation

First, it needs some parameter validation:

$FilePath should be a string and always be a PowerShell file. It should also be the first parameter and be required.

$Certificate should be an x509 certificate.

$Timestampserver should be a string in the form of a url and always start with http:// or https://

For our param() block we should do:

    Param(
        [Parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidatePattern(".ps1$|.psd1$|.psm1$")]
        [string]$FilePath
        ,
        [X509Certificate]$Certificate = (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)
        ,
        [ValidatePattern("^(http|https)://")]
        [string]$TimestampServer = 'http://timestamp.comodoca.com/authenticode'
    )

I won’t go into too much detail here, as I’ll hopefully get around to a detailed post on parameter validation.

Before each variable you define what type of value it is so that PowerShell can cast it properly. For instance, if you pass 2 to an un-typed variable, PowerShell will cast it as an integer, but if you pass it to a string variable ([string]$str = 2) then PowerShell will cast it as a string. This is really important when it comes to things like addition. Try adding two string 2s together and you get 22 instead of 4.

Secondly, the [ValidatePattern()] is a regular expression that PowerShell runs to validate that proper input was passed. So you can always get what the function expects.

Lastly, the [Parameter()] has a LOT of different options, Mandatory and Position are the ones I use the most to require it and tell it which position to be in if the parameter isn’t explicitly named, respectively.

Pipeline support

If you want to be able to pass output from Get-ChildItem to Set-ScriptSignature, then you need some additions to support the pipeline, specifically passing the FullName from Get-ChildItem to the $FilePath parameter in our Set-ScriptSignature. This is how to do that:

Function Set-ScriptSignature{
    Param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidatePattern(".ps1$|.psd1$|.psm1$")]
        [Alias('FullName')]
        [string]$FilePath
        ,
        [X509Certificate]$Certificate = (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)
        ,
        [ValidatePattern("^(http|https)://")]
        [string]$TimestampServer = 'http://timestamp.comodoca.com/authenticode'
    )
    Begin{}
    Process{
        Set-AuthenticodeSignature -FilePath $FilePath -Certificate $Certificate -TimestampServer $TimestampServer
    }
    End{}
}
Set-Alias -Name Sign-Script -Value Set-ScriptSignature

Again, not going to go into super detail here as I’ll do another post on accepting pipeline input.

Inside [Parameter()] setting ValueFromePipelineByPropertyName to $true tells PowerShell to accept an object from the pipeline and pass a property that has the same name matching the referenced parameter to that parameter. But, my parameter is named FilePath so I inserted the [Alias('FullName')] block since the FullName property on the output of Get-ChildItem is the file path of each file.

Then we have the Begin{}, Process{}, and End{} blocks. The code inside of Begin{} runs once, then Process{} runs for each object passed, and then End{} runs once after the pipeline process finishes with this function.

With this addition, we will be able to:

Get-ChildItem C:\GIT\AllThePSScripts -Filter *.ps1 | Sign-Script

And that will sign all our scripts in that directory! Woohoo!

But wait, there’s more!

Ok, last advanced step, I promise.

If you wanted to add verbosity, which you should want to add, you just need to add [cmdletbinding()] above your param block and then some Write-Verbose statements, like this:

Function Set-ScriptSignature{
    [cmdletbinding()]
    Param(
        ...
    )
    Begin{}
    Process{
        Write-Verbose "Sigining $FilePath with certificate: $($Certificate.Subject) using timestamp server: $TimestampServer."
        Set-AuthenticodeSignature -FilePath $FilePath -Certificate $Certificate -TimestampServer $TimestampServer
    }
    End{}
}
Set-Alias -Name Sign-Script -Value Set-ScriptSignature

Now you can run your script with the -Verbose parameter and get some nice output!

In closing

Hopefully that gets you started writing tools to help your PowerShell dealings. Be sure to check back for my next post on creating a personal module to easily group your scripts.

If you liked this post, let me know! You can leave a comment, reach out on Twitter, email me, whatever. I appreciate feedback!

Leave a Comment