Search code examples
powershellconcatenationrenamevar

Powershell : Combine two variable names, for calling dynamic named variables


I created dynamic variable like this :

$Data = @()
$i = 1
Foreach ($S in $SB) {
     New-variable "JR$i" "content"
     New-variable "SN$i" "content"
     New-variable "T$i" "content"
     New-variable "FN$i" "content"
     $i = $i++
}

My goal is to put them in a psobject as follow :

$Data += new-object psobject -property ([ordered]@{JR = "<tr><td>$JR$i</td>"; SN = "<tr><td>$SN$i</td>"; T = "<tr><td>$T$i</td>"; FN = "<tr><td>$FN$i</td>"})

Solution

  • Why your current code doesn't work

    1. Misuse of the increment operator

    $i = $i++ - this doesn't do what you think it does. You probably want $i++ or $i += 1 instead.

    What your code is actually doing is complicated. The increment operator documentation says:

    The postfix version of the operator increments a variable after its value is used in the statement.

    In other words your code $i = $i++ does this:

    • On the right-hand side, evaluate the expression $i ( = 1)
    • Increment $i ($i now equals 2)
    • Assign the previously calculated expression value (1) to the left hand side variable $i
    • $i now equals 1 again!

    You can try this yourself:

    $i = 1;
    $i = $i++;
    $i
    # 1
    

    You can fix that by using one of these options instead of $i = $i++:

    • $i++;
    • $i += 1;
    • $i = $i + 1;

    2. Interpolation problem

    In your New-Variable lines, Powershell is doing string interpolation to create a variable named, e.g JR$i -> JR1, but in the second example <td>$JR$i</td> is expanding two different variables - $JR -> $null (because $JR isn’t defined) and $i -> 1 rather than what you’re probably expecting which is $JR1 -> content.

    You can test this as well:

    $i = 1;
    New-Variable -Name "JR$i" -Value "content";
    
    $JR1
    # content
    
    "<td>$JR$i</td>"
    # <td>1</td>
    

    It's clearer if you enable Strict Mode as you get an error instead:

    Set-StrictMode -Version "Latest";
    
    "<td>$JR$i</td>"
    # InvalidOperation: The variable '$JR' cannot be retrieved because it has not been set.
    

    What you want instead is "<td>$JR1</td>", but it looks like you need to dynamically generate the variable name because you're looping over multiple values for $i, so you're going to be stuck doing this:

    $i = 1;
    $jr = (Get-Variable -Name "JR$i").Value;
    
    "<td>$jr</td>"
    # "<td>content</td>"
    

    What you can do to fix it

    1. Increment properly

    This one's easy - just use $i++ or $i += 1 instead

    2. Use a hashtable instead

    See @iRon's general best-practice advice mentioned in comments for some more background information - I'll apply this to your specific case below.

    Instead of creating lots of "dynamic variables", just pack the values into a hashtable using the name as a key - it'll be easier to retrieve the values later...

    $SB = @( 1, 2 )
    $vars = [ordered] @{};
    $i = 1
    foreach( $S in $SB )
    {
       $vars.Add("JR$i", "content");
       $vars.Add("SN$i", "content");
       $vars.Add("T$i", "content");
       $vars.Add("FN$i", "content");
       $i++
    }
    
    $vars
    # Name                           Value
    # ----                           -----
    # JR1                            content
    # SN1                            content
    # T1                             content
    # FN1                            content
    # JR2                            content
    # SN2                            content
    # T2                             content
    # FN2                            content
    

    Note - strictly speaking, I'm using an OrderedDictionary instead of a hashtable so the keys get shown in the order they're added, but other than that they're basically the same.

    And then to use the hashtable / ordereddictionary:

    $i = 1;
    foreach( $S in $SB )
    {
        "<tr><td>$($vars["JR$i"])</td>";
        $i++
    }
    # <tr><td>content</td>
    # <tr><td>content</td>
    

    However, if you're just trying to generate the pscustomobjects, a cleaner way to do that from your source data might be this, and bypass the entire "dynamic variables" and temporary hashtables / OrderdDictionary completely:

    # generate the source data
    $SB = @(
      @{
        "JR_Property" = "jr content 1"
        "SN_Property" = "sn content 1"
        "T_Property"  = "t content 1"
        "FN_Property" = "fn content 1"
      },
      @{
        "JR_Property" = "jr content 2"
        "SN_Property" = "sn content 2"
        "T_Property"  = "t content 2"
        "FN_Property" = "fn content 2"
      }
    );
    
    $data = $SB | foreach-object {
        [pscustomobject] @{
            "JR" = "<tr><td>$($_.JR_Property)</td>";
            "SN" = "<tr><td>$($_.SN_Property)</td>";
            "T"  = "<tr><td>$($_.T_Property)</td>";
            "FN" = "<tr><td>$($_.FN_Property)</td>"}
    }
    
    $data | format-list *
    # JR : <tr><td>jr content 1</td>
    # SN : <tr><td>sn content 1</td>
    # T  : <tr><td>t content 1</td>
    # FN : <tr><td>fn content 1</td>
    # 
    # JR : <tr><td>jr content 2</td>
    # SN : <tr><td>sn content 2</td>
    # T  : <tr><td>t content 2</td>
    # FN : <tr><td>fn content 2</td>