passing a function as a parameter in PowerShell

reflectionsA powerful pattern in software engineering is where you pass in a function or an object to another, which executes this given one dynamically. This allows you to extend behavior of code without having to edit that code. It’s known as the Strategy Pattern and allows for nice, clean and decoupled code.

In Powershell you can do this by passing a function as an argument to another function. When I tried to do this, I found out this wasn’t as trivial as I thought it would be, so here’s the nitty gritty on to work that magical extend-ability pattern.

Basically you want to do something like passing in a function which processes a single item in a loop controlled by another piece of code. Something like:

function Print-Number($number)
{
    echo "Number is $number"
}

function Do-Loop($function)
{
    $numbers = 1..10
    foreach ($number in $numbers)
    {
        # Here we should call $function and pass in $number
    }    
}

# Call Do-Loop with the PrintNumber function as a parameter here.

I ran into 2 problems here.

  1. How do I pass in a function as a reference to another one?
  2. How do I call one of those function blocks from the function that receives it?

First things first. The syntax for passing in a function inline looks like this:

Do-Loop ${function:Print-Number}

Note the function: prefix there. It’s the magic bit. This accesses the function object’s script block without executing it.
You can also list all functions available in your PowerShell session like this:

ls function:

If you want to pass in a script block like an anonymous lamba C# style, without defining a function first, you can do this:

Do-Loop { param($content) write-host $content }

Want to reuse that function a few times and store it in a variable? No problemo, just do this:

$function = { param($content) write-host $content }
Do-Loop $function
Do-SomethingElse $function

That pretty much covers all the options for problem number 1.
So now on the second problem: calling that passed in function or script block inside our host function.

Let’s say we want to call that function from a loop. To call the function you need to use the Invoke-Command commandlet and pass in the argument using the ArgumentList parameter like this:

foreach ($number in $numbers)
{
    Invoke-Command $function -ArgumentList $number
}

Pretty simple right?
The argument list expects an array as it’s value. So if you want to pass in 2 parameters like a number and a message text that would look something like this:

Invoke-Command $function -ArgumentList $number, $message

Putting it all together, here’s the working sample code:

function Print-Number($number)
{
    echo "Number is $number"
}

function Do-Loop($function)
{
    $numbers = 1..3
    foreach ($number in $numbers)
    {
        Invoke-Command $function -ArgumentList $number
    }    
}

Do-Loop ${function:\Print-Number}

Because of this array-as-a-parameter thing however I did run into a little snag for my actual code.
What if the first and only parameter is an array in itself? How do make it clear to the Invoke-Command commandlet that the array is a single parameter, not a list of parameters to pass into the function?

In my case I was passing in the content of a text file which is an array of strings. My first argument ended up being the first string of that array and I was lacking the rest of the file’s lines.

$array = Get-Content .\somefile.txt
Invoke-Command $function -ArgumentList $array # ?????

Again, there’s a little trick to that which I found on [Stack Overflow](https://stackoverflow.com/questions/7152740/how-to-pass-an-array-as-a-parameter-to-another-script). The syntax to pass in an array as a parameter is:

Invoke-Command $function -ArgumentList (,data)

The last bit seems to work by creating an array (that comma) where the first element is nothing and the second is our array.
Apparently the first element is then skipped and our second is passed in as the required array parameter.
A silly example to demonstrate this:

function Enhance($lines)
{
    $lines | % { "  > $_" }
}

function Do-It($function)
{
    $content = get-content .\awesome.txt
    Invoke-Command $function -ArgumentList (,$content)
}

Do-It ${function:\Enhance}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.