Powershell script variable treats comparison operations differently - powershell

I have an odd issue where a variable that is part of an if statement is not performing a comparison to a value as expected
The value is in a CSV file. It is a number, not a string so it should work fine. If the value comparison is set to -eq "0" it will print out the file entries that have only zero. If I use -gt it lists only the values entries not zero. So far so good.
If I use -le or -lt all file entries will get printed. If I use multiple comparisons such as -gt "0" and -lt "5" this also does not work - it prints out all values (except those equal to zero of course). I'm at a loss how to get the code to work properly (ignore Write-host, that is just so I can see the output).
I'm using Foreach because I'll never have more than about 10-20 entries, otherwise I'd go with Foreach-Object. Code details are below:
$extractFile = "C:\SrvInventory\DBLoginChk.csv"
$passwdexp = 5
$DBdetails = Import-csv $extractfile
Foreach ($DBCreds in $DBdetails) {
If (($DBCreds.DaysUntilExpiration -gt "0") -and
($DBCreds.DaysUntilExpiration -le $passwdexp)) {
$Expdt = $($DBCreds.DaysUntilExpiration)
$DBName = $($DBCreds.LoginName)
$Emailaddr = $DBName+'#'+'myplace.com'
Write-host $Expdt
}
}
File contents look like this:
"LoginName","DaysUntilExpiration"
"spot","35"
"pup","22"
"cat","21"
"rhino","3"
"camel","1"
"Bull","0"
"warthog","0"
"GViewer","0"
"grandrapids","0"

Related

Check if a condition is met by a line within a TXT but "in an advanced way"

