Home > Blockchain >  Powershell 5.1 Write-Output in nested for, ForEach
Powershell 5.1 Write-Output in nested for, ForEach

Time:10-08

I'm not getting expected output from a script, and I think it has to do with variable scope, but I don't know how to get around it. I have an input file like this:

product,team,goals
Product A,Team A,"in-stock,0,days,default,on-call;in-stock,25,days,default,manager;in-stock,30,days,default,director;in-stock,45,days,default,VP"
Product B,Team B,"in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
Product C,Team B,"in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
Product D,Team B,"in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
Product E,Team B,"in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
Product F,Team B,"in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,all,team"
Product G,Team C,"in-stock,0,days,default,on-call;in-stock,5,days,next,on-call;in-stock,10,days,all,team"
Product H,Team D,"in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"

and Powershell 5.1 script like this:

# a Powershell script to validate some goals

### this needs to go in cmdline before running script
### $FormatEnumerationLimit = -1

# set some variables, the files are in a subdirectory called Data
$timestamp = $(Get-Date -f yyyy-MM-dd_HH_mm_ss)
$infile = "Data\Powershell_test_2021_10_07.csv"
$outfile = "Data\Powershell_test_" $timestamp ".csv"

### current goals
$cfile = Import-Csv $infile

# define the expected goals
$expectedGoals = @{
    0 = "on-call"
    7 = "on-call"
    15 = "next on-call"
    20 = "all team"
    25 = "manager"
    30 = "director"
    45 = "VP"
}
$goalkeys = @($expectedGoals.Keys | Sort)

# do the validation
$dfile = ( $cfile | Select -Property breach,* | ForEach {
    $thisgoal = $_
    $thisgoal.breach = ""
    $agoals = $thisgoal.goals -split ';'
    If ($agoals.Count -lt 7) { $thisgoal.breach = "Goals have < 7 steps"; Write-Output ($thisgoal)}
    For($i=0; $i -lt $agoals.Count; $i  ){
        $goalflds = $agoals[$i] -Split ','
        $thiskey = $goalkeys[$i]
        If ($goalflds[2] -ne "days") { $thisgoal.breach = "Goal step $i interval is not in days"; Write-Output ($thisgoal)}
        If ($goalflds[1] -gt $thiskey) { $thisgoal.breach = "Goal step $i is > $thiskey days"; Write-Output ($thisgoal)}
    }
} )
##$flds = @($goal.condition, $goal.interval.time, $goal.interval.timeUnit, $goal.notifyType, $goal.recipient.type)

# export to a csv file
$dfile | Export-Csv -NoTypeInformation -Path $outfile

The script outputs this:

"breach","product","team","goals"
"Goal step 3 is > 20 days","Product A","Team A","in-stock,0,days,default,on-call;in-stock,25,days,default,manager;in-stock,30,days,default,director;in-stock,45,days,default,VP"
"Goal step 3 is > 20 days","Product A","Team A","in-stock,0,days,default,on-call;in-stock,25,days,default,manager;in-stock,30,days,default,director;in-stock,45,days,default,VP"
"Goal step 3 is > 20 days","Product A","Team A","in-stock,0,days,default,on-call;in-stock,25,days,default,manager;in-stock,30,days,default,director;in-stock,45,days,default,VP"
"Goal step 3 is > 20 days","Product B","Team B","in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 3 is > 20 days","Product B","Team B","in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 3 is > 20 days","Product B","Team B","in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 3 is > 20 days","Product C","Team B","in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 3 is > 20 days","Product C","Team B","in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 3 is > 20 days","Product C","Team B","in-stock,0,days,default,on-call;in-stock,5,days,default,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product D","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product D","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product D","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product D","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product E","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product E","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product E","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product E","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,default,director"
"Goal step 5 is > 30 days","Product F","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,all,team"
"Goal step 5 is > 30 days","Product F","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,all,team"
"Goal step 5 is > 30 days","Product F","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,all,team"
"Goal step 5 is > 30 days","Product F","Team B","in-stock,0,days,default,on-call;in-stock,4,days,default,on-call;in-stock,8,days,next,on-call;in-stock,16,days,next,on-call;in-stock,30,days,default,manager;in-stock,45,days,all,team"
"Goals have < 7 steps","Product G","Team C","in-stock,0,days,default,on-call;in-stock,5,days,next,on-call;in-stock,10,days,all,team"
"Goal step 5 is > 30 days","Product H","Team D","in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"
"Goal step 5 is > 30 days","Product H","Team D","in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"
"Goal step 5 is > 30 days","Product H","Team D","in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"
"Goal step 5 is > 30 days","Product H","Team D","in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"
"Goal step 5 is > 30 days","Product H","Team D","in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"
"Goal step 5 is > 30 days","Product H","Team D","in-stock,0,days,default,on-call;in-stock,8,days,default,on-call;in-stock,16,days,next,on-call;in-stock,45,days,default,manager;in-stock,60,days,default,director;in-stock,90,days,default,VP"

