Learn Computing from the Experts | The Rheinwerk Computing Blog

Functions and Parameters in PowerShell

Written by Rheinwerk Computing | Oct 21, 2024 1:00:00 PM

Functions provide the option to bundle multiple PowerShell commands, which facilitates organizing extensive scripts. In its simplest form, a function has the following structure:

 

function Do-Something {

   command1

   command2

   ...

}

 

Within a script, functions can only be used after they have been defined. Thus, a script file first contains all the functions, while the code that is ultimately to be executed is located at the end.

 

Naming Rules: The PowerShell documentation recommends naming custom functions, such as cmdlets, according to the verb-noun pattern. The hyphen required in the name, like

almost any other character, can be used without special marking.

 

Function Results

What is unusual compared to “real” programming languages is the structure of the return value of a function in PowerShell: The result is obtained from all the outputs created in the function! All partial results caused by the commands within the function are returned collectively. Statements whose results end up in variables are not taken into account.

 

For example, when the following three statements are run within the function, the function result is an array formed by the result of command1, the "lorem ipsum" string, and the result of command2:

 

command1

command2

Write-Output "lorem ipsum"

# Result: Array with the partial results from command1 and

# command2 and a string

 

In the second example, only $a + $b, which is a number, causes output to the success stream:

 

$a = 123

$b = command # command returns 456 as the result

$a + $b

# Result: integer number 579

 

Additionally, three statements are executed in the third example. But because the results of command1 and command2 are stored in variables, and because Write-Debug does not write to the success stream, no result is produced!

 

$a = command1

Write-Debug "lorem ipsum"

$b = command2

# no result (therefore $null)

 

The structure of the result outlined in this context naturally also applies to commands that are executed in loops. Each output within the loop expands the function result that is returned at the end.

 

Be Careful with Write-Output!: For debugging purposes, you might be tempted to include Write-Output in a function to output intermediate results. Don’t do this! Write-Output produces an output that becomes part of the function result. A better approach is to use Write-Debug and enable debugging outputs via $DebugPreference = "Continue"! As an alternative, you can also use Write-Host. But this approach has a disadvantage in that you must remove these statements later. Output from Write-Host cannot be redirected or otherwise disabled. Compared to Write-Output, using Write-Host has an advantage in that it does not change the function result.

 

return

A function typically ends with the execution of the last command. However, you can also terminate a function prematurely using return (e.g., within a loop as soon as a condition is fulfilled). If you use return without parameters, the function result is composed of all previous outputs as before:

 

command1

command2

return

# Result: Array with the partial results of command1 and command2

 

Alternatively, you can pass a variable, expression, or command to return. In this case, the result of this expression is added to the previous partial results. (As a matter of fact, one would assume that only the return expression determines the result. But this is not the case with PowerShell!)

 

command1

command2

return command3

# Result: Array with the partial results of command1, command2

# and command3

 

Parameters

Usually, parameters are supposed to be passed to functions. Declaring the position, name, data type, default value, and other properties in advance using param makes sense. PowerShell offers a wealth of syntactic options, of which I will only consider the most important ones in this book. In the simplest case, you simply specify a list of parameters that are mandatory to pass. Specifying the data type in square brackets is optional; if you choose to do so, the data type will be checked when a parameter is passed.

 

function Do-Something {

   param(

       [type1] $Parameter1

       [type2] $Parameter2

       [type3] $Parameter3

   )

}

 

Alternatively, the following short notation is allowed:

 

function Do-Something ([type1]$Para1, [t2]$P2, [t3]$P3) { ... }

 

If you specify a default value for parameters, as in the following example, the parameter becomes optional, and thus does not need to be specified when called:

 

function Do-It ([int]$A, [int]$B = 2, [String]C = "abc") { ... }

 

Functions without an Explicit Parameter List: The use of param is optional. All data that does not correspond to the predefined parameter list is accessible within the function in the predefined $args variable. If you do not declare a parameter list, then $args applies to all parameters. The PowerShell documentation speaks of positional parameters in this context because the parameters have no name and can only be analyzed based on their position. However, the analysis of $args is prone to errors. Internally, $args is an array, so $args(0) is the first parameter, $args(1) is the second one, etc. $args.Length specifies how many parameters not declared via param were passed.

 

Calling Functions

Remember to pass parameters when calling functions without naming parentheses! This behavior is equivalent to calling cmdlets but, of course, differs from the behavior of almost all other programming languages.

 

Do-It 1 2 "abc"

 

Parameters can be used like options. The associated values follow either separated by a space or in the -Parameter name:Value notation. The order then no longer plays a role. The following two calls of Do-It are equivalent to the previous line:

 

Do-It -C "abc" -A 1 -B 2

Do-It -B:2 -C:"abc" -A:1

 

Processing Standard Input

Sometimes, a function is to be programmed in such a way that it can process objects from the standard input; that is, it can be called via the pipe operator in the following form: ... | My-Function. The easiest approach in such cases is to select the predefined $input variable. The following function calculates the total of the Length properties of all objects, divides the result by 1,000,000, and then displays the calculated result:

 

function Get-TotalSize {

   $TotalSize = 0

   foreach($item in $input) {

       $TotalSize += $Item.Length

   }

   $TotalSize /= 1000000

   "Total size: {0:F1} MB" -f $TotalSize

}

 

To calculate the space required by all files in the Downloads directory, the function can be called in the following way:

 

> Get-ChildItem -Recurse C:\Users\kofler\Downloads\ |

  Get-TotalSize

 

  Total size: 247.4 MB

 

This script has a flaw: It relies on the fact that any object passed via standard input actually has a Length property. Set-StrictMode also interferes with this script if you pass a version number greater than 1. The simplest solution is to check the data type within the loop and then make an assignment into a typed variable, as in the following example:

 

foreach($Item in $input) {

   if ($Item.GetType().Name -eq "System.IO.FileInfo") {

         [System.IO.FileInfo] $FInfo = $Item

         $TotalSize += $FInfo.Length

   }

}

 

If the data type does not match, the script will trigger an error.

 

Syntax Variants

For functions that are supposed to process standard input, PowerShell provides two syntax variants. One version involves organizing the function into the three blocks, begin, process, and end:

 

function Process-Data {

   begin { initialization ... }

   process { process data ... }

   end { output, cleanup ... }

}

 

The process block is run for each element of the standard input, where $_ provides access to the currently active element. All three blocks are optional. If you define one of these blocks in a function, the entire code of the function must be specified in begin, process, and end. The function Get-TotalSize can then be programmed in the following way:

 

function Get-TotalSize {

   begin {

       $TotalSize = 0

   }

   process {

       $TotalSize += $_.Length

   }

   end {

       $TotalSize /= 1000000

       "Total size: {0:F1} MB" -f $TotalSize

   }

}

 

With Set-StrictMode -Version 2, PowerShell complains again that it is not sure if $_ has a Length property. If necessary, you need to rebuild the code, as explained earlier.

 

filter is a short notation for a function where the begin and end blocks are empty:

 

filter Process-Data {

   code

}

# equivalent

function Process-Data {

   begin { }

   process { code }

   end { }

}

 

In practice, filter is only useful in exceptional cases. Without the initialization performed in begin or without final commands in end, programming a meaningful function can be difficult.

 

Editor’s note: This post has been adapted from a section of the book Scripting: Automation with Bash, PowerShell, and Python by Michael Kofler.