Using a ScriptBlock Parameter With a PowerShell Function

You may have noticed in some cmdlets that you can apply a scriptblock to certain parameters and have the scriptblock run during the code execution of the cmdlet. Take Sort-Object for example. I can use a scriptblock to perform a sort by the length of the filenames.

Why Use a ScriptBlock?

So why do this? Well, if a cmdlet supports pipeline input, you don’t want to pipe that information into  ForEach {} and have to iterate through each item because that defeats the purpose of just being able to chain commands together. What looks better?

This:

 
Get-ChildItem | Invoke-SomeAction -Action {$($_.name).OLD}

Or This:

 
Get-ChildItem | ForEach {
    Invoke-SomeAction -Action "$($_.Name).OLD
}

Personally, I am more of a fan of utilizing the pipeline to its maximum capability if allowed to do so.

Let’s take a look at a couple of examples that use parameters which support a scriptblock.

 
Get-ChildItem -File | 
Sort-Object {$_.Name.length} -Descending | 
Select Name

image

Rename-Item is another cmdlet that has similar capabilities.

 
Get-ChildItem -Filter *.Zip | 
Rename-Item -NewName  {$_.Name -replace 'zip','OLD'} –WhatIf

image

Here I found any .ZIP file and renamed it to .OLD instead.

So the next question is “How can I do something like this with my own functions?”. Glad you asked that! It really isn’t too difficult to setup a parameter which supports scriptblock use in your script or function.

Edit: Fellow PowerShell MVP Dave Wyatt pointed out in the comments that the scriptblock evaluation happens on its own through an undocumented feature in PowerShell that will invoke the scriptblock, thus erasing the need for any of the code samples that I use below. I am keeping this though for historical reasons and because this continues to show why I blog because if I am wrong, I promise that the correct information will show up at some point by someone who has more knowledge of the subject.

Basically the parameter binder will invoke the scriptblock as long as the parameter supports pipeline input. A quick look at this can be done using Dave’s function as an example and then running a Trace-Command against ParameterBinding.

 
Function Test-ScriptBlockParam {
    Param (
    [parameter(Mandatory, ValueFromPipelineByPropertyName)]
    [string] $Name
    )

    Process {
        $Name
    }  
}

Trace-Command ParameterBinding -PSHost {
    Get-ChildItem | Test-ScriptBlockParam -Name {$_.fullName -replace '(.*)\..*','$1'}
}

scriptblockbinder

Here you can see where the scriptblock is being invoked and the results of it are shown a couple lines later. Much easier to let PowerShell do all of the heavy lifting than to code it youself :).

Build Your Own Parameter

My function that I am going to build out will be called Test-ScriptBlockParam and I will allow a couple of parameters: InputObject and Name. I need one of these parameters to have pipeline support and the other can function without pipeline support (but will be my scriptblock parameter).

 
Function Test-ScriptBlockParam {
    Param (
        [parameter(Mandatory,ValueFromPipeline)]
        [Object[]]$InputObject,
        [parameter(Mandatory)]
        [Object]$Name
    )

So far so good. I am going to define how Name will be used as a scriptblock in the beginning of the function (or Begin{] block since I am using pipeline parameters).

 
Begin {
    If ($PSBoundParameters['Name']) {
        If ($Name -isnot [scriptblock]) {
            $Name = [scriptblock]::Create("Write-Output $Name")
        } Else {
            $Name = [scriptblock]::Create( ($Name -replace '\$_','$Object'))
        }
    }   
}

I want this to work regardless of whether a scriptblock was assigned as the parameter type or not. If a string is used, then it will build out a scriptblock using [scriptblock]::Create() and if a scriptblock has been applied, it makes an adjustment so it works properly in the Process block by replacing the $Name with $Object. This will make more sense after you see the Process{} block.

 
    Process {
        ForEach ($Object in $InputObject) {
            $Name.InvokeReturnAsIs()
        }
    }  
}

