Powershell - foreach loop is exiting prematurely? - powershell

Forgive me, but I'm very new to Powershell. I'm having a problem understanding why my code is abruptly exiting the foreach loop once the code finds the book with an ID called "bk103". I'm expecting that the foreach loop iterate through the for loop 12 times since there are 12 books in the xml file. Why is it exiting early?
Thank you!
Set-Location 'C:\PowershellPractice\' #'
[int]$index = 0
[string] $xmlFilePath=’C:\PowershellPractice\books.xml’
[xml] $xmlContent = [xml] (Get-Content -Path $xmlFilePath)
$newSubNode = $xmlContent.CreateElement("security")
$newSubNode.SetAttribute("Mode", "transport")
$collection = $xmlContent.catalog.ChildNodes
Write-Host "Collection has " $collection.Count " elements in it!"
foreach ($item in $collection){
Write-Host "In foreach, index is " $index
if ($item.id -eq "bk103" -OR
$item.id -eq "bk105" -OR
$item.id -eq "bk108" -OR
$item.id -eq "bk109"){
Write-Host "Found book called " $ $item.id
$elementCopy = $xmlContent.catalog.book[$index].Clone()
$elementCopy.AppendChild($newSubNode)
$xmlContent.catalog.RemoveChild($item)
$xmlContent.catalog.InsertBefore($elementCopy,$xmlContent.catalog.book[$index])
}
$index++
}
$xmlContent.Save('C:\PowershellPractice\books-edited.xml')

As mentioned in the comments, you cannot modify the collection while you're currently iterating over it.
Since you only really need to modify the elements in the collections themselves, I would recommend just doing that (instead of cloning the book node and re-attaching it):
foreach ($item in $collection){
Write-Host "In foreach, index is " $index
if ($item.id -eq "bk103" -OR
$item.id -eq "bk105" -OR
$item.id -eq "bk108" -OR
$item.id -eq "bk109"){
# Add subnode to matching $item
[void]$item.AppendChild($newSubNode)
}
}
If you ever find yourself in a situation where you cannot modify the element in place, use two loops - one to find the interesting elements, and another one to replace them by iterating over the resulting subset:
# collect matching results
$foundbooks = foreach ($item in $collection){
Write-Host "In foreach, index is " $index
if ($item.id -eq "bk103" -OR
$item.id -eq "bk105" -OR
$item.id -eq "bk108" -OR
$item.id -eq "bk109"){
# return matching $item
$item
}
}
# now modify based on initial results
foreach($book in $foundbooks){
Write-Host "Found book called " $ $book.id
$elementCopy = $book.Clone()
[void]$elementCopy.AppendChild($newSubNode)
[void]$xmlContent.catalog.InsertBefore($elementCopy,$book)
[void]$xmlContent.catalog.RemoveChild($book)
}

Related

Nesting ForEach within If statement (PowerShell)

