Skip to content

Functions

Note

Learn how to create reusable blocks of code with functions, parameters, and return values.

Overview

Functions are named blocks of code that you can call multiple times throughout your script. Instead of copying and pasting the same code, you put it in a function and call that function whenever you need it. This makes your code cleaner, easier to maintain, and reusable across different scripts.

Basic Syntax

# Simple function
function FunctionName {
    # Code goes here
    Write-Output "Function executed"
}

# Call the function
FunctionName

# Function with parameters
function Get-Greeting {
    param(
        [string]$Name
    )

    Write-Output "Hello, $Name!"
}

# Call with parameter
Get-Greeting -Name "Raymond"

Key Points

  • Use approved verbs: Get, Set, New, Remove, Test, etc. (check with Get-Verb)
  • Follow Verb-Noun naming: Get-UserInfo, not GetInfo or User-Get
  • Use param() block for parameters - always at the top
  • Functions can return values (everything that outputs becomes the return value)
  • Use comment-based help for documentation

Testing Your Functions

Before saving a function to a script file, you should test it to make sure it works correctly. PowerShell makes this easy by allowing you to define functions directly in the console.

Copy-Paste Testing (Quick & Easy)

The fastest way to test a function:

  1. Write your function in a script editor (VS Code, PowerShell ISE, or notepad)
  2. Select the entire function definition (from function to the closing })
  3. Copy it (Ctrl+C)
  4. Paste it into PowerShell console and press Enter
  5. Now call the function to test it
# 1. Copy this entire function
function Get-TimeGreeting {
    param([string]$Name)

    $hour = (Get-Date).Hour
    $timeOfDay = if ($hour -lt 12) { "Morning" }
                 elseif ($hour -lt 17) { "Afternoon" }
                 else { "Evening" }

    "Good $timeOfDay, $Name!"
}

# 2. Paste it into PowerShell console
# 3. Now test it:
Get-TimeGreeting -Name "Raymond"

Why This Works

When you paste a function definition into PowerShell, it loads the function into your current session's memory. The function stays available until you close PowerShell or redefine it.

Testing in VS Code PowerShell Extension

If you're using VS Code with the PowerShell extension:

  1. Write your function in a .ps1 file
  2. Select the function definition
  3. Press F8 (or right-click → "Run Selection")
  4. The function loads into the integrated terminal
  5. Call it in the terminal to test
# In your .ps1 file
function Test-IsEven {
    param([int]$Number)
    ($Number % 2) -eq 0
}

# Select the function, press F8
# Then in terminal:
Test-IsEven -Number 10    # $true
Test-IsEven -Number 7     # $false

Testing in PowerShell ISE

If you're using PowerShell ISE:

  1. Write your function in the script pane (top)
  2. Select the entire function
  3. Press F8 (or click "Run Selection")
  4. Test it in the console pane (bottom)

Dot-Source Testing (For Script Files)

If your function is saved in a .ps1 file:

# Your function is in: C:\Scripts\MyFunctions.ps1
# Load all functions from that file into your session:
. C:\Scripts\MyFunctions.ps1

# Now you can call any function from that file
Get-TimeGreeting -Name "Raymond"

The . (dot-space) at the start is called "dot-sourcing" - it runs the script in your current scope, making all functions available.

Iterative Testing Workflow

The typical development cycle:

# 1. Write initial function (in VS Code, ISE, or editor)
function Get-FileSize {
    param([string]$Path)
    $file = Get-Item $Path
    $file.Length
}

# 2. Paste/run it in PowerShell (F8 in VS Code)

# 3. Test it
Get-FileSize -Path "C:\Windows\notepad.exe"  # Returns bytes

# 4. Realize you want GB instead - modify the function
function Get-FileSize {
    param([string]$Path)
    $file = Get-Item $Path
    [math]::Round($file.Length / 1GB, 2)  # Changed to GB
}

# 5. Run the modified function again (F8)
# This overwrites the previous version in memory

# 6. Test again
Get-FileSize -Path "C:\Windows\notepad.exe"  # Returns GB

# 7. Repeat until satisfied

Testing with Different Inputs

Always test with various inputs:

# Define your function
function Get-DayOfWeek {
    param([datetime]$Date = (Get-Date))
    $Date.DayOfWeek
}

