The automatic variable PSBoundParameters stores the parameters that you explicitly passed to a function in a hash table. With the help of splatting, you can use this PowerShell feature to simplify passing of parameters to functions.

If you are like me, you often write PowerShell functions based on an underlying native cmdlet. These wrapper functions are quite common and help abstract complex but repetitive tasks. Here's an example of a function built around Get-ChildItem.

Function Get-FolderSizeA {
  [cmdletbinding()]
  Param(
    [parameter(Position = 0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ })]
    [string]$Path,
    [switch]$Recurse,
    [ValidateNotNullOrEmpty()]
    [string]$Filter = "*.*"
  )

  Write-Verbose "Starting $($myinvocation.MyCommand)"
  Write-Verbose "Getting stats for files ($filter) in $path"

  if ($recurse) {
    Write-Verbose "Recursing"
    $stats = Get-ChildItem -Path $Path -Filter $filter -File -Recurse | Measure-Object -Property Length -Sum -Average
  }
  else {
    $stats = Get-ChildItem -Path $Path -Filter $filter -File | Measure-Object -Property Length -Sum -Average
  }

  Write-Verbose "Found $($stats.count) files"

  #write a custom object to the pipeline
  [pscustomobject]@{
    Path      = $Path
    Filter    = $filter
    Files     = $stats.count
    TotalKB   = [math]::round($stats.sum / 1KB, 2)
    AverageKB = [math]::round($stats.average / 1KB, 2)
    Date      = (Get-Date).ToShortDateString()
  }

  Write-Verbose "Ending $($myinvocation.MyCommand)"
}
PowerShell function processing passed parameters the common way

PowerShell function processing passed parameters the common way

My function needs logic to decide how to run Get-ChildItem based on the parameters. But there is an easier way and something that can simplify my code. I have always found that the simpler the code, the less likely the chance of errors, and the easier it is to debug.

Using splatting

I am assuming you are familiar with splatting. This technique can shorten your command syntax and make it easier to read your code. You might have a command like this:

Get-ChildItem c:\work -Filter *.txt -Recurse -File

As an alternative, you can define a hash table of command parameters.

$paramHash = @{
    Path    = "c:\work"
    Filter  = "*.txt"
    Recurse = $True
    File    = $True
}

You can pass this hash table using the @ symbol.

Get-ChildItem @paramHash

Note that there is no $ before the variable. Using splatting makes your code more "vertical," which I find easier to read than scrolling horizontally. Hash tables are also handy because you can create and modify them on the fly.

Using PSBoundParameters

In my code, the function parameters are identical to what I need for Get-ChildItem. I could simplify my code by splatting. I can do this by leveraging an intrinsic variable, $PSBoundParameters. This is a special hash table that is automatically created in every function.

Methods and properties of PSBoundParameters

Methods and properties of PSBoundParameters

The typename is a bit different than a standard hash table, but for all practical purposes, you can treat it as a hash table. Here's a revised version of the function using PSBoundParameters.

Function Get-FolderSizeB {
  [cmdletbinding()]
  Param(
    [parameter(Position = 0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ })]
    [string]$Path,
    [switch]$Recurse,
    [ValidateNotNullOrEmpty()]
    [string]$Filter
  )

  Write-Verbose "Starting $($myinvocation.MyCommand)"
  Write-Verbose "Using these PSBoundParameters"
  $PSBoundParameters | Out-String | Write-Verbose
  Write-Verbose "Getting stats for files in $path"

  #splat PSBoundParameters to Get-ChildItem
  $stats = Get-ChildItem @PSBoundParameters -File | Measure-Object -Property Length -Sum -Average
  Write-Verbose "Found $($stats.count) files"

  #write a custom object to the pipeline
  [pscustomobject]@{
    Path      = $Path
    Filter    = $filter
    Files     = $stats.count
    TotalKB   = [math]::round($stats.sum / 1KB, 2)
    AverageKB = [math]::round($stats.average / 1KB, 2)
    Date      = (Get-Date).ToShortDateString()
  }
  Write-Verbose "Ending $($myinvocation.MyCommand)"
}
A usage example with PSBoundParameters

A usage example with PSBoundParameters

My Verbose output includes PSBoundParameters. Notice that with splatting, you can still include other parameters.

Get-ChildItem @PSBoundParameters -File

It would be cleaner not to have to do that. Also notice that because I didn't specify a Filter parameter, it isn't detected as a bound parameter. My output also lacks filter information. Get-ChildItem essentially uses a filter of *.*, but I'm not capturing it. Let's fix this.

Modifying PSBoundParameters

