Why does my break statement break "too far"? - loops

Here's a snippet of my code (based on this previous question):
$rgdNames = (Get-AzureRmResourceGroupDeployment -ResourceGroupName "$azureResGrpName").DeploymentName
$siblings = $rgdNames | Where-Object{$_ -match "^($hostname)(\d+)$" }
if ($siblings) {
# Make a list of all numbers in the list of hostnames
$serials = #()
foreach ($sibling in $siblings) {
# $sibling -split ($sibling -split '\d+$') < split all digits from end, then strip off everything at the front
# Then convert it to a number and add that to $serials
$hostnumber = [convert]::ToInt32([string]$($sibling -split ($sibling -split '\d+$'))[1], 10)
$serials += $hostnumber
}
(1..$siblingsMax) | foreach { # Iterate over all valid serial numbers
if (!$serials.Contains($_)) { # Stop when we find a serial number that isn't in the list of existing hosts
$serial = $_
# break # FIXME: For some reason, this break statement breaks "too far"
}
}
} else {
$serial = 1
}
write-output("serial = $serial") # does not print
# ...more statements here, but they're never called :(
I have been looking at it for a while, but can't figure out why the break statement (if un-commented) stops my program instead of just breaking out of its foreach loop. Is there something about foreach that I'm not aware of, or does break just work differently than it would in Java?
For the time being, I'm working around this using an extra if test, and it doesn't mean (too) much that the loop runs its entire length. But it's ugly!