# Test with default (no parameter)
Get-DayOfWeek

# Test with specific date
Get-DayOfWeek -Date "2025-12-25"

# Test with different formats
Get-DayOfWeek -Date "January 1, 2025"

# Test edge cases
Get-DayOfWeek -Date "1900-01-01"  # Very old date

Checking If a Function Exists

Useful when testing to see what's loaded:

# Check if a function is loaded in your session
Get-Command Get-TimeGreeting

# List all loaded functions
Get-Command -CommandType Function

# Remove a function from session (to start fresh)
Remove-Item Function:\Get-TimeGreeting

Test Parameters and Validation

# Define function with validation
function Set-ServiceStatus {
    param(
        [ValidateSet("Running", "Stopped")]
        [string]$Status
    )
    "Setting status to: $Status"
}

# Load it (paste or F8)

# Test valid input
Set-ServiceStatus -Status "Running"  # Works

# Test invalid input (should error)
Set-ServiceStatus -Status "Paused"   # ERROR - not in ValidateSet

# Test mandatory parameters
function Get-User {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username
    )
    "User: $Username"
}

# Load and call without parameter (should prompt)
Get-User  # PowerShell prompts: "Username:"

Functions Persist in Session

Once you paste/load a function into PowerShell, it stays in memory until:

  • You close PowerShell
  • You remove it with Remove-Item Function:\FunctionName
  • You redefine it (paste a new version)

This is great for testing but can be confusing if you forget which version is loaded!

Quick Function Reload

If you're repeatedly testing the same function: 1. Keep your editor and PowerShell side-by-side 2. Modify function in editor 3. Select all → F8 (or copy-paste) 4. Test immediately in console 5. Repeat

This is much faster than saving to a file and dot-sourcing each time.

Testing Output Types

# Function that returns an object
function Get-SystemInfo {
    [PSCustomObject]@{
        ComputerName = $env:COMPUTERNAME
        CurrentUser = $env:USERNAME
        PowerShellVersion = $PSVersionTable.PSVersion.ToString()
    }
}

# Load and test
$info = Get-SystemInfo

# Verify it's an object (not just text)
$info.GetType()           # Should be PSCustomObject
$info.ComputerName        # Should access property
$info | Get-Member        # Shows all properties and methods
$info | ConvertTo-Json    # Should serialize properly

Simple Functions

Function Without Parameters

function Show-SystemInfo {
    $os = Get-CimInstance Win32_OperatingSystem
    $computer = Get-CimInstance Win32_ComputerSystem

    Write-Output "Computer: $($computer.Name)"
    Write-Output "OS: $($os.Caption)"
    Write-Output "Version: $($os.Version)"
    Write-Output "Memory: $([math]::Round($computer.TotalPhysicalMemory / 1GB, 2)) GB"
}

# Call it
Show-SystemInfo

Function With Simple Parameter

function Test-IsEven {
    param(
        [int]$Number
    )

    if ($Number % 2 -eq 0) {
        return $true
    } else {
        return $false
    }
}

# Call it
Test-IsEven -Number 4   # Returns $true
Test-IsEven -Number 7   # Returns $false

Parameters

Parameter Block (param)

Always put param() first in your function:

function Do-Something {
    param(
        [string]$Name,
        [int]$Age,
        [bool]$IsActive
    )

    # Function code here
}

Mandatory Parameters

function Get-UserInfo {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username,

        [Parameter(Mandatory=$false)]  # Optional parameter
        [string]$Domain = "DEFAULT"     # Default value
    )

    Write-Output "User: $Username"
    Write-Output "Domain: $Domain"
}

# Must provide Username, Domain is optional
Get-UserInfo -Username "jdoe"
Get-UserInfo -Username "jdoe" -Domain "CORPORATE"

Parameter Validation

function Set-ServiceStatus {
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet("Running", "Stopped")]  # Only these values allowed
        [string]$Status,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]  # Cannot be empty or null
        [string]$ServiceName,

        [ValidateRange(1, 10)]  # Number must be between 1 and 10
        [int]$RetryCount = 3
    )

    Write-Output "Setting $ServiceName to $Status (retry up to $RetryCount times)"
}

# This works
Set-ServiceStatus -Status "Running" -ServiceName "Spooler"