Function Get-FolderSizeC {
  [cmdletbinding()]
  Param(
    [parameter(Position = 0)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ })]
    [string]$Path = ".",
    [switch]$Recurse,
    [ValidateNotNullOrEmpty()]
    [string]$Filter = "*.*"
  )

  Write-Verbose "Starting $($myinvocation.MyCommand)"
  Write-Verbose "Using these PSBoundParameters"
  $PSBoundParameters | Out-String | Write-Verbose
  Write-Verbose "Getting stats for files in $path"

  $PSBoundParameters.Add("File",$True)
  if (-Not ($PSBoundParameters.ContainsKey("Filter"))) {
    #add the parameter default
    $PSBoundParameters.Add("Filter",$Filter)
  }

  $stats = Get-ChildItem @psboundparameters | Measure-Object -Property Length -Sum -Average
  Write-Verbose "Found $($stats.count) files"

  #write a custom object to the pipeline
  [pscustomobject]@{
    Path      = $Path
    Filter    = $filter
    Files     = $stats.count
    TotalKB   = [math]::round($stats.sum / 1KB, 2)
    AverageKB = [math]::round($stats.average / 1KB, 2)
    Date      = (Get-Date).ToShortDateString()
  }
  Write-Verbose "Ending $($myinvocation.MyCommand)"
}

In this version, I'm going to add -File to PSBoundParameters.

$PSBoundParameters.Add("File",$True)

I'm also adding my Filter default if it isn't detected as a bound parameter.

  if (-Not ($PSBoundParameters.ContainsKey("Filter"))) {
    #add the parameter default
    $PSBoundParameters.Add("Filter",$Filter)
  }

This allows me to specify a parameter default value, yet still use it as a bound parameter.

Using PSBoundParameters with a filter

Using PSBoundParameters with a filter

Or I can specify a filter.

Passing a filter

Passing a filter

But you know, maybe I want the Average value to be optional. And since I'm already splatting to Get-ChildItem, why not also splat to Measure-Object?

Function Get-FolderSizeD {
  [cmdletbinding()]
  Param(
    [parameter(Position = 0)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ })]
    [string]$Path = ".",
    [switch]$Recurse,
    [ValidateNotNullOrEmpty()]
    [string]$Filter = "*.*",
    #Include the average size
    [switch]$Average
  )

  Write-Verbose "Starting $($myinvocation.MyCommand)"
  Write-Verbose "Using these detected PSBoundParameters"
  $PSBoundParameters | Out-String | Write-Verbose

  #Convert path and add to PSBoundParameters
  #this is another way of defining a hash table value, even if it
  #doesn't already exist
  $PSBoundParameters["Path"] = Convert-Path $path

  Write-Verbose "Getting stats for files in $($PSBoundParameters.Path)"

  $PSBoundParameters.Add("File", $True)
  if (-Not ($PSBoundParameters.ContainsKey("Filter"))) {
    #add the parameter default
    $PSBoundParameters.Add("Filter", $Filter)
  }
  #remove Average if bound since it isn't a valid parameter for Get-ChildItem
  if ($PSBoundParameters.ContainsKey("Average")) {
    #the remove method writes a Boolean result to the pipeline which I'll suppress
    [void]$PSBoundParameters.Remove("Average")
  }
  #parameters to splat to Measure-Object
  $mo = @{
    Property = "Length"
    Sum      = $True
    Average  = $Average
  }

  Write-Verbose "Invoking Get-ChildItem with these parameters"
  $PSBoundParameters | Out-String | Write-Verbose
  Write-Verbose "Invoking Measure-Command with these parameters"
  $mo | Out-String | Write-Verbose
  $stats = Get-ChildItem @PSBoundParameters | Measure-Object @mo

  Write-Verbose "Found $($stats.count) files"

  #write a custom object to the pipeline
  [pscustomobject]@{
    Path      = (Convert-Path $Path)
    Filter    = $filter
    Files     = $stats.count
    TotalKB   = [math]::round($stats.sum / 1KB, 2)
    AverageKB = [math]::round($stats.average / 1KB, 2)
    Date      = (Get-Date).ToShortDateString()
  }
  Write-Verbose "Ending $($myinvocation.MyCommand)"
}

In this version, I also added Path to PSBoundParameters. In all of my previous examples, I have specified a value, but the function has a parameter default of the current location. However, I don't want to see a period as the path; I want to see the full file system path so I'll convert the parameter value.

You can also see where I'm defining a hash table of parameter values for Measure-Object. Since -Average is a switch, it has an implicit Boolean value, which I can use in the hash table.

Using Measure Object

Using Measure Object

PSBoundParameters and the pipeline

There is one thing you need to watch. My examples thus far have used a simple function, so everything is bound at the beginning. However, if your command is taking in pipeline input, this can change.