I am trying to loop through a list of Windows services and if any of them match a certain criteria, I would like to return exit code 1.
If there is no match, I would like to return exit code 0.
I am struggling to put this within an If statement, I think I'm putting the code in the wrong place!
Could anyone lend me a hand? Script below.
Thanks in advance.
Adrian
try
{
#Pull list of services from registry
$svclist = Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\services | ForEach-Object {Get-ItemProperty $_.PsPath}
#Ignore anything after .exe, filter for vulnerable services
ForEach ($svc in $svclist) {
$svcpath = $svc.ImagePath -split ".exe"
if(($svcpath[0] -like "* *") -and ($svcpath[0] -notlike '"*') -and ($svcpath[0] -notlike "\*")) {
$svc | fl -Property DisplayName,ImagePath,PsPath
}
}
if (($svc -ne $null)){
Write-Host "Match"
Return $svc.count
exit 1
}
else{
Write-Host "No_Match"
exit 0
}
}
catch{
$errMsg = $_.Exception.Message
Write-Error $errMsg
exit 1
}
You can either return early or use a variable with a single [bool] value to keep track of whether anything was matched:
return early
foreach($svc in $svcList)
{
$svcpath = $svc.ImagePath -split ".exe"
if(($svcpath[0] -like "* *") -and ($svcpath[0] -notlike '"*') -and ($svcpath[0] -notlike "\*")) {
return 1
}
}
# if we've reached this point then no matches occurred
return 0
Using a [bool]
$matchFound = $false
foreach($svc in $svcList)
{
$svcpath = $svc.ImagePath -split ".exe"
if(($svcpath[0] -like "* *") -and ($svcpath[0] -notlike '"*') -and ($svcpath[0] -notlike "\*")) {
$matchFound = $true
}
}
return [int]$matchFound # $false = 0, $true = 1
I always try to use the most powershell-correct methods to achieve what I want. Especially when going through data I prefer to try filtering with the x-object cmdlets.
In your case my suggestion would be to simply loop over the original list with the Where-Object command, this allows you to retrieve a list of items that conform to your search, kind of like an SQL query:
$resultList = $svclist | Where-Object {
($_.ImagePath -like "* *") -and ($_.ImagePath -notlike '"*') -and ($_.ImagePath -notlike "\*")
}
In this case I skipped over the -split ".exe" part, as I didn't quite understand it's purpose, but you could also put that in your filter using regular expressions with the -match operator instead of the -like and -notlike values you make one regex match
Then you can check if that list is populated or not:
if ($resultList) {
return 1
}
else {
return 0
}
It is also considered best practice to only use the aliases for commands (e.g. fl should be Format-List). This will increase readability for future maintenance, of course if it's a one-time script is would be more appropriate. I just try to avoid it as much as I can these days.

Can get variables to work in setting a recipientRestrictionFilter