# This fails validation
Set-ServiceStatus -Status "Paused" -ServiceName "Spooler"  # ERROR: "Paused" not in ValidateSet

Switch Parameters (True/False Flags)

function Get-FileList {
    param(
        [string]$Path = "C:\Temp",

        [switch]$IncludeHidden,  # Switch parameter - just include or don't

        [switch]$Recursive
    )

    $params = @{
        Path = $Path
    }

    if ($IncludeHidden) {
        $params.Force = $true
    }

    if ($Recursive) {
        $params.Recurse = $true
    }

    Get-ChildItem @params
}

# Call with switches
Get-FileList -Path C:\Temp -IncludeHidden -Recursive
Get-FileList  # Uses defaults, no switches

Return Values

Implicit Return (PowerShell Way)

Everything that outputs becomes the return value:

function Get-Double {
    param([int]$Number)

    $Number * 2  # This outputs, so it's returned
}

$result = Get-Double -Number 5
Write-Output $result  # 10

Multiple Return Values

function Get-MinMax {
    param([int[]]$Numbers)

    $min = ($Numbers | Measure-Object -Minimum).Minimum
    $max = ($Numbers | Measure-Object -Maximum).Maximum

    # Return multiple values as an object
    [PSCustomObject]@{
        Minimum = $min
        Maximum = $max
        Range = $max - $min
    }
}

$stats = Get-MinMax -Numbers @(5, 12, 3, 19, 7)
Write-Output "Min: $($stats.Minimum)"
Write-Output "Max: $($stats.Maximum)"
Write-Output "Range: $($stats.Range)"

Explicit Return

function Test-FileAge {
    param(
        [string]$Path,
        [int]$DaysOld = 30
    )

    if (-not (Test-Path $Path)) {
        Write-Warning "File not found: $Path"
        return $false  # Exit early
    }

    $file = Get-Item $Path
    $age = (Get-Date) - $file.LastWriteTime

    if ($age.TotalDays -gt $DaysOld) {
        return $true
    } else {
        return $false
    }
}

Write-Output vs Write-Host (IMPORTANT!)

Understanding output is critical for functions that work with the pipeline:

# BAD: Write-Host breaks the pipeline
function Get-BadData {
    Write-Host "Here's your data: Important Value"  # Goes to console only!
}

$result = Get-BadData
Write-Output $result  # Empty! Can't capture Write-Host output

# GOOD: Use Write-Output or implicit output
function Get-GoodData {
    Write-Output "Here's your data: Important Value"  # Goes to pipeline
}

$result = Get-GoodData
Write-Output $result  # Works! Returns the string

# EVEN BETTER: Implicit output (most PowerShell-like)
function Get-BestData {
    "Here's your data: Important Value"  # Automatically goes to pipeline
}

$result = Get-BestData  # Works perfectly

When to use each:

  • Implicit output or Write-Output: For data you want returned/piped
  • Write-Host: ONLY for display text (progress messages, colored output for user)
  • Write-Verbose: For optional detailed messages (-Verbose flag)
  • Write-Information: For informational messages that can be captured
function Process-Data {
    param([string]$Data)

    # Display progress (not returned)
    Write-Host "Processing $Data..." -ForegroundColor Yellow

    # Return actual result (can be captured/piped)
    [PSCustomObject]@{
        Input = $Data
        ProcessedAt = Get-Date
        Success = $true
    }
}

$result = Process-Data -Data "MyData"  # Gets the object, not the Write-Host text

Return Objects, Not Formatted Text

PowerShell works best when functions return structured objects:

# BAD: Returns formatted string
function Get-UserInfoBad {
    param([string]$Username)

    $user = Get-ADUser -Identity $Username
    "Name: $($user.Name), Email: $($user.EmailAddress)"  # Just text
}

$info = Get-UserInfoBad -Username "jdoe"
# Can't do: $info.Name (it's just a string!)
# Can't export to CSV easily
# Can't filter or sort

# GOOD: Returns object
function Get-UserInfoGood {
    param([string]$Username)

    $user = Get-ADUser -Identity $Username

    [PSCustomObject]@{
        Name = $user.Name
        Email = $user.EmailAddress
        Department = $user.Department
        Enabled = $user.Enabled
    }
}