Function Get-FolderSizeE {
  [cmdletbinding()]
  Param(
    [parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ })]
    [alias("fullname")]
    [string]$Path = ".",
    [switch]$Recurse,
    [ValidateNotNullOrEmpty()]
    [string]$Filter = "*.*",
    [Parameter(HelpMessage = "Include the average file size")]
    [switch]$Average
  )

  Begin {
    Write-Verbose "Starting $($myinvocation.MyCommand)"
    Write-Verbose "Starting with these PSBoundParameters"
    $PSBoundParameters | Out-String | Write-Verbose

    #update PSBoundParameters
    $PSBoundParameters.Add("File", $True)
    #remove Average if bound since it isn't a valid parameter for Get-ChildItem
    if (-Not ($PSBoundParameters.ContainsKey("Filter"))) {
      #add the parameter default
      $PSBoundParameters.Add("Filter", $Filter)
    }
    if ($PSBoundParameters.ContainsKey("Average")) {
      #the remove method writes a Boolean result to the pipeline which I'll suppress
      [void]$PSBoundParameters.Remove("Average")
    }

    #parameters to splat to Measure-Object
    $mo = @{
      Property = "Length"
      Sum      = $True
      Average  = $Average
    }

  } #begin

  Process {
    #Convert path and add to PSBoundParameters
    $PSBoundParameters["Path"] = Convert-Path $path

    Write-Verbose "Using these PSBoundParameters in the Process block"
    $PSBoundParameters | Out-String | Write-Verbose

    Write-Verbose "Getting stats for files in $($PSBoundParameters.Path)"
    $stats = Get-ChildItem @PSBoundParameters | Measure-Object @mo

    Write-Verbose "Found $($stats.count) files"

    #write a custom object to the pipeline
    [pscustomobject]@{
      Path      = (Convert-Path $Path)
      Filter    = $filter
      Files     = $stats.count
      TotalKB   = [math]::round($stats.sum / 1KB, 2)
      AverageKB = [math]::round($stats.average / 1KB, 2)
      Date      = (Get-Date).ToShortDateString()
    }
  } #process

  End {
    Write-Verbose "Ending $($myinvocation.MyCommand)"
  } #end

}

In this version, the function will accept a path as pipeline input. This means that the Path parameter doesn't get bound until the Process script block, so I move my parameter handling code there.

PSBoundParameters and the pipeline

PSBoundParameters and the pipeline

The final function

Let's pull this all together into a final function.

Function Get-FolderSize {
  [cmdletbinding(DefaultParameterSetName = 'default')]
  [alias("gfs")]
  [OutputType("jhFolderSize")]
  Param(
    [parameter(
      Position = 0,
      ValueFromPipeline,
      ValueFromPipelineByPropertyName,
      ParameterSetName = "default"
    )]
    [Parameter(
      Position = 0,
      ValueFromPipeline,
      ValueFromPipelineByPropertyName,
      ParameterSetName = "recurse"
    )]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ })]
    [alias("fullname")]
    [string]$Path = ".",
    [Parameter(ParameterSetName = "recurse")]
    [switch]$Recurse,
    [ValidateNotNullOrEmpty()]
    [string]$Filter = "*.*",
    [Parameter(ParameterSetName = "recurse")]
    [string]$Exclude,
    [Parameter(HelpMessage = "Include the average file size")]
    [switch]$Average
  )

  Begin {
    Write-Verbose "Starting $($myinvocation.MyCommand)"
    Write-Debug "Starting with these PSBoundParameters"
    $PSBoundParameters | Out-String | Write-Debug

    #update PSBoundParameters
    $PSBoundParameters.Add("File", $True)
    #remove Average if bound since it isn't a valid parameter for Get-ChildItem
    if (-Not ($PSBoundParameters.ContainsKey("Filter"))) {
      #add the parameter default
      $PSBoundParameters.Add("Filter", $Filter)
    }
    if ($PSBoundParameters.ContainsKey("Average")) {
      #the remove method writes a Boolean result to the pipeline which I'll suppress
      [void]$PSBoundParameters.Remove("Average")
    }

    #parameters to splat to Measure-Object
    $mo = @{
      Property = "Length"
      Sum      = $True
      Average  = $Average
    }

  } #begin

  Process {
    #Convert path and add to PSBoundParameters
    $PSBoundParameters["Path"] = Convert-Path $path
    Write-Debug "Using these PSBoundParameters"
    $PSBoundParameters | Out-String | Write-Debug

    Write-Debug "Invoking Get-ChildItem with these parameters"
    $PSBoundParameters | Out-String | Write-Debug
    Write-Verbose "Getting stats for files in $($PSBoundParameters.Path)"
    $stats = Get-ChildItem @PSBoundParameters | Measure-Object @mo

    Write-Verbose "Found $($stats.count) files"

    #write a custom object to the pipeline
    #leave sizes as raw byte values Use a custom format file to display
    #formatted output in KB or MB.
    [pscustomobject]@{
      PSTypename = "jhFolderSize"
      Path       = (Convert-Path $Path)
      Filter     = $filter
      Files      = $stats.count
      Total      = $stats.sum
      Average    = $stats.Average
      Date       = (Get-Date).ToShortDateString()
    }
  } #process

  End {
    Write-Verbose "Ending $($myinvocation.MyCommand)"
  } #end

}

