PowerShell scriptblock scope weirdness - powershell

I am using Invoke-Command on the local (not remote) computer to run a scriptblock that includes a function. Some of the variables defined at the "root" of the scriptblock are available in the function, and some require me to scope them as Global:.
So in quasi-code, this fails:
{
function test() {
if (test-path $filepath.trim('t')) {
$taskFolder.gettasks(1) |out-file '$env:temp\tasks.txt'
}
}
$filepath = '$env:temp\test.txt'
$TaskService = new-object -ComObject('Schedule.Service')
$TaskService.connect()
$TaskFolder = $TaskService.GetFolder('\')
test
}
with a "You cannot call a method on a null-valued expression." (i.e. $filepath -eq $null).
However, this works:
{
function test() {
if (test-path $Global:filepath.trim('t')) {
$taskFolder.gettasks(1) |out-file '$env:temp\tasks.txt'
}
}
$Global:filepath = '$env:temp\test.txt'
$TaskService = new-object -ComObject('Schedule.Service')
$TaskService.connect()
$TaskFolder = $TaskService.GetFolder('\')
test
}
even though $TaskFolder is not Global.
I can't seem to replicate the problem with short, simple code like this here, but the effect of ONLY globally scoping a couple variables makes all the difference.
I initially noticed the issue when I was attempting to use the variables with Script: scope and that was failing. I assumed that was because this was a script block and not an actual script. Changing to Global: resolved the problem and then I noticed the non-scoped $taskfolder variable.
I am not asking about scheduled tasks! I only included it in the example because that is the variable that is working fine without being assigned a global scope.
Obviously, I'm not understanding something about scoping, invoking, or maybe the Schedule Service.
Can someone enlighten me?

Related

Access a variable from parent scope

In Single Module Scenario: Running Set-Var returns 10.
# m.psm1
function Set-Var {
$MyVar = 10
Get-Var
}
function Get-Var {
$MyVar
}
In Nested Modules Scenario: Running Set-Var does not return any value.
# m1.psm1
function Get-Var {
$MyVar
}
# m.psm1
Import-Module .\m1.psm1
function Set-Var {
$MyVar = 10
Get-Var
}
How do I achieve the same effect as a single module with nested modules? Using $script:MyVar also does not work. However, I would like to keep the scope of the variable local to enable concurrent executions with different values.
Your code doesn't work because local variables are not inherited by functions in nested module context.
You can do this instead:
$null = New-Module {
function Get-Var {
[CmdletBinding()] param()
$PSCmdlet.SessionState.PSVariable.Get('MyVar').Value
}
}
The New-Module command creates an in-memory module, because this code only works when the caller is in a different module or script.
Use the CmdletBinding attribute to create an advanced function. This is a prerequisite to use the automatic $PSCmdlet variable, which we need in the next step.
Use its SessionState.PSVariable member to get or set a variable from the parent (module) scope.
This answer shows an example how to set a variable in the parent (module) scope.
See also: Is there any way for a powershell module to get at its caller's scope?

Use local functions in PowerShell Job

I am writing custom cmdlets to perform a task. One of the cmdlets depends on the other ones and is taking a really long time, so I wanted to perform that task inside a job. Well guess what, you can't because the other cmdlets are not available inside that Job's scope. Why? The other languages out there like C++, Java, C# allow you to use variables, objects, functions from whitin the same scope, why isn't this available in PowerShell? Why is it so decoupled? I feel like it makes it harder for developers. Maybe I don't get the topic, but I would like to do something like this:
function Write-Yes {
Write-Host "yes"
}
function Write-No {
Write-Host "no"
}
function Write-Random {
$result = #($true, $false) | Get-Random
if ($result) {
Write-Yes
}
else {
Write-No
}
}
Start-Job -ScriptBlock { Write-Random }
This is not possible. You have to do some hacks like providing the scriptblock of the function as the argument and call it using the call operator or something like this. Or even, use Import-Module to reimport the same file that you are working in. This feels overly complicated. The only module that I saw that is able to do something like this is PoshRSJob that allows you to name the cmdlets that will be used inside the job and it will create them dynamically for you, with some lexical parsing and again, overly complicated things.
Why are the things like they are and is there any way to do what I'm trying in the example in an elegat way?
Start-Job is very limited and slow. It was implemented very poorly imho and I never use it. You can use runspaces for fast and lightweight "background jobs", and import functions and variables from your current session.
Example:
function Write-Yes { "yes" }
function Write-No { "no" }
function Write-Random {
if ($true, $false | Get-Random) {
Write-Yes
}
else {
Write-No
}
}
# setup session and import functions
$session = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
"Write-Yes", "Write-No", "Write-Random" | foreach {
$session.Commands.Add((
New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry $_, (Get-Content "Function:\$_")
))
}
# setup separate powershell instance
$job = [Powershell]::Create($session)
[void]$job.AddScript({ Write-Random })
# start async
$asyncResult = $job.BeginInvoke()
# do stuff ...
# wait for completion
$job.EndInvoke($asyncResult)
$job.Dispose()
But in general, Powershell is not made for complex parallel processing. In general, it's best to put everything inside a script file and run that, as a task or background job etc.

Access External Variable from with-in Mock Script Block (Pester)

Pretend I have a function like...
function Get-Something {
return Get-DogShit
}
...in my Pester test script...
$var = 1
Mock 'Get-Dogshit' { return $var }
it 'should return true' {
Get-Something | should $var
}
This doesn't work, but you see what I'm trying to do here? I want to get the value from a local variable into a MOCK script block. I want to avoid hard coding the return value in the mock and the expected result in the it-block. Any ideas on how I can achieve this?
I had this problem myself, and script scope didn't work and I didn't care to use global scope. A bit of research shows how you can use closures for this.
$var = 1
Mock 'Get-Dogshit' { return $var }.GetNewClosure()
it 'should return true' {
Get-Something | Should be $var
}
Wasn't sure if it would work as not messed with pester before but apparently it follows the same scope rules as standard PowerShell.
So $script:var = 1, and the brute-force $global:var = 1 if it doesn't or if you need to call it from outside the script scope.

Powershell cannot pass object to function

I am unable to pass an object of type [System.Messaging.Message] into a function within my script.
e.g. (outline of code)
function global:CopyQueue() {
$vTotalCountInMSMQ = $global:qSource.GetAllMessages()
foreach ($msg in $vTotalCountInMSMQ)
{
ReadAndCopyMessage $destinationQueue ([REF]$msg)
}
}
Target Function:
function global:ReadAndCopyMessage($destinationQueueName, [REF]$message)
{
$message = $message.BodyStream.Position
.etc.....
}
Unable to access properties (Property 'Position' cannot be found on this object; make sure it exists and is settable.). However, if this code is run within the CopyQueue function, everything works as expected.
I am having trouble to outsource this and process the $msg object out of the loop.
Thanks for your help in advance
similiar questions didn't work:
PowerShell pass by reference not working for me
Powershell argument passing to function seemingly not working
It appears that you shouldn't use [REF] anymore. Also, I must have made the common "," error between parameters before and thus it didn't work.
The code above works fine without [REF]
Call:
ReadAndCopyMessage $destinationQueue $msg
Function:
function global:ReadAndCopyMessage($destinationQueueName, $message)

How does name lookup work in Powershell script blocks?

User cashfoley has posted what appears to be a fairly elegant set of code at codeplex for a "module" called PSClass.
When I dot-source the psclass code into some code of my own, I am able to write code like:
$Animal = New-PSClass Animal {
constructor {
param( $name, $legs )
# ...
}
method -override ToString {
"A $($this.Class.ClassName) named $($this.name) with $($this.Legs) Legs"
}
}
When I tried to create a module out of the PSClass code, however, I started getting errors. The constructor and method names are no longer recognized.
Looking at the actual implementation, what I see is that constructor, method, etc. are actually nested functions inside the New-PSClass function.
Thus, it seems to me that when I dot-source the PSClass.ps1 file, my script-blocks are allowed to contain references to functions nested inside other local functions. But when the PSClass code becomes a module, with the New-PSClass function exported (I tried both using a manifest and using Export-ModuleMember), the names are no longer visible.
Can someone explain to me how the script blocks, scoping rules, and visibility rules for nested functions work together?
Also, kind of separately, is there a better class definition protocol for pure Powershell scripting? (Specifically, one that does not involve "just write it in C# and then do this...")
The variables in your script blocks don't get evaluated until they are executed. If the variables in the script block don't exist in the current scope when the block is executed, the variables won't have any values. Script blocks aren't closures: they don't capture the context at instantiation time.
Remove-variable FooBar
function New-ScriptBlock
{
$FooBar = 1
$scriptBlock = {
Write-Host "FooBar: $FooBar"
}
$FooBar = 2
& $scriptBlock # Outputs FooBar: 2 because $FooBar was set to 2 before invocation
return $scriptBlock
}
function Invoke-ScriptBlock
{
param(
$ScriptBlock
)
& $ScriptBlock
}
$scriptBlock = New-ScriptBlock
& $scriptBlock # Prints nothing since $FooBar doesn't exist in this scope
$FooBar = 3
Invoke-ScriptBlock $scriptBlock # Prints $FooBar: 3 since FooBar set to 3