Learning Module Scope the Hard Way

When you set a variable and it gets ignored >:(

Words: 700

Time to read: ~ 3.5 minutes

One of my last articles was talking about how to run scriptblocks in PSCustomObjects.
This came about from a need to update a function depending on whether one of multiple variables were populated.
During that example I encountered a case where I was setting the value of a variable but then, when I called the variable later on, my value wasn’t there!
It was then that I learned the joys of module scope and now I hope to show you what I mean!

FizzBuzz

Let’s take the a moderation of FizzBuzz as an example. Let’s get the running totals of the fizz buzz numbers.

The initial way that I would do this would be a straight up switch statement


#region Normal way
[int]$FizzRunningTotal = 0
[int]$FizzBuzzRunningTotal = 0
[int]$BuzzRunningTotal = 0
# Works
1..15 | ForEach-Object {
switch ($_) {
{ $_ % 15 -eq 0 } {
$FizzBuzzRunningTotal = $FizzBuzzRunningTotal + $_
break
}
{ $_ % 5 -eq 0 } {
$BuzzRunningTotal = $BuzzRunningTotal + $_
break
}
{ $_ % 3 -eq 0 } {
$FizzRunningTotal = $FizzRunningTotal + $_
break
}
}
[PSCustomObject]@{
Number = $_
FizzRunningTotal = $FizzRunningTotal
BuzzRunningTotal = $BuzzRunningTotal
FizzBuzzRunningTotal = $FizzBuzzRunningTotal
}
} | Format-Table AutoSize

view raw

NormalWay.ps1

hosted with ❤ by GitHub

NormalWay
3 is Fizz, 5 is Buzz, 15 is FizzBuzz

ScriptBlocks

Now what happens if we don’t want to use switch? What happens if we tried to do this with script blocks instead?

I’ve created 3 different script blocks here just for ease of use. If you want to create a single script block to do this then I encourage it! Let me know how you get on.

We’re also passing in the parameter into the script block by passing it in the brackets of the .InvokeReturnAsIs() method.

Quick little tip, if you want to see what you can do with methods, run the method without specifying brackets

([Scriptblock]::Create('$i')).InvokeReturnAsIs

MethodOverloadDefs
System.Object[] huh?

 


[int]$FizzRunningTotal = 0
[int]$FizzBuzzRunningTotal = 0
[int]$BuzzRunningTotal = 0
# Scriptblocks
$Mod15 = [Scriptblock]::Create('param([int]$Number) $FizzBuzzRunningTotal = $FizzBuzzRunningTotal + $_')
$Mod5 = [Scriptblock]::Create('param([int]$Number) $BuzzRunningTotal = $BuzzRunningTotal + $_')
$Mod3 = [Scriptblock]::Create('param([int]$Number) $FizzRunningTotal = $FizzRunningTotal + $_')
1..15 | ForEach-Object {
switch ($_) {
{ $_ % 15 -eq 0 } {$Mod15.InvokeReturnAsIs($_); break }
{ $_ % 5 -eq 0 } {$Mod5.InvokeReturnAsIs($_); break }
{ $_ % 3 -eq 0 } {$Mod3.InvokeReturnAsIs($_); break }
}
[PSCustomObject]@{
Number = $_
FizzRunningTotal = $FizzRunningTotal
BuzzRunningTotal = $BuzzRunningTotal
FizzBuzzRunningTotal = $FizzBuzzRunningTotal
}
} | Format-Table AutoSize

So we run the above and the results that we get are…

ScriptblockResults
everything is not awesome…

0 + 3 is 0 apparently…as well as 10 + 5 being 0 as well? Splendid!

Why? Let’s try troubleshooting it first.

Troubleshooting

When trying to figure out what’s wrong, I figured I’d move the Modulus operator to be inside the script block and just return the value instead of saving the value to the variable.


[int]$FizzRunningTotal = 0
[int]$FizzBuzzRunningTotal = 0
[int]$BuzzRunningTotal = 0
$Mod15 = [Scriptblock]::Create('param([int]$Number) if ($Number % 15 -eq 0) { $FizzBuzzRunningTotal + $_ }')
$Mod5 = [Scriptblock]::Create('param([int]$Number) if ($Number % 5 -eq 0) { $BuzzRunningTotal + $_ }')
$Mod3 = [Scriptblock]::Create('param([int]$Number) if ($Number % 3 -eq 0) { $FizzRunningTotal + $_ }')
1..15 | ForEach-Object {
[PSCustomObject]@{
Number = $_
FizzRunningTotal = $Mod3.InvokeReturnAsIs($_)
BuzzRunningTotal = $Mod5.InvokeReturnAsIs($_)
FizzBuzzRunningTotal = $Mod15.InvokeReturnAsIs($_)
}
} | Format-Table AutoSize

Lo and behold it works!…ish

Works_ish
I mean…it does give what I wanted…

Great-ish!

There’s something subtle that I took from this and it’s probably best I show you. If you take a look at the screenshot above and you can see the numbers are aligned to the left. Yet I declared my variables to be ints and that means that they should be right aligned like this:

 


[int]$number = 5
[PSCustomObject]@{ Id = 1; Num = $number }
[string]$number2 = 5
[PSCustomObject]@{ Id = 1; Num = $number }

subtleString
int = right, string = left

Also I’m sure you noticed the lack of 0’s in the screenshot? Another confirmation.

[int]$i; [string]$i

DefaultValues
0 and blank

We can see that an int defaults to 0 but a string defaults to nothing.

My take away from this is that my script blocks are able to find my variables that I set at the start of the script. They just seem to have a problem with updating them for me…

Then let’s just update the variables from the value output by the script blocks, shall we? All we need for this is $var = $Scriptblock.Invoke().

Good but bloated


[int]$FizzRunningTotal = 0
[int]$FizzBuzzRunningTotal = 0
[int]$BuzzRunningTotal = 0
$Mod15 = [Scriptblock]::Create('param([int]$Number) $FizzBuzzRunningTotal + $_ ')
$Mod5 = [Scriptblock]::Create('param([int]$Number) $BuzzRunningTotal + $_ ')
$Mod3 = [Scriptblock]::Create('param([int]$Number) $FizzRunningTotal + $_ ')
1..15 | ForEach-Object {
switch ($_) {
{ $_ % 15 -eq 0 } {
$FizzBuzzRunningTotal = $Mod15.InvokeReturnAsIs($_)
break
}
{ $_ % 5 -eq 0 } {
$BuzzRunningTotal = $Mod5.InvokeReturnAsIs($_)
break
}
{ $_ % 3 -eq 0 } {
$FizzRunningTotal = $Mod3.InvokeReturnAsIs($_)
break
}
}
[PSCustomObject]@{
Number = $_
FizzRunningTotal = $FizzRunningTotal
BuzzRunningTotal = $BuzzRunningTotal
FizzBuzzRunningTotal = $FizzBuzzRunningTotal
}
} | Format-Table AutoSize

view raw

OneWay.ps1

hosted with ❤ by GitHub

This gives us the lovely results of:
OneWay
Yay!

Leaner?

Can we make that leaner? Let’s try putting the modulus check back up into the script block again.


[int]$FizzRunningTotal = 0
[int]$FizzBuzzRunningTotal = 0
[int]$BuzzRunningTotal = 0
$Mod15 = [Scriptblock]::Create('param([int]$Number) if ($Number % 15 -eq 0) { $FizzBuzzRunningTotal + $_ }')
$Mod5 = [Scriptblock]::Create('param([int]$Number) if ($Number % 5 -eq 0) { $BuzzRunningTotal + $_ }')
$Mod3 = [Scriptblock]::Create('param([int]$Number) if ($Number % 3 -eq 0) { $FizzRunningTotal + $_ }')
1..15 | ForEach-Object {
$FizzRunningTotal = $Mod3.InvokeReturnAsIs($_)
$BuzzRunningTotal = $Mod5.InvokeReturnAsIs($_)
$FizzBuzzRunningTotal = $Mod15.InvokeReturnAsIs($_)
[PSCustomObject]@{
Number = $_
FizzRunningTotal = $FizzRunningTotal
BuzzRunningTotal = $BuzzRunningTotal
FizzBuzzRunningTotal = $FizzBuzzRunningTotal
}
} | Format-Table AutoSize

view raw

Leaner.ps1

hosted with ❤ by GitHub

Which gives us the results:

Leaner
Wait a second, what’s with the zeros?

Dammit! We’re back to the default values again; we’re not outputting any values from the script blocks when they don’t match the modulus. So we’re getting 0s back.


[int]$Num = 2
$sb = [ScriptBlock]::Create('if ($Num -eq 2) { $Num + $Num }')
$Num = $sb.InvokeReturnAsIs()
$Num
$Num = 1
$Num = $sb.InvokeReturnAsIs()
$Num

AndISaid

Because our script blocks doesn’t execute any code when $Num is not equal to 2, it returns nothing and that nothing is getting converted to a 0!

A quick change to return the variable no matter what and we should be good to go!


[int]$FizzRunningTotal = 0
[int]$FizzBuzzRunningTotal = 0
[int]$BuzzRunningTotal = 0
$Mod15 = [Scriptblock]::Create('param([int]$Number) if ($Number % 15 -eq 0) { $FizzBuzzRunningTotal + $_ } else { $FizzBuzzRunningTotal }')
$Mod5 = [Scriptblock]::Create('param([int]$Number) if ($Number % 5 -eq 0) { $BuzzRunningTotal + $_ } else { $BuzzRunningTotal }')
$Mod3 = [Scriptblock]::Create('param([int]$Number) if ($Number % 3 -eq 0) { $FizzRunningTotal + $_ } else { $FizzRunningTotal }')
1..15 | ForEach-Object {
$FizzRunningTotal = $Mod3.InvokeReturnAsIs($_)
$BuzzRunningTotal = $Mod5.InvokeReturnAsIs($_)
$FizzBuzzRunningTotal = $Mod15.InvokeReturnAsIs($_)
[PSCustomObject]@{
Number = $_
FizzRunningTotal = $FizzRunningTotal
BuzzRunningTotal = $BuzzRunningTotal
FizzBuzzRunningTotal = $FizzBuzzRunningTotal
}
} | Format-Table AutoSize

But does it work?

LeanMean
Addition is fun!

Yes! 0 + 3 is 3; 3 + 6 is 9; 9 + 9 is 18!

More Information:

Short version:

PowerShell will check up the scope for a variable but will not check downwards.

So our script block will check upwards for the variable we set and find it. BUT if you update the variable inside the script block, then PowerShell won’t check downwards and get the updated value.

I highly recommend you check out these resources by Richard Siddaway ( blog | twitter ) on script blocks: like this blog post on scope and this video on script blocks!

He can explain this to a better standard than I can. Think I’ll just learn by failing until I get to his level 🙂

 

Author: Shane O'Neill

DBA, T-SQL and PowerShell admirer, Food, Coffee, Whiskey (not necessarily in that order)...

2 thoughts on “Learning Module Scope the Hard Way”

Leave a Reply

%d bloggers like this: