Functions and Scripts

Reviewed & published by Brayan K

By the end of this lesson you'll wrap reusable logic in named functions with typed, validated, and mandatory parameters, accept input straight from the pipeline, and understand exactly what a function returns — so your tools behave like real cmdlets instead of leaking surprises.

Part of the free PowerShell course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A function is a kitchen appliance. A blender has labelled inputs (the lid, the speed dial) — those are your param() entries. Some inputs are required: you can't blend without the jug attached, just like [Parameter(Mandatory)]. You feed ingredients in through the top one at a time — that's pipeline input arriving in the process {} block. And the appliance produces one thing: the smoothie that pours out. In PowerShell, everything you pour out is the result — so if you accidentally drop a stray spoon in the jug, it comes out in the smoothie too. That "accidental spoon" is the output-pollution bug you'll learn to avoid.

1. The function keyword & param() blocks

A function is a named, reusable block of code. You write function Verb-Noun { ... } — the same Verb-Noun naming PowerShell uses for built-in cmdlets like Get-Date, which makes your tools feel native. Inputs go in a param() block, where each parameter can declare a type (like [string] or [int]) and a default value. Crucially, you call a function the shell way: space-separated, dash-named arguments like Get-Greeting -Name "Alice" — not with commas in brackets. Read this worked example and run it.

# 'function' gives a block of code a name so you can reuse it.
# Naming convention is Verb-Noun, the same as built-in cmdlets (Get-Date).
function Get-Greeting {
    # param() lists the inputs. [string] is the TYPE; = "World" is a DEFAULT.
    param([string]$Name = "World")

    # Any value you leave "hanging" (not assigned to a variable) is OUTPUT.
    "Hello, $Name!"
}

# Call it. PowerShell args are SPACE-separated and named with a leading dash:
Get-Greeting                 # uses the default -> Hello, World!
Get-Greeting -Name "Alice"   # passes a value   -> Hello, Alice!
Get-Greeting Bob             # positional (no -Name) also works -> Hello, Bob!

Your turn. The function below is almost complete — fill in the ___ blanks using the hints, then run it. Notice how a typed parameter with a default lets the caller skip arguments.

# 🎯 YOUR TURN — replace each ___ , then run it.
function Get-Area {
    param(
        # 1) Give -Width a type of [int] and a default of 1
        [___]$Width = ___,    # 👉 type is [int], default is 1

        # 2) Give -Height a type of [int] and a default of 1
        [int]$Height = ___    # 👉 default is 1
    )

    # 3) Output the area. Wrap maths in $(...) so it calculates inside the text
    "Area is $(___)"          # 👉 $Width * $Height
}

Get-Area -Width 4 -Height 3
Get-Area                       # both default to 1

# ✅ Expected output:
#    Area is 12
#    Area is 1

2. Required input with [Parameter(Mandatory)] & [CmdletBinding()]

Sometimes a parameter is essential and there's no sensible default. Mark it [Parameter(Mandatory)] and PowerShell will refuse to run without it — if the caller forgets, the shell interactively prompts for the value instead of crashing. Adding [CmdletBinding()] at the top of the param() block promotes your function to an advanced function: it instantly gains the common parameters every real cmdlet has, like -Verbose and -ErrorAction. Write-Verbose messages only appear when the caller asks for them with -Verbose, and they never mix into your data output.

# [Parameter(Mandatory)] forces the caller to supply a value.
# [CmdletBinding()] turns this into an "advanced function" — it now gets
# free common parameters like -Verbose and -ErrorAction, just like a real cmdlet.
function New-Password {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]    # if you omit -Length, PowerShell PROMPTS for it
        [int]$Length,

        [string]$Prefix = "pw"     # optional, has a default
    )

    # Write-Verbose only prints when the caller passes -Verbose. It does NOT
    # pollute the function's real output — handy for progress messages.
    Write-Verbose "Building a password of length $Length"

    $chars = -join ((1..$Length) | ForEach-Object { 'x' })
    "$Prefix-$chars"
}

New-Password -Length 5
New-Password -Length 3 -Prefix "key"

3. Pipeline input: ValueFromPipeline + process {}

PowerShell's superpower is the pipeline (|), and your functions can plug straight into it. Two pieces make it work: mark a parameter [Parameter(ValueFromPipeline)] so values are allowed to arrive from the pipe, and put your logic in a process {} block, which runs once for every item that comes down the pipeline. Inside process, your parameter (and the automatic variable $_) holds the current item. Skip the process block and your function only ever sees the last piped item — a very common beginner bug.

