Update Variables

In 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:

wrong

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:

products

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:

measures and strings

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
}

unchanged

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:

changed

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
    }
} 

objects

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.

Previous Post