$info = Get-UserInfoGood -Username "jdoe"
# Now you can:
$info.Name                                    # Access properties
$info | Export-Csv users.csv                  # Export to CSV
$info | Where-Object {$_.Enabled -eq $true}   # Filter
$info | Select-Object Name, Email             # Select columns

Why this matters:

  • Objects can be piped to other cmdlets
  • Properties can be accessed directly
  • Can be exported to CSV, JSON, XML
  • Can be filtered, sorted, grouped
  • Follows PowerShell conventions

Advanced Functions

CmdletBinding (Makes it a "Real" Cmdlet)

function Get-ProcessInfo {
    [CmdletBinding()]  # Adds common parameters and features
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$ProcessName
    )

    begin {
        # Runs once before processing pipeline input
        Write-Verbose "Starting process lookup..."
    }

    process {
        # Runs once for each pipeline input
        $proc = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue

        if ($proc) {
            [PSCustomObject]@{
                Name = $proc.Name
                ID = $proc.Id
                MemoryMB = [math]::Round($proc.WS / 1MB, 2)
                CPU = $proc.CPU
            }
        }
    }

    end {
        # Runs once after all pipeline input processed
        Write-Verbose "Process lookup complete"
    }
}

# Now you get -Verbose, -Debug, etc. for free
Get-ProcessInfo -ProcessName "powershell" -Verbose

# Works with pipeline
"powershell", "code", "chrome" | Get-ProcessInfo

Comment-Based Help

Functions can include built-in help documentation using special comment keywords. This help integrates with PowerShell's Get-Help system, making your functions self-documenting and easy to use.

Quick Example

function Get-DiskSpace {
    <#
    .SYNOPSIS
    Gets free disk space for specified drive

    .DESCRIPTION
    Retrieves the free space for a drive and returns it in gigabytes.
    Optionally warns if space is below a threshold.

    .PARAMETER DriveLetter
    The drive letter to check (e.g., 'C', 'D').

    .PARAMETER WarnThresholdGB
    Warn if free space is below this many gigabytes. Default is 0 (no warning).

    .EXAMPLE
    Get-DiskSpace -DriveLetter C
    Returns free space on C: drive.

    .EXAMPLE
    Get-DiskSpace -DriveLetter C -WarnThresholdGB 50
    Warns if C: has less than 50GB free.

    .NOTES
    Author: Your Name
    Version: 1.0.0
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidatePattern('^[A-Z]$')]
        [string]$DriveLetter,

        [int]$WarnThresholdGB = 0
    )

    $drive = Get-PSDrive -Name $DriveLetter -ErrorAction Stop
    $freeGB = [math]::Round($drive.Free / 1GB, 2)

    if ($WarnThresholdGB -gt 0 -and $freeGB -lt $WarnThresholdGB) {
        Write-Warning "Low disk space on ${DriveLetter}: ${freeGB}GB free"
    }

    [PSCustomObject]@{
        Drive = $DriveLetter
        FreeSpaceGB = $freeGB
        TotalSpaceGB = [math]::Round($drive.Used / 1GB + $freeGB, 2)
    }
}

Using Get-Help

Once you've defined a function with help, you can view it using Get-Help:

# View basic help
Get-Help Get-DiskSpace

# View detailed help
Get-Help Get-DiskSpace -Full

# View just examples
Get-Help Get-DiskSpace -Examples

# View specific parameter help
Get-Help Get-DiskSpace -Parameter DriveLetter

Placement in Functions

There are three ways to place help in a function:

Option 1: Inside function, at top (Most Common)

function Get-Data {
    <#
    .SYNOPSIS
    Brief description
    #>

    param($Name)
    # Code...
}

Option 2: Before the function keyword

<#
.SYNOPSIS
Brief description
#>
function Get-Data {
    param($Name)
    # Code...
}

Option 3: Inside function, at end (Rare)

function Get-Data {
    param($Name)
    # Code...

    <#
    .SYNOPSIS
    Brief description
    #>
}

Recommendation: Use Option 1 - it's the most common and keeps help close to parameters.

Complete Help Reference

For comprehensive documentation on all help keywords (.SYNOPSIS, .DESCRIPTION, .PARAMETER, .EXAMPLE, .NOTES, .LINK, .INPUTS, .OUTPUTS, etc.) and detailed examples, see Comment-Based Help.