# Make a function accept PIPELINE input with two pieces:
#   [Parameter(ValueFromPipeline)]  -> "values can arrive from the pipeline"
#   a process {} block              -> runs ONCE PER item that comes down the pipe
function ConvertTo-Upper {
    param(
        [Parameter(ValueFromPipeline)]
        [string]$Text
    )
    process {
        # Inside process{}, $Text is the CURRENT item. $_ also refers to it.
        $Text.ToUpper()
    }
}

# Send three strings down the pipeline — process{} fires three times.
"hello", "world", "powershell" | ConvertTo-Upper

# It still works as a normal parameter too:
ConvertTo-Upper -Text "direct"

Your turn. Combine what you've learned: make Get-Square accept numbers from the pipeline and require a label. Fill in the three blanks.

# 🎯 YOUR TURN — make Get-Square accept pipeline input and a required label.
function Get-Square {
    [CmdletBinding()]
    param(
        # 1) Mark -Number so values can arrive FROM the pipeline
        [Parameter(___)]            # 👉 ValueFromPipeline
        [int]$Number,

        # 2) Make -Label required
        [Parameter(___)]            # 👉 Mandatory
        [string]$Label
    )
    # 3) This must run once per piped item — name the block correctly
    ___ {                          # 👉 process
        "$Label $Number = $($Number * $Number)"
    }
}

1, 2, 3 | Get-Square -Label "sq"

# ✅ Expected output:
#    sq 1 = 1
#    sq 2 = 4
#    sq 3 = 9

4. return and what a function really outputs

Here's the idea that trips up everyone coming from C#, Python, or JavaScript: in PowerShell, every value a function emits is part of its return value — not just the thing after return. A bare "Starting..." on its own line is emitted and ends up in the output. return $x doesn't mean "this is THE result"; it means "stop the function now and emit $x". To send a value back you simply leave it un-assigned. To print a human message without polluting the output, use Write-Host or Write-Verbose, which write to separate channels.

# THE BIG RULE: everything a function emits becomes its output.
# 'return' does NOT mean "this is the result" — it just STOPS the function and
# hands back whatever you put after it (which is also added to the output).
function Get-Total {
    param([int[]]$Numbers)

    "Starting..."                      # ⚠️ this string is ALSO output (a leak!)
    $sum = ($Numbers | Measure-Object -Sum).Sum   # assigned -> NOT output
    return $sum                        # stops here and emits $sum
}

# Because "Starting..." was emitted, the caller gets TWO objects back:
$result = Get-Total -Numbers 1, 2, 3
"Captured: $result"

# Fix: don't emit stray values. Use Write-Host/Write-Verbose for messages,
# which are NOT part of the data output.
function Get-CleanTotal {
    param([int[]]$Numbers)
    Write-Host "Starting..."           # message channel, not the output channel
    ($Numbers | Measure-Object -Sum).Sum
}
$clean = Get-CleanTotal -Numbers 1, 2, 3
"Clean: $clean"

Watch the output channel

In the first example, $result captured both "Starting..." and 6 because both were emitted. The fix is to never leave a stray value un-handled: assign it, pipe it to Out-Null, or use Write-Host/Write-Verbose for messages. This is the single most common reason a PowerShell function "returns weird extra stuff".

Deep Dive: variable scope

Variables you create inside a function are local to it — they vanish when the function ends and don't leak out to the caller. That's a good thing: it keeps your functions self-contained.

The twist is that a function can read a variable from the surrounding (parent) scope, but if it assigns to one, it creates a new local copy and leaves the original untouched. So this prints 1, not 99:

$count = 1
function Bump { $count = 99 }   # creates a LOCAL $count
Bump
$count                          # still 1 — the outer one never changed

Prefer passing values in as parameters and returning results out — relying on outer variables makes functions fragile. If you truly must change a caller's variable, that's what [ref] parameters or explicit $script: / $global: scopes are for, but reach for them rarely.

Common Errors (and the fix)

📋 Quick Reference

ConceptSyntax
Define a functionfunction Verb-Noun { ... }
Typed param + defaultparam([int]$N = 1)
Required param[Parameter(Mandatory)]
Pipeline input[Parameter(ValueFromPipeline)] + process {}
Advanced function[CmdletBinding()]
Return a valuejust emit it (or return $x to stop early)
Call a functionVerb-Noun -Param value
Suppress outputexpression | Out-Null

Mini-Challenge: Convert-Temperature

No blanks this time — just a brief and an outline. Build a pipeline-friendly function from scratch, run it, and check your output against the example in the comments. This is exactly the shape of a real reusable tool.