I have a TXT file with 1300 megabytes (huge thing). I want to build code that does two things:
Every line contains a unique ID at the beginning. I want to check for all lines with the same unique ID if the conditions is met for that "group" of IDs. (This answers me: For how many lines with the unique ID X have all conditions been met)
If the script is finished I want to remove all lines from the TXT where the condition was met (see 2). So I can rerun the script with another condition set to "narrow down" the whole document.
After few cycles I finally have a set of conditions that applies to all lines in the document.
It seems that my current approach is very slow.( one cycle needs hours). My final result is a set of conditions that apply to all lines of code.
If you find an easier way to do that, feel free to recommend.
Help is welcome :)
Code so far (does not fullfill everything from 1&2)
foreach ($item in $liste)
{
# Check Conditions
if ( ($item -like "*XXX*") -and ($item -like "*YYY*") -and ($item -notlike "*ZZZ*")) {
# Add a line to a document to see which lines match condition
Add-Content "C:\Desktop\it_seems_to_match.txt" "$item"
# Retrieve the unique ID from the line and feed array.
$array += $item.Split("/")[1]
# Remove the line from final document
$liste = $liste -replace $item, ""
}
}
# Pipe the "new cleaned" list somewhere
$liste | Set-Content -Path "C:\NewListToWorkWith.txt"
# Show me the counts
$array | group | % { $h = #{} } { $h[$_.Name] = $_.Count } { $h } | Out-File "C:\Desktop\count.txt"
Demo Lines:
images/STRINGA/2XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGA/3XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGB/4XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGB/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGC/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
performance considerations:
Add-Content "C:\Desktop\it_seems_to_match.txt" "$item"
try to avoid wrapping cmdlet pipelines
See also: Mastering the (steppable) pipeline
$array += $item.Split("/")[1]
Try to avoid using the increase assignment operator (+=) to create a collection
See also: Why should I avoid using the increase assignment operator (+=) to create a collection
$liste = $liste -replace $item, ""
This is a very expensive operation considering that you are reassigning (copying) a long list ($liste) with each iteration.
Besides it is a bad practice to change an array that you are currently iterating.
$array | group | ...
Group-Object is a rather slow cmdlet, you better collect (or count) the items on-the-fly (where you do $array += $item.Split("/")[1]) using a hashtable, something like:
$Name = $item.Split("/")[1]
if (!$HashTable.Contains($Name)) { $HashTable[$Name] = [Collections.Generic.List[String]]::new() }
$HashTable[$Name].Add($Item)
To minimize memory usage it may be better to read one line at a time and check if it already exists. Below code I used StringReader and you can replace with StreamReader for reading from a file. I'm checking if the entire string exists, but you may want to split the line. Notice I have duplicaes in the input but not in the dictionary. See code below :
$rows= #"
images/STRINGA/2XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGA/3XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/4XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGC/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGA/2XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGA/3XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/4XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGC/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
"#
$dict = [System.Collections.Generic.Dictionary[int, System.Collections.Generic.List[string]]]::new();
$reader = [System.IO.StringReader]::new($rows)
while(($row = $reader.ReadLine()) -ne $null)
{
$hash = $row.GetHashCode()
if($dict.ContainsKey($hash))
{
#check if list contains the string
if($dict[$hash].Contains($row))
{
#string is a duplicate
}
else
{
#add string to dictionary value if it is not in list
$list = $dict[$hash].Value
$list.Add($row)
}
}
else
{
#add new hash value to dictionary
$list = [System.Collections.Generic.List[string]]::new();
$list.Add($row)
$dict.Add($hash, $list)
}
}
$dict

Foreach with Where-Object not yielding correct results

I have the following code:
$ErrCodes = Get-AlarmIDs_XML -fileNamePath $Paper_Dialog_FullBasePath
$excelDevice = Get_ErrorCodes -errorCodeListFilePath $outFilePath -ws "DEVICE COMPONENT MAP"
foreach ($errCode in $ErrCodes | Where-Object{$excelDevice.Common_Alarm_Number -eq $errCode.Value })
{
#$dataList = [System.Collections.Generic.List[psobject]]::new()
#$resultHelp = [System.Collections.Generic.List[psobject]]::new()
Write-Host "another:"
$err = $errCode.Value #get each error code to lookup in mdb file
$key = $errCode.Key
Write-Host $err
...
}
But it's definitely getting in the foreach loop when it shouldn't.
My intention is to use the foreach, and if it has a value in the $ErrCodes, then it should continue with the code that follows.
Let me know if you need to see the Functions that do the file reads, but the data structures look like this:
$excelDevice:
[Object[57]]
[0]:#{Common_Alarm_Number=12-2000}
[1]:#{Common_Alarm_Number=12-5707}
[2]:#{Common_Alarm_Number=12-9}
[3]:#{Common_Alarm_Number=12-5703}
...
$ErrCodes:
[Object[7]]
[0]:#{Key=A;Value=12-5702}
[1]:#{Key=B;Value=12-5704}
[2]:#{Key=C;Value=12-5706}
[3]:#{Key=D;Value=12-5707}
...
So we only care about the ones in $ErrCodes that are also in $excelDevice.
When I step through the code, it's getting into the foreach code for 12-5702 for some reason, when it shouldn't be there (prints 12-5702 to screen). I know I wouldn't want 12-5702 to be used because it isn't in $excelDevice list.
How would I get that Where-Object to filter out $ErrCodes that aren't in $excelDevice list? I don't want to process error codes that don't have data for this device.
Right now you're testing whether any of the values in $excelDevice.Common_Alarm_Number (which presumably evaluates to an array) is exactly the same value as all the values in $errCodes.Value - which doesn't make much sense.
It looks like you'll want to test each error code for whether it is contained in the $excelDevice.Common_Alarm_Number list instead. Use $_ to refer to the individual input items received via the pipeline:
foreach ($errCode in $ErrCodes | Where-Object{ $excelDevice.Common_Alarm_Number -contains $_.Value }) { ... }

Counting in powershell?

Can someone explain to me whats going on here? Its a piece of code i got from a script we use here at work and i believe that i understand why it counts but, from there im lost. Any generalization on why/how it does so would be greatly appreciated.
Please note, i did search everywhere before asking on here.
$gc = Get-ChildItem C:\users | Select-Object -ExpandProperty Name
$ls = #($gc)
$gcls = $ls.count
For($i=0; $I -lt $gcls; $i++){
Write-host "$($i): $($ls[$i])"
}
$selection = Read-Host "Enter Number"
$selection = $selection -split " "
$gc[$selection]
gc is self explanatory.
ls is as well throwing the output into an array
gcls is creating the variable to the list of counted strings
I kinda understand whats going on in the for statement where its setting $i to 0, saying if $i -lt the counted strings in $gcls (which it is due to $i=0), and it is counting the output. Now im still kind of following but, I just don't seem to understand how its outputting the strings the way it is.
Anyone familiar with this?
Lee_Dailey also answered this above as a comment.
Inlined comments explaining what each line does and where the count comes from, how the write-host works, etc.
$gc = Get-ChildItem C:\users | Select-Object -ExpandProperty Name #gets all items in c:\users
$ls = #($gc) #this seems redundant to me, but, puts output from get-childitem above into $ls
$gcls = $ls.count #stores a count of items found in get-childitem in $gcls
For($i=0; $I -lt $gcls; $i++){
<#
check out https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_for?view=powershell-7.1
specifically:
The For statement (also known as a For loop) is a language construct you can use to create a loop
that runs commands in a command block while a specified condition evaluates to $true.
So this will run the statement in the scriptblock (Write-host) while $i is less than $gcls (the count of items found in get-childitem).
each time it loops, it willll print $($i): $($ls[$i]) to the console and then increase $i by 1 (the $i++ in the For)
breaking down the print statement:
$($i) - prints the current loop count. The $() is a subexpression operator. It isnt really needed here, but it isnt hurting anything see https://ss64.com/ps/syntax-operators.html
$($ls[$i]) - we have a subexpression operator again. This time were printing a value in the variable $ls. The [$i] gets an item from the array. We need the $(), otherwise it would print all the contents of $ls rather than just the one item we wanted - try it yourself write-host "$($ls[0])" vs write-host "$ls[0]"
$ls[0] would get the first item in the array
$ls[1] would get the second, so on and so forth. Can see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.1 for more info
#>
Write-host "$($i): $($ls[$i])"
}
$selection = Read-Host "Enter Number" #prompts the user for input, expects INT seperated by spaces - 5 10
$selection = $selection -split " " #splits the user input
$gc[$selection] #prints the names using user input above. If the user enters 5, theyd get the 5th item returned by Get-ChildItem. Entering "5 10" would get the 5th and the 10th item. Again, see about_arrays above

Powershell assistance

I am currently using the below PS script to check if the currents months MS patches are installed on the system. The script is set to check the $env:COMPUTERNAME.mbsa and the Patch_NA.txt file and send the result to the $env:COMPUTERNAME.csv file.
I now need to modify this script to also pull information from other POS devices in the same location (C:\Users\Cambridge\SecurityScans) and send the results to the $env:COMPUTERNAME.csv file.
The POS devices are listed like this:
172.26.210.1.mbsa
172.26.210.2.mbsa
172.26.210.3.mbsa
and so forth.
The IP range at all our locations (last octet) is 1 - 60. Any ideas on how I can set this up?
Script:
$logname = "C:\temp\PatchVerify\$env:COMPUTERNAME.csv"
[xml]$x=type "C:\Users\Cambridge\SecurityScans\$env:COMPUTERNAME.mbsa"
#This list is created based on a text file that is provided.
$montlyPatches = type "C:\Temp\PatchVerify\Patches_NA.txt"|
foreach{if ($_ -mat"-KB(? <KB>\d+)"){$matches.KB}}
$patchesNotInstalled=$x.SecScan.check | where {$_.id -eq 500} |foreach{`
$_.detail.updatedata|where {$_.isinstalled -eq "false"}}|Select -expandProperty KBID
$patchesInstalled =$x.SecScan.check | where {$_.id -eq 500} |foreach{`
$_.detail.updatedata|where {$_.isinstalled -eq "true"}}|Select -expandProperty KBID
"Store,Patch,Present"> $logname
$store = "$env:COMPUTERNAME"
foreach ($patch in $montlyPatches)
{
$result = "Unknown"
if ( $patchesInstalled -contains $patch)
{
$result = "YES"
}
if ( $patchesNotInstalled -contains $patch)
{
$result = "NO"
}
"$store,KB$($patch),$result" >>$logname
}
You can find lots of information on creating functions on the web, but a simple example would be:
Function Check-Patches{
Param($FileName)
$logname = "C:\temp\PatchVerify\$FileName.csv"
[xml]$x=type "C:\Users\Cambridge\SecurityScans\$FileName.mbsa"
The rest of your existing code goes here...
}
Check-Patches "$env:ComputerName"
For($i=1;$i -le 60;$i++){
Check-Patches "172.26.210.$i"
}
If you need me to break down anything in that let me know and I'll go into further explanation, but from what you already have it looks like you have a decent grasp on PowerShell theory and just needed to know what resources are available.
Edit: I updated my example to better fit your script, having it accept a file name, and then applying that file name to the $logname and $x variables within the function.
The break down...
First we declare that we are creating a Function using the Function keyword. Following that is the name of the function that you will use later to call it, and an opening curly brace to start the scriptblock that makes up the actual function.
Next is the Param line, which in this case is very simple only declaring one variable as input. This could alternatively be done as Function Check-Patches ($FileName){ but when you start getting into more advanced functions that only gets confusing, so my recommendation is to stick with putting the parameters inside the function's scriptblock. This is the first thing you want inside of your function in most cases, excluding any Help that you would write up for the function.
Then we have updated lines for $logname and [xml]$x that use the $FileName that the function gets as input.
After that comes all of your code that parses the patch logs, and outputs to your CSV, and the closing curly brace that ends the scriptblock, and the function.
Then we call it for the ComputerName, and run a For loop. The For loop runs everything between 1 and 60, and for each loop it uses that number as the last octet of the file name to feed into the function and check those files.
A few comments on the rest of your code. $monthlypatches = could be changed to = type | ?{$_ -match "-KB(? <KB>\d+)"}|%{$matches.KB} so that the results are filtered before the ForEach loop, which could cut down on some time.
On the $patchesInstalled and $patchesNotInstalled lines you don't need the backtick at the end of that line. You can naturally have a linebreak after the beginning of the scriptblock for a ForEach loop. Having it there can be hard to see later if the script breaks, and if there is anything after it (including a space) the script can break and throw errors that are hard to track down.
Lastly, you loop through $x twice, and then $monthlyPatches once, and do a lot of individual writes to the log file. I would suggest creating an array, filling it with custom objects that have 3 properties (Store, Patch, and Present), and then outputting that at the end of the function. That changes things a little bit, but then your function outputs an object, which you could pipe to Export-CSV, or maybe later you could want it to do something else, but at least then you'd have it. To do that I'd run $x through a switch to see if things are installed, then I'd flush out the array by setting all of the monthlypatches that aren't already in that array to Unknown. That would go something like:
Function Check-Patches{
Param($FileName)
$logname = "C:\temp\PatchVerify\$FileName.csv"
[xml]$x=type "C:\Users\Cambridge\SecurityScans\$FileName.mbsa"
$PatchStatus = #()
#This list is created based on a text file that is provided.
$monthlyPatches = GC "C:\Temp\PatchVerify\Patches_NA.txt"|?{$_ -match "-KB(? <KB>\d+)"} | %{$matches.KB}
#Create objects for all the patches in the updatelog that were in the monthly list.
Switch($x.SecScan.Check|?{$_.KBID -in $monthlyPatches -and $_.id -eq 500}){
{$_.detail.updatedata.isinstalled -eq "true"}{$PatchStatus+=[PSCustomObject][Ordered]#{Store=$FileName;Patch=$_.KBID;Present="YES"};Continue}
{$_.detail.updatedata.isinstalled -eq "false"}{$PatchStatus+=[PSCustomObject][Ordered]#{Store=$FileName;Patch=$_.KBID;Present="NO"};Continue}
}
#Populate all of the monthly patches that weren't found on the machine as installed or failed
$monthlyPatches | ?{$_ -notin $PatchStatus.Patch} | %{$PatchStatus += [PSCustomObject][Ordered]#{Store=$FileName;Patch=$_;Present="Unknown"}}
#Output results
$PatchStatus
}
#Check patches on current computer
Check-Patches "$env:ComputerName"|Export-Csv "C:\temp\PatchVerify\$env:ComputerName.csv" -NoTypeInformation
#Check patches on POS Devices
For($i=1;$i -le 60;$i++){
Check-Patches "172.26.210.$i"|Export-Csv "C:\temp\PatchVerify\172.26.210.$i.csv" -NoTypeInformation
}

Why Is It Possible to Loop Through a Null Array

Given the following PowerShell code:
$FolderItems = Get-ChildItem -Path "C:\Test"
Write-Host "FolderItems Is Null: $($FolderItems -eq $null)"
foreach ($FolderItem in $FolderItems)
{
Write-Host "Inside the loop: $($FolderItem.Name)"
}
Write-Host "Done."
When I test it with one file in the C:\Test folder, it outputs this:
FolderItems Is Null: False
Inside the loop: MyFile.txt
Done.
However, when I test it with ZERO files in the folder, it outputs this:
FolderItems Is Null: True
Inside the loop:
Done."
If $FolderItems is null, then why does it enter the foreach loop?
This was an intentional design choice made in V1 and revisited in V3.
In most languages, the foreach statement can only loop over collections of things. PowerShell has always been a little different, and in V1, you could loop over a single value in addition to collections of values.
For example:
foreach ($i in 42) { $i } # prints 42
In V1, if a value was a collection, foreach would iterate over each element in the collection, otherwise it would enter the loop for just that value.
Note in the above sentence, $null isn't special. It's just another value. From a language design point of view, this is fairly clean and concisely explained.
Unfortunately many people did not expect this behavior and it caused many bugs. I think some confusion arises because people expect the foreach statement to behave almost like the foreach-object cmdlet. In other words, I think people expect the following to work the same:
$null | foreach { $_ }
foreach ($i in $null) { $i }
In V3, we decided that it was important enough to change behavior because we could help scripters avoid introducing bugs in their scripts.
Note that changing the behavior could in theory break existing scripts in unexpected ways. We ultimately decided that most scripts that potentially see $null in the foreach statement already guard the foreach statement with an if, e.g.:
if ($null -ne $c)
{
foreach ($i in $c) { ... }
}
So in reality, most real world scripts would not see a change in behavior.
This was something of an idiosyncracy/bug in ForEach in V1 and V2. It was corrected in the V3 release.
Seems to me like you need to wrap your foreach within a conditional that checks if $FolderItem != null. This way, it'll never get in the if statement whenever $FolderItems is NULL
If (-NOT $FolderItems -eq $null) {
foreach ($FolderItem in $FolderItems)
{
Write-Host "Inside the loop: $($FolderItem.Name)"
}
}
This may be of help as well http://bit.ly/1brKRRk