Here is the Process block where you see why I had to replace $Name with $Object. Because $Object is the variable that represents an item in the $InputObject collection, it can now run the scriptblock using InvokeReturnAsIs(). This means that whatever I have defined in the scriptblock will run and return the output.

Let’s give this a run and see what happens!

 
Get-ChildItem | 
Test-ScriptBlockParam -Name {"Name of File is: $($_.Name)"}

image

 
Get-ChildItem | 
Test-ScriptBlockParam -Name {$_.Name -replace '(.*)\..*','$1'}

image

 
Get-ChildItem | 
Test-ScriptBlockParam -Name {
    If ($_.PSIsContainer) {
        Write-host $_ -ForegroundColor Green
    } Else {
        Write-Host $_ -ForegroundColor Yellow
    }
}

image

So there you go! We have created a function that allows the use of a scriptblock parameter to perform an action that still allows us to use the pipeline more efficiently rather than working with ForEach.

In Closing

Of course, these are very simple examples of what you can do, but I would love to see what you have done using this a scriptblock parameter. So feel free to drop me a comment letting me know what you have done.

This entry was posted in powershell and tagged , , . Bookmark the permalink.

11 Responses to Using a ScriptBlock Parameter With a PowerShell Function

  1. js2010 says:

    That’s an advantage of the -inputobject parameter. Even if it doesn’t take a collection, you can use a script block with it, since it can take pipe input. You’ll see examples of -inputobject and script blocks in the help.

  2. js2010 says:

    But trace-command’s -expression parameter doesn’t accept pipe input. But it’s a script block type.

  3. Renaud says:

    Thanks a lot for this great blog which I often refer to.
    I hope I’m not mixing up 2 different things – my skill in Powershell is average – but I think the feature mentioned by Dave Wyatt above is presented by Jeffrey Snover in his article “Flexible pipelining with script block parameters” (https://blogs.msdn.microsoft.com/powershell/2006/06/23/flexible-pipelining-with-scriptblock-parameters/). This article dates back to june 2006, so maybe it’s obsolete. Anyway, J. Snover explains here that “script blocks paramaters” may be used even with parameters which do not accept pipeline and gives an example with “get-wmiobject” (but I could not make it work). Indeed, this feature deserves to be better documented by Microsoft…

  4. ErPe says:

    Hey,

    Quite interesting post here. Would you be able to desribe how “function that allows the use of a scriptblock parameter to perform an action that still allows us to use the pipeline more efficiently rather than working with ForEach.” ?

    As if I look from a perspective of Powershell beginner just using

    get-item | % {$($_.Name) -replace “.zip”,”.zip_old”}

  5. Great post Boe,

    It looks like WordPress GUI editor has messed up your code formatting.
    What I do is, before saving or posting, do a final edit in the Text section and re-paste in the lines of code.

  6. Dave Wyatt says:

    Looks like these comments are formatted with markdown or something like it; code should show up better this time:

    Function Test-ScriptBlockParam {
        Param (
            [parameter(Mandatory, ValueFromPipelineByPropertyName)]
            [string] $Name
        )
    
        Process {
            $Name
        }  
    }
    
    Trace-Command ParameterBinding -PSHost {
        Get-ChildItem | Test-ScriptBlockParam -Name {"Name of File is: $($_.Name)"}
    }
    
  7. Dave Wyatt says:

    It’s not terribly well known or documented, but PowerShell will do this for you with any parameter that supports Pipeline input (whether by property name or by value, doesn’t matter.) If the parameter is of a type other than ScriptBlock, and you bind a scriptblock to it, that script block will be invoked by the parameter binder. That way you can still strongly type your parameters in most cases, and don’t have to do the work of testing for [scriptblock] or invoking it yourself.

    Function Test-ScriptBlockParam {
    Param (
    [parameter(Mandatory, ValueFromPipelineByPropertyName)]
    [string] $Name
    )

    Process {
        $Name
    }  
    

    }

    Trace-Command ParameterBinding -PSHost {
    Get-ChildItem | Test-ScriptBlockParam -Name {“Name of File is: $($_.Name)”}
    }

Leave a comment