I've got a Pester It block, that looks like the below:
It "should add a header" {
{
$DifferenceObject = Get-Content -Path $PathToFile1
Set-PowershellFile -Path $PathToFile2
$ReferencedObject = Get-Content -Path $PathToFile2
Compare-Object -ReferenceObject $ReferencedObject -DifferenceObject $DifferenceObject | should -be $null
}
}
Set-PowershellFile just updates a file with some new text. I thought that the above code would do the following but I just want to confirm:
- It gets the content of a file located at 'PathToFile1'
- It sets the file at 'PathToFile2' using Set-PowershellFile, then gets the new content of that file.
- Compares the newly updated file to the content of the first file, making sure that they are the same.
Is this correct? When I run my unit tests, the file change made in Set-PowershellFile does not persist, but the unit test passes. I assumed that wrapping this code inside the if block in {} causes the synchronous behavior but doesn't 'commit' the file changes. Is this all correct?
CodePudding user response:
Mathias R. Jessen provided the crucial pointer in a comment - simply omit the inner {
and }
- but since I've seen the underlying misconception before, it's worth digging deeper:
While { ... }
blocks are a regular part of most PowerShell statements, such as foreach
and if
, in isolation they do not provide a scoped block of statements that are immediately executed, the way it would work in C#, for instance.
Instead, { ... }
in isolation is the literal form of a PowerShell script block, i.e. a block of statements for later execution, on demand, either via &
, the call operator, for execution in a child scope of, or via .
, the dot-sourcing operator, for execution directly in the scope of origin - which may or may not be the caller's scope (see caveat below).
Thus, such a script-block literal must either be saved in a variable or passed to a command expecting a script block in order to be executed later.
Absent that, a script block is implicitly output to the success output stream, which by default goes to the console (host), causing it to be rendered by its .ToString()
value, which is simply the verbatim content of the script block, excluding the delimiters ({
and }
).
For instance:
# Script block literal is output by its *verbatim content* (without delimiters)
PS> { "Honey, I'm $HOME" }
"Honey, I'm $HOME"
In order to use a script block properly, you must execute it, typically with &
:
# `& ` executes in a child scope of and
# `. ` executes directly in the scope of origin, typically the current scope.
PS> & { "Honey, I'm $HOME" }
Honey, I'm C:\Users\jdoe # e.g.
Caveat:
Script-block literals - unlike script blocks constructed with [scriptblock]::Create('...')
- are tied to the scope domain (a.k.a "session state") in which they are defined.
This means:
Script-block literals defined outside a module, when also called from outside a module, do run in a child scope of (
&
) / directly in (.
) the caller's scope.By contrast, script-block literals defined inside a module, are tied to that module's scope domain, meaning that an invocation - irrespective of where the call is made from - execute in that module's top-level scope (with
.
) or a child scope thereof (with&
).
Examples:
PS> $foo = 1; function Get-ScriptBlock { ( $foo) }; . Get-ScriptBlock
2
That is, the script block created in the non-module scope domain invoked with .
ran directly in the non-module caller's scope.
PS> $foo = 1; $null = New-Module { $foo = 99; function Get-ScriptBlock { ( $foo) } }; . Get-ScriptBlock
100
That is, the script block created in the scope domain of the dynamic module created with New-Module
, invoked with .
, ran in that module's top-level scope, even though it was invoked from a non-module caller.