Best Practices for Function Help

Always include these minimum keywords:

  • .SYNOPSIS - One-line description
  • .DESCRIPTION - Detailed explanation
  • .PARAMETER - Document every parameter
  • .EXAMPLE - At least one working example

Tips for good help:

# Good .SYNOPSIS (concise, clear)
.SYNOPSIS
Gets disk space information for specified drives

# Bad .SYNOPSIS (too vague)
.SYNOPSIS
Gets information

# Good .PARAMETER (explains purpose, notes defaults)
.PARAMETER WarnThresholdGB
Warn if free space is below this many gigabytes. Default is 0 (no warning).

# Bad .PARAMETER (just repeats the name)
.PARAMETER WarnThresholdGB
The warn threshold in GB

# Good .EXAMPLE (shows real command + explains what it does)
.EXAMPLE
Get-DiskSpace -DriveLetter C -WarnThresholdGB 50
Checks C: drive and warns if less than 50GB free.

# Bad .EXAMPLE (no explanation)
.EXAMPLE
Get-DiskSpace -DriveLetter C

Additional recommendations:

  • Include multiple .EXAMPLE entries for different use cases
  • Use .NOTES for author, version, and requirements
  • Use .LINK to reference related cmdlets or documentation
  • Keep examples realistic and copy-pasteable
  • Update help when you modify the function

Real-World Examples

Example: Logging Function

Scenario: Create a reusable logging function for all your scripts

function Write-Log {
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [string]$Message,

    [ValidateSet("INFO", "WARNING", "ERROR")]
    [string]$Level = "INFO",

    [string]$LogPath = "C:\Logs\script.log"
)

process {
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] [$Level] $Message"

    # Write to console with color
    $color = switch ($Level) {
        "INFO"    { "White" }
        "WARNING" { "Yellow" }
        "ERROR"   { "Red" }
    }
    Write-Host $logEntry -ForegroundColor $color

    # Write to file
    Add-Content -Path $LogPath -Value $logEntry
}
}

# Usage
Write-Log "Script started"
Write-Log "Disk space low" -Level WARNING
Write-Log "Failed to connect" -Level ERROR

# Pipeline usage
"Process 1", "Process 2" | Write-Log

Example: File Cleanup Function

Scenario: Clean up old files with safety checks and logging

function Remove-OldFiles {
[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [Parameter(Mandatory=$true)]
    [string]$Path,

    [Parameter(Mandatory=$true)]
    [int]$DaysOld,

    [string]$Filter = "*.*",

    [switch]$Recurse
)

begin {
    if (-not (Test-Path $Path)) {
        throw "Path not found: $Path"
    }

    $cutoffDate = (Get-Date).AddDays(-$DaysOld)
    Write-Verbose "Removing files older than: $cutoffDate"
}

process {
    $params = @{
        Path = $Path
        Filter = $Filter
        File = $true
    }

    if ($Recurse) {
        $params.Recurse = $true
    }

    $oldFiles = Get-ChildItem @params |
        Where-Object { $_.LastWriteTime -lt $cutoffDate }

    foreach ($file in $oldFiles) {
        if ($PSCmdlet.ShouldProcess($file.FullName, "Delete")) {
            try {
                Remove-Item -Path $file.FullName -Force
                Write-Verbose "Deleted: $($file.Name)"
            }
            catch {
                Write-Error "Failed to delete $($file.Name): $_"
            }
        }
    }
}

end {
    Write-Verbose "Cleanup complete"
}
}

# Usage
Remove-OldFiles -Path C:\Logs -DaysOld 30 -Filter "*.log" -Verbose

# WhatIf mode (doesn't actually delete)
Remove-OldFiles -Path C:\Logs -DaysOld 30 -WhatIf

Example: Retry Logic Function

Scenario: Reusable function to retry operations with exponential backoff

function Invoke-WithRetry {
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [scriptblock]$ScriptBlock,

    [int]$MaxRetries = 3,

    [int]$InitialDelaySeconds = 2,

    [switch]$ExponentialBackoff
)

$attempt = 0
$success = $false