Sorry if this is a lot of text.

When this runs, I am expecting the breach field to be different for each line that is produced for a particular team and product, but it looks like they all come out as whatever was set last.

How can I fix this?? Thanks for any help! Dave

CodePudding user response:

The problem is that you keep updating the very same object in your ForEach-Object loop.

When the loop ends, $dfile contains an array whose elements all reference that one object, and its properties contain the values you assigned last.

In order to create variants of the original object with different property values, you must create a copy every time, which in the case of the [pscustomobject] instances (as output by Import-Csv and Select-Object) can be achieved with .psobject.Copy()

  • While it won't be a problem with the input objects in your case, it is important to note that the copies created are shallow ones. That is, those property values that happen to contain instances of .NET reference types will reference the same instances in the object copies - see this answer for more information.

A simplified example:

$modifiedCopies =
  [pscustomobject] @{ prop = 'a' }, [pscustomobject] @{ prop = 'b' } | ForEach-Object {
    # Create, modify and output two copies
    foreach ($i in 1..2) {
      $copy = $_.psobject.Copy() # create a copy
      $copy.prop  = $i           # modify it
      $copy                      # output it (Note: no need for Write-Output)
    }
  }

Outputting $modifiedCopies afterwards yields:

prop
----
a1
a2
b1
b2

Applied to your code (apply analogously to the other lines where you update $thisgoal.breach and then call Write-Output ($thisgoal)):

if ($agoals.Count -lt 7) { 
  $copy = $thisgoal.psobject.Copy()      # create a copy
  $copy.breach = "Goals have < 7 steps"  # update the copy
  $copy                                  # output it
}

Or, more succinctly, using PowerShell's ability to use assignments as expressions:

if ($agoals.Count -lt 7) {
  ($copy = $thisgoal.psobject.Copy()).breach = "Goals have < 7 steps"; $copy
}

CodePudding user response:

This is not a problem with variable scoping, it's a problem of object lifetime management.

Objects like $thisgoal are passed by reference, so $dfile doesn't contain 3 separate copies of $thisgoal, it really just contains 3 references to the same copy - which is why you see the result of the last modification of the breach property.

You need to create a new object everytime you report a new "breach" as output.

Select-Object can indeed do that for you, but since there's a 1-to-many relationship between input and output here, you'll want to pipe the input to ForEach-Object first, at which point we can then call Select-Object multiple times (thereby creating multiple copies) for each input object if necessary:

$dfile = $cfile |ForEach-Object {
    $allGoals = $_.goals -split ';'
    If ($allGoals.Count -lt 7) {
        $_ |Select-Object @{Name='Breach';Expression={"Goals have < 7 steps"}},*
    }
    For ($i = 0; $i -lt $allGoals.Count; $i  ) {
        $goalflds = $allGoals[$i] -Split ','
        $thiskey = $goalkeys[$i]
        If ($goalflds[2] -ne "days") {
            $_ |Select-Object @{Name='Breach';Expression={"Goal step $i interval is not in days"}},*
        }
        If ($goalflds[1] -gt $thiskey) { 
            $_ |Select-Object @{Name='Breach';Expression={"Goal step $i is > $thiskey days"}},*
        }
    }
}
  • Related