# 🎯 MINI-CHALLENGE: a Convert-Temperature function
# 1. function Convert-Temperature with [CmdletBinding()]
# 2. Params:
#      -Celsius  [double]  marked [Parameter(Mandatory)] AND ValueFromPipeline
#      -Round    [int]     default 1   (decimal places)
# 3. In a process {} block, work out fahrenheit = $Celsius * 9 / 5 + 32
#    and OUTPUT one line:  "$Celsius C = <fahrenheit> F"
#      (use [math]::Round(<value>, $Round) to round)
# 4. Make sure NOTHING else is emitted (no stray strings -> no output leak).
#
# Try it with:   0, 37, 100 | Convert-Temperature
#
# ✅ Expected output:
#    0 C = 32 F
#    37 C = 98.6 F
#    100 C = 212 F

# your code here

🎉 Lesson Complete!

Practice quiz

Which keyword defines a named function in PowerShell?

  • function
  • def
  • sub
  • func

Answer: function. You declare a function with the function keyword, e.g. function Get-Greeting { }.

Where do you declare a function's parameters?

  • In an args block
  • In a param() block
  • In the function name
  • In a return statement

Answer: In a param() block. Parameters go in a param() block, e.g. param([string]$Name = 'World').

How should you call a function in PowerShell?

  • Get-Greeting('Alice')
  • Get-Greeting{Alice}
  • Get-Greeting -Name 'Alice'
  • Get-Greeting=Alice

Answer: Get-Greeting -Name 'Alice'. PowerShell uses space-separated, dash-named arguments, not commas in brackets.

What does [Parameter(Mandatory)] do?

  • Sets a default value
  • Hides the parameter
  • Adds verbose output
  • Forces the caller to supply a value

Answer: Forces the caller to supply a value. Mandatory makes the parameter required, prompting if it is omitted.

Which two pieces let a function accept pipeline input?

  • ValueFromPipeline plus a process {} block
  • Mandatory plus return
  • CmdletBinding plus begin
  • param plus end

Answer: ValueFromPipeline plus a process {} block. Mark the parameter ValueFromPipeline and put logic in a process {} block.

In PowerShell, what becomes a function's output?

  • Only the value after return
  • Every value the function emits
  • Only Write-Host text
  • Nothing unless return is used

Answer: Every value the function emits. Every un-assigned value a function emits is part of its output.

What does the return keyword actually do?

  • Marks the only result
  • Suppresses all output
  • Stops the function and emits what follows it
  • Starts a loop

Answer: Stops the function and emits what follows it. return stops the function early and emits its value; it does not mark the sole result.

Which cmdlet prints a message without polluting data output?

  • Return-Value
  • Write-Verbose
  • Emit-Data
  • Echo-Line

Answer: Write-Verbose. Write-Verbose (or Write-Host) uses a separate channel from data output.

What does [CmdletBinding()] add to a function?

  • A return type
  • Faster execution
  • Automatic parameters
  • Common parameters like -Verbose and -ErrorAction

Answer: Common parameters like -Verbose and -ErrorAction. It promotes the function to an advanced function with common parameters.

Without a process {} block, a pipeline-input function sees which items?

  • Every item
  • The first item only
  • No items
  • Only the last piped item

Answer: Only the last piped item. Without process {}, the body runs once and only sees the last piped item.

Continue this course

Frequently asked questions

Why does Get-Greeting -Name 'Alice' use a space and a dash, not ("Alice")?

PowerShell is a shell, so it parses commands like a command line, not like C# or Python. Arguments are separated by spaces and named with a leading dash (-Name). Writing Get-Greeting("Alice") passes a single array argument to the first positional parameter, and Get-Greeting("Alice", "Bob") passes ONE two-item array — almost never what you want. Always call functions as: Name -Param value -Param2 value2.

Do I need 'return' at the end of a function?

No. In PowerShell every value you leave un-assigned is automatically added to the output, so the last expression is already returned. 'return' only stops the function early — it is not how you mark 'the result'. Use it for guard clauses (return early if input is bad), not at the natural end of a function.

My function returns extra junk I didn't ask for. Why?

Output pollution. Any expression that isn't captured into a variable, piped, or sent to Out-Null becomes part of the output — including a stray string, or the return value of something like $list.Add(1). Assign such values away, pipe them to Out-Null, or use Write-Host / Write-Verbose for human messages so they don't mix into the data you return.

What is the difference between begin, process, and end blocks?

When a function takes pipeline input you can split its body into three blocks. begin {} runs once before any items, process {} runs once for EACH item coming down the pipeline (this is where $_ / your ValueFromPipeline parameter holds the current item), and end {} runs once after all items. If you don't write these blocks, your whole function body behaves like an end block and only sees the LAST piped item — which is a classic pipeline bug.

What does [CmdletBinding()] actually give me?

It promotes a basic function into an 'advanced function' that behaves like a compiled cmdlet. You get the common parameters for free — -Verbose, -Debug, -ErrorAction, -ErrorVariable, -WhatIf/-Confirm (with SupportsShouldProcess) — plus stricter parameter binding. It costs you nothing, so it's good practice on any function that takes parameters.