Update Variables
09, OctIn this article I demonstrate how to update a collection of variables. It's a bit more complex than you may think, but not hard at all.
DRY
We don't like to repeat the same task over and over, do we? If we need to apply some logic or the same manipulation on multiple objects, we prefer to write something once and call that piece of code whenever we want it.
Here we will manipulate multiple variables in the same way. But how?
Case
It was Constantine Kokkinos, a valued pillar of the dbatools team, who came up with a little problem: when creating a variable that contains other variables and we update them in a foreach
loop, the 'original' variables are not updated. The theory around this problem would describe the difference between Reference by Name and Reference by Value. Let's just dive into the practical use.
Reconstruction
This little piece of code did not return the expected result:
$a, $b, $c = 'aaa','bbb','ccc'
$varlist = $a,$b,$c
foreach ($txt in $varlist)
{
$txt = $txt * 3
$txt
}
$a
$b
$c
We assign a value to three variables,
$a, $b, $c
, assemble those in a collection $varlist
, and loop over that to multiply each one by three.
We return each iteration $txt
and in the end take a look at $a,$b,$c
to check our change:
No success! Our original variables are not updated.
It's quite logical, since we never assigned the new variables directly to $a, $b, $c
. But what if we really want those to be updated?
A practical example
We found this little text file and want to work with the products that are listed in it. Fortunately, the lines with the product info have a fixed structure and are easily recognizable:
BIG ANNOUNCEMENT
!!! SPECIAL PROMOTIONS !!!
From october to august 2424
NEW !!! NEW !!!
--F35 Lightning----------; 618.1 ; 181.1 ; 421.2 ; Roses ; America
-------------------------------------------------------------
Don't Miss This:
GOOD OLD
--F-16 Fighting Falcon---; 592.5 ; 196.8 ; 372 ; Roses ; Europe
-------------------------------------------------------------
From The Other Side:
--Mig35------------------; 748 ; 236.2 ; 590.5 ; Daisies ; Asia
Fly Fly Fly !!!
--F15 Eagle--------------; 764.9 ; 221.6 ; 513.8 ; Roses ; America
No time to waste
Call 012.444 555 6
!!!
If we Get-Content
and use a bit of regex, we extract the lines we need:
$text = Get-Content C:\Path\To\File\Adv.txt
$Products = $text.where({ $_ -match '(.*;){5}'})
.
stands for any character
*
means it should be repeated one or more times
;
is literally';'
{5}
means the preceding pattern (.*;)
should be repeated 5 times
with $_ -match
we get back the lines that match this pattern:
Repeatable work found
Somehow we find out that those lines contain _Product name, length, height, width, owner club and region of origin. We can assign all those parts in one line to the appropriate variables:
foreach ($Product in $Products )
{
$ProductName, $PrLength, $PrHeight, $PrWidth, $Club, $Origin = $Product.split(';')
}
But now comes the challenge: we see that some of those variables need the same 'treatment':
- Length, height and with are numerics. They are expressed in inches, which we like to convert to meter. Before we can do that, however, we will need to trim them and convert to decimal.
- Productname, club and origin are strings, but contain spaces and dashes we wish to remove.
We will group our variables in two collections:
$Measures = @($PrLength, $PrHeight, $PrWidth)
$strings = @($ProductName, $Club, $Origin)
And here it comes: we define the manipulation we want for both:
foreach ( $measure in $Measures )
{
$measure = [decimal]$measure.trim() * 2.54 / 100
$measure
}
foreach ( $string in $strings )
{
$string = $string.trim(" -")
$string
}
As we return the resulting $measure
and $string
every time, we have proof that our manipulation worked:
But, if we look at the actual variables we want:
foreach ($Product in $Products )
{
$ProductName, $PrLength, $PrHeight, $PrWidth, $Club, $Origin = $Product.split(';')
$Measures = @($PrLength, $PrHeight, $PrWidth)
$strings = @($ProductName, $Club, $Origin)
foreach ( $measure in $Measures )
{
$measure = [decimal]$measure.trim() * 2.54 / 100
}
foreach ( $string in $strings )
{
$string = $string.trim(" -")
}
$ProductName
$PrLength
$PrHeight
$PrWidth
$Club
$Origin
}
If we expect $PrLength, $PrHeight, $PrWidth
to be updated, because they will be referred in $measure
one by one, we are wrong.
Although the placeholder variables $measure
and $string
were updated every time, this had no influence on the original variables. So we're nowhere yet. Product name is surrounded by dashes, other strings by spaces, and the measures are in inches and with leading and trailing spaces as well.
The trick
We turn to two cmdlets: Get-Variable
and Set-Variable
. They may seem redundant as we get and set variables all the time without those. Maybe you have even never heard of these two cmdlets.
Well here they prove their usefulness:
we will set the variables using their names, and use the current value as a starting point.
An important rule to remember is this:
$
is a token to indicate we're dealing with a variable, but it is not part of the variable name!
Both Get-Variable
and Set-Variable
have a -Name
property that accepts the variable name. This means we need PrLength
and not $PrLength
!
We build new collections like this:
$Measures = @('PrLength', 'PrHeight', 'PrWidth')
$strings = @('ProductName', 'Club', 'Origin')
A small but significant difference with our previous attempt.
And the expressions to update the measures and the strings are now:
foreach ($measure in $Measures)
{
set-variable -Name $measure -Value ([decimal](get-variable -name $measure -ValueOnly).trim() * 2.54 / 100 )
}
foreach ($string in $strings)
{
set-variable -Name $string -Value ((get-variable -name $string -ValueOnly).trim(" -") )
}
$ProductName
$PrLength
$PrHeight
$PrWidth
$Club
$Origin
$measure
and $string
now contain a variable name instead of its value. They can now serve as value for the -Name
parameter. To set the new value we Get-Variable
first, do our magic, and wrap it in ()
to pass it as the new value for Set-Variable
.
What do we see when we look at all original variables now:
Spaces and dashes are gone and the measures are in meter.
The complete script
As final improvements, we have rounded our measures to two decimal places, and we create an object to be returned:
$text = Get-Content C:\Path\To\File\Adv.txt
$Products = $text.where({ $_ -match '(.*;){5}'})
foreach ($Product in $Products )
{
$ProductName, $PrLength, $PrHeight, $PrWidth, $Club, $Origin = $Product.split(';')
$Measures = @('PrLength', 'PrHeight', 'PrWidth')
$strings = @('ProductName', 'Club', 'Origin')
foreach ($measure in $Measures)
{
set-variable -Name $measure -Value ([math]::Round([decimal](get-variable -name $measure -ValueOnly).trim() * 2.54 / 100,2) )
}
foreach ($string in $strings)
{
set-variable -Name $string -Value ((get-variable -name $string -ValueOnly).trim(" -") )
}
[PSCustomObject]@{
ProductName = $ProductName
PrLength = $PrLength
PrHeight = $PrHeight
PrWidth = $PrWidth
Club = $Club
Origin = $Origin
}
}
Isn't this an amazing result? Check the file we started with, the conciseness of the script we created, and the usefulness of the objects we end up with!
To Be Continued
Every time you need something similar, it will be just a bit different. So you'll have to figure out
- What is the distinctive mark of your information.
- How to select the desired information.
- What regexp or other filtering you can use.
- What manipulation you need to compose.
- In what format you wish to return the results.
- If you need to reuse this regularly, you could wrap it in a
function {}
Let the creativity flow.