while ($attempt -lt $MaxRetries -and -not $success) {
    $attempt++

    try {
        Write-Verbose "Attempt $attempt of $MaxRetries..."

        # Execute the script block
        $result = & $ScriptBlock

        $success = $true
        return $result
    }
    catch {
        Write-Warning "Attempt $attempt failed: $($_.Exception.Message)"

        if ($attempt -lt $MaxRetries) {
            if ($ExponentialBackoff) {
                $delay = $InitialDelaySeconds * [math]::Pow(2, $attempt - 1)
            } else {
                $delay = $InitialDelaySeconds
            }

            Write-Verbose "Waiting $delay seconds before retry..."
            Start-Sleep -Seconds $delay
        }
    }
}

if (-not $success) {
    throw "Operation failed after $MaxRetries attempts"
}
}

# Usage
$result = Invoke-WithRetry -ScriptBlock {
Test-Connection -ComputerName "Server01" -Count 1 -ErrorAction Stop
} -MaxRetries 5 -ExponentialBackoff -Verbose

Scope and Variables

# Variables in functions are local by default
function Test-Scope {
    $localVar = "I'm local to the function"
    Write-Output $localVar
}

Test-Scope  # Works
Write-Output $localVar  # Empty - variable doesn't exist outside function

# Access global variables
$global:sharedValue = "Available everywhere"

function Use-Global {
    Write-Output $global:sharedValue  # Can read global
    $global:sharedValue = "Modified"  # Can modify global (use sparingly!)
}

# Script scope (shared across functions in same script)
$script:scriptLevel = "Shared in script"

function One {
    Write-Output $script:scriptLevel  # Can access
    $script:scriptLevel = "Changed"
}

function Two {
    Write-Output $script:scriptLevel  # Sees the change
}

Tips & Tricks

Use Approved Verbs

# Check approved verbs
Get-Verb | Out-GridView

# Common approved verbs:
Get, Set, New, Remove, Add, Clear, Test, Invoke, Start, Stop

# DON'T use:
function Fetch-Data { }   # Use Get-Data
function Create-File { }  # Use New-File
function Delete-Item { }  # Use Remove-Item

Use Splatting for Readable Parameter Passing

function Get-FileList {
param($Path, $Filter, $Recurse)
Get-ChildItem @PSBoundParameters
}

# Or build hash table
$params = @{
Path = "C:\Temp"
Filter = "*.log"
Recurse = $true
}

Get-ChildItem @params  # Clean and readable

Return Objects, Not Formatted Text

# BAD: Returns formatted text
function Get-UserInfo {
$user = Get-ADUser -Identity "jdoe"
Write-Output "Name: $($user.Name), Email: $($user.Email)"
}

# GOOD: Returns object that can be used
function Get-UserInfo {
$user = Get-ADUser -Identity "jdoe"
[PSCustomObject]@{
    Name = $user.Name
    Email = $user.Email
}
}

# Now you can: pipe it, select properties, export to CSV, etc.
Get-UserInfo | Export-Csv users.csv

Don't Use Write-Host for Output

# BAD: Write-Host breaks the pipeline
function Get-Data {
Write-Host "Here's your data"  # This can't be captured or piped!
}

# GOOD: Use Write-Output (or just output directly)
function Get-Data {
Write-Output "Here's your data"  # Can be captured and piped
# Or just:
"Here's your data"  # Implicit output
}

# Write-Host is only for display (like progress messages)
# Use Write-Verbose or Write-Information for informational output

Watch Out for Unwanted Output

function Add-ToList {
$list = [System.Collections.ArrayList]@()

$list.Add("Item1")  # Returns the index added (0)
$list.Add("Item2")  # Returns 1

return $list  # Returns: 0, 1, ArrayList
}

# FIX: Suppress unwanted output
function Add-ToList {
$list = [System.Collections.ArrayList]@()

$list.Add("Item1") | Out-Null  # Suppress index output
$list.Add("Item2") | Out-Null

return $list  # Returns only ArrayList
}

Parameter Defaults and Mandatory Don't Mix

# This makes no sense - it's mandatory but has a default?
param(
[Parameter(Mandatory=$true)]
[string]$Name = "Default"  # Contradiction!
)

# Pick one or the other:
param(
[Parameter(Mandatory=$true)]  # Required, no default
[string]$Name
)

# OR
param(
[string]$Name = "Default"  # Optional with default
)

Additional Resources