This construct:
(1..$siblingsMax) | foreach { # Iterate over all valid serial numbers
# do stuff
}
is NOT a foreach loop - it's a pipeline with a call to the ForEach-Object cmdlet (aliased as foreach) - and keywords designed to interrupt the control flow of a loop (like break or continue) will NOT act in the same way in these two different cases.
Use a proper foreach loop and break will behave as you expect:
foreach($i in 1..$siblingsMax){
# do stuff
if($shouldBreak)
{
break
}
}
Alternatively, you can abuse the fact that continue behaves like you would expect break in a ForEach-Object process block:
(1..$siblingsMax) | foreach { # Iterate over all valid serial numbers
if($shouldBreak)
{
continue
}
}
although I would strongly discourage this approach (it'll only lead to more confusion)

Related

String comparison in PowerShell doesn't seem to work

I am trying to compare strings from one array to every string from the other. The current method works, as I have tested it with simpler variables and it retrieves them correctly. However, the strings that I need it to compare don't seem to be affected by the comparison.
The method is the following:
$counter = 0
for ($num1 = 0; $num1 -le $serverid_oc_arr.Length; $num1++) {
for ($num2 = 0; $num2 -le $moss_serverid_arr.Length; $num2++) {
if ($serverid_oc_arr[$num1] -eq $moss_serverid_arr[$num2]) {
break
}
else {
$counter += 1
}
if ($counter -eq $moss_serverid_arr.Length) {
$unmatching_serverids += $serverid_oc_arr[$num1]
$counter = 0
break
}
}
}
For each string in the first array it is iterating between all strings in the second and comparing them. If it locates equality, it breaks and goes to the next string in the first array. If it doesn't, for each inequality it is adding to the counter and whenever the counter hits the length of the second array (meaning no equality has been located in the second array) it adds the corresponding string to a third array that is supposed to retrieve all strings that don't match to anything in the second array in the end. Then the counter is set to 0 again and it breaks so that it can go to the next string from the first array.
This is all okay in theory and also works in practice with simpler strings, however the strings that I need it to work with are server IDs and look like this:
289101b4-3e6c-4495-9c67-f317589ba92c
Hence, the script seems to completely ignore the comparison and just puts all the strings from the first array into the third one and retrieves them at the end (sometimes also some random strings from both first and second array).
Another method I tried with similar results was:
$unmatching_serverids = $serverid_oc_arr | Where {$moss_serverid_arr -NotContains $_}
Can anyone spot any mistake that I may be making anywhere?
The issue with your code is mainly the use of -le instead of -lt, collection index starts at 0 and the collection's Length or Count property starts at 1. This was causing $null values being added to your result collection.
In addition to the above, $counter was never getting reset if the following condition was not met:
if ($counter -eq $moss_serverid_arr.Length) { ... }
You would need to a add a $counter = 0 outside inner for loop to prevent this.
Below you can find the same, a little bit improved, algorithm in addition to a test case that proves it's working.
$ran = [random]::new()
$ref = [System.Collections.Generic.HashSet[int]]::new()
# generate a 100 GUID collection
$arr1 = 1..100 | ForEach-Object { [guid]::NewGuid() }
# pick 90 unique GUIDs from arr1
$arr2 = 1..90 | ForEach-Object {
do {
$i = $ran.Next($arr1.Count)
} until($ref.Add($i))
$arr1[$i]
}
$result = foreach($i in $arr1) {
$found = foreach($z in $arr2) {
if ($i -eq $z) {
$true; break
}
}
if(-not $found) { $i }
}
$result.Count -eq 10 # => Must be `$true`
As side, the above could be reduced to this using the .ExceptWith(..) method from HashSet<T> Class:
$hash = [System.Collections.Generic.HashSet[guid]]::new([guid[]]$arr1)
$hash.ExceptWith([guid[]]$arr2)
$hash.Count -eq 10 # => `$true`
The working answer that I found for this is the below:
$unmatching_serverids = #()
foreach ($item in $serverid_oc_arr)
{
if ($moss_serverid_arr -NotContains $item)
{
$unmatching_serverids += $item
}
}
No obvious differences can be seen between it and the other methods (especially for the for-loop, this is just a simplified variant), but somehow this works correctly.

Powershell - print the list length respectively

I want to write two things in Powershell.
For example;
We have a one list:
$a=#('ab','bc','cd','dc')
I want to write:
1 >> ab
2 >> bc
3 >> cd
4 >> dc
I want this to be dynamic based on the length of the list.
Thanks for helping.
Use a for loop so you can keep track of the index:
for( $i = 0; $i -lt $a.Count; $i++ ){
"$($i + 1) >> $($a[$i])"
}
To explain how this works:
The for loop is defined with three sections, separated by a semi-colon ;.
The first section declares variables, in this case we define $i = 0. This will be our index reference.
The second section is the condition for the loop to continue. As long as $i is less than $a.Count, the loop will continue. We don't want to go past the length of the list or you will get undesired behavior.
The third section is what happens at the end of each iteration of the loop. In this case we want to increase our counter $i by 1 each time ($i++ is shorthand for "increment $i by 1")
There is more nuance to this notation than I've included but it has no bearing on how the loop works. You can read more here on Unary Operators.
For the loop body itself, I'll explain the string
Returning an object without assigning to a variable, such as this string, is effectively the same thing as using Write-Output.
In most cases, Write-Output is actually optional (and often is not what you want for displaying text on the screen). My answer here goes into more detail about the different Write- cmdlets, output streams, and redirection.
$() is the sub-expression operator, and is used to return expressions for use within a parent expression. In this case we return the result of $i + 1 which gets inserted into the final string.
It is unique in that it can be used directly within strings unlike the similar-but-distinct array sub-expression operator and grouping operator.
Without the subexpression operator, you would get something like 0 + 1 as it will insert the value of $i but will render the + 1 literally.
After the >> we use another sub-expression to insert the value of the $ith index of $a into the string.
While simple variable expansion would insert the .ToString() value of array $a into the final string, referencing the index of the array must be done within a sub-expression or the [] will get rendered literally.
Your solution using a foreach and doing $a.IndexOf($number) within the loop does work, but while $a.IndexOf($number) works to get the current index, .IndexOf(object) works by iterating over the array until it finds the matching object reference, then returns the index. For large arrays this will take longer and longer with each iteration. The for loop does not have this restriction.
Consider the following example with a much larger array:
# Array of numbers 1 through 65535
$a = 1..65535
# Use the for loop to output "Iteration INDEXVALUE"
# Runs in 106 ms on my system
Measure-Command { for( $i = 0; $i -lt $a.Count; $i++ ) { "Iteration $($a[$i])" } }
# Use a foreach loop to do the same but obtain the index with .IndexOf(object)
# Runs in 6720 ms on my system
Measure-Command { foreach( $i in $a ){ "Iteration $($a.IndexOf($i))" } }
Another thing to watch out for is that while you can change properties and execute methods on collection elements, you can't change the element values of a non-collection collection (any collection not in the System.Concurrent.Collections namespace) when its enumerator is in use. While invisible, foreach (and relatedly ForEach-Object) implicitly invoke the collection's .GetEnumerator() method for the loop. This won't throw an error like in other .NET languages, but IMO it should. It will appear to accept a new value for the collection but once you exit the loop the value remains unchanged.
This isn't to say the foreach loop should never be used or that you did anything "wrong", but I feel these nuances should be made known before you do find yourself in a situation where a better construct would be appropriate.
Okey,
I fixed that;
$a=#('ab','bc','cd','dc')
$a.Length
foreach ($number in $a) {
$numberofIIS = $a.IndexOf($number)
Write-Host ($numberofIIS,">>>",$number)
}
Bender's answer is great, but I personally avoid for loops if at all possible. They usually require some awkward indexing into arrays and that ugly setup... The whole thing just ends up looking like hieroglyphics.
With a foreach loop it's our job to keep track of the index (which is where this answer differs from yours) but I think in the end it is more readable then a for loop.
$a = #('ab', 'bc', 'cd', 'dc')
# Pipe the items of our array to ForEach-Object
# We use the -Begin block to initialize our index variable ($x)
$a | ForEach-Object -Begin { $x = 1 } -Process {
# Output the expression
"$x" + ' >> ' + $_
# Increment $x for next loop
$x++
}
# -----------------------------------------------------------
# You can also do this with a foreach statement
# We just have to intialize our index variable
# beforehand
$x = 1
foreach ($number in $a){
# Output the expression
"$x >> $number"
# Increment $x for next loop
$x++
}

Validate members of array

I have a string I am pulling from XML that SHOULD contain comma separated integer values. Currently I am using this to convert the string to an array and test each member of the array to see if it is an Int. Ultimately I still want an array in the end, as I also have an array of default success codes and I want to combine them. That said, I have never found this pattern of setting the test condition true then looping and potentially setting it to false to be all that elegant. So, I am wondering if there is a better approach. I mean, this works, and it's fast, and the code is easy to read, so in a sense there is no reason to change it, but if there is a better way...
$supplamentalSuccessCode = ($string.Split(',')).Trim()
$validSupplamentalSuccessCode = $true
foreach ($code in $supplamentalSuccessCode) {
if ($code -as [int] -isNot [int]) {
$validSupplamentalSuccessCode = $false
}
}
EDIT: To clarify, this example is fairly specific, but I am curious about a more generic solution. So imagine the array could contain values that need to be checked against a lookup table, or local drive paths that need to be checked with Test-Path. So more generically, I wonder if there is a better solution than the Set variable true, foreach, if test fails set variable false logic.
Also, I have played with a While loop, but in most situations I want to find ALL bad values, not exit validation on the first bad one, so I can provide the user with a complete error in a log. Thus the ForEach loop approach I have been using.
In PSv4+ you can enlist the help of the .Where() collection "operator" to determine all invalid values:
Here's a simplified example:
# Sample input.
$string = '10, no, 20, stillno, -1'
# Split the list into an array.
$codes = ($string.Split(',')).Trim()
# Test all array members with a script block passed to. Where()
# As usual, $_ refers to the element at hand.
# You can perform whatever validation is necessary inside the block.
$invalidCodes = $codes.Where({ $null -eq ($_ -as [int]) })
$invalidCodes # output the invalid codes, if any
The above yields:
no
stillno
Note that what .Where() returns is not a regular PowerShell array ([object[]]), but an instance of [System.Collections.ObjectModel.Collection[PSObject]], but in most situations the difference shouldn't matter.
A PSv2-compatible solution is a bit more cumbersome:
# Sample input.
$string = '10, no, 20, stillno, -1'
# Split the list into an array.
# Note: In PSv*3* you could use the simpler $codes = ($string.Split(',')).Trim()
# as in the PSv4+ solution.
$codes = foreach ($code in $string.Split(',')) { $code.Trim() }
# Emulate the behavior of .Where() with a foreach loop:
# Note that do you get an [object[]] instance back this time.
$invalidCodes = foreach ($code in $codes) { if ($null -eq ($code -as [int])) { $code } }

How can I repeat an iteration in a PowerShell foreach loop?

I frequently have to loop through sets of data (SQL results, lists of computers, etc.) in PowerShell, performing the same operation (e.g function X) on each time. I use foreach loops almost exclusively as they are simple, effective and easily understood by others who may need to follow my code.
I would like to make some of my code more robust, in the sense of retrying operations that fail (up to Y times). There's more than one way to achieve this, for example within the foreach loop, wrapping function X in a do{} while() loop. In this example, assume that function X only returns non-null results when it is "successful":
foreach($dataitem in $dataset){
$Result = $null
$Attempts = 0
do{
$Attempts++
$Result = <call function X on $dataitem>
} while(($Attempts -lt 3) -and (-not $Result))
}
I was wondering whether there was any way to flatten this logic a bit, i.e. a more advanced way of using foreach loops, so I can do away with the do{} while() loop.
I have encountered the opposite of what I want, namely using $foreach.MoveNext() to skip forwards in the loop, but haven't found anything that (dangerously?) would keep foreach processing the same item.
Essentially: Can a foreach loop be made to re-do an iteration?.
I wouldn't try to re-do an iteration of a foreach loop. That doesn't feels right to me. Instead, I would create a function that implement something like a retry-logic pattern:
function Retry-Process()
{
Param(
[scriptblock]$action,
[scriptBlock]$validator,
[int]$retryCount
)
1 .. $retryCount | % {
$result = & $action
if (& $validator ($result))
{
$result
break
}
}
}
Example call:
Retry-Process -action { '3' } -validator { Param($a) $a -eq '3'} -retryCount 5
And within your foreach loop:
foreach($dataitem in $dataset) {
$Result = Retry-Process `
-action { <call function X on $dataitem> } `
-validator { Param($returnValue) $returnValue } ` # Validate return value != null
-retryCount 3
}
While I agree with Jisaak, to answer your question you could achieve this using something like the following
foreach ($i in 1..10) {
$i
if ($limit++ -gt 10) { break } # just to stop infinite loop
if ($i -eq 4) {
foreach.Reset()
1..($i-1) | Foreach-Object {
[void]$foreach.MoveNext()
}
}
}
Of course I wouldn't do anything this silly outside of an exercise. The mix of foreach() {} and Foreach-Object is done to simplify the scope of the automatic IEnumeration object $foreach.
Edit
I thought it useful to include a link to a page that describes the difference between foreach as a keyword and as a cmdlet:
http://www.powershelladmin.com/wiki/PowerShell_foreach_loops_and_ForEach-Object
about_Foreach
Foreach-Object

Get specific value from text and insert in array, then compare on it

I'm new to Powershell and I'm trying to make a script in order to extract some data and then launch a command.
Here is the deal:
I have an application that sometimes goes in error (I have to look in the log file) with a specific error code (always the same) and in the same line I have an "ID", the IDT.
So my problem is:
I have to take a look in this log file every 5 minutes and search for the error code CFTT82E and in this line to get the value of my IDT that is numeric, here what looks like the line:
14/01/15 12:20:06 CFTT82E transfer aborted IDTU=A0009L7Q PART=DGISSL IDF=LKEMDDIB IDT=A1512489 730 ABO 215>
Once I got the value in a variable, I have to wait 5 more minutes and then to launch a command by using the value of my IDT, that is to say:
cmd -toto $IDT_value
Now, once the command succeeded (I have to compare only with CFTR12I code AND my IDT value), I will have a message like that:
14/01/15 15:48:39 CFTR12I END Treated for USER Système PART=DGISSL IDF=* IDT=A1512489>
At this moment, I'll exit, else, I'll relaunch the same command.
But, as this script should work in Loop every 5 minutes, the next time that I'll parse the log file, I should ignore the IDT that I already used before.
I thought to make an array with the daily IDT values (at the end of the date we could empty the array or the variable or whatever this object will be) and then compare the values at every loop.
I already started with this but as I'm newbie,I think that I didn't make a good start :
clear
Get-Content C:\CFT\LOG2014011523590001 |
foreach {
$line = $_.Trim()
If ($line.Contains('CFTT82E')) {
$str = $line.Split(' ')
$TIDT = [ordered]#{
IDT = $_.SubString(101)
}
New-Object PsObject -Property $TIDT
}
} | Out-String
I know that I ask a lot but, any suggestion is welcome.
This may help you. Create an ArrayList for finding stored values then look at your file. Do something with IDT if it's not in the list, then add it to the list so it will be ignored on subsequent runs.
$list = New-Object System.Collections.ArrayList
while (1) {
$content = get-content "C:\your\file.txt"
foreach ($line in $content) {
if ($line -match "CFTT82E.*IDT=(.+?)\b.*") {
$idt = $matches[1]
if(!$list.contains($idt) {
#
# Do whatever you need to do with $idt
#
$list.add($idt)
}
}
}
sleep -Seconds(5*60)
}

Resources