This version adds the Exclude parameter, which only works when using -Recurse, so I created a separate parameter set. I also moved the verbose statements showing PSBoundParameters to the debug stream, since I don't need to see them in my Verbose output.

The final function

The final function

I also removed the size formatting and am displaying values in bytes. I can use a custom formatting file if I want to display values in KB.

Summary

If you are new to using PSBoundParameters, I encourage you to try out my function variations. Even though I argued that using PSBoundParameters can simplify your code, I'll admit that the final version of my function is more complicated than where I began. However, I tried to demonstrate a variety of techniques. Depending on how you structure your parameters, you may not need to adjust PSBoundParameters much. Or you might only need to check PSBoundParameters as a final parameter validation test.

Subscribe to 4sysops newsletter!

You can read more in the about_Automatic_Variables and about_Splatting help topics.

avataravatar
5 Comments
  1. Kane Atkinson 11 months ago

    Excellent article Jeff thanks

  2. s31064 11 months ago

    While I realize this was written just to demonstrate PSBoundParameters, somehow

    Get-ChildItem c:\work -Filter *.txt -Recurse -File | Measure-Object -Property Length -Sum -Average

    seems a hell of a lot simpler than

    Function Get-FolderSize {
      [cmdletbinding(DefaultParameterSetName = 'default')]
      [alias("gfs")]
      [OutputType("jhFolderSize")]
      Param(
        [parameter(
          Position = 0,
          ValueFromPipeline,
          ValueFromPipelineByPropertyName,
          ParameterSetName = "default"
        )]
        [Parameter(
          Position = 0,
          ValueFromPipeline,
          ValueFromPipelineByPropertyName,
          ParameterSetName = "recurse"
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ })]
        [alias("fullname")]
        [string]$Path = ".",
        [Parameter(ParameterSetName = "recurse")]
        [switch]$Recurse,
        [ValidateNotNullOrEmpty()]
        [string]$Filter = "*.*",
        [Parameter(ParameterSetName = "recurse")]
        [string]$Exclude,
        [Parameter(HelpMessage = "Include the average file size")]
        [switch]$Average
      )
    
      Begin {
        Write-Verbose "Starting $($myinvocation.MyCommand)"
        Write-Debug "Starting with these PSBoundParameters"
        $PSBoundParameters | Out-String | Write-Debug
    
        #update PSBoundParameters
        $PSBoundParameters.Add("File", $True)
        #remove Average if bound since it isn't a valid parameter for Get-ChildItem
        if (-Not ($PSBoundParameters.ContainsKey("Filter"))) {
          #add the parameter default
          $PSBoundParameters.Add("Filter", $Filter)
        }
        if ($PSBoundParameters.ContainsKey("Average")) {
          #the remove method writes a Boolean result to the pipeline which I'll suppress
          [void]$PSBoundParameters.Remove("Average")
        }
    
        #parameters to splat to Measure-Object
        $mo = @{
          Property = "Length"
          Sum      = $True
          Average  = $Average
        }
    
      } #begin
    
      Process {
        #Convert path and add to PSBoundParameters
        $PSBoundParameters["Path"] = Convert-Path $path
        Write-Debug "Using these PSBoundParameters"
        $PSBoundParameters | Out-String | Write-Debug
    
        Write-Debug "Invoking Get-ChildItem with these parameters"
        $PSBoundParameters | Out-String | Write-Debug
        Write-Verbose "Getting stats for files in $($PSBoundParameters.Path)"
        $stats = Get-ChildItem @PSBoundParameters | Measure-Object @mo
    
        Write-Verbose "Found $($stats.count) files"
    
        #write a custom object to the pipeline
        #leave sizes as raw byte values Use a custom format file to display
        #formatted output in KB or MB.
        [pscustomobject]@{
          PSTypename = "jhFolderSize"
          Path       = (Convert-Path $Path)
          Filter     = $filter
          Files      = $stats.count
          Total      = $stats.sum
          Average    = $stats.Average
          Date       = (Get-Date).ToShortDateString()
        }
      } #process
    
      End {
        Write-Verbose "Ending $($myinvocation.MyCommand)"
      } #end
    
    }

  3. Author
    Jeff Hicks 11 months ago

    The final code is definitely more complicated than where I began. But the code itself isn’t the point of the article. I was demonstrating different ways you can use PSBoundparameters. You might use one of the concepts in your code to simplify something.

Leave a reply

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

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account