100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Script Blocks and Scopes

Understand PowerShell script blocks as first-class values, how scoping (global, script, local, private) governs variable visibility, and how closures capture variables.

Control Flow & FunctionsAdvanced11 min readJul 10, 2026
Analogies

Script Blocks as First-Class Values

A script block, written as { ... }, is a chunk of PowerShell code stored as a value of type [scriptblock] that can be assigned to a variable, passed as a parameter, stored in a data structure, or invoked later with the call operator & or the .Invoke() method. This makes script blocks PowerShell's mechanism for passing behavior around, similar to lambdas or anonymous functions in other languages — cmdlets like Where-Object, ForEach-Object, and Invoke-Command all accept script blocks as their core building block for deferred execution.

🏏

Cricket analogy: Like a captain writing a bowling plan on a card before the match ({ bowl yorkers at the death }) and only 'invoking' it in the 19th over, the plan exists as a reusable object independent of when it's executed, similar to how death-over specialists are briefed in advance.

Delegates: Where-Object and ForEach-Object

Where-Object { $_.Status -eq 'Running' } and ForEach-Object { $_.Name } both receive a script block that's executed once per pipeline object, with $_ bound to the current item inside the block. Because the script block is just a value, you can also store it in a variable first ($filter = { $_.Status -eq 'Running' }) and reuse it across multiple pipelines, or pass it into a custom function's own parameter typed as [scriptblock] to build your own filtering or transformation logic.

🏏

Cricket analogy: Like reusing the same LBW-decision criteria script block across every match of a series rather than rewriting the umpiring logic each time, ensuring consistent decisions from Trent Bridge to the Gabba.

Variable Scopes: Global, Script, Local, Private

PowerShell scopes nest: global (the whole session), script (the currently running .ps1 file), local (the current function, script block, or the default when unspecified), and private (like local, but explicitly invisible to child scopes even though local variables are normally readable by children). A child scope can read a parent's variables by default but cannot write to them without an explicit scope modifier prefix like $global:varName or $script:varName; without that prefix, assigning inside a function creates a new local variable that merely shadows the outer one for the rest of that function's execution.

🏏

Cricket analogy: Like a franchise's team strategy (global scope) being visible to every match, while a specific game plan for today's match (script scope) only applies to that fixture, and a bowler's individual over plan (local scope) doesn't leak back to affect the team's season-long strategy unless explicitly reported up.

Closures: Capturing Variables with GetNewClosure

By default, a script block does not capture the value of outer variables at creation time — it resolves them by name when it eventually runs, so if the outer variable changes before invocation, the script block sees the new value. Calling .GetNewClosure() on a script block creates a bound copy that captures the current values of all outer variables at that moment, which is essential when generating multiple script blocks in a loop that must each remember a different value (like a distinct counter or item) rather than all referencing whatever the loop variable ended up as after the loop finished.

🏏

Cricket analogy: Like a bowler's plan referencing 'the current batsman' by role rather than a name — if the batsman changes mid-over the plan updates automatically — versus GetNewClosure() being like locking in a plan specifically against Virat Kohli regardless of who bats next.

powershell
function New-Multiplier {
    param([int]$Factor)
    # Without GetNewClosure(), every stored block would reference the
    # same outer $Factor and behave identically once the loop finishes.
    return { param($x) $x * $Factor }.GetNewClosure()
}

$multipliers = @{}
foreach ($n in 2, 3, 5) {
    $multipliers[$n] = New-Multiplier -Factor $n
}

& $multipliers[3] 10   # 30, correctly bound to Factor = 3
& $multipliers[5] 10   # 50, correctly bound to Factor = 5

$script:runCount = 0
function Invoke-Tracked {
    param([scriptblock]$Action)
    $script:runCount++
    & $Action
    Write-Verbose "Total tracked invocations: $script:runCount"
}

The dot-source operator (. ) runs a script block or .ps1 file in the CURRENT scope rather than creating a new child scope, which is how you load functions and variables from a helper script into your session. Compare this to the call operator (&), which always runs in a new child scope, isolating the callee's variables from the caller by default.

Assigning to a variable inside a function without a scope modifier never modifies a same-named variable in an outer scope — it silently creates a new local variable that shadows the outer one for the rest of that function, and the outer variable is left completely unchanged after the function returns. Use $script: or $global: explicitly if you intend to modify an outer-scope variable.

  • Script blocks ({ ... }) are first-class [scriptblock] values that can be stored, passed, and invoked with & or .Invoke().
  • Where-Object, ForEach-Object, and Invoke-Command all accept script blocks with $_ bound to the current pipeline item.
  • PowerShell scopes nest: global, script, local (default), and private, with children able to read but not write parent variables by default.
  • Use $global: or $script: scope modifiers to explicitly write to an outer scope from inside a function.
  • Script blocks resolve outer variables by name at invocation time, not at creation time, unless bound with .GetNewClosure().
  • .GetNewClosure() is essential when generating multiple script blocks in a loop that must each remember a distinct value.
  • Dot-sourcing (. script.ps1) runs in the current scope; the call operator (&) always creates a new child scope.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#PowerShellStudyNotes#ScriptBlocksAndScopes#Script#Blocks#Scopes#Class#StudyNotes#SkillVeris#ExamPrep