We have an Exchange Management scope per country, where we try to adjust the Recipient filter. Have created following code:
Foreach ($Obj in $Scopes) {
$Scope = Get-ManagementScope $Obj.Name
Write-Host "Retrieved $($Scope)"
$Country = ($Scope.Name -split "_")[1]
Write-Host "Retrieved $($Country)"
$CountryCode = $Co | ? {$_.Country -like "$Country*"}
Write-Host "Retrieved $($CountryCode.Alpha3) for $($Country)" -ForegroundColor Green
$Filter = {((CountryOrRegion -eq $Country) -or (Name -Like "$($CountryCode.Alpha3)_*"))}
Set-ManagementScope $($Scope.name) -recipientRestrictionFilter $Filter
}
What we get is this
((-not(CountryOrRegion -eq $Country) -or (Name -Like "$($CountryCode.Alpha3)_*")), while it should be ((CountryOrRegion -eq 'USA') -or (Name -like 'USA_*'))
We have more than 50 managementscopes to adjust, and do not want to adjust them manually.
The $co array contains the countries along with their alpha-2 and alpha-3 country codes.
Any tips are welcome.

Checking if two variables either both not null or both null

I have been developing a script to send mail based on variables. I have a script like below.
Each of these 2 variables may be $null or not $null. What's the best practice to check for such condition?
Here is my script:
$variableA = ""
$variableB = ""
if($variableA) {
Write-Host "mail send variableA"
} else {
Write-Host "mail not send variableA"
}
if($variableB) {
Write-Host "mail send variableB"
} else {
Write-Host "mail not send variableB"
}
You can use negated -xor operator:
# Either both nulls or both have values
(-not (($null -eq $a) -xor ($null -eq $b)))
One can disagree what's more readable. I'd personally just go with more explicit formula:
(($null -eq $a) -and ($null -eq $b)) -or
(($null -ne $a) -and ($null -ne $b))
Remember to put $null in the left side of the comparison, it's considered as a best practice.

ForEach loop over string in powershell

I am trying to find all last job statuses fro a list of VMs backed up with veeam backup.
Strangely the loop do not go to the next vm. Here is what I do:
Add-PSSnapin VeeamPSSnapin
$VMlist = "vm1, vm2"
$VMlist = $VMlist.split(",");
Foreach ($i in $VMlist) {
foreach($Job in (Get-VBRJob))
{
$Session = $Job.FindLastSession()
if(!$Session){continue;}
$Tasks = $Session.GetTaskSessions()
$Tasks | ?{$_.Name -eq $VMlist} | %{write-host $_.Name ":" $_.Status}
It seems I have a problem in the for each loop, since it stuck and I do not get any output. What is thebest way to iterate over the slit of VMs?
Thanks in advance!
You're looking for the $VMList array in $Tasks not the individual VM $i, just change: {$_.Name -eq $i}
Also your VM names will include leading spaces, either remove the spaces from your input string "vm1,vm2", or use Trim() after Split()
Add-PSSnapin VeeamPSSnapin
$VMlist = "vm1,vm2"
$VMlist = $VMlist.split(",");
foreach ($i in $VMlist) {
foreach ($Job in (Get-VBRJob)) {
$Session = $Job.FindLastSession()
if (!$Session) {continue; }
$Tasks = $Session.GetTaskSessions()
$Tasks | Where-Object {$_.Name -eq $i} | ForEach-Object {Write-Host $_.Name ":" $_.Status}
}
}

PowerShell: Filtering an array by another [filter] array if any part of the text matches.

Okay, I'm still fairly new to PowerShell.
I've written a piece of code that works, but it smells entirely wrong. What is the right way to write this from a PowerShell standpoint?
$filters = #("*a*","*b*")
$data = #("c/a","c/b","c/a/d","e","a","e/f")
$desiredResults = #("e","e/f")
Write-Host "Filters" -ForegroundColor Yellow
$filters
Write-Host "Data" -ForegroundColor Yellow
$data
$results = #()
foreach ($d in $data)
{
[bool] $skip = $false
foreach ($filter in $filters)
{
if ($d -like $filter)
{
$skip = $true
}
}
if ($skip -eq $false)
{
$results += $d
}
}
Write-Host "Desired Results" -ForegroundColor Yellow
$desiredResults
Write-Host "Results" -ForegroundColor Yellow
$results
you can do like this
$filters = #("a","b")
$data = #("c/a","c/b","c/a/d","e","a","e/f")
$desiredResults = #("e","e/f")
$data | Select-String -Pattern $filters -NotMatch
The right way? I don't know that there is a right way. I can give you what I consider a better way using RegEx:
$filters = #("a","b")
$data = #("c/a","c/b","c/a/d","e","a","e/f")
$desiredResults = #("e","e/f")
$RegExFilter = ($Filters|%{[regex]::escape($_)}) -join '|'
$Results = $data | Where{$_ -notmatch $RegExFilter}
Write-Host "Desired Results" -ForegroundColor Yellow
$desiredResults
Write-Host "Results" -ForegroundColor Yellow
$results
This basically takes each element of the $filters array, performs a regex escape on it to escape any special characters, and then joins all of those results with a pipe character. So in the example given $RegExFilter equals a|b. Then it filters $data against anything that matches that pattern.
The only real problem here - is you have to compare string to and array of strings, which I don't think powershell can do elegantly (but I might be wrong), so I've done this:
foreach ($d in $data)
{
if ($d -notlike $filters[0] -and $d -notlike $filters[1])
{
$results += $d
}
}
I assume the syntaxis is clean enough and doesn't need any explanation. if it needs explanation, tell me in comments.
try this
$data | %{$val=$_; $Nb=($filters | where {$val -like $_}).Count ; if ($Nb -eq 0) {$val} }
I frequently have problems with double pipelines so I switch one to ForEach.
Here is a one-liner.
$filters = #("*a*","*b*")
$data = #("c/a","c/b","c/a/d","e","a","e/f")
ForEach ($Item in $data) { If ( !($filters | Where-Object {$Item -like $_})) { $Item } }